@remnic/core 9.3.700 → 9.3.702

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 (323) hide show
  1. package/dist/access-boundary.d.ts +7 -7
  2. package/dist/access-boundary.js +17 -14
  3. package/dist/access-cli.js +40 -37
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +7 -7
  6. package/dist/access-http.js +20 -17
  7. package/dist/access-mcp.d.ts +7 -7
  8. package/dist/access-mcp.js +19 -16
  9. package/dist/access-operations.d.ts +7 -7
  10. package/dist/access-operations.js +18 -15
  11. package/dist/{access-service-Cte3ol0W.d.ts → access-service-COCzhgEL.d.ts} +397 -19
  12. package/dist/access-service.d.ts +6 -6
  13. package/dist/access-service.js +16 -13
  14. package/dist/access-surface-catalog.d.ts +7 -7
  15. package/dist/action-confidence.d.ts +1 -1
  16. package/dist/active-memory-bridge.d.ts +1 -1
  17. package/dist/active-memory-bridge.js +2 -2
  18. package/dist/active-recall.d.ts +1 -1
  19. package/dist/active-recall.js +7 -6
  20. package/dist/active-recall.js.map +1 -1
  21. package/dist/behavior-learner.d.ts +1 -1
  22. package/dist/behavior-signals.d.ts +1 -1
  23. package/dist/bootstrap.d.ts +5 -5
  24. package/dist/briefing.d.ts +1 -1
  25. package/dist/briefing.js +8 -7
  26. package/dist/buffer-surprise-report.d.ts +1 -1
  27. package/dist/buffer.d.ts +1 -1
  28. package/dist/calibration.d.ts +1 -1
  29. package/dist/capabilities.d.ts +1 -1
  30. package/dist/{catalog-DN1PzThs.d.ts → catalog-DxCzhjE6.d.ts} +28 -54
  31. package/dist/causal-behavior.d.ts +1 -1
  32. package/dist/causal-consolidation.d.ts +1 -1
  33. package/dist/causal-consolidation.js +9 -8
  34. package/dist/causal-consolidation.js.map +1 -1
  35. package/dist/{chunk-XJNBEDFE.js → chunk-3FAMU5TX.js} +31 -74
  36. package/dist/chunk-3FAMU5TX.js.map +1 -0
  37. package/dist/{chunk-PCZR32VL.js → chunk-3JJWNZTT.js} +2 -2
  38. package/dist/{chunk-YXLT4EMM.js → chunk-4463LUIE.js} +12 -2
  39. package/dist/chunk-4463LUIE.js.map +1 -0
  40. package/dist/{chunk-R5DB26G6.js → chunk-4SBW47WK.js} +9 -23
  41. package/dist/chunk-4SBW47WK.js.map +1 -0
  42. package/dist/{chunk-CCOXIDRM.js → chunk-5VXVXJH6.js} +149 -6
  43. package/dist/{chunk-CCOXIDRM.js.map → chunk-5VXVXJH6.js.map} +1 -1
  44. package/dist/{chunk-PQG4T5V3.js → chunk-64XXOQRW.js} +49 -45
  45. package/dist/chunk-64XXOQRW.js.map +1 -0
  46. package/dist/{chunk-YXIFA36P.js → chunk-6TAETM63.js} +2 -2
  47. package/dist/{chunk-6JDGADXK.js → chunk-6VVP6NK7.js} +2 -2
  48. package/dist/{chunk-FN2SM5SN.js → chunk-ABCGDW3A.js} +75 -12
  49. package/dist/chunk-ABCGDW3A.js.map +1 -0
  50. package/dist/{chunk-CHM274U6.js → chunk-AMNLZ6SF.js} +2 -2
  51. package/dist/{chunk-JX3YZVII.js → chunk-CZJ6QSKG.js} +3 -3
  52. package/dist/{chunk-GYVVQYA3.js → chunk-EOOVJK2U.js} +3 -3
  53. package/dist/{chunk-JKW5XSWC.js → chunk-EYJD6KIO.js} +2 -2
  54. package/dist/{chunk-XY4WJTEX.js → chunk-EZR35XHX.js} +2 -2
  55. package/dist/{chunk-UU6MVCJ6.js → chunk-FIOYURII.js} +32 -25
  56. package/dist/chunk-FIOYURII.js.map +1 -0
  57. package/dist/{chunk-2NWHLAXX.js → chunk-FVI5B7DE.js} +2 -2
  58. package/dist/{chunk-X74FJSW7.js → chunk-GFIARMA7.js} +16 -8
  59. package/dist/chunk-GFIARMA7.js.map +1 -0
  60. package/dist/{chunk-PONNZ54D.js → chunk-GY3SKOS4.js} +4 -4
  61. package/dist/{chunk-U33LWTQQ.js → chunk-HV57RHMD.js} +4 -4
  62. package/dist/{chunk-RC3CNIPK.js → chunk-IO5NQEGZ.js} +2 -2
  63. package/dist/{chunk-IKNQAGBV.js → chunk-IX3UQT4H.js} +1 -1
  64. package/dist/{chunk-IKNQAGBV.js.map → chunk-IX3UQT4H.js.map} +1 -1
  65. package/dist/{chunk-HDLC75NX.js → chunk-IX72AAMZ.js} +2 -2
  66. package/dist/{chunk-DR2JTSLZ.js → chunk-JMA4RYRN.js} +62 -163
  67. package/dist/chunk-JMA4RYRN.js.map +1 -0
  68. package/dist/{chunk-G5PKTQ5J.js → chunk-JTKFZMZ7.js} +2 -2
  69. package/dist/{chunk-YMTGXDN6.js → chunk-KC6TCAWV.js} +5 -5
  70. package/dist/{chunk-ROZJACKP.js → chunk-KKK7YTYN.js} +4 -1
  71. package/dist/chunk-KKK7YTYN.js.map +1 -0
  72. package/dist/{chunk-EC2AYKRX.js → chunk-L6W77GWW.js} +10 -24
  73. package/dist/chunk-L6W77GWW.js.map +1 -0
  74. package/dist/{chunk-ED35D32I.js → chunk-LQ4J7ELC.js} +2 -2
  75. package/dist/{chunk-YPR7DOPD.js → chunk-LTJAMRGI.js} +4 -4
  76. package/dist/{chunk-YPR7DOPD.js.map → chunk-LTJAMRGI.js.map} +1 -1
  77. package/dist/chunk-LUPVCGYK.js +63 -0
  78. package/dist/chunk-LUPVCGYK.js.map +1 -0
  79. package/dist/{chunk-RJ2THZ4H.js → chunk-MBUM2Y3L.js} +2 -2
  80. package/dist/{chunk-T5QAZIBO.js → chunk-MOXFPLD6.js} +3 -3
  81. package/dist/{chunk-O54DY26V.js → chunk-MXEWQKM7.js} +2 -2
  82. package/dist/{chunk-33L6XHU2.js → chunk-NUIJEGVD.js} +6 -6
  83. package/dist/{chunk-3E5WRQNQ.js → chunk-OXEAMU42.js} +529 -9
  84. package/dist/chunk-OXEAMU42.js.map +1 -0
  85. package/dist/{chunk-EOBJRBLC.js → chunk-QGJAGC2J.js} +2 -2
  86. package/dist/{chunk-SMIVW7XC.js → chunk-QIMFOCSH.js} +2 -2
  87. package/dist/{chunk-IJEZMWKA.js → chunk-SFOAQQDJ.js} +3 -3
  88. package/dist/{chunk-HRUULBBV.js → chunk-SHRRWOVY.js} +81 -4
  89. package/dist/chunk-SHRRWOVY.js.map +1 -0
  90. package/dist/{chunk-NINRTFSV.js → chunk-SINGJCUR.js} +5 -5
  91. package/dist/{chunk-ZPQVJEVQ.js → chunk-SK2CR6MW.js} +146 -2
  92. package/dist/chunk-SK2CR6MW.js.map +1 -0
  93. package/dist/{chunk-SEWF2O74.js → chunk-TGAHHCB6.js} +2 -2
  94. package/dist/{chunk-D75JXBV4.js → chunk-WN4GHSDH.js} +2 -2
  95. package/dist/{chunk-K4DWSPMW.js → chunk-WRGPE6AW.js} +2 -2
  96. package/dist/{chunk-GA5A6MJH.js → chunk-XGMCUY5P.js} +62 -116
  97. package/dist/chunk-XGMCUY5P.js.map +1 -0
  98. package/dist/{chunk-SDPDU2PM.js → chunk-YTMDF6S7.js} +2 -2
  99. package/dist/{chunk-ZDK2IW5F.js → chunk-Z7XEIAV4.js} +2 -2
  100. package/dist/{chunk-NHBEO3F3.js → chunk-ZCEI242W.js} +24 -24
  101. package/dist/{cli--yVN9yEV.d.ts → cli-BM4xQPp4.d.ts} +3 -3
  102. package/dist/cli.d.ts +7 -7
  103. package/dist/cli.js +35 -34
  104. package/dist/compounding/engine.d.ts +1 -1
  105. package/dist/compounding/engine.js +8 -7
  106. package/dist/compounding/preference-consolidator.d.ts +1 -1
  107. package/dist/compression-optimizer.d.ts +1 -1
  108. package/dist/config.d.ts +1 -1
  109. package/dist/config.js +4 -2
  110. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  111. package/dist/connectors/codex-materialize-runner.js +8 -7
  112. package/dist/connectors/codex-materialize.d.ts +1 -1
  113. package/dist/connectors/index.d.ts +1 -1
  114. package/dist/connectors/index.js +9 -8
  115. package/dist/consolidation-provenance-check.d.ts +1 -1
  116. package/dist/consolidation-undo.d.ts +1 -1
  117. package/dist/contradiction/index.d.ts +2 -2
  118. package/dist/conversation-index/backend.d.ts +1 -1
  119. package/dist/conversation-index/chunker.d.ts +1 -1
  120. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  121. package/dist/conversation-index/indexer.d.ts +1 -1
  122. package/dist/conversation-index/search.d.ts +1 -1
  123. package/dist/day-summary.d.ts +1 -1
  124. package/dist/delinearize.d.ts +1 -1
  125. package/dist/direct-answer-wiring.d.ts +1 -1
  126. package/dist/direct-answer.d.ts +1 -1
  127. package/dist/embedding-fallback.d.ts +1 -1
  128. package/dist/enrichment/index.d.ts +1 -1
  129. package/dist/entity-retrieval.d.ts +1 -1
  130. package/dist/entity-retrieval.js +8 -7
  131. package/dist/entity-schema.d.ts +1 -1
  132. package/dist/event-order-recall.js +2 -1
  133. package/dist/explicit-capture.d.ts +5 -5
  134. package/dist/explicit-capture.js +2 -2
  135. package/dist/explicit-cue-recall.js +2 -1
  136. package/dist/extraction-faithfulness.d.ts +1 -1
  137. package/dist/extraction-judge-telemetry.d.ts +1 -1
  138. package/dist/extraction-judge-training.d.ts +1 -1
  139. package/dist/extraction-judge.d.ts +1 -1
  140. package/dist/extraction.d.ts +14 -1
  141. package/dist/extraction.js +5 -3
  142. package/dist/fallback-llm.d.ts +1 -1
  143. package/dist/{forget-BEXG5PQC.js → forget-6SOIPUMQ.js} +3 -3
  144. package/dist/identity-continuity.d.ts +1 -1
  145. package/dist/importance.d.ts +1 -1
  146. package/dist/index.d.ts +123 -123
  147. package/dist/index.js +57 -56
  148. package/dist/index.js.map +1 -1
  149. package/dist/intent.d.ts +1 -1
  150. package/dist/lcm/engine.d.ts +1 -1
  151. package/dist/lcm/index.d.ts +1 -1
  152. package/dist/lcm/tools.d.ts +1 -1
  153. package/dist/lifecycle.d.ts +1 -1
  154. package/dist/live-connectors-runner.d.ts +1 -1
  155. package/dist/local-llm.d.ts +1 -1
  156. package/dist/maintenance/memory-governance.d.ts +1 -1
  157. package/dist/maintenance/memory-governance.js +8 -7
  158. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -7
  159. package/dist/maintenance/rebuild-memory-projection.js +9 -8
  160. package/dist/mcp-memory-inspector-app.d.ts +7 -7
  161. package/dist/memory-action-policy.d.ts +1 -1
  162. package/dist/memory-cache.d.ts +1 -1
  163. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  164. package/dist/memory-projection-store.d.ts +1 -1
  165. package/dist/memory-provenance.d.ts +1 -1
  166. package/dist/memory-worth-outcomes.d.ts +1 -1
  167. package/dist/models-json.d.ts +1 -1
  168. package/dist/namespaces/migrate.d.ts +2 -2
  169. package/dist/namespaces/migrate.js +9 -8
  170. package/dist/namespaces/principal.d.ts +1 -1
  171. package/dist/namespaces/search.d.ts +1 -1
  172. package/dist/namespaces/storage.d.ts +14 -3
  173. package/dist/namespaces/storage.js +8 -7
  174. package/dist/native-knowledge.d.ts +1 -1
  175. package/dist/operator-toolkit.d.ts +1 -1
  176. package/dist/operator-toolkit.js +14 -13
  177. package/dist/orchestration/maintenance.d.ts +2 -2
  178. package/dist/orchestration/maintenance.js +10 -9
  179. package/dist/{orchestrator-CJI4xdqV.d.ts → orchestrator-C9CDWAm6.d.ts} +4 -4
  180. package/dist/orchestrator.d.ts +5 -5
  181. package/dist/orchestrator.js +33 -31
  182. package/dist/patterns-cli.d.ts +1 -1
  183. package/dist/policy-runtime.d.ts +1 -1
  184. package/dist/provenance.d.ts +36 -2
  185. package/dist/provenance.js +5 -1
  186. package/dist/qmd-recall-cache.d.ts +1 -1
  187. package/dist/qmd.d.ts +1 -1
  188. package/dist/recall-disclosure-escalation.d.ts +1 -1
  189. package/dist/recall-explain-renderer.d.ts +1 -1
  190. package/dist/recall-explain-renderer.js +3 -3
  191. package/dist/recall-pipeline-stages.d.ts +15 -0
  192. package/dist/recall-pipeline-stages.js +3 -56
  193. package/dist/recall-pipeline-stages.js.map +1 -1
  194. package/dist/recall-planner-llm.d.ts +1 -1
  195. package/dist/recall-state.d.ts +1 -1
  196. package/dist/recall-tag-filter.d.ts +1 -1
  197. package/dist/recall-xray-cli.d.ts +1 -1
  198. package/dist/recall-xray-cli.js +4 -4
  199. package/dist/recall-xray-renderer.d.ts +1 -1
  200. package/dist/recall-xray-renderer.js +3 -3
  201. package/dist/recall-xray.d.ts +1 -1
  202. package/dist/recall-xray.js +2 -2
  203. package/dist/resolve-auth-token.d.ts +1 -1
  204. package/dist/response-guidance-recall.js +2 -1
  205. package/dist/resume-bundles.js +7 -5
  206. package/dist/retrieval-agents.d.ts +1 -1
  207. package/dist/retrieval-tiers.d.ts +1 -1
  208. package/dist/routing/engine.d.ts +1 -1
  209. package/dist/routing/store.d.ts +1 -1
  210. package/dist/schemas.d.ts +19 -0
  211. package/dist/schemas.js +1 -1
  212. package/dist/search/embed-helper.d.ts +1 -1
  213. package/dist/search/factory.d.ts +1 -1
  214. package/dist/search/index.d.ts +1 -1
  215. package/dist/search/lancedb-backend.d.ts +1 -1
  216. package/dist/search/meilisearch-backend.d.ts +1 -1
  217. package/dist/search/noop-backend.d.ts +1 -1
  218. package/dist/search/orama-backend.d.ts +1 -1
  219. package/dist/search/port.d.ts +1 -1
  220. package/dist/search/remote-backend.d.ts +1 -1
  221. package/dist/{semantic-DJR8_DMQ.d.ts → semantic-SLAa_prH.d.ts} +1 -1
  222. package/dist/{semantic-consolidation-BtUfv-AL.d.ts → semantic-consolidation-DgFXyALl.d.ts} +1 -1
  223. package/dist/semantic-consolidation.d.ts +2 -2
  224. package/dist/semantic-consolidation.js +9 -8
  225. package/dist/semantic-rule-promotion.js +8 -7
  226. package/dist/semantic-rule-verifier.d.ts +1 -1
  227. package/dist/semantic-rule-verifier.js +8 -7
  228. package/dist/session-observer-bands.d.ts +1 -1
  229. package/dist/session-observer-state.d.ts +1 -1
  230. package/dist/shared-context/manager.d.ts +1 -1
  231. package/dist/signal.d.ts +1 -1
  232. package/dist/storage.d.ts +13 -1
  233. package/dist/storage.js +7 -6
  234. package/dist/summarizer.d.ts +1 -1
  235. package/dist/summarizer.js +3 -2
  236. package/dist/summary-snapshot.d.ts +1 -1
  237. package/dist/summary-snapshot.js +2 -1
  238. package/dist/targeted-fact-recall.js +2 -1
  239. package/dist/temporal-supersession.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 +1 -1
  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/transcript.js +2 -2
  247. package/dist/{types-PuiPZ9iE.d.ts → types-MWKPZnM0.d.ts} +25 -1
  248. package/dist/types.d.ts +1 -1
  249. package/dist/types.js +1 -1
  250. package/dist/utility-runtime.d.ts +1 -1
  251. package/dist/utils/serialize-mutations.js +1 -1
  252. package/dist/verified-recall.js +8 -7
  253. package/package.json +2 -2
  254. package/src/access-http.ts +162 -0
  255. package/src/access-service.ts +215 -0
  256. package/src/admin/admin-surfaces.test.ts +458 -0
  257. package/src/admin/admin-surfaces.ts +819 -0
  258. package/src/event-order-recall.test.ts +53 -0
  259. package/src/event-order-recall.ts +61 -37
  260. package/src/explicit-cue-recall.ts +21 -1
  261. package/src/extraction.ts +91 -6
  262. package/src/namespaces/catalog.test.ts +222 -14
  263. package/src/namespaces/catalog.ts +54 -187
  264. package/src/namespaces/storage.ts +87 -80
  265. package/src/orchestrator.ts +46 -5
  266. package/src/provenance-extraction.test.ts +838 -0
  267. package/src/provenance.ts +413 -0
  268. package/src/recall-pipeline-parity.test.ts +288 -0
  269. package/src/recall-pipeline-stages.test.ts +43 -0
  270. package/src/recall-pipeline-stages.ts +22 -0
  271. package/src/response-guidance-recall.ts +32 -36
  272. package/src/schemas.ts +7 -0
  273. package/src/storage.ts +23 -0
  274. package/src/summary-snapshot.test.ts +63 -1
  275. package/src/summary-snapshot.ts +61 -80
  276. package/src/targeted-fact-recall.ts +25 -32
  277. package/src/types.ts +24 -0
  278. package/src/utils/serialize-mutations.ts +10 -6
  279. package/dist/chunk-3E5WRQNQ.js.map +0 -1
  280. package/dist/chunk-DR2JTSLZ.js.map +0 -1
  281. package/dist/chunk-EC2AYKRX.js.map +0 -1
  282. package/dist/chunk-FN2SM5SN.js.map +0 -1
  283. package/dist/chunk-GA5A6MJH.js.map +0 -1
  284. package/dist/chunk-HRUULBBV.js.map +0 -1
  285. package/dist/chunk-PQG4T5V3.js.map +0 -1
  286. package/dist/chunk-R5DB26G6.js.map +0 -1
  287. package/dist/chunk-ROZJACKP.js.map +0 -1
  288. package/dist/chunk-UU6MVCJ6.js.map +0 -1
  289. package/dist/chunk-X74FJSW7.js.map +0 -1
  290. package/dist/chunk-XJNBEDFE.js.map +0 -1
  291. package/dist/chunk-YXLT4EMM.js.map +0 -1
  292. package/dist/chunk-ZPQVJEVQ.js.map +0 -1
  293. /package/dist/{chunk-PCZR32VL.js.map → chunk-3JJWNZTT.js.map} +0 -0
  294. /package/dist/{chunk-YXIFA36P.js.map → chunk-6TAETM63.js.map} +0 -0
  295. /package/dist/{chunk-6JDGADXK.js.map → chunk-6VVP6NK7.js.map} +0 -0
  296. /package/dist/{chunk-CHM274U6.js.map → chunk-AMNLZ6SF.js.map} +0 -0
  297. /package/dist/{chunk-JX3YZVII.js.map → chunk-CZJ6QSKG.js.map} +0 -0
  298. /package/dist/{chunk-GYVVQYA3.js.map → chunk-EOOVJK2U.js.map} +0 -0
  299. /package/dist/{chunk-JKW5XSWC.js.map → chunk-EYJD6KIO.js.map} +0 -0
  300. /package/dist/{chunk-XY4WJTEX.js.map → chunk-EZR35XHX.js.map} +0 -0
  301. /package/dist/{chunk-2NWHLAXX.js.map → chunk-FVI5B7DE.js.map} +0 -0
  302. /package/dist/{chunk-PONNZ54D.js.map → chunk-GY3SKOS4.js.map} +0 -0
  303. /package/dist/{chunk-U33LWTQQ.js.map → chunk-HV57RHMD.js.map} +0 -0
  304. /package/dist/{chunk-RC3CNIPK.js.map → chunk-IO5NQEGZ.js.map} +0 -0
  305. /package/dist/{chunk-HDLC75NX.js.map → chunk-IX72AAMZ.js.map} +0 -0
  306. /package/dist/{chunk-G5PKTQ5J.js.map → chunk-JTKFZMZ7.js.map} +0 -0
  307. /package/dist/{chunk-YMTGXDN6.js.map → chunk-KC6TCAWV.js.map} +0 -0
  308. /package/dist/{chunk-ED35D32I.js.map → chunk-LQ4J7ELC.js.map} +0 -0
  309. /package/dist/{chunk-RJ2THZ4H.js.map → chunk-MBUM2Y3L.js.map} +0 -0
  310. /package/dist/{chunk-T5QAZIBO.js.map → chunk-MOXFPLD6.js.map} +0 -0
  311. /package/dist/{chunk-O54DY26V.js.map → chunk-MXEWQKM7.js.map} +0 -0
  312. /package/dist/{chunk-33L6XHU2.js.map → chunk-NUIJEGVD.js.map} +0 -0
  313. /package/dist/{chunk-EOBJRBLC.js.map → chunk-QGJAGC2J.js.map} +0 -0
  314. /package/dist/{chunk-SMIVW7XC.js.map → chunk-QIMFOCSH.js.map} +0 -0
  315. /package/dist/{chunk-IJEZMWKA.js.map → chunk-SFOAQQDJ.js.map} +0 -0
  316. /package/dist/{chunk-NINRTFSV.js.map → chunk-SINGJCUR.js.map} +0 -0
  317. /package/dist/{chunk-SEWF2O74.js.map → chunk-TGAHHCB6.js.map} +0 -0
  318. /package/dist/{chunk-D75JXBV4.js.map → chunk-WN4GHSDH.js.map} +0 -0
  319. /package/dist/{chunk-K4DWSPMW.js.map → chunk-WRGPE6AW.js.map} +0 -0
  320. /package/dist/{chunk-SDPDU2PM.js.map → chunk-YTMDF6S7.js.map} +0 -0
  321. /package/dist/{chunk-ZDK2IW5F.js.map → chunk-Z7XEIAV4.js.map} +0 -0
  322. /package/dist/{chunk-NHBEO3F3.js.map → chunk-ZCEI242W.js.map} +0 -0
  323. /package/dist/{forget-BEXG5PQC.js.map → forget-6SOIPUMQ.js.map} +0 -0
