@remnic/core 9.18.0 → 9.20.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 (309) hide show
  1. package/dist/access-admin-ops-surface.d.ts +8 -8
  2. package/dist/access-admin-ops-surface.js +8 -8
  3. package/dist/access-boundary.d.ts +8 -8
  4. package/dist/access-boundary.js +8 -8
  5. package/dist/access-cli.js +32 -32
  6. package/dist/access-http.d.ts +8 -8
  7. package/dist/access-http.js +12 -12
  8. package/dist/access-identity-continuity-surface.d.ts +6 -6
  9. package/dist/access-identity-continuity-surface.js +8 -8
  10. package/dist/access-lcm-surface.d.ts +8 -8
  11. package/dist/access-lcm-surface.js +8 -8
  12. package/dist/access-mcp.d.ts +8 -8
  13. package/dist/access-mcp.js +11 -11
  14. package/dist/access-memory-search-fanout.d.ts +2 -2
  15. package/dist/{access-namespace-preflight-IbQ0O8Kf.d.ts → access-namespace-preflight-tmwQgCT6.d.ts} +1 -1
  16. package/dist/access-namespace-preflight.d.ts +3 -3
  17. package/dist/access-namespace-preflight.js +8 -8
  18. package/dist/access-observe-write-surface.d.ts +8 -8
  19. package/dist/access-observe-write-surface.js +8 -8
  20. package/dist/access-operations-batch.js +9 -9
  21. package/dist/access-operations.d.ts +8 -8
  22. package/dist/access-operations.js +10 -10
  23. package/dist/access-recall-concurrency.d.ts +8 -8
  24. package/dist/access-recall-concurrency.js +8 -8
  25. package/dist/access-recall-response.d.ts +8 -8
  26. package/dist/access-recall-response.js +8 -8
  27. package/dist/access-recall-surface.d.ts +8 -8
  28. package/dist/access-recall-surface.js +8 -8
  29. package/dist/{access-service-BqjsJRn_.d.ts → access-service-BqUii7LY.d.ts} +6 -6
  30. package/dist/access-service.d.ts +8 -8
  31. package/dist/access-service.js +8 -8
  32. package/dist/access-surface-catalog.d.ts +8 -8
  33. package/dist/action-confidence.d.ts +1 -1
  34. package/dist/active-memory-bridge.d.ts +1 -1
  35. package/dist/active-recall.d.ts +1 -1
  36. package/dist/active-recall.js +1 -1
  37. package/dist/{auto-sync-IH3G25MT.js → auto-sync-UPTUVE4X.js} +3 -3
  38. package/dist/behavior-learner.d.ts +1 -1
  39. package/dist/behavior-signals.d.ts +1 -1
  40. package/dist/bootstrap.d.ts +6 -6
  41. package/dist/briefing.d.ts +2 -2
  42. package/dist/briefing.js +4 -4
  43. package/dist/buffer-surprise-report.d.ts +1 -1
  44. package/dist/buffer.d.ts +2 -2
  45. package/dist/bulk-import/index.d.ts +3 -3
  46. package/dist/calibration.d.ts +1 -1
  47. package/dist/capabilities.d.ts +1 -1
  48. package/dist/{catalog-D_ToaAZM.d.ts → catalog-iWvWz61I.d.ts} +1 -1
  49. package/dist/causal-behavior.d.ts +1 -1
  50. package/dist/causal-consolidation.d.ts +1 -1
  51. package/dist/causal-consolidation.js +5 -5
  52. package/dist/causal-trajectory-graph.d.ts +1 -1
  53. package/dist/{chunk-52H7OB7Y.js → chunk-275ENJDK.js} +2 -2
  54. package/dist/{chunk-OR3OQHHS.js → chunk-3ZUY2VQF.js} +2 -2
  55. package/dist/{chunk-NEFMSUC2.js → chunk-4F6AANKS.js} +3 -3
  56. package/dist/{chunk-NGO4QKMY.js → chunk-4PYLBXX6.js} +2 -2
  57. package/dist/{chunk-UQXIW7OZ.js → chunk-53WDAG7X.js} +2 -2
  58. package/dist/{chunk-CNNQVI2A.js → chunk-5IXPWEQB.js} +5 -5
  59. package/dist/{chunk-XGYCI23S.js → chunk-5JQLAQUZ.js} +2 -2
  60. package/dist/{chunk-ALPBE5EC.js → chunk-62QM5MKY.js} +7 -7
  61. package/dist/{chunk-KPEIWAZY.js → chunk-6NZ7U4R4.js} +1 -1
  62. package/dist/chunk-6NZ7U4R4.js.map +1 -0
  63. package/dist/{chunk-ZHWEUG7Y.js → chunk-7M6APKJL.js} +333 -8
  64. package/dist/chunk-7M6APKJL.js.map +1 -0
  65. package/dist/{chunk-AKWX5E6R.js → chunk-AT3TDN6O.js} +1017 -296
  66. package/dist/chunk-AT3TDN6O.js.map +1 -0
  67. package/dist/{chunk-LU22QKFA.js → chunk-B7C3TF5H.js} +2 -2
  68. package/dist/{chunk-JHIEQCMG.js → chunk-EILX7SQR.js} +3 -3
  69. package/dist/{chunk-WIP3MQZY.js → chunk-EXU5IQ3S.js} +2 -2
  70. package/dist/{chunk-UL6WTQN4.js → chunk-EZKKHVFY.js} +2 -2
  71. package/dist/{chunk-LTGSOACX.js → chunk-GEUDRENF.js} +2 -2
  72. package/dist/{chunk-B3R5CC35.js → chunk-HJEEAO4O.js} +2 -2
  73. package/dist/{chunk-7NHIHUBW.js → chunk-HPOY2PHQ.js} +3 -3
  74. package/dist/{chunk-VMALXCR5.js → chunk-JFDEYXF7.js} +2 -2
  75. package/dist/{chunk-WMMT6W7C.js → chunk-JN3IUA2Q.js} +2 -2
  76. package/dist/{chunk-A2KGXUOU.js → chunk-JWMYVQAM.js} +2 -2
  77. package/dist/{chunk-6P6ZR6JR.js → chunk-KHRKEELC.js} +17 -17
  78. package/dist/{chunk-UVCY7VDX.js → chunk-LI4ZOFHW.js} +2 -2
  79. package/dist/{chunk-6YJLLDWO.js → chunk-LMHDFOVV.js} +3 -3
  80. package/dist/{chunk-UHOUNNY3.js → chunk-N7EVMKP6.js} +6 -6
  81. package/dist/{chunk-7JXXI3GW.js → chunk-Q3FWRDCP.js} +2 -2
  82. package/dist/{chunk-JEMLK7ED.js → chunk-QO2KODU6.js} +2 -2
  83. package/dist/{chunk-7KXE7KN5.js → chunk-TCZWBJBO.js} +2 -2
  84. package/dist/{chunk-B3ABVQ36.js → chunk-UHVXE24Y.js} +2 -1
  85. package/dist/{chunk-UX3ZXN34.js → chunk-UV72DEZD.js} +2 -2
  86. package/dist/{chunk-FO3WM76R.js → chunk-V5TSUGRB.js} +5 -5
  87. package/dist/{chunk-SGCWBLHN.js → chunk-X5QS5C2O.js} +2 -2
  88. package/dist/{cli-D2g5mRgf.d.ts → cli-DPEhq0Cp.d.ts} +5 -5
  89. package/dist/cli.d.ts +10 -10
  90. package/dist/cli.js +25 -25
  91. package/dist/compounding/engine.d.ts +2 -2
  92. package/dist/compounding/engine.js +4 -4
  93. package/dist/compounding/preference-consolidator.d.ts +1 -1
  94. package/dist/compression-optimizer.d.ts +1 -1
  95. package/dist/config.d.ts +1 -1
  96. package/dist/config.js +1 -1
  97. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  98. package/dist/connectors/codex-materialize-runner.js +4 -4
  99. package/dist/connectors/codex-materialize.d.ts +1 -1
  100. package/dist/connectors/index.d.ts +1 -1
  101. package/dist/connectors/index.js +4 -4
  102. package/dist/consolidation-provenance-check.d.ts +2 -2
  103. package/dist/consolidation-undo.d.ts +2 -2
  104. package/dist/contradiction/index.d.ts +2 -2
  105. package/dist/conversation-index/backend.d.ts +1 -1
  106. package/dist/conversation-index/chunker.d.ts +1 -1
  107. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  108. package/dist/conversation-index/indexer.d.ts +1 -1
  109. package/dist/conversation-index/search.d.ts +1 -1
  110. package/dist/day-summary.d.ts +1 -1
  111. package/dist/delinearize.d.ts +1 -1
  112. package/dist/direct-answer-wiring.d.ts +1 -1
  113. package/dist/direct-answer.d.ts +1 -1
  114. package/dist/embedding-fallback.d.ts +1 -1
  115. package/dist/enrichment/index.d.ts +1 -1
  116. package/dist/entity-retrieval.d.ts +2 -2
  117. package/dist/entity-retrieval.js +4 -4
  118. package/dist/entity-schema.d.ts +1 -1
  119. package/dist/explicit-capture.d.ts +6 -6
  120. package/dist/extraction-faithfulness.d.ts +1 -1
  121. package/dist/extraction-judge-telemetry.d.ts +1 -1
  122. package/dist/extraction-judge-training.d.ts +1 -1
  123. package/dist/extraction-judge.d.ts +1 -1
  124. package/dist/extraction.d.ts +1 -1
  125. package/dist/fallback-llm.d.ts +1 -1
  126. package/dist/graph-dashboard-diff.d.ts +1 -1
  127. package/dist/graph-dashboard-key.d.ts +1 -1
  128. package/dist/graph-dashboard-parser.d.ts +1 -1
  129. package/dist/graph-edge-reinforcement.d.ts +1 -1
  130. package/dist/graph-snapshot.d.ts +1 -1
  131. package/dist/graph.d.ts +1 -1
  132. package/dist/importance.d.ts +1 -1
  133. package/dist/importers/index.d.ts +1 -1
  134. package/dist/in-flight-reads.d.ts +1 -1
  135. package/dist/index.d.ts +470 -90
  136. package/dist/index.js +66 -36
  137. package/dist/intent.d.ts +1 -1
  138. package/dist/lcm/engine.d.ts +1 -1
  139. package/dist/lcm/index.d.ts +1 -1
  140. package/dist/lcm/tools.d.ts +1 -1
  141. package/dist/lifecycle.d.ts +1 -1
  142. package/dist/live-connectors-runner.d.ts +1 -1
  143. package/dist/local-llm.d.ts +1 -1
  144. package/dist/local-model-endpoint.d.ts +1 -1
  145. package/dist/maintenance/memory-governance.d.ts +1 -1
  146. package/dist/maintenance/memory-governance.js +4 -4
  147. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
  148. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
  149. package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
  150. package/dist/maintenance/rebuild-memory-projection.js +5 -5
  151. package/dist/mcp-memory-inspector-app.d.ts +8 -8
  152. package/dist/memory-action-policy.d.ts +1 -1
  153. package/dist/memory-cache.d.ts +1 -1
  154. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  155. package/dist/memory-projection-store.d.ts +1 -1
  156. package/dist/memory-provenance.d.ts +1 -1
  157. package/dist/memory-worth-outcomes.d.ts +2 -2
  158. package/dist/models-json.d.ts +1 -1
  159. package/dist/namespaces/migrate.d.ts +3 -3
  160. package/dist/namespaces/migrate.js +5 -5
  161. package/dist/namespaces/principal.d.ts +1 -1
  162. package/dist/namespaces/search.d.ts +1 -1
  163. package/dist/namespaces/storage.d.ts +3 -3
  164. package/dist/namespaces/storage.js +4 -4
  165. package/dist/native-knowledge.d.ts +1 -1
  166. package/dist/offline-sync-impression-drain.d.ts +1 -1
  167. package/dist/operator-toolkit.d.ts +2 -2
  168. package/dist/operator-toolkit.js +9 -9
  169. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  170. package/dist/orchestration/maintenance.d.ts +3 -3
  171. package/dist/orchestration/maintenance.js +9 -9
  172. package/dist/{orchestrator-DrILRi-n.d.ts → orchestrator-DB93S48I.d.ts} +5 -5
  173. package/dist/orchestrator.d.ts +6 -6
  174. package/dist/orchestrator.js +32 -32
  175. package/dist/patterns-cli.d.ts +1 -1
  176. package/dist/{pipeline-BYo4b_y9.d.ts → pipeline-DmGc-H66.d.ts} +1 -1
  177. package/dist/policy-runtime.d.ts +1 -1
  178. package/dist/proactive-contention.d.ts +1 -1
  179. package/dist/provenance.d.ts +1 -1
  180. package/dist/{qmd-BIrL8e69.d.ts → qmd-cJT0crkF.d.ts} +1 -1
  181. package/dist/qmd-preflight.d.ts +2 -2
  182. package/dist/qmd-recall-cache.d.ts +1 -1
  183. package/dist/qmd.d.ts +2 -2
  184. package/dist/recall-concurrency-config.d.ts +1 -1
  185. package/dist/recall-disclosure-escalation.d.ts +1 -1
  186. package/dist/recall-explain-renderer.d.ts +1 -1
  187. package/dist/recall-explain-renderer.js +3 -3
  188. package/dist/recall-memory-map.d.ts +1 -1
  189. package/dist/recall-planner-llm.d.ts +1 -1
  190. package/dist/recall-state.d.ts +1 -1
  191. package/dist/recall-tag-filter.d.ts +1 -1
  192. package/dist/recall-timings.d.ts +1 -1
  193. package/dist/recall-xray-cli.d.ts +1 -1
  194. package/dist/recall-xray-cli.js +4 -4
  195. package/dist/recall-xray-renderer.d.ts +1 -1
  196. package/dist/recall-xray-renderer.js +3 -3
  197. package/dist/recall-xray.d.ts +1 -1
  198. package/dist/recall-xray.js +2 -2
  199. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  200. package/dist/replay/normalizers/claude.d.ts +1 -1
  201. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  202. package/dist/replay/normalizers/shared.d.ts +1 -1
  203. package/dist/replay/runner.d.ts +1 -1
  204. package/dist/replay/types.d.ts +1 -1
  205. package/dist/resolve-auth-token.d.ts +1 -1
  206. package/dist/resume-bundles.js +2 -2
  207. package/dist/retrieval-agents.d.ts +2 -2
  208. package/dist/retrieval-tiers.d.ts +1 -1
  209. package/dist/routing/engine.d.ts +1 -1
  210. package/dist/routing/store.d.ts +1 -1
  211. package/dist/salvage-envelope.d.ts +1 -1
  212. package/dist/schemas.d.ts +22 -22
  213. package/dist/{scope-profiles-BiQ9yXP4.d.ts → scope-profiles-KG2nODX7.d.ts} +1 -1
  214. package/dist/search/embed-helper.d.ts +1 -1
  215. package/dist/search/factory.d.ts +1 -1
  216. package/dist/search/index.d.ts +1 -1
  217. package/dist/search/lancedb-backend.d.ts +1 -1
  218. package/dist/search/meilisearch-backend.d.ts +1 -1
  219. package/dist/search/noop-backend.d.ts +1 -1
  220. package/dist/search/orama-backend.d.ts +1 -1
  221. package/dist/search/port.d.ts +1 -1
  222. package/dist/search/remote-backend.d.ts +1 -1
  223. package/dist/{semantic-consolidation-C-cSATcR.d.ts → semantic-consolidation-BaZlg0D8.d.ts} +1 -1
  224. package/dist/semantic-consolidation.d.ts +2 -2
  225. package/dist/semantic-consolidation.js +5 -5
  226. package/dist/semantic-rule-promotion.js +4 -4
  227. package/dist/semantic-rule-verifier.d.ts +1 -1
  228. package/dist/semantic-rule-verifier.js +4 -4
  229. package/dist/session-observer-bands.d.ts +1 -1
  230. package/dist/session-observer-state.d.ts +1 -1
  231. package/dist/shared-context/manager.d.ts +1 -1
  232. package/dist/signal.d.ts +1 -1
  233. package/dist/{storage-ByTv05qM.d.ts → storage-BKhHHnRo.d.ts} +2 -221
  234. package/dist/storage.d.ts +2 -2
  235. package/dist/storage.js +3 -3
  236. package/dist/summarizer.d.ts +1 -1
  237. package/dist/summary-snapshot.d.ts +1 -1
  238. package/dist/temporal-supersession.d.ts +2 -2
  239. package/dist/temporal-timeline-recall.d.ts +1 -1
  240. package/dist/temporal-validity.d.ts +1 -1
  241. package/dist/threading.d.ts +1 -1
  242. package/dist/tier-migration.d.ts +2 -2
  243. package/dist/tier-routing.d.ts +1 -1
  244. package/dist/topics.d.ts +1 -1
  245. package/dist/transcript.d.ts +1 -1
  246. package/dist/transfer/types.d.ts +12 -12
  247. package/dist/trust-score-stage.d.ts +1 -1
  248. package/dist/trust-score.d.ts +1 -1
  249. package/dist/{types-e0_MI3Iq.d.ts → types-GBQZuT7l.d.ts} +397 -1
  250. package/dist/types.d.ts +1 -1
  251. package/dist/types.js +1 -1
  252. package/dist/utility-runtime.d.ts +1 -1
  253. package/dist/verified-recall.js +4 -4
  254. package/dist/write-envelope.d.ts +1 -1
  255. package/package.json +2 -2
  256. package/src/config.test.ts +11 -0
  257. package/src/config.ts +5 -6
  258. package/src/meetings/build.test.ts +646 -0
  259. package/src/meetings/build.ts +365 -0
  260. package/src/meetings/cli.test.ts +276 -0
  261. package/src/meetings/cli.ts +242 -0
  262. package/src/meetings/config.test.ts +72 -0
  263. package/src/meetings/config.ts +183 -0
  264. package/src/meetings/errors.ts +13 -0
  265. package/src/meetings/fuse.test.ts +208 -0
  266. package/src/meetings/fuse.ts +297 -0
  267. package/src/meetings/index.ts +11 -1
  268. package/src/meetings/memory-generator.ts +116 -0
  269. package/src/meetings/recall-exclusion.test.ts +88 -0
  270. package/src/meetings/store.test.ts +232 -0
  271. package/src/meetings/store.ts +465 -0
  272. package/src/meetings/types.ts +111 -0
  273. package/src/orchestration/orchestrator-helpers.ts +23 -3
  274. package/src/orchestration/recall-search-pipeline.test.ts +182 -0
  275. package/src/orchestration/recall-search-pipeline.ts +36 -15
  276. package/src/types.ts +8 -0
  277. package/dist/chunk-AKWX5E6R.js.map +0 -1
  278. package/dist/chunk-KPEIWAZY.js.map +0 -1
  279. package/dist/chunk-ZHWEUG7Y.js.map +0 -1
  280. /package/dist/{auto-sync-IH3G25MT.js.map → auto-sync-UPTUVE4X.js.map} +0 -0
  281. /package/dist/{chunk-52H7OB7Y.js.map → chunk-275ENJDK.js.map} +0 -0
  282. /package/dist/{chunk-OR3OQHHS.js.map → chunk-3ZUY2VQF.js.map} +0 -0
  283. /package/dist/{chunk-NEFMSUC2.js.map → chunk-4F6AANKS.js.map} +0 -0
  284. /package/dist/{chunk-NGO4QKMY.js.map → chunk-4PYLBXX6.js.map} +0 -0
  285. /package/dist/{chunk-UQXIW7OZ.js.map → chunk-53WDAG7X.js.map} +0 -0
  286. /package/dist/{chunk-CNNQVI2A.js.map → chunk-5IXPWEQB.js.map} +0 -0
  287. /package/dist/{chunk-XGYCI23S.js.map → chunk-5JQLAQUZ.js.map} +0 -0
  288. /package/dist/{chunk-ALPBE5EC.js.map → chunk-62QM5MKY.js.map} +0 -0
  289. /package/dist/{chunk-LU22QKFA.js.map → chunk-B7C3TF5H.js.map} +0 -0
  290. /package/dist/{chunk-JHIEQCMG.js.map → chunk-EILX7SQR.js.map} +0 -0
  291. /package/dist/{chunk-WIP3MQZY.js.map → chunk-EXU5IQ3S.js.map} +0 -0
  292. /package/dist/{chunk-UL6WTQN4.js.map → chunk-EZKKHVFY.js.map} +0 -0
  293. /package/dist/{chunk-LTGSOACX.js.map → chunk-GEUDRENF.js.map} +0 -0
  294. /package/dist/{chunk-B3R5CC35.js.map → chunk-HJEEAO4O.js.map} +0 -0
  295. /package/dist/{chunk-7NHIHUBW.js.map → chunk-HPOY2PHQ.js.map} +0 -0
  296. /package/dist/{chunk-VMALXCR5.js.map → chunk-JFDEYXF7.js.map} +0 -0
  297. /package/dist/{chunk-WMMT6W7C.js.map → chunk-JN3IUA2Q.js.map} +0 -0
  298. /package/dist/{chunk-A2KGXUOU.js.map → chunk-JWMYVQAM.js.map} +0 -0
  299. /package/dist/{chunk-6P6ZR6JR.js.map → chunk-KHRKEELC.js.map} +0 -0
  300. /package/dist/{chunk-UVCY7VDX.js.map → chunk-LI4ZOFHW.js.map} +0 -0
  301. /package/dist/{chunk-6YJLLDWO.js.map → chunk-LMHDFOVV.js.map} +0 -0
  302. /package/dist/{chunk-UHOUNNY3.js.map → chunk-N7EVMKP6.js.map} +0 -0
  303. /package/dist/{chunk-7JXXI3GW.js.map → chunk-Q3FWRDCP.js.map} +0 -0
  304. /package/dist/{chunk-JEMLK7ED.js.map → chunk-QO2KODU6.js.map} +0 -0
  305. /package/dist/{chunk-7KXE7KN5.js.map → chunk-TCZWBJBO.js.map} +0 -0
  306. /package/dist/{chunk-B3ABVQ36.js.map → chunk-UHVXE24Y.js.map} +0 -0
  307. /package/dist/{chunk-UX3ZXN34.js.map → chunk-UV72DEZD.js.map} +0 -0
  308. /package/dist/{chunk-FO3WM76R.js.map → chunk-V5TSUGRB.js.map} +0 -0
  309. /package/dist/{chunk-SGCWBLHN.js.map → chunk-X5QS5C2O.js.map} +0 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Meeting building orchestration (issue #1900, Phase 4 slice 2).
3
+ *
4
+ * Ties the pure pieces together: detect the day's meetings, fuse each window's
5
+ * transcript + screen context, and compose idempotent records. Day data is
6
+ * supplied through an injected source so fixtures (and the later activity/
7
+ * wearables wiring) share one code path — no hidden IO here.
8
+ *
9
+ * Gated on `meetings.enabled`: a disabled subsystem builds nothing on every
10
+ * surface (degradation-matrix row). Degrades within an enabled build too — a
11
+ * window with no audio yields an empty transcript, and no activity yields a
12
+ * record with no screen-context section — because `fuseMeeting` handles both.
13
+ */
14
+
15
+ import { detectMeetings } from "./detect.js";
16
+ import { fuseMeeting, type MeetingFusionOptions } from "./fuse.js";
17
+ import { composeMeetingRecord, type MeetingRecordStore, type MeetingRecordSummary } from "./store.js";
18
+ import { log } from "../logger.js";
19
+ import type {
20
+ MeetingEpisodeGenResult,
21
+ MeetingMemoryGenerator,
22
+ MeetingMemoryOutcome,
23
+ MeetingsDayFactGenResult,
24
+ } from "./memory-generator.js";
25
+ import type { FusionConversationInput } from "../wearables/fusion/types.js";
26
+ import type {
27
+ DetectedMeeting,
28
+ MeetingActivitySnapshot,
29
+ MeetingRecord,
30
+ MeetingsConfig,
31
+ MeetingsDetectionInput,
32
+ } from "./types.js";
33
+
34
+ /** All of a day's already-loaded signals a build needs. */
35
+ export interface MeetingDayData {
36
+ /** App spans + audio windows for the day (fed to the detector). */
37
+ detection: MeetingsDetectionInput;
38
+ /** Fusion conversation inputs across all wearable sources for the day. */
39
+ conversations: FusionConversationInput[];
40
+ /** Screen-activity snapshots for the day. */
41
+ activity?: MeetingActivitySnapshot[];
42
+ }
43
+
44
+ /** Supplies a day's signals. Backed by fixtures in tests; by the activity +
45
+ * wearables stores in the wiring slice. */
46
+ export interface MeetingsDaySource {
47
+ loadDayData(date: string): Promise<MeetingDayData> | MeetingDayData;
48
+ }
49
+
50
+ export function buildMeetingRecordsForDay(
51
+ data: MeetingDayData,
52
+ config: MeetingsConfig,
53
+ options: MeetingFusionOptions = {},
54
+ /** Map a detected meeting to the id its record should carry. Defaults to the
55
+ * detector's start-anchored id; the builder overrides this to preserve an
56
+ * existing overlapping record's id across shifted-start resyncs. */
57
+ resolveMeetingId: (meeting: DetectedMeeting) => string = (meeting) => meeting.id,
58
+ ): MeetingRecord[] {
59
+ const meetings = detectMeetings(data.detection, {
60
+ appPatterns: config.appPatterns,
61
+ minOverlapMinutes: config.minOverlapMinutes,
62
+ audioOnlyMinMinutes: config.audioOnlyMinMinutes,
63
+ mergeGapMinutes: config.mergeGapMinutes,
64
+ });
65
+ return meetings.map((meeting) => {
66
+ const fused = fuseMeeting(
67
+ { meeting, conversations: data.conversations, ...(data.activity !== undefined ? { activity: data.activity } : {}) },
68
+ config,
69
+ options,
70
+ );
71
+ return composeMeetingRecord(
72
+ {
73
+ id: resolveMeetingId(meeting),
74
+ date: meeting.date,
75
+ startUtc: meeting.startUtc,
76
+ endUtc: meeting.endUtc,
77
+ ...(meeting.app !== undefined ? { app: meeting.app } : {}),
78
+ detectionSource: meeting.detectionSource,
79
+ ...(meeting.title !== undefined ? { title: meeting.title } : {}),
80
+ },
81
+ fused,
82
+ );
83
+ });
84
+ }
85
+
86
+ /** Per-meeting outcome of a day build. */
87
+ export interface MeetingBuildOutcome {
88
+ id: string;
89
+ startUtc: string;
90
+ endUtc: string;
91
+ detectionSource: string;
92
+ sources: string[];
93
+ attendees: string[];
94
+ snapshotCount: number;
95
+ /** False when the record's contentHash was unchanged (nothing rewritten). */
96
+ written: boolean;
97
+ }
98
+
99
+ /** Result of building one day's meetings. */
100
+ export interface MeetingsDayBuildSummary {
101
+ date: string;
102
+ /** False when `meetings.enabled` is off (zero behavior). */
103
+ enabled: boolean;
104
+ meetings: MeetingBuildOutcome[];
105
+ /** Records written (new or changed). */
106
+ built: number;
107
+ /** Records skipped (unchanged contentHash). */
108
+ skipped: number;
109
+ /** Ids of stale same-day records deleted because they no longer detect. */
110
+ removed: string[];
111
+ /**
112
+ * Set when the optional reindex hook rejected AFTER records were already
113
+ * persisted. The store mutation still succeeded — the build is reported as
114
+ * successful with this generic warning, and the next scheduled index catches
115
+ * up. Deliberately does NOT embed the raw error text: this string is printed
116
+ * to the CLI's stdout, and internal error detail is logged separately.
117
+ */
118
+ reindexWarning?: string;
119
+ /**
120
+ * Set when the injected `memoryGenerator` rejected AFTER records were already
121
+ * persisted, OR when a successful generation returned non-fatal warnings. Like
122
+ * `reindexWarning`, the record writes still succeeded, so the build is reported
123
+ * successful with this warning; on rejection the next build (or scheduled
124
+ * maintenance) regenerates the missing memories. Carries no raw exception text —
125
+ * only the generic rejection message or the generator's own warning strings.
126
+ */
127
+ memoryWarning?: string;
128
+ /**
129
+ * Deterministic episode-memory counts, present only when a `memoryGenerator`
130
+ * was injected. One recall-anchor episode is written per built record (idempotent).
131
+ */
132
+ episodes?: MeetingEpisodeGenResult;
133
+ /**
134
+ * Trust-gated summary/fact counts (decisions, commitments, questions, and the
135
+ * synthesized summary), present only when a `memoryGenerator` with summary deps
136
+ * ran and `summaryMode !== "off"`. Aggregated across the day's records.
137
+ */
138
+ facts?: MeetingsDayFactGenResult;
139
+ }
140
+
141
+ /** Optional side effects the builder fires after a day build. */
142
+ export interface MeetingsBuilderHooks {
143
+ /**
144
+ * Invoked once per build when records changed (`built > 0 || removed > 0`).
145
+ * Meeting records live inside the QMD collection root for full-text search,
146
+ * so a host that wants them discoverable immediately (rather than at the next
147
+ * scheduled reindex) supplies a reindex here. No-op when omitted — records are
148
+ * still picked up by the next index update.
149
+ */
150
+ reindex?(summary: MeetingsDayBuildSummary): void | Promise<void>;
151
+ }
152
+
153
+ /** Orchestrates a store-backed build of a day's meetings. */
154
+ export class MeetingsBuilder {
155
+ constructor(
156
+ private readonly opts: {
157
+ source: MeetingsDaySource;
158
+ store: MeetingRecordStore;
159
+ config: MeetingsConfig;
160
+ fusionOptions?: MeetingFusionOptions;
161
+ hooks?: MeetingsBuilderHooks;
162
+ /**
163
+ * When present, the memory-generation layer runs after the record-store
164
+ * writes: deterministic recall-anchor episodes per built record, the
165
+ * trust-gated summary/facts layer, and retract-on-removal — all behind the
166
+ * `MeetingMemoryGenerator` seam (issue #1900). Omitted → pure
167
+ * detect+fuse+store with no memory writes. The engine depends only on this
168
+ * interface, never on the memory-gen module.
169
+ */
170
+ memoryGenerator?: MeetingMemoryGenerator;
171
+ },
172
+ ) {}
173
+
174
+ async buildDay(date: string): Promise<MeetingsDayBuildSummary> {
175
+ if (!this.opts.config.enabled) {
176
+ return { date, enabled: false, meetings: [], built: 0, skipped: 0, removed: [] };
177
+ }
178
+ const data = await this.opts.source.loadDayData(date);
179
+ // The source contract is loadDayData(date) → THAT day's signals. If the
180
+ // returned detection day disagrees, records would be written under
181
+ // data.detection.date while listing/adoption/reconciliation run on `date`,
182
+ // orphaning records and mis-keying the summary — reject the desync loudly.
183
+ if (data.detection.date !== date) {
184
+ throw new RangeError(
185
+ `meetings: day source returned detection.date "${data.detection.date}" for requested day "${date}".`,
186
+ );
187
+ }
188
+ // Existing records for the day, so an overlapping shifted-start resync keeps
189
+ // its original id instead of being deleted + re-created under a new one.
190
+ const existing = await this.opts.store.listMeetingSummaries(date);
191
+ const claimed = new Set<string>();
192
+ const resolveMeetingId = (meeting: DetectedMeeting): string =>
193
+ this.adoptExistingId(meeting, existing, claimed);
194
+ const records = buildMeetingRecordsForDay(
195
+ data,
196
+ this.opts.config,
197
+ this.opts.fusionOptions ?? {},
198
+ resolveMeetingId,
199
+ );
200
+ const meetings: MeetingBuildOutcome[] = [];
201
+ let built = 0;
202
+ let skipped = 0;
203
+ const builtIds = new Set<string>();
204
+ // Ids that existed before THIS build, so a same-id rewrite (contentHash
205
+ // changed) is distinguished from a brand-new record and from an untouched one.
206
+ const priorIds = new Set(existing.map((summary) => summary.id));
207
+ // Built ids whose stored record was IDENTICAL this build (nothing rewritten):
208
+ // the generator must skip them so an idempotent rebuild triggers no LLM calls
209
+ // and no duplicate memories.
210
+ const unchangedIds: string[] = [];
211
+ // Built ids that already existed and were rewritten this build (same id, new
212
+ // contentHash): the generator must refresh (retract + regenerate) their stale
213
+ // episode/summary memories.
214
+ const updatedIds: string[] = [];
215
+ for (const record of records) {
216
+ const save = await this.opts.store.saveMeetingRecord(record);
217
+ if (save.written) {
218
+ built++;
219
+ if (priorIds.has(record.id)) updatedIds.push(record.id);
220
+ } else {
221
+ skipped++;
222
+ unchangedIds.push(record.id);
223
+ }
224
+ builtIds.add(record.id);
225
+ meetings.push({
226
+ id: record.id,
227
+ startUtc: record.startUtc,
228
+ endUtc: record.endUtc,
229
+ detectionSource: record.detectionSource,
230
+ sources: record.sources,
231
+ attendees: record.attendees,
232
+ snapshotCount: record.snapshotCount,
233
+ written: save.written,
234
+ });
235
+ }
236
+ // Reconcile genuinely stale records: a prior build's meeting the current
237
+ // rebuild neither reproduced nor adopted (by overlap) is deleted so it does
238
+ // not linger and orphan its provenance. Delete only within THIS day's dir.
239
+ const removed: string[] = [];
240
+ for (const existingId of await this.opts.store.listMeetingIds(date)) {
241
+ if (builtIds.has(existingId)) continue;
242
+ await this.opts.store.deleteMeetingRecord(date, existingId);
243
+ removed.push(existingId);
244
+ }
245
+ const summary: MeetingsDayBuildSummary = { date, enabled: true, meetings, built, skipped, removed };
246
+ // Memory generation runs behind the seam: the deterministic engine hands the
247
+ // built records, removed ids, and the unchanged/updated id sets to the
248
+ // injected generator and folds the returned counts + reindex signal into the
249
+ // summary. The engine never imports the memory-gen module.
250
+ //
251
+ // The record writes above already succeeded; a generator rejection must NOT
252
+ // fail the whole build and lose those persisted records. Isolate it exactly
253
+ // like the reindex hook: surface a generic warning, log the detail, and let
254
+ // the next build regenerate the missing memories.
255
+ let outcome: MeetingMemoryOutcome | undefined;
256
+ if (this.opts.memoryGenerator !== undefined) {
257
+ try {
258
+ outcome = await this.opts.memoryGenerator.onRecordsBuilt({
259
+ built: records,
260
+ removedIds: removed,
261
+ unchangedIds,
262
+ updatedIds,
263
+ });
264
+ } catch (err) {
265
+ log.error("meetings: memory generation failed after records were persisted", err);
266
+ summary.memoryWarning = "memory generation failed after records were persisted; see logs for detail";
267
+ }
268
+ }
269
+ if (outcome?.episodes !== undefined) summary.episodes = outcome.episodes;
270
+ if (outcome?.facts !== undefined) summary.facts = outcome.facts;
271
+ // A SUCCESSFUL generation can still raise non-fatal warnings (e.g. a summary
272
+ // dep degraded). The fatal catch above leaves `outcome` undefined, so these
273
+ // paths are mutually exclusive: fold the generator's own warning strings into
274
+ // the same `memoryWarning` the CLI/JSON already surfaces so a green build does
275
+ // not silently hide them. These are generator-authored strings, not raw error
276
+ // text, so surfacing them is safe.
277
+ if (outcome?.warnings !== undefined && outcome.warnings.length > 0) {
278
+ summary.memoryWarning = outcome.warnings.join("; ");
279
+ }
280
+ // Meeting records AND their episode/summary memories live inside the QMD
281
+ // collection root. Fire the isolated reindex whenever anything discoverable
282
+ // changed — records written/removed (engine) OR newly written episodes/facts
283
+ // (folded into outcome.reindexNeeded). A rebuild whose records were unchanged
284
+ // (built == 0 && removed == 0) but whose prior episode write had failed still
285
+ // creates active memories that must be indexed now, not left dark until the
286
+ // next scheduled maintenance pass.
287
+ if (built > 0 || removed.length > 0 || (outcome?.reindexNeeded ?? false)) {
288
+ // The store mutation already succeeded; a reindex-hook rejection must not
289
+ // fail the whole build. Log the detail and surface a GENERIC warning — the
290
+ // raw error must not leak to the CLI's stdout — then let the next scheduled
291
+ // index update catch up.
292
+ try {
293
+ await this.opts.hooks?.reindex?.(summary);
294
+ } catch (err) {
295
+ log.error("meetings: reindex hook failed after records were persisted", err);
296
+ summary.reindexWarning = "reindex hook failed after records were persisted; see logs for detail";
297
+ }
298
+ }
299
+ return summary;
300
+ }
301
+
302
+ /**
303
+ * Pick the existing same-day record whose window best overlaps `meeting` and
304
+ * reuse its id — but ONLY when adoption is safe: the windows substantially
305
+ * coincide (overlap covers a majority of the shorter window), OR a corroborating
306
+ * identity signal (same app, same title, or a shared transcript source) is
307
+ * BACKED by a non-marginal overlap (>= a quarter of the shorter window). Coarse
308
+ * identity signals alone never license adoption across a thin sliver of overlap,
309
+ * or two genuinely different back-to-back same-app meetings would overwrite each
310
+ * other's record and `show <old-id>` would point at the wrong meeting. Candidate
311
+ * selection is deterministic (max overlap, then earliest start, then id).
312
+ * Returns the detector's fresh id when nothing qualifies.
313
+ */
314
+ private adoptExistingId(
315
+ meeting: DetectedMeeting,
316
+ existing: readonly MeetingRecordSummary[],
317
+ claimed: Set<string>,
318
+ ): string {
319
+ const dStart = Date.parse(meeting.startUtc);
320
+ const dEnd = Date.parse(meeting.endUtc);
321
+ if (!Number.isFinite(dStart) || !Number.isFinite(dEnd)) return meeting.id;
322
+ let best: { record: MeetingRecordSummary; overlap: number; existingDurationMs: number } | undefined;
323
+ for (const record of existing) {
324
+ if (claimed.has(record.id)) continue;
325
+ const eStart = Date.parse(record.startUtc);
326
+ const eEnd = Date.parse(record.endUtc);
327
+ if (!Number.isFinite(eStart) || !Number.isFinite(eEnd)) continue;
328
+ const overlap = Math.min(dEnd, eEnd) - Math.max(dStart, eStart);
329
+ if (overlap <= 0) continue;
330
+ const better =
331
+ best === undefined ||
332
+ overlap > best.overlap ||
333
+ (overlap === best.overlap &&
334
+ (record.startUtc < best.record.startUtc ||
335
+ (record.startUtc === best.record.startUtc && record.id < best.record.id)));
336
+ if (better) best = { record, overlap, existingDurationMs: eEnd - eStart };
337
+ }
338
+ if (best === undefined) return meeting.id;
339
+ // Adoption bar (issue #1900): substantial overlap OR a corroborating identity
340
+ // signal that is BACKED by a non-marginal overlap — otherwise treat as
341
+ // removed + new. App name and transcript-source label are COARSE (many
342
+ // distinct meetings carry "Zoom"/"desktop"), so an identity match on its own
343
+ // must not license adoption across a thin sliver of overlap, or two genuinely
344
+ // different back-to-back same-app meetings would clobber each other's record.
345
+ const shorterWindowMs = Math.min(dEnd - dStart, best.existingDurationMs);
346
+ if (shorterWindowMs <= 0) return meeting.id;
347
+ // A shifted-start resync of the SAME meeting: overlap covers a majority of the
348
+ // shorter window. Adopts on its own.
349
+ const substantial = best.overlap * 2 >= shorterWindowMs;
350
+ const sharedSource = meeting.sources.some((source) => best!.record.sources.includes(source));
351
+ const sameApp =
352
+ meeting.app !== undefined &&
353
+ best.record.app !== undefined &&
354
+ meeting.app.toLowerCase() === best.record.app.toLowerCase();
355
+ const meetingTitle = meeting.title?.trim() ?? "";
356
+ const sameTitle = meetingTitle.length > 0 && meetingTitle === (best.record.title?.trim() ?? "");
357
+ const hasIdentitySignal = sharedSource || sameApp || sameTitle;
358
+ // Non-marginal overlap gate: an identity signal only corroborates when the
359
+ // windows overlap by at least a quarter of the shorter window.
360
+ const nonMarginalOverlap = best.overlap * 4 >= shorterWindowMs;
361
+ if (!substantial && !(hasIdentitySignal && nonMarginalOverlap)) return meeting.id;
362
+ claimed.add(best.record.id);
363
+ return best.record.id;
364
+ }
365
+ }
@@ -0,0 +1,276 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { MeetingsBuilder, type MeetingDayData, type MeetingsDaySource } from "./build.js";
5
+ import { runMeetingsCliCommand, type MeetingsCliDeps } from "./cli.js";
6
+ import { DEFAULT_MEETINGS_CONFIG } from "./config.js";
7
+ import { MeetingRecordStore, type MeetingRecordFileIo } from "./store.js";
8
+ import type { MeetingsConfig } from "./types.js";
9
+
10
+ const MEMORY_DIR = "/mem";
11
+ const DATE = "2026-03-10";
12
+ const START = "2026-03-10T14:00:00.000Z";
13
+ const END = "2026-03-10T15:00:00.000Z";
14
+
15
+ class InMemoryIo implements MeetingRecordFileIo {
16
+ files = new Map<string, string>();
17
+ async writeFile(p: string, c: string): Promise<void> {
18
+ this.files.set(p, c);
19
+ }
20
+ async readFile(p: string): Promise<string> {
21
+ const v = this.files.get(p);
22
+ if (v === undefined) throw enoent();
23
+ return v;
24
+ }
25
+ async readDir(dirPath: string): Promise<string[]> {
26
+ const prefix = dirPath.endsWith("/") ? dirPath : `${dirPath}/`;
27
+ const names = new Set<string>();
28
+ let found = false;
29
+ for (const key of this.files.keys()) {
30
+ if (!key.startsWith(prefix)) continue;
31
+ found = true;
32
+ names.add(key.slice(prefix.length).split("/")[0]!);
33
+ }
34
+ if (!found) throw enoent();
35
+ return [...names];
36
+ }
37
+ async deleteFile(p: string): Promise<void> {
38
+ if (!this.files.delete(p)) throw enoent();
39
+ }
40
+ async realpath(p: string): Promise<string> {
41
+ return p;
42
+ }
43
+ async lstat(): Promise<{ isSymbolicLink: boolean }> {
44
+ return { isSymbolicLink: false };
45
+ }
46
+ }
47
+
48
+ function enoent(): NodeJS.ErrnoException {
49
+ const err = new Error("ENOENT") as NodeJS.ErrnoException;
50
+ err.code = "ENOENT";
51
+ return err;
52
+ }
53
+
54
+ function config(overrides: Partial<MeetingsConfig> = {}): MeetingsConfig {
55
+ return { ...DEFAULT_MEETINGS_CONFIG, appPatterns: [...DEFAULT_MEETINGS_CONFIG.appPatterns], enabled: true, ...overrides };
56
+ }
57
+
58
+ function dayData(): MeetingDayData {
59
+ return {
60
+ detection: { date: DATE, appSpans: [{ app: "Zoom", startUtc: START, endUtc: END }], audioWindows: [{ source: "desktop", startUtc: START, endUtc: END, distinctNonWearerSpeakers: 2 }] },
61
+ conversations: [
62
+ { source: "desktop", conversationId: "d1", startIso: START, endIso: END, segments: [{ speaker: "Jane", isSelf: false, text: "hello everyone", startIso: "2026-03-10T14:05:00.000Z" }] },
63
+ ],
64
+ };
65
+ }
66
+
67
+ function makeDeps(data: MeetingDayData = dayData(), cfg: MeetingsConfig = config()): MeetingsCliDeps {
68
+ const source: MeetingsDaySource = { loadDayData: () => data };
69
+ const store = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
70
+ return { store, builder: new MeetingsBuilder({ source, store, config: cfg }), config: cfg };
71
+ }
72
+
73
+ interface Captured {
74
+ out: string;
75
+ err: string;
76
+ code: number;
77
+ }
78
+
79
+ async function run(deps: MeetingsCliDeps, args: string[]): Promise<Captured> {
80
+ let out = "";
81
+ let err = "";
82
+ const code = await runMeetingsCliCommand(deps, args, {
83
+ stdout: { write: (c: string) => (out += c) },
84
+ stderr: { write: (c: string) => (err += c) },
85
+ });
86
+ return { out, err, code };
87
+ }
88
+
89
+ test("build --date requires a valid date", async () => {
90
+ const deps = makeDeps();
91
+ const missing = await run(deps, ["build"]);
92
+ assert.equal(missing.code, 1);
93
+ assert.match(missing.err, /--date <YYYY-MM-DD> is required/);
94
+
95
+ const bad = await run(deps, ["build", "--date", "2026-13-40"]);
96
+ assert.equal(bad.code, 1);
97
+ assert.match(bad.err, /real YYYY-MM-DD/);
98
+ });
99
+
100
+ test("list --date rejects a malformed date and a missing flag value", async () => {
101
+ const deps = makeDeps();
102
+ const bad = await run(deps, ["list", "--date", "nope"]);
103
+ assert.equal(bad.code, 1);
104
+ assert.match(bad.err, /real YYYY-MM-DD/);
105
+
106
+ const noValue = await run(deps, ["list", "--date"]);
107
+ assert.equal(noValue.code, 1);
108
+ assert.match(noValue.err, /requires a value/);
109
+ });
110
+
111
+ test("unknown flags and commands error with guidance", async () => {
112
+ const deps = makeDeps();
113
+ const flag = await run(deps, ["list", "--bogus"]);
114
+ assert.equal(flag.code, 1);
115
+ assert.match(flag.err, /unknown flag --bogus/);
116
+
117
+ const cmd = await run(deps, ["frobnicate"]);
118
+ assert.equal(cmd.code, 1);
119
+ assert.match(cmd.err, /unknown meetings command 'frobnicate'/);
120
+ });
121
+
122
+ test("show validates the id and reports a missing record", async () => {
123
+ const deps = makeDeps();
124
+ const noId = await run(deps, ["show"]);
125
+ assert.equal(noId.code, 1);
126
+ assert.match(noId.err, /requires a meeting id/);
127
+
128
+ const badId = await run(deps, ["show", "not-an-id"]);
129
+ assert.equal(badId.code, 1);
130
+ assert.match(badId.err, /invalid meeting id/);
131
+
132
+ const missing = await run(deps, ["show", "mtg-2026-03-10-abcdef01"]);
133
+ assert.equal(missing.code, 1);
134
+ assert.match(missing.err, /not found/);
135
+ });
136
+
137
+ test("build then list then show round-trips a real record", async () => {
138
+ const deps = makeDeps();
139
+ const built = await run(deps, ["build", "--date", DATE]);
140
+ assert.equal(built.code, 0);
141
+ assert.match(built.out, /1 meeting \(1 written/);
142
+
143
+ const ids = await deps.store.listMeetingIds(DATE);
144
+ assert.equal(ids.length, 1);
145
+ const id = ids[0]!;
146
+
147
+ const listed = await run(deps, ["list", "--date", DATE]);
148
+ assert.equal(listed.code, 0);
149
+ assert.match(listed.out, new RegExp(id));
150
+ assert.match(listed.out, /sources=desktop/);
151
+
152
+ const shown = await run(deps, ["show", id]);
153
+ assert.equal(shown.code, 0);
154
+ assert.match(shown.out, /kind: meeting/);
155
+ assert.match(shown.out, /\*\*Jane\*\* \[14:05\]: hello everyone/);
156
+ });
157
+
158
+ test("list --json emits machine-readable output", async () => {
159
+ const deps = makeDeps();
160
+ await run(deps, ["build", "--date", DATE]);
161
+ const json = await run(deps, ["list", "--date", DATE, "--json"]);
162
+ assert.equal(json.code, 0);
163
+ const parsed = JSON.parse(json.out) as Array<{ id: string; sources: string[] }>;
164
+ assert.equal(parsed.length, 1);
165
+ assert.deepEqual(parsed[0]?.sources, ["desktop"]);
166
+ });
167
+
168
+ test("build reports the disabled state when the subsystem is off", async () => {
169
+ const deps = makeDeps(dayData(), config({ enabled: false }));
170
+ const result = await run(deps, ["build", "--date", DATE]);
171
+ assert.equal(result.code, 0);
172
+ assert.match(result.out, /meetings disabled/);
173
+ });
174
+
175
+ test("show rejects a syntactically valid but impossible calendar id via MeetingsInputError", async () => {
176
+ const deps = makeDeps();
177
+ // mtg-2026-13-40-... matches the id SHAPE but 2026-13-40 is not a real date;
178
+ // this must be a clean input error (exit 1), never a 500 from the path validator.
179
+ const result = await run(deps, ["show", "mtg-2026-13-40-abcdef01"]);
180
+ assert.equal(result.code, 1);
181
+ assert.match(result.err, /not a real calendar date/);
182
+ });
183
+
184
+ test("build text output surfaces a SANITIZED reindex-hook warning (no raw error text)", async () => {
185
+ const source: MeetingsDaySource = { loadDayData: () => dayData() };
186
+ const store = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
187
+ const deps: MeetingsCliDeps = {
188
+ store,
189
+ builder: new MeetingsBuilder({
190
+ source,
191
+ store,
192
+ config: config(),
193
+ hooks: { reindex: () => { throw new Error("index offline at 10.0.0.5:9200"); } },
194
+ }),
195
+ config: config(),
196
+ };
197
+ const result = await run(deps, ["build", "--date", DATE]);
198
+ assert.equal(result.code, 0, "reindex failure must not fail the build");
199
+ assert.match(result.out, /warning: reindex hook failed after records were persisted/);
200
+ assert.doesNotMatch(result.out, /index offline|10\.0\.0\.5|9200/, "raw internal reindex error must not leak to CLI stdout");
201
+ });
202
+
203
+ test("findings 7+8 — list/show expose a record when enabled, nothing when meetings.enabled is false", async () => {
204
+ // Build a real record with the subsystem enabled, then prove EACH read surface
205
+ // surfaces that record when enabled (so the disabled negatives below are not
206
+ // vacuous), then flip config off and confirm the same surfaces expose nothing.
207
+ const store = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
208
+ const source: MeetingsDaySource = { loadDayData: () => dayData() };
209
+ const enabled: MeetingsCliDeps = { store, builder: new MeetingsBuilder({ source, store, config: config() }), config: config() };
210
+ await run(enabled, ["build", "--date", DATE]);
211
+ const id = (await store.listMeetingIds(DATE))[0]!;
212
+
213
+ // Positive baseline: enabled reads DO surface the record on every path.
214
+ const listAllEnabled = await run(enabled, ["list"]);
215
+ assert.match(listAllEnabled.out, new RegExp(id), "enabled list surfaces the stored record");
216
+ const listDayEnabled = await run(enabled, ["list", "--date", DATE]);
217
+ assert.match(listDayEnabled.out, new RegExp(id), "enabled date-scoped list surfaces the stored record");
218
+ const listJsonEnabled = await run(enabled, ["list", "--date", DATE, "--json"]);
219
+ assert.match(listJsonEnabled.out, new RegExp(id), "enabled date-scoped json list surfaces the stored record");
220
+ const shownEnabled = await run(enabled, ["show", id]);
221
+ assert.match(shownEnabled.out, /kind: meeting/, "enabled show prints the record");
222
+ assert.match(shownEnabled.out, new RegExp(id), "enabled show prints the requested record's id");
223
+
224
+ // Now the same surfaces with the subsystem OFF must expose nothing.
225
+ const disabled: MeetingsCliDeps = {
226
+ store,
227
+ builder: new MeetingsBuilder({ source, store, config: config({ enabled: false }) }),
228
+ config: config({ enabled: false }),
229
+ };
230
+ const listAll = await run(disabled, ["list"]);
231
+ assert.equal(listAll.code, 0);
232
+ assert.doesNotMatch(listAll.out, new RegExp(id), "disabled list must not expose a stored record");
233
+ assert.match(listAll.out, /meetings disabled/);
234
+
235
+ const listDay = await run(disabled, ["list", "--date", DATE]);
236
+ assert.equal(listDay.code, 0);
237
+ assert.doesNotMatch(listDay.out, new RegExp(id), "disabled date-scoped list must not expose the record it surfaces when enabled");
238
+
239
+ const listJson = await run(disabled, ["list", "--date", DATE, "--json"]);
240
+ assert.equal(listJson.out.trim(), "[]", "disabled json list exposes nothing");
241
+
242
+ const shown = await run(disabled, ["show", id]);
243
+ assert.equal(shown.code, 0);
244
+ assert.doesNotMatch(shown.out, /kind: meeting/, "disabled show must not print the record it prints when enabled");
245
+ assert.doesNotMatch(shown.out, new RegExp(id), "disabled show must not echo the record id");
246
+ assert.match(shown.out, /meetings disabled/);
247
+ });
248
+
249
+ test("build text output surfaces a SANITIZED memory-generation warning (no raw error text)", async () => {
250
+ const source: MeetingsDaySource = { loadDayData: () => dayData() };
251
+ const store = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
252
+ const deps: MeetingsCliDeps = {
253
+ store,
254
+ builder: new MeetingsBuilder({
255
+ source,
256
+ store,
257
+ config: config(),
258
+ // The injected generator rejects AFTER records persist; buildDay isolates
259
+ // it into summary.memoryWarning. The text renderer must surface that line.
260
+ memoryGenerator: { onRecordsBuilt: async () => { throw new Error("qdrant offline at 10.0.0.9:6333"); } },
261
+ }),
262
+ config: config(),
263
+ };
264
+ const result = await run(deps, ["build", "--date", DATE]);
265
+ assert.equal(result.code, 0, "memory-generation failure must not fail the build");
266
+ assert.match(result.out, /warning: memory generation failed after records were persisted/);
267
+ assert.doesNotMatch(result.out, /qdrant offline|10\.0\.0\.9|6333/, "raw internal memory-gen error must not leak to CLI stdout");
268
+ });
269
+
270
+ test("list --date rejects a malformed date even when meetings.enabled is false", async () => {
271
+ const deps = makeDeps(dayData(), config({ enabled: false }));
272
+ const bad = await run(deps, ["list", "--date", "nope", "--json"]);
273
+ assert.equal(bad.code, 1, "a malformed --date must reject regardless of enabled state");
274
+ assert.match(bad.err, /real YYYY-MM-DD/);
275
+ assert.doesNotMatch(bad.out, /\[\]/, "malformed --date must not short-circuit to an empty disabled result");
276
+ });