@@ -0,0 +1,819 @@
1
+ /**
2
+ * Admin console surfaces (issue #1502).
3
+ *
4
+ * Pure delegation functions that read from the SAME core APIs the runtime
5
+ * uses — `resolveScopePlan` (issue #1521), `NamespaceCatalog` (#1499),
6
+ * `NamespaceSearchRouter` health, the session-transcript migration planner
7
+ * (#1496), and `StorageManager` writes. The admin HTTP layer and the static
8
+ * console shell call these; they NEVER re-resolve scope, re-derive promotion
9
+ * targets, or re-implement namespace listing. That is the #1492/#1494
10
+ * invariant: the dashboard is not a second source of truth for namespace
11
+ * authorization or scope resolution.
12
+ *
13
+ * Security contract (issue #1502 "Security and Privacy Requirements"):
14
+ * - every list/inspection surface redacts fields that may carry credentials
15
+ * (`redactSensitive` strips bearer-token-shaped strings);
16
+ * - promotion requires a non-empty reason and authorizes the target through
17
+ * `canWriteNamespace` (or the scope-profile promotion resolution) before
18
+ * any write;
19
+ * - transcript audit is dry-run only — the destructive apply path lives in
20
+ * the CLI migration command, not here.
21
+ *
22
+ * Chokepoints used: ScopePlan resolver (#1521), NamespaceCatalog (#1499),
23
+ * NamespaceSearchRouter health. No new scope-resolution or namespace-listing
24
+ * logic is introduced.
25
+ */
26
+ import path from "node:path";
27
+ import type { NamespaceCatalog, NamespaceRecord } from "../namespaces/catalog.js";
28
+ import { canReadNamespace, canWriteNamespace } from "../namespaces/principal.js";
29
+ import type { ResolvedScopeProfilePlan } from "../namespaces/scope-profiles.js";
30
+ import { type ScopePlan, resolveScopePlan } from "../scopes/scope-plan.js";
31
+ import { type SessionMigrationPlan, planSessionTranscriptMigration } from "../session-transcript-migration.js";
32
+ import type { CodingContext, MemoryCategory, MemoryFrontmatter, PluginConfig } from "../types.js";
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Shared redaction
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Marker substituted for any string value that looks like a credential.
40
+ * The dashboard must never echo back a bearer token, API key, or password
41
+ * that an operator may have pasted into a namespace alias / note field.
42
+ */
43
+ export const REDACTED = "<redacted>";
44
+
45
+ const SENSITIVE_KEY_RE = /(token|secret|password|api[_-]?key|bearer|authorization|credential|private[_-]?key)/i;
46
+ const BEARER_VALUE_RE = /^(bearer|sk-|pk-|xox[bp]-|AKIA)/i;
47
+ const LONG_OPAQUE_RE = /^[A-Za-z0-9_\-]{32,}$/;
48
+
49
+ /**
50
+ * Diagnostic keys whose name collides with the sensitive-key regex (e.g.
51
+ * `identityToken` contains "token") but are NOT credentials. `identityToken`
52
+ * is the deterministic namespace storage-path hash (`ns-<hex>`); operators
53
+ * need it for routing diagnostics and it is derived from the namespace name,
54
+ * never from a secret. Listed keys skip the sensitive-key redaction branch;
55
+ * the bearer-value and long-opaque checks below still apply.
56
+ */
57
+ const SAFE_DIAGNOSTIC_KEYS = new Set(["identityToken"]);
58
+
59
+ /**
60
+ * Walk a parsed JSON value and replace credential-shaped values in place.
61
+ * The check is conservative on purpose — values that LOOK like a long opaque
62
+ * secret OR sit under a sensitive key are redacted. Namespace identity tokens
63
+ * (16-hex `storagePathHash` outputs) are explicitly NOT redacted: operators
64
+ * need them for storage diagnostics and they are derived from the namespace
65
+ * name, not credentials.
66
+ */
67
+ export function redactSensitive<T>(value: T): T {
68
+ return walkRedact(value, undefined) as T;
69
+ }
70
+
71
+ function walkRedact(value: unknown, key: string | undefined): unknown {
72
+ if (typeof value === "string") {
73
+ if (
74
+ key &&
75
+ SENSITIVE_KEY_RE.test(key) &&
76
+ !SAFE_DIAGNOSTIC_KEYS.has(key)
77
+ ) {
78
+ return REDACTED;
79
+ }
80
+ if (BEARER_VALUE_RE.test(value)) return REDACTED;
81
+ if (!(key && SAFE_DIAGNOSTIC_KEYS.has(key)) && LONG_OPAQUE_RE.test(value) && value.length >= 40 && !/^[0-9a-f]{16}$/.test(value)) {
82
+ return REDACTED;
83
+ }
84
+ return value;
85
+ }
86
+ if (Array.isArray(value)) {
87
+ return value.map((entry) => walkRedact(entry, undefined));
88
+ }
89
+ // value is `object` here (Array branch handled above). Object.entries accepts
90
+ // a plain object; the explicit record type documents the iteration contract.
91
+ if (value && typeof value === "object") {
92
+ const record = value as Record<string, unknown>;
93
+ const out: Record<string, unknown> = {};
94
+ for (const [k, v] of Object.entries(record)) {
95
+ out[k] = walkRedact(v, k);
96
+ }
97
+ return out;
98
+ }
99
+ return value;
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Effective scope inspector
104
+ // ---------------------------------------------------------------------------
105
+
106
+ /**
107
+ * One layer's contribution to the effective scope, with a human-readable
108
+ * reason it was included or skipped. Mirrors the layer set surfaced by the
109
+ * scope-profile plan plus the coding overlay and explicit override.
110
+ */
111
+ export interface ScopeInspectionLayer {
112
+ /** Layer id — `userProject` / `teamProject` / `userGlobal` / `serverShared` / `coding` / `explicit` / `self`. */
113
+ id: string;
114
+ /** Resolved namespace for the layer, when applicable. */
115
+ namespace?: string;
116
+ /** Whether the layer is in the active read set. */
117
+ readable: boolean;
118
+ /** Whether the layer is the effective write target. */
119
+ writable: boolean;
120
+ /** Whether the layer is an authorized promotion target. */
121
+ promotable: boolean;
122
+ /** Human-readable explanation — used verbatim by the console "why" column. */
123
+ reason: string;
124
+ }
125
+
126
+ /** Promotion target resolved against policy + scope profile. */
127
+ export interface ScopeInspectionPromotionTarget {
128
+ target: string;
129
+ namespace?: string;
130
+ authorized: boolean;
131
+ reason: string;
132
+ }
133
+
134
+ /** Output of {@link inspectScope}. */
135
+ export interface ScopeInspection {
136
+ /** Resolved principal (or `undefined` in single-user mode). */
137
+ principal: string | undefined;
138
+ /** Explicit namespace override, when readable. */
139
+ namespaceOverride: string | undefined;
140
+ /** Effective write namespace. */
141
+ writeNamespace: string;
142
+ /** Read namespaces in the order recall searches them. */
143
+ readNamespaces: string[];
144
+ /** Coding overlay details (branch → project → root fallbacks). */
145
+ codingOverlay: { namespace: string; readFallbacks: readonly string[] } | null;
146
+ /** Active scope profile id, if any. */
147
+ scopeProfileId: string | null;
148
+ /** Per-layer explanations. */
149
+ layers: ScopeInspectionLayer[];
150
+ /** Promotion targets available for this session/principal. */
151
+ promotionTargets: ScopeInspectionPromotionTarget[];
152
+ /** Operator warnings (missing project, disabled scope, auth failures, …). */
153
+ warnings: string[];
154
+ /**
155
+ * Frozen copy of the {@link ScopePlan} the runtime resolver produced. The
156
+ * dashboard MUST display this verbatim — never a re-derived copy — so the
157
+ * "scope inspector returns the same plan as runtime resolver" acceptance
158
+ * test (issue #1502) holds by construction.
159
+ */
160
+ plan: ScopePlan;
161
+ }
162
+
163
+ /** Operation type the caller intends to inspect (informational only). */
164
+ export type ScopeInspectionOperation = "recall" | "observe" | "memory_store" | "maintenance" | "dashboard";
165
+
166
+ /** Inputs for {@link inspectScope}. */
167
+ export interface InspectScopeOptions {
168
+ readonly config: PluginConfig;
169
+ readonly sessionKey?: string;
170
+ readonly namespace?: string;
171
+ readonly principalOverride?: string;
172
+ readonly codingContext?: CodingContext | null;
173
+ readonly operation?: ScopeInspectionOperation;
174
+ /**
175
+ * Whether namespace routing is enabled. Callers that already resolved the
176
+ * flag (e.g. the access-service layer, threading it through the shared
177
+ * scope-plan chokepoint) pass it here so this pure helper does not need to
178
+ * re-read the namespace gate. Defaults to reading the config flag when
179
+ * omitted so direct callers (tests, CLI) keep working.
180
+ */
181
+ readonly namespacesEnabled?: boolean;
182
+ }
183
+
184
+ /**
185
+ * Resolve an effective scope inspection by delegating to the runtime
186
+ * {@link resolveScopePlan} resolver and decorating its result with the
187
+ * per-layer explanations the console renders. Pure — no side effects.
188
+ */
189
+ export function inspectScope(options: InspectScopeOptions): ScopeInspection {
190
+ const { config } = options;
191
+ // Honour the pre-read flag when the caller supplied one (avoids a scattered
192
+ // read here); fall back to the config flag for direct callers (tests/CLI).
193
+ const namespacesEnabled = options.namespacesEnabled ?? config.namespacesEnabled === true;
194
+
195
+ const plan = resolveScopePlan({
196
+ config,
197
+ sessionKey: options.sessionKey,
198
+ namespace: options.namespace,
199
+ principalOverride: options.principalOverride,
200
+ codingContext: options.codingContext ?? null,
201
+ namespacesEnabled,
202
+ });
203
+
204
+ const warnings: string[] = [];
205
+ if (namespacesEnabled && !plan.principal) {
206
+ warnings.push("namespace routing is enabled but no principal resolved for this session key");
207
+ }
208
+ if (config.codingMode?.projectScope && namespacesEnabled && options.codingContext && !plan.codingOverlay) {
209
+ warnings.push("coding project scope is enabled but the overlay was suppressed by an explicit namespace override");
210
+ }
211
+ if (options.namespace && options.namespace.trim().length > 0 && !plan.namespaceOverride) {
212
+ warnings.push(
213
+ `requested namespace override '${options.namespace}' is not readable by this principal and was ignored`
214
+ );
215
+ }
216
+
217
+ const layers: ScopeInspectionLayer[] = [];
218
+ const scopeProfilePlan = plan.scopeProfilePlan;
219
+ if (scopeProfilePlan) {
220
+ for (const layer of scopeProfilePlan.layers) {
221
+ layers.push({
222
+ id: layer.id,
223
+ namespace: layer.namespace,
224
+ readable: layer.readable,
225
+ writable: layer.writable,
226
+ promotable: layer.promotable,
227
+ reason: layer.reason,
228
+ });
229
+ }
230
+ for (const target of scopeProfilePlan.promotionTargets) {
231
+ if (!layers.some((layer) => layer.id === target.target)) {
232
+ layers.push({
233
+ id: target.target,
234
+ namespace: target.namespace,
235
+ readable: false,
236
+ writable: false,
237
+ promotable: target.authorized,
238
+ reason: target.reason,
239
+ });
240
+ }
241
+ }
242
+ } else {
243
+ // Non-profile scope: synthesize a self layer + optional coding + override.
244
+ layers.push({
245
+ id: "self",
246
+ namespace: plan.baseNamespace,
247
+ readable: canReadNamespace(plan.principal, plan.baseNamespace, config),
248
+ writable: canWriteNamespace(plan.principal, plan.baseNamespace, config),
249
+ promotable: false,
250
+ reason: "principal self namespace (no scope profile active)",
251
+ });
252
+ }
253
+ if (plan.codingOverlay) {
254
+ layers.push({
255
+ id: "coding",
256
+ namespace: plan.codingOverlay.namespace,
257
+ readable: true,
258
+ writable: true,
259
+ promotable: false,
260
+ reason: "coding overlay namespace combined with the principal base (rule 42)",
261
+ });
262
+ }
263
+ if (plan.namespaceOverride) {
264
+ layers.push({
265
+ id: "explicit",
266
+ namespace: plan.namespaceOverride,
267
+ readable: true,
268
+ writable: canWriteNamespace(plan.principal, plan.namespaceOverride, config),
269
+ promotable: false,
270
+ reason: "explicit namespace override authorized for this principal",
271
+ });
272
+ }
273
+
274
+ const promotionTargets: ScopeInspectionPromotionTarget[] = scopeProfilePlan
275
+ ? scopeProfilePlan.promotionTargets.map((target) => ({
276
+ target: target.target,
277
+ namespace: target.namespace,
278
+ authorized: target.authorized,
279
+ reason: target.reason,
280
+ }))
281
+ : [];
282
+
283
+ for (const warning of scopeProfilePlan?.warnings ?? []) {
284
+ warnings.push(warning);
285
+ }
286
+
287
+ return {
288
+ principal: plan.principal,
289
+ namespaceOverride: plan.namespaceOverride,
290
+ writeNamespace: plan.baseNamespace,
291
+ readNamespaces: [...plan.readNamespaces],
292
+ codingOverlay: plan.codingOverlay
293
+ ? {
294
+ namespace: plan.codingOverlay.namespace,
295
+ readFallbacks: plan.codingOverlay.readFallbacks,
296
+ }
297
+ : null,
298
+ scopeProfileId: scopeProfilePlan?.profileId ?? null,
299
+ layers,
300
+ promotionTargets,
301
+ warnings,
302
+ plan,
303
+ };
304
+ }
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // Namespace browser
308
+ // ---------------------------------------------------------------------------
309
+
310
+ /** Filters the namespace browser accepts (superset of {@link NamespaceCatalogFilter}). */
311
+ export interface AdminNamespaceFilter {
312
+ kind?: NamespaceRecord["kind"];
313
+ principal?: string;
314
+ projectId?: string;
315
+ discoveredBy?: NamespaceRecord["discoveredBy"];
316
+ /** Only namespaces with a write at/after this instant. */
317
+ writtenSince?: Date;
318
+ /** Only namespaces whose last write is older than this instant (stale). */
319
+ staleBefore?: Date;
320
+ }
321
+
322
+ /** One namespace row augmented with the operator-facing diagnostics the issue lists. */
323
+ export interface AdminNamespaceEntry {
324
+ namespace: string;
325
+ identityToken: string;
326
+ kind: NamespaceRecord["kind"];
327
+ principal?: string;
328
+ projectId?: string;
329
+ branch?: string;
330
+ parentNamespace?: string;
331
+ createdAt: string;
332
+ lastReadAt?: string;
333
+ lastWriteAt?: string;
334
+ lastMaintenanceAt?: Record<string, string>;
335
+ storageDir: string;
336
+ discoveredBy: NamespaceRecord["discoveredBy"];
337
+ /** True when last write is older than `staleThresholdDays` (or never written). */
338
+ stale: boolean;
339
+ }
340
+
341
+ /** Output of {@link listAdminNamespaces}. */
342
+ export interface AdminNamespaceList {
343
+ /** Whether the namespace catalog is enabled. `false` collapses the list. */
344
+ enabled: boolean;
345
+ entries: AdminNamespaceEntry[];
346
+ }
347
+
348
+ /** Inputs for {@link listAdminNamespaces}. */
349
+ export interface ListAdminNamespacesOptions {
350
+ readonly catalog: NamespaceCatalog;
351
+ readonly filter?: AdminNamespaceFilter;
352
+ /** A namespace whose last write is older than this many days is "stale". */
353
+ readonly staleThresholdDays?: number;
354
+ }
355
+
356
+ /**
357
+ * List configured + discovered namespaces from the catalog, applying the
358
+ * admin filters the console exposes. Delegates to {@link NamespaceCatalog}
359
+ * — never re-scans disk.
360
+ */
361
+ export async function listAdminNamespaces(options: ListAdminNamespacesOptions): Promise<AdminNamespaceList> {
362
+ const { catalog, filter } = options;
363
+ if (!catalog.enabled) {
364
+ return { enabled: false, entries: [] };
365
+ }
366
+ const staleMs = (options.staleThresholdDays ?? 30) * 24 * 60 * 60 * 1000;
367
+ const staleBefore = new Date(Date.now() - staleMs);
368
+
369
+ const records = await catalog.listNamespaces({
370
+ kind: filter?.kind,
371
+ discoveredBy: filter?.discoveredBy,
372
+ writtenSince: filter?.writtenSince,
373
+ });
374
+
375
+ let entries: AdminNamespaceEntry[] = records.map((record) => toAdminEntry(record, staleBefore));
376
+
377
+ if (filter?.principal) {
378
+ entries = entries.filter((entry) => entry.principal === filter.principal);
379
+ }
380
+ if (filter?.projectId) {
381
+ entries = entries.filter((entry) => entry.projectId === filter.projectId);
382
+ }
383
+ if (filter?.staleBefore) {
384
+ const cutoff = filter.staleBefore.getTime();
385
+ entries = entries.filter((entry) => {
386
+ if (!entry.lastWriteAt) return true;
387
+ const ms = Date.parse(entry.lastWriteAt);
388
+ return Number.isFinite(ms) && ms <= cutoff;
389
+ });
390
+ }
391
+
392
+ return { enabled: true, entries };
393
+ }
394
+
395
+ function toAdminEntry(record: NamespaceRecord, staleBefore: Date): AdminNamespaceEntry {
396
+ const lastWriteMs = record.lastWriteAt ? Date.parse(record.lastWriteAt) : Number.NaN;
397
+ const stale = !Number.isFinite(lastWriteMs) || lastWriteMs < staleBefore.getTime();
398
+ return {
399
+ namespace: record.namespace,
400
+ identityToken: record.identityToken,
401
+ kind: record.kind,
402
+ principal: record.principal,
403
+ projectId: record.projectId,
404
+ branch: record.branch,
405
+ parentNamespace: record.parentNamespace,
406
+ createdAt: record.createdAt,
407
+ lastReadAt: record.lastReadAt,
408
+ lastWriteAt: record.lastWriteAt,
409
+ lastMaintenanceAt: record.lastMaintenanceAt,
410
+ storageDir: record.storageDir,
411
+ discoveredBy: record.discoveredBy,
412
+ stale,
413
+ };
414
+ }
415
+
416
+ // ---------------------------------------------------------------------------
417
+ // Maintenance and QMD health
418
+ // ---------------------------------------------------------------------------
419
+
420
+ /** QMD/search health snapshot for one namespace (delegated to NamespaceSearchRouter). */
421
+ export interface AdminNamespaceQmdHealth {
422
+ namespace: string;
423
+ collection: string;
424
+ available: boolean;
425
+ collectionState: string;
426
+ debugStatus: string;
427
+ installedVersion: string | null;
428
+ supportedVersion: string | null;
429
+ supported: boolean | null;
430
+ upgradeAvailable: boolean | null;
431
+ daemonMode: boolean | null;
432
+ }
433
+
434
+ /** Maintenance + QMD health for one namespace. */
435
+ export interface AdminNamespaceHealth {
436
+ namespace: string;
437
+ kind?: NamespaceRecord["kind"];
438
+ lastMaintenanceAt?: Record<string, string>;
439
+ /** True when no maintenance has ever been recorded. */
440
+ maintenanceMissing: boolean;
441
+ /** True when QMD is unavailable OR its collection is missing for this namespace. */
442
+ qmdDegraded: boolean;
443
+ qmd?: AdminNamespaceQmdHealth;
444
+ /** Reason the QMD probe failed, when it did. */
445
+ qmdError?: string;
446
+ }
447
+
448
+ /** Output of {@link gatherMaintenanceHealth}. */
449
+ export interface MaintenanceHealthReport {
450
+ /** Whether the catalog is enabled. */
451
+ enabled: boolean;
452
+ /** Aggregate degraded-mode flag — true when ANY namespace is QMD-degraded. */
453
+ degradedMode: boolean;
454
+ perNamespace: AdminNamespaceHealth[];
455
+ }
456
+
457
+ /**
458
+ * Injected per-namespace QMD health probe. Implementations call
459
+ * `orchestrator.searchHealthForNamespace(namespace)` — the admin module
460
+ * stays free of orchestrator/state coupling.
461
+ */
462
+ export type NamespaceQmdHealthProvider = (namespace: string) => Promise<AdminNamespaceQmdHealth | null>;
463
+
464
+ /** Inputs for {@link gatherMaintenanceHealth}. */
465
+ export interface MaintenanceHealthOptions {
466
+ readonly catalog: NamespaceCatalog;
467
+ /** When omitted, QMD columns stay empty (catalog-only report). */
468
+ readonly qmdHealthProvider?: NamespaceQmdHealthProvider;
469
+ }
470
+
471
+ /**
472
+ * Build a per-namespace maintenance + QMD health report. Reads maintenance
473
+ * timestamps from the catalog and (optionally) QMD diagnostics from the
474
+ * injected provider.
475
+ */
476
+ export async function gatherMaintenanceHealth(options: MaintenanceHealthOptions): Promise<MaintenanceHealthReport> {
477
+ const { catalog, qmdHealthProvider } = options;
478
+ if (!catalog.enabled) {
479
+ return { enabled: false, degradedMode: false, perNamespace: [] };
480
+ }
481
+ const records = await catalog.listNamespaces();
482
+ let degradedMode = false;
483
+ const perNamespace: AdminNamespaceHealth[] = await Promise.all(
484
+ records.map(async (record): Promise<AdminNamespaceHealth> => {
485
+ const entry: AdminNamespaceHealth = {
486
+ namespace: record.namespace,
487
+ kind: record.kind,
488
+ lastMaintenanceAt: record.lastMaintenanceAt,
489
+ maintenanceMissing: !record.lastMaintenanceAt || Object.keys(record.lastMaintenanceAt).length === 0,
490
+ qmdDegraded: false,
491
+ };
492
+ if (qmdHealthProvider) {
493
+ try {
494
+ const qmd = await qmdHealthProvider(record.namespace);
495
+ if (qmd) {
496
+ entry.qmd = qmd;
497
+ entry.qmdDegraded =
498
+ !qmd.available ||
499
+ qmd.collectionState === "missing" ||
500
+ qmd.collectionState === "unknown";
501
+ }
502
+ } catch (err) {
503
+ entry.qmdDegraded = true;
504
+ // Sanitize: never echo raw error messages to the dashboard. The
505
+ // generic diagnostic is sufficient for operators; the full error
506
+ // is logged by the qmdHealthProvider's caller (access-service).
507
+ entry.qmdError = "QMD health probe failed";
508
+ }
509
+ }
510
+ if (entry.qmdDegraded) degradedMode = true;
511
+ return entry;
512
+ })
513
+ );
514
+ return { enabled: true, degradedMode, perNamespace };
515
+ }
516
+
517
+ // ---------------------------------------------------------------------------
518
+ // Transcript / session audit
519
+ // ---------------------------------------------------------------------------
520
+
521
+ /** One file the dry-run migration planner would re-home. */
522
+ export interface TranscriptAuditFile {
523
+ sourceRelPath: string;
524
+ fileName: string;
525
+ distinctSessions: number;
526
+ movedEntries: number;
527
+ unmovableLines: number;
528
+ }
529
+
530
+ /** Output of {@link auditTranscripts}. */
531
+ export interface TranscriptAuditReport {
532
+ /** Generated timestamp (ISO). */
533
+ generatedAt: string;
534
+ /** Always true — the admin surface is dry-run only. */
535
+ dryRun: true;
536
+ /** Absolute transcripts directory scanned. */
537
+ transcriptsDir: string;
538
+ /** True when any legacy fallback file (e.g. `other/default`) holds mixed sessions. */
539
+ mixedOtherDefault: boolean;
540
+ /** Distinct sessions the planner would re-home. */
541
+ distinctSessions: number;
542
+ /** Total JSONL entries that would move. */
543
+ movedEntries: number;
544
+ /** Per-file plans with at least one entry to move. */
545
+ files: TranscriptAuditFile[];
546
+ /** Operator-facing summary of the risk. */
547
+ summary: string;
548
+ }
549
+
550
+ /**
551
+ * Run a dry-run transcript/session audit. Delegates to
552
+ * {@link planSessionTranscriptMigration} with `apply: false` — the admin
553
+ * surface never applies a destructive migration. The CLI owns the apply
554
+ * path with its confirmation flow.
555
+ */
556
+ export async function auditTranscripts(memoryDir: string): Promise<TranscriptAuditReport> {
557
+ const plan: SessionMigrationPlan = await planSessionTranscriptMigration({
558
+ memoryDir,
559
+ apply: false,
560
+ });
561
+ const files: TranscriptAuditFile[] = plan.files.map((file) => ({
562
+ sourceRelPath: file.sourceRelPath,
563
+ fileName: file.fileName,
564
+ distinctSessions: file.groups.length,
565
+ movedEntries: file.groups.reduce((sum, group) => sum + group.entryCount, 0),
566
+ unmovableLines: file.unmovableLines,
567
+ }));
568
+ const mixedOtherDefault = files.some((file) => file.sourceRelPath.split(path.sep).includes("other"));
569
+ const summary =
570
+ plan.movedEntries === 0
571
+ ? "No mixed-session transcript data detected; nothing to migrate."
572
+ : `Found ${plan.movedEntries} entries across ${plan.distinctSessions} distinct sessions stranded in shared/legacy directories. Run the CLI migration command to re-home them.`;
573
+ return {
574
+ generatedAt: plan.generatedAt,
575
+ dryRun: true,
576
+ transcriptsDir: plan.transcriptsDir,
577
+ mixedOtherDefault,
578
+ distinctSessions: plan.distinctSessions,
579
+ movedEntries: plan.movedEntries,
580
+ files,
581
+ summary,
582
+ };
583
+ }
584
+
585
+ // ---------------------------------------------------------------------------
586
+ // Memory promotion (manual, reason-required, policy-enforced)
587
+ // ---------------------------------------------------------------------------
588
+
589
+ /** Promotion target kind the console can request. */
590
+ export type MemoryPromotionTargetKind = "teamProject" | "serverShared" | "userProject" | "userGlobal" | "explicit";
591
+
592
+ /** Result of one promotion attempt. */
593
+ export interface MemoryPromotionTargetResult {
594
+ target: MemoryPromotionTargetKind;
595
+ namespace: string;
596
+ authorized: boolean;
597
+ promoted: boolean;
598
+ promotedMemoryId?: string;
599
+ reason: string;
600
+ }
601
+
602
+ /** Output of {@link promoteMemory}. */
603
+ export interface MemoryPromotionResult {
604
+ /** Whether the caller-supplied reason was accepted. */
605
+ ok: boolean;
606
+ /** Source memory id. */
607
+ sourceMemoryId: string;
608
+ /** Source namespace the memory was read from. */
609
+ sourceNamespace: string;
610
+ /** Per-target outcomes. Empty when authorization fails up front. */
611
+ targets: MemoryPromotionTargetResult[];
612
+ /** Audit record — caller appends to the access audit log. */
613
+ audit: MemoryPromotionAudit;
614
+ }
615
+
616
+ /** Audit trail for a promotion operation. */
617
+ export interface MemoryPromotionAudit {
618
+ at: string;
619
+ actor: string;
620
+ sourceMemoryId: string;
621
+ sourceNamespace: string;
622
+ reason: string;
623
+ targets: Array<{ target: MemoryPromotionTargetKind; namespace: string; promoted: boolean }>;
624
+ }
625
+
626
+ /** Error thrown when a required promotion field is missing or unauthorized. */
627
+ export class AdminPromotionError extends Error {
628
+ readonly code: string;
629
+ constructor(code: string, message: string) {
630
+ super(message);
631
+ this.name = "AdminPromotionError";
632
+ this.code = code;
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Storage accessor injected by the service layer. Keeps the admin module
638
+ * decoupled from `StorageManager` and the orchestrator's storage router.
639
+ */
640
+ export interface PromotionStorageProvider {
641
+ /** Read a memory from the source namespace. Returns null when missing. */
642
+ readMemory(
643
+ namespace: string,
644
+ memoryId: string
645
+ ): Promise<{
646
+ category: MemoryCategory;
647
+ content: string;
648
+ frontmatter: MemoryFrontmatter;
649
+ } | null>;
650
+ /** Write a promoted memory into the target namespace. Returns the new id. */
651
+ writePromotedMemory(
652
+ namespace: string,
653
+ memory: {
654
+ category: MemoryCategory;
655
+ content: string;
656
+ confidence: number;
657
+ tags: string[];
658
+ entityRef?: string;
659
+ sourceMemoryId: string;
660
+ sourceNamespace: string;
661
+ reason: string;
662
+ actor: string;
663
+ validAt?: string;
664
+ /**
665
+ * Lineage persisted onto the promoted memory's frontmatter. Always
666
+ * `[sourceMemoryId]` so a downstream reader can trace a promotion
667
+ * back to its origin (matches the runtime extraction pipeline).
668
+ */
669
+ lineage: string[];
670
+ }
671
+ ): Promise<string>;
672
+ }
673
+
674
+ /** Inputs for {@link promoteMemory}. */
675
+ export interface PromoteMemoryOptions {
676
+ readonly config: PluginConfig;
677
+ readonly sourceMemoryId: string;
678
+ /** Source namespace (resolved through the readable resolver BEFORE this call). */
679
+ readonly sourceNamespace: string;
680
+ /** Authenticated principal driving the promotion. */
681
+ readonly principal?: string;
682
+ /** Requested targets. */
683
+ readonly targets: ReadonlyArray<{
684
+ kind: MemoryPromotionTargetKind;
685
+ /** Required when kind === "explicit". Must be writable by the principal. */
686
+ namespace?: string;
687
+ }>;
688
+ /** Non-empty operator reason (audit-logged). */
689
+ readonly reason: string;
690
+ /** Operator identity for the audit trail. */
691
+ readonly actor: string;
692
+ readonly storage: PromotionStorageProvider;
693
+ /** Optional scope-profile plan, used to resolve teamProject/userProject/serverShared targets. */
694
+ readonly scopeProfilePlan?: ResolvedScopeProfilePlan | null;
695
+ }
696
+
697
+ /**
698
+ * Manually promote a memory into one or more authorized targets. Reuses the
699
+ * same `canWriteNamespace` gate and scope-profile promotion resolution that
700
+ * the runtime extraction pipeline uses — there is no dashboard-only write
701
+ * path. Requires a non-empty reason; throws {@link AdminPromotionError}
702
+ * (`reason_required`) otherwise.
703
+ */
704
+ export async function promoteMemory(options: PromoteMemoryOptions): Promise<MemoryPromotionResult> {
705
+ const reason = options.reason.trim();
706
+ if (reason.length === 0) {
707
+ throw new AdminPromotionError("reason_required", "promotion requires a non-empty reason");
708
+ }
709
+ if (options.targets.length === 0) {
710
+ throw new AdminPromotionError("targets_required", "promotion requires at least one target");
711
+ }
712
+ const { config, principal, scopeProfilePlan } = options;
713
+
714
+ // Resolve each requested target to a concrete namespace + authorization flag.
715
+ const resolvedTargets: MemoryPromotionTargetResult[] = options.targets.map((requested) => {
716
+ if (requested.kind === "explicit") {
717
+ const namespace = requested.namespace?.trim();
718
+ if (!namespace) {
719
+ return {
720
+ target: requested.kind,
721
+ namespace: "",
722
+ authorized: false,
723
+ promoted: false,
724
+ reason: "explicit promotion requires a namespace",
725
+ };
726
+ }
727
+ const authorized = canWriteNamespace(principal, namespace, config);
728
+ return {
729
+ target: requested.kind,
730
+ namespace,
731
+ authorized,
732
+ promoted: false,
733
+ reason: authorized
734
+ ? "explicit namespace writable by this principal"
735
+ : "explicit namespace is not writable by this principal",
736
+ };
737
+ }
738
+ const layer = scopeProfilePlan?.promotionTargets.find((t) => t.target === requested.kind);
739
+ if (!layer) {
740
+ return {
741
+ target: requested.kind,
742
+ namespace: "",
743
+ authorized: false,
744
+ promoted: false,
745
+ reason: `promotion target '${requested.kind}' is not configured on the active scope profile`,
746
+ };
747
+ }
748
+ return {
749
+ target: requested.kind,
750
+ namespace: layer.namespace ?? "",
751
+ authorized: layer.authorized && Boolean(layer.namespace),
752
+ promoted: false,
753
+ reason: layer.reason,
754
+ };
755
+ });
756
+
757
+ const sourceMemory = await options.storage.readMemory(options.sourceNamespace, options.sourceMemoryId);
758
+ if (!sourceMemory) {
759
+ throw new AdminPromotionError(
760
+ "source_not_found",
761
+ `source memory ${options.sourceMemoryId} not found in namespace ${options.sourceNamespace}`
762
+ );
763
+ }
764
+
765
+ const auditTargets: MemoryPromotionAudit["targets"] = [];
766
+ for (const target of resolvedTargets) {
767
+ if (!target.authorized || !target.namespace) {
768
+ auditTargets.push({ target: target.target, namespace: target.namespace, promoted: false });
769
+ continue;
770
+ }
771
+ if (target.namespace === options.sourceNamespace) {
772
+ target.reason = "target namespace equals source namespace; nothing to promote";
773
+ auditTargets.push({ target: target.target, namespace: target.namespace, promoted: false });
774
+ continue;
775
+ }
776
+ try {
777
+ const promotedId = await options.storage.writePromotedMemory(target.namespace, {
778
+ category: sourceMemory.category,
779
+ content: sourceMemory.content,
780
+ confidence: sourceMemory.frontmatter.confidence ?? 0.5,
781
+ tags: [...(sourceMemory.frontmatter.tags ?? []), `admin-promotion-${target.target}`],
782
+ entityRef: sourceMemory.frontmatter.entityRef,
783
+ sourceMemoryId: options.sourceMemoryId,
784
+ sourceNamespace: options.sourceNamespace,
785
+ reason,
786
+ actor: options.actor,
787
+ validAt: sourceMemory.frontmatter.valid_at,
788
+ lineage: [options.sourceMemoryId],
789
+ });
790
+ target.promoted = true;
791
+ target.promotedMemoryId = promotedId;
792
+ auditTargets.push({ target: target.target, namespace: target.namespace, promoted: true });
793
+ } catch (err) {
794
+ // Sanitize: never echo raw error messages in the promotion result.
795
+ // The operator sees a generic failure indicator; the full error is
796
+ // logged server-side by the storage provider.
797
+ target.reason = "promotion write failed";
798
+ auditTargets.push({ target: target.target, namespace: target.namespace, promoted: false });
799
+ }
800
+ }
801
+
802
+ const at = new Date().toISOString();
803
+ const audit: MemoryPromotionAudit = {
804
+ at,
805
+ actor: options.actor,
806
+ sourceMemoryId: options.sourceMemoryId,
807
+ sourceNamespace: options.sourceNamespace,
808
+ reason,
809
+ targets: auditTargets,
810
+ };
811
+
812
+ return {
813
+ ok: resolvedTargets.some((t) => t.promoted),
814
+ sourceMemoryId: options.sourceMemoryId,
815
+ sourceNamespace: options.sourceNamespace,
816
+ targets: resolvedTargets,
817
+ audit,
818
+ };
819
+ }