@remnic/core 9.3.751 → 9.3.753

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 (300) hide show
  1. package/dist/access-admin-ops-surface.d.ts +194 -0
  2. package/dist/access-admin-ops-surface.js +128 -0
  3. package/dist/access-boundary.d.ts +5 -5
  4. package/dist/access-boundary.js +30 -30
  5. package/dist/access-cli.js +92 -92
  6. package/dist/access-http.d.ts +5 -5
  7. package/dist/access-http.js +38 -38
  8. package/dist/access-mcp.d.ts +5 -5
  9. package/dist/access-mcp.js +35 -35
  10. package/dist/access-operations-batch.js +32 -32
  11. package/dist/access-operations.d.ts +11 -11
  12. package/dist/access-operations.js +34 -34
  13. package/dist/access-recall-surface.d.ts +363 -0
  14. package/dist/access-recall-surface.js +128 -0
  15. package/dist/access-schema.d.ts +116 -116
  16. package/dist/access-schema.js +8 -8
  17. package/dist/{access-service-DmAEOJdJ.d.ts → access-service-MitduesD.d.ts} +75 -118
  18. package/dist/access-service.d.ts +5 -5
  19. package/dist/access-service.js +33 -29
  20. package/dist/access-surface-catalog.d.ts +5 -5
  21. package/dist/action-confidence.d.ts +1 -1
  22. package/dist/active-memory-bridge.d.ts +1 -1
  23. package/dist/active-recall.d.ts +1 -1
  24. package/dist/active-recall.js +6 -6
  25. package/dist/adapters/index.js +4 -4
  26. package/dist/adapters/registry.js +2 -2
  27. package/dist/behavior-learner.d.ts +1 -1
  28. package/dist/behavior-signals.d.ts +1 -1
  29. package/dist/bootstrap.d.ts +4 -4
  30. package/dist/briefing.d.ts +3 -3
  31. package/dist/briefing.js +10 -10
  32. package/dist/buffer-surprise-report.d.ts +1 -1
  33. package/dist/buffer.d.ts +3 -3
  34. package/dist/calibration.d.ts +1 -1
  35. package/dist/capabilities.d.ts +1 -1
  36. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-CZJSLEFG.js} +3 -3
  37. package/dist/capsule-crypto-CZJSLEFG.js.map +1 -0
  38. package/dist/{catalog-BpOcwHdE.d.ts → catalog-BEmNVRN0.d.ts} +1 -1
  39. package/dist/causal-behavior.d.ts +1 -1
  40. package/dist/causal-behavior.js +2 -2
  41. package/dist/causal-chain.js +2 -2
  42. package/dist/causal-consolidation.d.ts +1 -1
  43. package/dist/causal-consolidation.js +15 -15
  44. package/dist/causal-retrieval.js +2 -2
  45. package/dist/causal-trajectory-graph.d.ts +1 -1
  46. package/dist/causal-trajectory.js +1 -1
  47. package/dist/{chunk-6SXNQFHB.js → chunk-243P7YYV.js} +7 -7
  48. package/dist/{chunk-2NLLXCJG.js → chunk-2BFLDH5F.js} +2 -2
  49. package/dist/{chunk-ZBVQ4ZM4.js → chunk-34THARMH.js} +17 -17
  50. package/dist/{chunk-SNEXVX3U.js → chunk-3HGZX26P.js} +21 -21
  51. package/dist/{chunk-5PZCDUJ6.js → chunk-3ZIKDH6C.js} +4 -4
  52. package/dist/{chunk-X7Y7WX73.js → chunk-437S3G37.js} +2 -2
  53. package/dist/{chunk-RS25QOKZ.js → chunk-64SE4X5G.js} +2 -2
  54. package/dist/{chunk-JBPKEARU.js → chunk-AU7Q3LSC.js} +4 -4
  55. package/dist/{chunk-6FNZ3NMS.js → chunk-AXBRVBT6.js} +2 -2
  56. package/dist/{chunk-W3BFT5OJ.js → chunk-BWOEYBRB.js} +2 -2
  57. package/dist/{chunk-UU32EAIY.js → chunk-C3IHRWIC.js} +7 -7
  58. package/dist/{chunk-DBRVU5PR.js → chunk-DKIZ4BUQ.js} +14 -14
  59. package/dist/{chunk-EEKDRAWX.js → chunk-DS4QM6I7.js} +4 -4
  60. package/dist/{chunk-WOX2UW4K.js → chunk-E3MRYDUQ.js} +2 -2
  61. package/dist/{chunk-K2R3DEEH.js → chunk-E6LENGYV.js} +2 -2
  62. package/dist/{chunk-CGZWGVQF.js → chunk-EEO4YWQ7.js} +9 -9
  63. package/dist/{chunk-ORAK3LNZ.js → chunk-ER3CCWK2.js} +2 -2
  64. package/dist/{chunk-TPLE6SNY.js → chunk-GWGWBK5Y.js} +7 -7
  65. package/dist/{chunk-BJMBJZ2Y.js → chunk-GWTFLF2P.js} +59 -59
  66. package/dist/chunk-GWTFLF2P.js.map +1 -0
  67. package/dist/{chunk-IKHKHADN.js → chunk-ITLJATQI.js} +5 -5
  68. package/dist/{chunk-E4PFDD3N.js → chunk-JES2O5Q6.js} +4 -4
  69. package/dist/{chunk-QOGXUNWL.js → chunk-JGPURIEH.js} +7 -7
  70. package/dist/{chunk-YGGXUNS4.js → chunk-KY4WOZ3P.js} +55 -55
  71. package/dist/{chunk-WA7FECSG.js → chunk-KYHGLDFU.js} +7 -7
  72. package/dist/{chunk-V5RVMULT.js → chunk-LZ73Y56P.js} +10 -10
  73. package/dist/{chunk-ARV3AUOM.js → chunk-M5QKGHCR.js} +2 -2
  74. package/dist/{chunk-SD34EK4Z.js → chunk-N3VE7OCY.js} +4 -4
  75. package/dist/{chunk-KQAFEZQX.js → chunk-OUXPVWTZ.js} +6 -6
  76. package/dist/{chunk-NNL4LJAD.js → chunk-OZMBT7N3.js} +5 -5
  77. package/dist/{chunk-RWZB6MAY.js → chunk-PN5AMCK2.js} +1065 -1176
  78. package/dist/chunk-PN5AMCK2.js.map +1 -0
  79. package/dist/{chunk-WWSXVOGY.js → chunk-QIRZI3C3.js} +2 -2
  80. package/dist/{chunk-B35L45HZ.js → chunk-TVISHYL5.js} +2 -2
  81. package/dist/{chunk-SQNED75R.js → chunk-V5UV7BOA.js} +1 -1
  82. package/dist/{chunk-7TWA7DKP.js → chunk-VVSV2QVY.js} +2 -2
  83. package/dist/{chunk-4NWIGAIC.js → chunk-XOPLMSVV.js} +2 -2
  84. package/dist/{chunk-K2JM4DZZ.js → chunk-YNBEHUZH.js} +2 -2
  85. package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
  86. package/dist/{chunk-UETBEHBA.js → chunk-ZECLLIMX.js} +3442 -3085
  87. package/dist/chunk-ZECLLIMX.js.map +1 -0
  88. package/dist/{chunk-QRRBXD24.js → chunk-ZIFPFKB2.js} +7 -7
  89. package/dist/{chunk-HYI2GP3F.js → chunk-ZMHDSIEX.js} +7 -7
  90. package/dist/{cli-lmf5RGyQ.d.ts → cli-DqonYBZ0.d.ts} +3 -3
  91. package/dist/cli.d.ts +6 -6
  92. package/dist/cli.js +64 -64
  93. package/dist/compounding/engine.d.ts +3 -3
  94. package/dist/compounding/engine.js +11 -11
  95. package/dist/compounding/preference-consolidator.d.ts +1 -1
  96. package/dist/compression-optimizer.d.ts +1 -1
  97. package/dist/config.d.ts +1 -1
  98. package/dist/config.js +3 -3
  99. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  100. package/dist/connectors/codex-materialize-runner.js +11 -11
  101. package/dist/connectors/codex-materialize.d.ts +1 -1
  102. package/dist/connectors/index.d.ts +1 -1
  103. package/dist/connectors/index.js +12 -12
  104. package/dist/consolidation-provenance-check.d.ts +1 -1
  105. package/dist/consolidation-undo.d.ts +1 -1
  106. package/dist/contradiction/index.d.ts +1 -1
  107. package/dist/contradiction/index.js +2 -2
  108. package/dist/{contradiction-scan-3FJYWS3G.js → contradiction-scan-2SODRNUF.js} +3 -3
  109. package/dist/contradiction-scan-2SODRNUF.js.map +1 -0
  110. package/dist/conversation-index/backend.d.ts +1 -1
  111. package/dist/conversation-index/chunker.d.ts +1 -1
  112. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  113. package/dist/conversation-index/indexer.d.ts +1 -1
  114. package/dist/conversation-index/search.d.ts +1 -1
  115. package/dist/dashboard-runtime.js +2 -2
  116. package/dist/day-summary.d.ts +1 -1
  117. package/dist/delinearize.d.ts +1 -1
  118. package/dist/direct-answer-wiring.d.ts +1 -1
  119. package/dist/direct-answer.d.ts +1 -1
  120. package/dist/embedding-fallback.d.ts +1 -1
  121. package/dist/enrichment/index.d.ts +1 -1
  122. package/dist/entity-retrieval.d.ts +1 -1
  123. package/dist/entity-retrieval.js +11 -11
  124. package/dist/entity-schema.d.ts +1 -1
  125. package/dist/explicit-capture.d.ts +6 -6
  126. package/dist/extraction-faithfulness.d.ts +1 -1
  127. package/dist/extraction-judge-telemetry.d.ts +1 -1
  128. package/dist/extraction-judge-training.d.ts +1 -1
  129. package/dist/extraction-judge.d.ts +1 -1
  130. package/dist/extraction.d.ts +1 -1
  131. package/dist/extraction.js +3 -3
  132. package/dist/fallback-llm.d.ts +1 -1
  133. package/dist/{first-start-migration-4I3VDSYS.js → first-start-migration-DY6YCRC2.js} +2 -2
  134. package/dist/{forget-WYLR4ZFV.js → forget-OEE5OELK.js} +2 -2
  135. package/dist/graph-dashboard-diff.d.ts +1 -1
  136. package/dist/graph-dashboard-key.d.ts +1 -1
  137. package/dist/graph-dashboard-parser.d.ts +1 -1
  138. package/dist/graph-edge-reinforcement.d.ts +1 -1
  139. package/dist/graph-snapshot.d.ts +1 -1
  140. package/dist/graph.d.ts +1 -1
  141. package/dist/identity-continuity.d.ts +1 -1
  142. package/dist/importance.d.ts +1 -1
  143. package/dist/index.d.ts +1284 -1284
  144. package/dist/index.js +148 -148
  145. package/dist/intent.d.ts +1 -1
  146. package/dist/lcm/engine.d.ts +1 -1
  147. package/dist/lcm/engine.js +4 -4
  148. package/dist/lcm/index.d.ts +1 -1
  149. package/dist/lcm/index.js +8 -8
  150. package/dist/lcm/tools.d.ts +1 -1
  151. package/dist/lifecycle.d.ts +1 -1
  152. package/dist/live-connectors-runner.d.ts +1 -1
  153. package/dist/local-llm.d.ts +1 -1
  154. package/dist/local-model-endpoint.d.ts +1 -1
  155. package/dist/maintenance/memory-governance.d.ts +1 -1
  156. package/dist/maintenance/memory-governance.js +10 -10
  157. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
  158. package/dist/maintenance/rebuild-memory-projection.js +11 -11
  159. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  160. package/dist/memory-action-policy.d.ts +1 -1
  161. package/dist/memory-cache.d.ts +1 -1
  162. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  163. package/dist/memory-projection-store.d.ts +1 -1
  164. package/dist/memory-provenance.d.ts +1 -1
  165. package/dist/memory-worth-outcomes.d.ts +1 -1
  166. package/dist/models-json.d.ts +1 -1
  167. package/dist/namespaces/migrate.d.ts +2 -2
  168. package/dist/namespaces/migrate.js +13 -13
  169. package/dist/namespaces/principal.d.ts +1 -1
  170. package/dist/namespaces/search.d.ts +1 -1
  171. package/dist/namespaces/search.js +1 -1
  172. package/dist/namespaces/storage.d.ts +2 -2
  173. package/dist/namespaces/storage.js +12 -12
  174. package/dist/native-knowledge.d.ts +1 -1
  175. package/dist/operator-toolkit.d.ts +1 -1
  176. package/dist/operator-toolkit.js +19 -19
  177. package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
  178. package/dist/orchestration/maintenance.d.ts +2 -2
  179. package/dist/orchestration/maintenance.js +15 -15
  180. package/dist/{orchestrator-D7bb8ZGp.d.ts → orchestrator-D7ZGOQLX.d.ts} +101 -180
  181. package/dist/orchestrator.d.ts +4 -4
  182. package/dist/orchestrator.js +92 -92
  183. package/dist/patterns-cli.d.ts +1 -1
  184. package/dist/policy-runtime.d.ts +1 -1
  185. package/dist/provenance.d.ts +1 -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-planner-llm.d.ts +1 -1
  191. package/dist/recall-state.d.ts +1 -1
  192. package/dist/recall-tag-filter.d.ts +1 -1
  193. package/dist/recall-xray-cli.d.ts +1 -1
  194. package/dist/recall-xray-renderer.d.ts +1 -1
  195. package/dist/recall-xray.d.ts +1 -1
  196. package/dist/resolve-auth-token.d.ts +1 -1
  197. package/dist/resume-bundles.js +3 -3
  198. package/dist/retrieval-agents.d.ts +1 -1
  199. package/dist/retrieval-tiers.d.ts +1 -1
  200. package/dist/routing/engine.d.ts +1 -1
  201. package/dist/routing/store.d.ts +1 -1
  202. package/dist/schemas.d.ts +100 -100
  203. package/dist/search/embed-helper.d.ts +1 -1
  204. package/dist/search/factory.d.ts +1 -1
  205. package/dist/search/factory.js +1 -1
  206. package/dist/search/index.d.ts +1 -1
  207. package/dist/search/index.js +1 -1
  208. package/dist/search/lancedb-backend.d.ts +1 -1
  209. package/dist/search/meilisearch-backend.d.ts +1 -1
  210. package/dist/search/noop-backend.d.ts +1 -1
  211. package/dist/search/orama-backend.d.ts +1 -1
  212. package/dist/search/port.d.ts +1 -1
  213. package/dist/search/remote-backend.d.ts +1 -1
  214. package/dist/secure-store/index.js +2 -2
  215. package/dist/{semantic-consolidation-CTfbVwCA.d.ts → semantic-consolidation-7Sndms2M.d.ts} +1 -1
  216. package/dist/semantic-consolidation.d.ts +2 -2
  217. package/dist/semantic-consolidation.js +13 -13
  218. package/dist/semantic-rule-promotion.js +10 -10
  219. package/dist/semantic-rule-verifier.d.ts +1 -1
  220. package/dist/semantic-rule-verifier.js +10 -10
  221. package/dist/session-observer-bands.d.ts +1 -1
  222. package/dist/session-observer-state.d.ts +1 -1
  223. package/dist/shared-context/manager.d.ts +9 -9
  224. package/dist/signal.d.ts +1 -1
  225. package/dist/storage.d.ts +1 -1
  226. package/dist/storage.js +9 -9
  227. package/dist/summarizer.d.ts +1 -1
  228. package/dist/summarizer.js +2 -2
  229. package/dist/summary-snapshot.d.ts +1 -1
  230. package/dist/temporal-supersession.d.ts +1 -1
  231. package/dist/temporal-validity.d.ts +1 -1
  232. package/dist/threading.d.ts +1 -1
  233. package/dist/tier-migration.d.ts +1 -1
  234. package/dist/tier-routing.d.ts +1 -1
  235. package/dist/{tier-stats-5XF4UBRQ.js → tier-stats-TG6GUZ6N.js} +4 -4
  236. package/dist/topics.d.ts +1 -1
  237. package/dist/transcript.d.ts +1 -1
  238. package/dist/transfer/autodetect.js +1 -1
  239. package/dist/transfer/backup.js +3 -3
  240. package/dist/transfer/capsule-export.js +4 -4
  241. package/dist/transfer/capsule-import.js +3 -3
  242. package/dist/transfer/types.d.ts +110 -110
  243. package/dist/trust-score-stage.d.ts +1 -1
  244. package/dist/trust-score.d.ts +1 -1
  245. package/dist/{types-D4NYDtXI.d.ts → types-DTEFxFuL.d.ts} +1 -1
  246. package/dist/types.d.ts +1 -1
  247. package/dist/utility-runtime.d.ts +1 -1
  248. package/dist/verified-recall.js +10 -10
  249. package/package.json +2 -2
  250. package/src/access-admin-ops-surface.ts +828 -0
  251. package/src/access-recall-surface.ts +1552 -0
  252. package/src/access-service.ts +70 -1917
  253. package/src/orchestration/recall-entry.ts +290 -0
  254. package/src/orchestration/session-context.ts +407 -0
  255. package/src/orchestrator.ts +125 -462
  256. package/dist/chunk-BJMBJZ2Y.js.map +0 -1
  257. package/dist/chunk-RWZB6MAY.js.map +0 -1
  258. package/dist/chunk-UETBEHBA.js.map +0 -1
  259. /package/dist/{capsule-crypto-7FJQINUR.js.map → access-admin-ops-surface.js.map} +0 -0
  260. /package/dist/{contradiction-scan-3FJYWS3G.js.map → access-recall-surface.js.map} +0 -0
  261. /package/dist/{chunk-6SXNQFHB.js.map → chunk-243P7YYV.js.map} +0 -0
  262. /package/dist/{chunk-2NLLXCJG.js.map → chunk-2BFLDH5F.js.map} +0 -0
  263. /package/dist/{chunk-ZBVQ4ZM4.js.map → chunk-34THARMH.js.map} +0 -0
  264. /package/dist/{chunk-SNEXVX3U.js.map → chunk-3HGZX26P.js.map} +0 -0
  265. /package/dist/{chunk-5PZCDUJ6.js.map → chunk-3ZIKDH6C.js.map} +0 -0
  266. /package/dist/{chunk-X7Y7WX73.js.map → chunk-437S3G37.js.map} +0 -0
  267. /package/dist/{chunk-RS25QOKZ.js.map → chunk-64SE4X5G.js.map} +0 -0
  268. /package/dist/{chunk-JBPKEARU.js.map → chunk-AU7Q3LSC.js.map} +0 -0
  269. /package/dist/{chunk-6FNZ3NMS.js.map → chunk-AXBRVBT6.js.map} +0 -0
  270. /package/dist/{chunk-W3BFT5OJ.js.map → chunk-BWOEYBRB.js.map} +0 -0
  271. /package/dist/{chunk-UU32EAIY.js.map → chunk-C3IHRWIC.js.map} +0 -0
  272. /package/dist/{chunk-DBRVU5PR.js.map → chunk-DKIZ4BUQ.js.map} +0 -0
  273. /package/dist/{chunk-EEKDRAWX.js.map → chunk-DS4QM6I7.js.map} +0 -0
  274. /package/dist/{chunk-WOX2UW4K.js.map → chunk-E3MRYDUQ.js.map} +0 -0
  275. /package/dist/{chunk-K2R3DEEH.js.map → chunk-E6LENGYV.js.map} +0 -0
  276. /package/dist/{chunk-CGZWGVQF.js.map → chunk-EEO4YWQ7.js.map} +0 -0
  277. /package/dist/{chunk-ORAK3LNZ.js.map → chunk-ER3CCWK2.js.map} +0 -0
  278. /package/dist/{chunk-TPLE6SNY.js.map → chunk-GWGWBK5Y.js.map} +0 -0
  279. /package/dist/{chunk-IKHKHADN.js.map → chunk-ITLJATQI.js.map} +0 -0
  280. /package/dist/{chunk-E4PFDD3N.js.map → chunk-JES2O5Q6.js.map} +0 -0
  281. /package/dist/{chunk-QOGXUNWL.js.map → chunk-JGPURIEH.js.map} +0 -0
  282. /package/dist/{chunk-YGGXUNS4.js.map → chunk-KY4WOZ3P.js.map} +0 -0
  283. /package/dist/{chunk-WA7FECSG.js.map → chunk-KYHGLDFU.js.map} +0 -0
  284. /package/dist/{chunk-V5RVMULT.js.map → chunk-LZ73Y56P.js.map} +0 -0
  285. /package/dist/{chunk-ARV3AUOM.js.map → chunk-M5QKGHCR.js.map} +0 -0
  286. /package/dist/{chunk-SD34EK4Z.js.map → chunk-N3VE7OCY.js.map} +0 -0
  287. /package/dist/{chunk-KQAFEZQX.js.map → chunk-OUXPVWTZ.js.map} +0 -0
  288. /package/dist/{chunk-NNL4LJAD.js.map → chunk-OZMBT7N3.js.map} +0 -0
  289. /package/dist/{chunk-WWSXVOGY.js.map → chunk-QIRZI3C3.js.map} +0 -0
  290. /package/dist/{chunk-B35L45HZ.js.map → chunk-TVISHYL5.js.map} +0 -0
  291. /package/dist/{chunk-SQNED75R.js.map → chunk-V5UV7BOA.js.map} +0 -0
  292. /package/dist/{chunk-7TWA7DKP.js.map → chunk-VVSV2QVY.js.map} +0 -0
  293. /package/dist/{chunk-4NWIGAIC.js.map → chunk-XOPLMSVV.js.map} +0 -0
  294. /package/dist/{chunk-K2JM4DZZ.js.map → chunk-YNBEHUZH.js.map} +0 -0
  295. /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
  296. /package/dist/{chunk-QRRBXD24.js.map → chunk-ZIFPFKB2.js.map} +0 -0
  297. /package/dist/{chunk-HYI2GP3F.js.map → chunk-ZMHDSIEX.js.map} +0 -0
  298. /package/dist/{first-start-migration-4I3VDSYS.js.map → first-start-migration-DY6YCRC2.js.map} +0 -0
  299. /package/dist/{forget-WYLR4ZFV.js.map → forget-OEE5OELK.js.map} +0 -0
  300. /package/dist/{tier-stats-5XF4UBRQ.js.map → tier-stats-TG6GUZ6N.js.map} +0 -0
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { PluginEntryResolverOptions, resolvePluginEntry } from './plugin-entry-resolver.js';
2
2
  export { isOpenaiApiKeyDisabled, parseConfig, resolveEnvVars } from './config.js';
3
3
  export { MigrationOptions, MigrationResult, RollbackResult, migrateFromEngram, rollbackFromEngramMigration } from './migrate/from-engram.js';
4
- import { b as SpeakerRegistry, W as WearablesService } from './orchestrator-D7bb8ZGp.js';
5
- export { D as DEFAULT_SELF_NAME, O as Orchestrator, c as ResolvedSpeaker, d as SpeakerOverride, e as WEARABLE_SOURCE_PREFIX, f as WearableDayTranscriptView, g as WearableMemoryGenDeps, h as WearableMemoryGenResult, i as WearableMemorySearchResult, j as WearableMemoryWriter, k as WearableSearchBackend, l as WearableStorageIo, m as WearableSyncDeps, n as WearableSyncOptions, o as WearableTranscriptSearchResult, p as WearablesServiceDeps, q as buildExtractionTurns, r as dateInTimezone, s as defaultTimezone, t as defaultWorkspaceDir, u as distinctSpeakerLabels, v as emptySpeakerRegistry, w as generateWearableMemories, x as importNativeMemories, y as loadSpeakerRegistry, z as locateTranscriptPath, A as memoryStatusForMode, B as resolveSpeaker, C as resolveSyncDates, E as sanitizeSessionKeyForFilename, F as saveSpeakerRegistry, H as speakerRegistryKey, J as speakersFilePath, K as syncWearableSource, L as wearableDayTag, M as wearableSourceLabel, N as writeDailyDigestMemory } from './orchestrator-D7bb8ZGp.js';
4
+ import { b as SpeakerRegistry, W as WearablesService } from './orchestrator-D7ZGOQLX.js';
5
+ export { D as DEFAULT_SELF_NAME, O as Orchestrator, c as ResolvedSpeaker, d as SpeakerOverride, e as WEARABLE_SOURCE_PREFIX, f as WearableDayTranscriptView, g as WearableMemoryGenDeps, h as WearableMemoryGenResult, i as WearableMemorySearchResult, j as WearableMemoryWriter, k as WearableSearchBackend, l as WearableStorageIo, m as WearableSyncDeps, n as WearableSyncOptions, o as WearableTranscriptSearchResult, p as WearablesServiceDeps, q as buildExtractionTurns, r as dateInTimezone, s as defaultTimezone, t as defaultWorkspaceDir, u as distinctSpeakerLabels, v as emptySpeakerRegistry, w as generateWearableMemories, x as importNativeMemories, y as loadSpeakerRegistry, z as locateTranscriptPath, A as memoryStatusForMode, B as resolveSpeaker, C as resolveSyncDates, E as sanitizeSessionKeyForFilename, F as saveSpeakerRegistry, H as speakerRegistryKey, J as speakersFilePath, K as syncWearableSource, L as wearableDayTag, M as wearableSourceLabel, N as writeDailyDigestMemory } from './orchestrator-D7ZGOQLX.js';
6
6
  export { normalizeProjectionPreview, normalizeProjectionTags } from './memory-projection-format.js';
7
7
  export { ModelCapabilities, ModelRegistry } from './model-registry.js';
8
8
  export { ACTIVE_STATUSES, ContradictionFilter, ContradictionJudgeBatchResult, ContradictionJudgeInput, ContradictionJudgeResult, ContradictionListResult, ContradictionVerdict, ResolutionResult, ResolutionVerb, ScanDependencies, ScanResult, computePairId, executeResolution, isCoolingDown, isValidResolutionVerb, judgeContradictionPairs, listPairs, readPair, resolvePair, runContradictionScan, writePair, writePairs } from './contradiction/index.js';
@@ -20,11 +20,11 @@ export { CodexCliFallbackConfig, CodexCliFallbackMessage, CodexCliFallbackOption
20
20
  export { BufferSurpriseProbe, SmartBuffer, TriggerDecision } from './buffer.js';
21
21
  export { ComputeSurpriseOptions, DEFAULT_SURPRISE_K, RecentMemoryLike, computeSurprise } from './buffer-surprise.js';
22
22
  export { BufferSurpriseDistribution, BufferSurpriseReader, BufferSurpriseReportOptions, reportBufferSurpriseDistribution } from './buffer-surprise-report.js';
23
- import { ax as CodingContext, aS as CodingModeConfig, P as PluginConfig, aT as CodingKnowledgeConfig, M as MemoryCategory, aU as WearableCleanupSettings, aF as WearableSourceSettings, aJ as WearablesConfig, aI as WearableSourceConnector, aE as WearableConversation, aN as WearableCorrectionRule, aV as WearableDayTranscriptMeta, aM as WearableDayTranscript } from './types-D4NYDtXI.js';
24
- export { X as AgentAccessAuthToken, r as BriefingActiveThread, aW as BriefingConfig, p as BriefingFocus, o as BriefingFollowup, aX as BriefingOpenCommitment, t as BriefingRecentEntity, s as BriefingResult, n as BriefingSections, aY as BriefingWindow, U as BufferSurpriseEvent, q as CalendarEvent, C as CalendarSource, aZ as CodexCompatConfig, aR as ConsolidationObservation, a9 as ContinuityImprovementLoop, a_ as DEFAULT_RECALL_DISCLOSURE, E as EntityFile, a5 as EntityStructuredSection, a$ as ExtractedFact, G as GatewayConfig, af as MemoryActionEligibilityContext, b0 as MemoryActionEligibilitySource, $ as MemoryActionType, g as MemoryFile, k as MemoryFrontmatter, b1 as MemoryObservation, b2 as MemoryScope, b3 as RECALL_DISCLOSURE_LEVELS, i as RecallDisclosure, W as SecretRef, b4 as WearableAuthCheck, b5 as WearableFetchOptions, b6 as WearableFetchPage, aH as WearableMemoryMode, aG as WearableNativeMemory, b7 as WearableNativeMemoryPage, aL as WearableSourceStatus, aK as WearableSyncSummary, b8 as WearableTranscriptSegment, b9 as isRecallDisclosure } from './types-D4NYDtXI.js';
23
+ import { az as CodingContext, aS as CodingModeConfig, P as PluginConfig, aT as CodingKnowledgeConfig, h as MemoryCategory, aU as WearableCleanupSettings, aF as WearableSourceSettings, aJ as WearablesConfig, aI as WearableSourceConnector, aE as WearableConversation, aN as WearableCorrectionRule, aV as WearableDayTranscriptMeta, aM as WearableDayTranscript } from './types-DTEFxFuL.js';
24
+ export { Z as AgentAccessAuthToken, m as BriefingActiveThread, aW as BriefingConfig, k as BriefingFocus, j as BriefingFollowup, aX as BriefingOpenCommitment, o as BriefingRecentEntity, n as BriefingResult, i as BriefingSections, aY as BriefingWindow, X as BufferSurpriseEvent, l as CalendarEvent, C as CalendarSource, aZ as CodexCompatConfig, aR as ConsolidationObservation, ab as ContinuityImprovementLoop, a_ as DEFAULT_RECALL_DISCLOSURE, E as EntityFile, a7 as EntityStructuredSection, a$ as ExtractedFact, G as GatewayConfig, ah as MemoryActionEligibilityContext, b0 as MemoryActionEligibilitySource, a1 as MemoryActionType, M as MemoryFile, q as MemoryFrontmatter, b1 as MemoryObservation, b2 as MemoryScope, b3 as RECALL_DISCLOSURE_LEVELS, R as RecallDisclosure, Y as SecretRef, b4 as WearableAuthCheck, b5 as WearableFetchOptions, b6 as WearableFetchPage, aH as WearableMemoryMode, aG as WearableNativeMemory, b7 as WearableNativeMemoryPage, aL as WearableSourceStatus, aK as WearableSyncSummary, b8 as WearableTranscriptSegment, b9 as isRecallDisclosure } from './types-DTEFxFuL.js';
25
25
  export { JudgeBatchResult, JudgeCandidate, JudgeVerdict, JudgeVerdictKind, clearVerdictCache, createVerdictCache, getVerdictKind, isDurableVerdict, isValidCachedVerdict, judgeFactDurability, normalizeCachedVerdict, verdictCacheSize } from './extraction-judge.js';
26
26
  export { hasBroadGraphIntent, inferIntentFromText, intentCompatibilityScore, isTaskInitiationIntent, planRecallMode } from './intent.js';
27
- export { i as EngramAccessInputError, g as EngramAccessService, P as ProcedureStatsConfigSnapshot, j as ProcedureStatsRecent, k as ProcedureStatsReport, l as ProcedureStatusCounts, m as computeProcedureStats, n as formatProcedureStatsText } from './access-service-DmAEOJdJ.js';
27
+ export { q as EngramAccessInputError, p as EngramAccessService, P as ProcedureStatsConfigSnapshot, r as ProcedureStatsRecent, s as ProcedureStatsReport, t as ProcedureStatusCounts, u as computeProcedureStats, v as formatProcedureStatsText } from './access-service-MitduesD.js';
28
28
  export { FILTER_LABELS as DIRECT_ANSWER_FILTER_LABELS, DirectAnswerCandidate, DirectAnswerConfig, DirectAnswerInput, DirectAnswerReason, DirectAnswerResult, isDirectAnswerEligible } from './direct-answer.js';
29
29
  export { MemoryTier, TierRoutingPolicy, TierTransitionDecision, computeTierValueScore, decideTierTransition } from './tier-routing.js';
30
30
  export { ApplyReasoningTraceBoostOptions, BoostableResult, DEFAULT_REASONING_TRACE_BOOST, applyReasoningTraceBoost, isReasoningTracePath, looksLikeProblemSolvingQuery } from './reasoning-trace-recall.js';
@@ -38,7 +38,7 @@ export { OramaBackend } from './search/orama-backend.js';
38
38
  export { MeilisearchBackend } from './search/meilisearch-backend.js';
39
39
  export { buildEntityRecallSection } from './entity-retrieval.js';
40
40
  export { resolvePrincipal } from './namespaces/principal.js';
41
- export { N as NamespaceCatalog, c as NamespaceCatalogFilter, d as NamespaceCatalogRebuildResult, e as NamespaceCatalogSkippedRoot, f as NamespaceDiscoverySource, b as NamespaceKind, a as NamespaceRecord, g as NamespaceTouchMetadata } from './catalog-BpOcwHdE.js';
41
+ export { N as NamespaceCatalog, c as NamespaceCatalogFilter, d as NamespaceCatalogRebuildResult, e as NamespaceCatalogSkippedRoot, f as NamespaceDiscoverySource, b as NamespaceKind, a as NamespaceRecord, g as NamespaceTouchMetadata } from './catalog-BEmNVRN0.js';
42
42
  export { SESSION_CHANNEL_TYPE, SessionIdentity, SessionStoragePaths, legacyParserReadbackDir, parseSessionIdentity, sessionStoragePaths } from './session-identity.js';
43
43
  export { storagePathHash } from './storage-paths.js';
44
44
  export { TrustZoneName, TrustZoneRecord, TrustZoneRecordKind, TrustZoneSourceClass, isTrustZoneName } from './trust-zones.js';
@@ -62,8 +62,8 @@ export { CitationBlock, CitationEntry, CitationMetadata, buildCitationGuidance,
62
62
  export { initLogger, log } from './logger.js';
63
63
  export { b as SemanticDedupDecision, a as SemanticDedupHit, S as SemanticDedupLookup, c as SemanticDedupOptions, d as decideSemanticDedup } from './semantic-DJR8_DMQ.js';
64
64
  export { OFFLINE_SYNC_APPLY_MAX_BODY_BYTES, OFFLINE_SYNC_CHANGESET_FORMAT, OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES, OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES, OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES, OFFLINE_SYNC_SNAPSHOT_FORMAT, OFFLINE_SYNC_STATE_VERSION, OfflineSyncApplyChangesetResult, OfflineSyncApplyFileContentChunkResult, OfflineSyncApplySnapshotResult, OfflineSyncChange, OfflineSyncChangeset, OfflineSyncConflict, OfflineSyncFileContentChunk, OfflineSyncFileDigest, OfflineSyncFileRecord, OfflineSyncFileState, OfflineSyncFileTarget, OfflineSyncFileWriteTarget, OfflineSyncSnapshot, OfflineSyncState, applyOfflineSyncChangeset, applyOfflineSyncFileContentChunk, applyOfflineSyncSnapshot, buildOfflineSyncChangeset, buildOfflineSyncChangesetFromSnapshot, buildOfflineSyncSnapshot, buildOfflineSyncSnapshotForPaths, buildOfflineSyncSnapshotFromBase, compileOfflineSyncExcludeGlobs, defaultOfflineSyncStatePath, fileStatesFromSnapshot, globToRegExp, iterateOfflineSyncSnapshotFileRecords, normalizeOfflineSyncChangeset, normalizeOfflineSyncSnapshot, offlineSyncStateFromSnapshot, readOfflineSyncFileContentChunk, readOfflineSyncState, shouldPreferIncomingOfflineRuntimeFile, summarizeOfflineSyncChangeset, summarizeOfflineSyncPendingChanges, summarizeOfflineSyncPendingFiles, writeOfflineSyncState } from './offline-sync.js';
65
- import { D as DiscoveredExtension } from './semantic-consolidation-CTfbVwCA.js';
66
- export { E as ExtensionSchema, R as REMNIC_EXTENSIONS_TOTAL_TOKEN_LIMIT, b as buildExtensionsBlockForConsolidation, d as discoverMemoryExtensions, r as resolveExtensionsRoot } from './semantic-consolidation-CTfbVwCA.js';
65
+ import { D as DiscoveredExtension } from './semantic-consolidation-7Sndms2M.js';
66
+ export { E as ExtensionSchema, R as REMNIC_EXTENSIONS_TOTAL_TOKEN_LIMIT, b as buildExtensionsBlockForConsolidation, d as discoverMemoryExtensions, r as resolveExtensionsRoot } from './semantic-consolidation-7Sndms2M.js';
67
67
  export { ConnectorCapability, ConnectorInstance, ConnectorManifest, ConnectorRegistry, DoctorCheck, DoctorResult, InstallOptions, InstallResult, MARKETPLACE_MANIFEST_FILENAME, MARKETPLACE_SCHEMA_VERSION, MarketplaceConfig, MarketplaceEntry, MarketplaceInstallResult, MarketplaceInstallType, MarketplaceLogger, MarketplaceManifest, MarketplaceValidation, RemoveResult, checkMarketplaceManifest, coerceInstallExtension, doctorConnector, generateMarketplaceManifest, getConnectorToken, installConnector, installFromMarketplace, listConnectors, loadRegistry, removeConnector, saveRegistry, validateMarketplaceManifest, writeMarketplaceManifest } from './connectors/index.js';
68
68
  import { L as LiveConnector, S as SyncIncrementalResult } from './framework-CNDn2164.js';
69
69
  export { c as CONNECTOR_ID_PATTERN, C as ConnectorConfig, b as ConnectorCursor, a as ConnectorDocument, d as ConnectorDocumentSource, e as SyncIncrementalArgs, i as isValidConnectorId, p as persistableConnectorConfig, r as redactConnectorConfigSecrets } from './framework-CNDn2164.js';
@@ -79,7 +79,7 @@ export { EnrichmentAuditEntry, EnrichmentCandidate, EnrichmentCostTier, Enrichme
79
79
  export { d as BulkImportError, B as BulkImportOptions, b as BulkImportResult, a as BulkImportSource, e as BulkImportSourceAdapter, f as ImportSourceRole, I as ImportTurn, g as ImportTurnValidationIssue, i as isImportRole, p as parseIsoTimestamp, v as validateImportTurn } from './types-gb_SNtjR.js';
80
80
  export { clearBulkImportSources, getBulkImportSource, listBulkImportSources, registerBulkImportSource } from './bulk-import/index.js';
81
81
  export { a as ProcessBatchContext, P as ProcessBatchFn, b as ProcessBatchResult, f as formatBatchTranscript, r as resolveBulkImportContext, c as runBulkImportPipeline, v as validateBatchSize } from './pipeline-DmqtvION.js';
82
- export { B as BulkImportCliCommandOptions, p as parseStrictCliDate, r as runBulkImportCliCommand } from './cli-lmf5RGyQ.js';
82
+ export { B as BulkImportCliCommandOptions, p as parseStrictCliDate, r as runBulkImportCliCommand } from './cli-DqonYBZ0.js';
83
83
  export { DEFAULT_IMPORT_BATCH_SIZE, ImportProgress, ImportedMemory, ImporterAdapter, ImporterParseOptions, ImporterTransformOptions, ImporterWriteResult, ImporterWriteTarget, RunImportOptions, RunImporterResult, defaultWriteMemoriesToOrchestrator, importedMemoryToTurn, runImporter, validateImportBatchSize, validateImportRateLimit } from './importers/index.js';
84
84
  export { FallbackLlmClient, FallbackLlmOptions, FallbackLlmResponse, FallbackLlmRuntimeContext } from './fallback-llm.js';
85
85
  export { ComputeMemoryWorthInput, MemoryWorthResult, computeMemoryWorth } from './memory-worth.js';
@@ -342,1473 +342,1473 @@ declare function buildProcedureMarkdownBody(steps: ProcedureStep[]): string;
342
342
  */
343
343
  declare function parseProcedureStepsFromBody(content: string): ProcedureStep[] | null;
344
344
 
345
- interface GitContext {
345
+ /**
346
+ * @remnic/core — Live Connectors Registry (issue #683 PR 1/N)
347
+ *
348
+ * Pure in-memory registry. No I/O. Concrete connectors register themselves at
349
+ * orchestrator boot (later PRs); the maintenance scheduler asks the registry
350
+ * for the active set when running due syncs.
351
+ */
352
+
353
+ /**
354
+ * Thrown when registering a duplicate id or an id that fails validation.
355
+ *
356
+ * Distinct error class so callers can distinguish framework-level mistakes
357
+ * (which are programmer errors) from connector-runtime failures.
358
+ */
359
+ declare class LiveConnectorRegistryError extends Error {
360
+ constructor(message: string);
361
+ }
362
+ /**
363
+ * In-memory registry of live connectors. One instance per orchestrator. Not
364
+ * safe for cross-process sharing — wire each process to its own registry.
365
+ */
366
+ declare class LiveConnectorRegistry {
367
+ private readonly connectors;
346
368
  /**
347
- * Stable identifier for the project. Derived from `git remote get-url origin`
348
- * when an origin remote is configured, otherwise from the repo root path.
369
+ * Register a connector. Throws `LiveConnectorRegistryError` if the id is
370
+ * malformed or already registered.
349
371
  *
350
- * Formatted as `origin:<hex>` or `root:<hex>` so that the source is visible
351
- * to operators (see `remnic doctor`, issue #569 acceptance criteria).
372
+ * Re-registration is rejected (rather than silently overwriting) because
373
+ * silent overwrites mask plugin loading bugs in development and could let
374
+ * a malicious extension shadow a built-in connector.
352
375
  */
353
- projectId: string;
376
+ register(connector: LiveConnector): void;
354
377
  /**
355
- * Current branch, e.g. `main`, `feat/foo`. `null` only in detached-HEAD
356
- * state (e.g. rebase in progress). Callers should treat `null` as "no
357
- * branch-scope overlay applies" without erroring.
378
+ * Look up a connector by id. Returns `undefined` if not registered.
358
379
  */
359
- branch: string | null;
380
+ get(id: string): LiveConnector | undefined;
360
381
  /**
361
- * Absolute path to the repository root (the directory containing `.git`).
362
- * Tilde-expanded per CLAUDE.md #17.
382
+ * Return all registered connectors, sorted by id for stable enumeration.
363
383
  */
364
- rootPath: string;
384
+ list(): LiveConnector[];
365
385
  /**
366
- * Best-effort default branch (usually `main` or `master`). Derived from the
367
- * `refs/remotes/origin/HEAD` symbolic ref. `null` when not available (e.g.
368
- * fresh clone without a default branch symref, or no origin remote).
386
+ * Remove a connector. Returns `true` if a connector was removed, `false`
387
+ * otherwise. The cursor / state file on disk is **not** touched — callers
388
+ * who want to fully decommission a connector must also delete its state
389
+ * file via the `state-store` module.
369
390
  */
370
- defaultBranch: string | null;
371
- }
372
- /**
373
- * Injectable git-invocation surface. Only the commands `resolveGitContext`
374
- * actually needs are exposed. Tests inject a mock implementation to avoid
375
- * spawning a real git process.
376
- */
377
- interface GitInvoker {
391
+ unregister(id: string): boolean;
378
392
  /**
379
- * Run `git <args>` with `cwd` as the working directory. Must return
380
- * `{ stdout, exitCode }` with `stdout` trimmed by the caller as needed.
381
- * Implementations should NOT throw for non-zero exit codes — they should
382
- * return the exit code so the resolver can decide how to recover.
393
+ * Number of registered connectors. Cheap; safe to call frequently.
383
394
  */
384
- (cwd: string, args: string[]): {
385
- stdout: string;
386
- exitCode: number;
387
- };
395
+ size(): number;
388
396
  }
397
+
389
398
  /**
390
- * Non-cryptographic stable hash. Used only to derive a deterministic
391
- * `projectId` from either the origin URL or the root path. The hash does not
392
- * need to be collision-resistant against adversarial input — it is purely a
393
- * namespace discriminator.
399
+ * @remnic/core Google Drive live connector (issue #683 PR 2/N)
394
400
  *
395
- * Uses FNV-1a 32-bit so we don't pull in `node:crypto` for a simple bucket
396
- * key. Output is lowercase hex, zero-padded to 8 characters.
397
- */
398
- declare function stableHash(input: string): string;
399
- /**
400
- * Normalize a git remote URL so that equivalent SSH / HTTPS forms of the
401
- * same repo produce the same `projectId`. Handles:
402
- * - `git@github.com:foo/bar.git` → `github.com/foo/bar`
403
- * - `https://github.com/foo/bar` → `github.com/foo/bar`
404
- * - `https://github.com/foo/bar.git` → `github.com/foo/bar`
405
- * - `ssh://git@github.com/foo/bar` → `github.com/foo/bar`
406
- * - `ssh://git@github.com:2222/foo/bar` → `github.com/foo/bar` (port stripped)
401
+ * Concrete `LiveConnector` implementation that incrementally imports text
402
+ * content from a user's Google Drive into Remnic. Built on top of the
403
+ * framework shipped in PR 1/N (`framework.ts` / `registry.ts` /
404
+ * `state-store.ts`).
407
405
  *
408
- * Case-insensitive (remote hostnames and most repo paths on major forges are
409
- * case-insensitive in practice).
410
- */
411
- declare function normalizeOriginUrl(rawUrl: string): string;
412
- interface ResolveGitContextOptions {
413
- /** Inject a git invoker (tests). Defaults to spawning real `git`. */
414
- invoker?: GitInvoker;
415
- }
416
- /**
417
- * Detect the git project + branch for `cwd`.
406
+ * Design notes:
418
407
  *
419
- * Returns `null` when:
420
- * - `cwd` is not an absolute path (invalid input, CLAUDE.md #51)
421
- * - `cwd` is not inside a git worktree
422
- * - `git` is not available on PATH
408
+ * - **Cursor semantics.** We page Drive with the official `changes` API
409
+ * when a `startPageToken` is available. The cursor is opaque from the
410
+ * framework's POV (`{kind: "drivePageToken", value: ...}`), and on the
411
+ * very first sync (cursor=null) we call `changes.getStartPageToken()`
412
+ * to seed it without importing anything. This matches the documented
413
+ * Drive incremental-sync recipe and means re-runs never re-ingest the
414
+ * same file as long as the cursor file survives.
423
415
  *
424
- * Never throws.
425
- */
426
- declare function resolveGitContext(cwd: string, options?: ResolveGitContextOptions): Promise<GitContext | null>;
427
-
428
- /**
429
- * Structural-context provider port (issue #1548 Track A PR 5).
416
+ * - **Folder scope.** When `folderIds` is non-empty, files are filtered
417
+ * to those whose `parents` intersect the configured folder set. Drive
418
+ * does not currently support server-side parent filtering on the
419
+ * `changes.list` endpoint, so we pull the change record's `file` payload
420
+ * and apply the filter on our side. Folder ids are validated up front so
421
+ * a typo in config doesn't silently cause a broad import.
430
422
  *
431
- * The architectural boundary between Track A (durable coding-memory
432
- * features) and Track B (the native @remnic/coding-graph engine). A
433
- * structural-context provider expands a unified diff into the SYMBOLS it
434
- * touches (and optionally yields architecture hints), so review-intent
435
- * recall can boost memories that mention those symbols — not just the
436
- * bare file paths that `review-context.ts` already boosts on.
423
+ * - **Content extraction.** Google-native MIME types
424
+ * (`application/vnd.google-apps.{document,spreadsheet,presentation}`)
425
+ * are exported via `files.export` to plaintext. Plain-text MIME types
426
+ * are pulled with `files.get?alt=media`. Everything else is skipped
427
+ * bytes from binary formats (images, PDFs, archives) belong in the
428
+ * binary-lifecycle pipeline, not in the textual ingestion path.
437
429
  *
438
- * Three providers satisfy this port, selected by
439
- * `codingKnowledge.structuralProvider`:
430
+ * - **Idempotency.** Each emitted `ConnectorDocument.source` carries
431
+ * `externalId = file.id` plus `externalRevision = file.modifiedTime`,
432
+ * so downstream dedup (CLAUDE.md gotcha #44 — never index content that
433
+ * failed to persist) can recognise repeat fetches even if the cursor is
434
+ * manually rewound.
440
435
  *
441
- * - `"none"` — no provider consulted (default; review-context stays
442
- * file-path-only, byte-identical to pre-feature rule 39).
443
- * - `"subprocess"` an external binary shelled out via `execFile` with an
444
- * argv ARRAY (never a shell string rule 10). The
445
- * built-in adapter lives in `structural-subprocess-provider.ts`;
446
- * codebase-memory-mcp's CLI mode is the canonical target,
447
- * but the provider is command-agnostic.
448
- * - `"native"` — the in-family @remnic/coding-graph engine, adapted to
449
- * this port by a future PR (#1551/#1554). Only the enum
450
- * value and this doc note land in this PR — no engine
451
- * code. codebase-memory-mcp remains a supported
452
- * subprocess provider through this same port regardless.
436
+ * - **Privacy.** No document content is ever logged. Folder ids and
437
+ * counts may be logged. OAuth credentials (`clientId`,
438
+ * `clientSecret`, `refreshToken`) are accepted via config but the
439
+ * intent is for callers to populate them from a secret store; we never
440
+ * persist credentials through the connector state-store. Per CLAUDE.md
441
+ * repository-privacy rules, no real credentials may appear in tests,
442
+ * fixtures, or comments.
453
443
  *
454
- * Every result is a tagged outcome shaped like #1536's `SearchDegradation`
455
- * (CLAUDE.md rule 34 a degraded/absent provider must NEVER masquerade as
456
- * "no structural matches"):
457
- * - `{ ok: true, symbols }` on success
458
- * - `{ ok: false, code }` on every failure (distinct codes per mode)
444
+ * - **À-la-carte packaging (CLAUDE.md gotcha #57).** `googleapis` is NOT
445
+ * listed as a hard dependency of `@remnic/core`. It is loaded via a
446
+ * computed-specifier dynamic import (`await import("google" + "apis")`)
447
+ * so bundlers cannot statically resolve it, and it is declared as an
448
+ * optional peer dependency. Operators who never enable the connector
449
+ * pay nothing for it.
459
450
  *
460
- * No module-level mutable state (rule 11): the registry is keyed per
461
- * orchestrator/service instance via a `Symbol.for(...)` slot on
462
- * `globalThis`, mirroring `host-embedding-provider.ts`.
451
+ * - **Read-only.** This connector only reads. It never marks files as
452
+ * read, edits metadata, or modifies sharing settings.
463
453
  */
464
454
 
465
455
  /**
466
- * Distinct failure codes for the structural-context port. Every code is
467
- * load-bearing: callers (review-context, `remnic doctor`, xray) switch on
468
- * `code` to distinguish "binary not installed" from "timed out" from
469
- * "protocol violation" so a silent provider failure can never look like an
470
- * empty-but-healthy result.
456
+ * Stable connector id. Lives in the registry under this exact string.
471
457
  */
472
- type StructuralContextErrorCode =
473
- /** Probe failed: binary missing / not executable, or native package absent. */
474
- "provider_unavailable"
475
- /** The provider exceeded its deadline. */
476
- | "provider_timeout"
477
- /** Output was present but not a valid JSON object/array (rule 18). */
478
- | "provider_malformed"
479
- /** Provider ran but returned an explicit error or threw. */
480
- | "provider_error";
458
+ declare const GOOGLE_DRIVE_CONNECTOR_ID = "google-drive";
481
459
  /**
482
- * A single symbol touched by a diff. The `symbol` name is the load-bearing
483
- * field it is what review-context adds to its match set. `path`/`kind`
484
- * are informational and surface in xray when present.
460
+ * Cursor `kind` we emit. Treated as opaque by the framework; documented
461
+ * here so tests can assert on it.
485
462
  */
486
- interface StructuralSymbol {
487
- /** Qualified or bare symbol name, e.g. `"AuthService.login"` or `"login"`. */
488
- readonly symbol: string;
489
- /** Source file the symbol lives in (forward-slashed, repo-relative when possible). */
490
- readonly path?: string;
491
- /** Coarse kind if the provider knows it (`function`/`method`/`class`/...). */
492
- readonly kind?: string;
493
- }
494
- interface SymbolsForDiffOk {
495
- readonly ok: true;
496
- readonly symbols: StructuralSymbol[];
497
- /** Provider-reported latency in ms, when available. */
498
- readonly latencyMs?: number;
499
- }
500
- interface SymbolsForDiffErr {
501
- readonly ok: false;
502
- readonly code: StructuralContextErrorCode;
503
- readonly detail?: string;
504
- }
505
- type SymbolsForDiffResult = SymbolsForDiffOk | SymbolsForDiffErr;
506
- interface ArchitectureHintsOk {
507
- readonly ok: true;
508
- readonly hints: string[];
509
- }
510
- interface ArchitectureHintsErr {
511
- readonly ok: false;
512
- readonly code: StructuralContextErrorCode;
513
- readonly detail?: string;
514
- }
515
- type ArchitectureHintsResult = ArchitectureHintsOk | ArchitectureHintsErr;
463
+ declare const GOOGLE_DRIVE_CURSOR_KIND = "drivePageToken";
516
464
  /**
517
- * The port contract. Both the built-in subprocess adapter
518
- * (`structural-subprocess-provider.ts`) and the future native engine
519
- * adapter (#1551) implement this. Registry callers retrieve an instance by
520
- * scope; never construct one inline at a call site.
465
+ * Default poll interval (5 minutes). Surfaced in `openclaw.plugin.json`
466
+ * defaults and in the documented config schema. Drive's `changes.list`
467
+ * endpoint is cheap, but polling sub-minute is wasteful for a personal
468
+ * memory layer.
521
469
  */
522
- interface StructuralContextProvider {
523
- /** Stable identifier for registry logging / `remnic doctor`. */
524
- readonly id: string;
525
- /**
526
- * Probe availability. Resolves to a tagged result so a missing binary
527
- * (subprocess) or absent package (native) is surfaced, never thrown
528
- * (rule 34). Implementations SHOULD cache the outcome per instance
529
- * (rule 11).
530
- */
531
- probe(): Promise<{
532
- available: boolean;
533
- detail?: string;
534
- }>;
535
- /**
536
- * Expand a unified diff to the symbols it touches. NEVER throws — every
537
- * failure path returns a tagged `{ ok: false, code }`. The optional
538
- * `signal` lets the caller cancel an in-flight subprocess (rule 40).
539
- */
540
- symbolsForDiff(diff: string, options?: {
541
- signal?: AbortSignal;
542
- }): Promise<SymbolsForDiffResult>;
543
- /**
544
- * Optional architecture hints for a repo root. Used by the architecture
545
- * card composition (#1548 Track A PR 3 + #1554). Implementations MAY omit
546
- * this; callers MUST handle its absence.
547
- */
548
- architectureHints?(root: string, options?: {
549
- signal?: AbortSignal;
550
- }): Promise<ArchitectureHintsResult>;
551
- /** Release any resources the provider holds (subprocess handles, etc.). */
552
- close?(): Promise<void> | void;
553
- }
470
+ declare const DEFAULT_POLL_INTERVAL_MS: number;
554
471
  /**
555
- * Backend tag is a literal so consumers can switch on it programmatically
556
- * alongside `SearchDegradation` (`backend: "qmd"`). `detail` is optional
557
- * and never load-bearing — `code` is the signal.
472
+ * Validated, frozen view of `connectors.googleDrive.*`.
558
473
  */
559
- interface StructuralContextDegradation {
560
- readonly backend: "structural-context";
561
- readonly code: StructuralContextErrorCode;
562
- readonly detail?: string;
474
+ interface GoogleDriveConnectorConfig {
475
+ readonly clientId: string;
476
+ readonly clientSecret: string;
477
+ readonly refreshToken: string;
478
+ /** Poll interval surfaced to the scheduler. */
479
+ readonly pollIntervalMs: number;
480
+ /** Folder ids to scope import to. Empty = "all accessible". */
481
+ readonly folderIds: readonly string[];
563
482
  }
564
483
  /**
565
- * Coerce a {@link SymbolsForDiffErr} into the degradation record consumers
566
- * attach to recall snapshots / xray. Centralised so the `{ backend: ... }`
567
- * tag is set in one place (rule 22 spirit).
484
+ * Optional injection point for tests. The real connector dynamically imports
485
+ * `googleapis`; tests pass a stub here to avoid the optional-peer-dep
486
+ * machinery and to keep the test suite hermetic.
487
+ *
488
+ * The shape only covers the tiny slice of the SDK we actually use.
568
489
  */
569
- declare function toStructuralContextDegradation(err: SymbolsForDiffErr): StructuralContextDegradation;
490
+ interface GoogleDriveClientFactory {
491
+ (config: GoogleDriveConnectorConfig): Promise<GoogleDriveClient>;
492
+ }
570
493
  /**
571
- * Register a provider for a scope (typically an orchestrator `serviceId`).
572
- * Overwriting a previous registration closes the old provider. Returns an
573
- * unregister function callers SHOULD invoke it on service teardown so a
574
- * hot-reloaded host does not leak subprocess handles.
494
+ * Minimal Drive client surface. Tests provide a fake; production wraps
495
+ * `googleapis` to fit. Method shapes mirror the upstream API where it
496
+ * matters (`startPageToken` / `nextPageToken` / `newStartPageToken`,
497
+ * `files.modifiedTime` ISO 8601 strings).
575
498
  */
576
- declare function registerStructuralContextProvider(scope: string, provider: StructuralContextProvider): () => void;
577
- /** Look up the provider registered for a scope, if any. */
578
- declare function getStructuralContextProvider(scope: string): StructuralContextProvider | undefined;
579
- /** Test-only: clear every registered provider. */
580
- declare function clearStructuralContextProvidersForTest(): void;
499
+ interface GoogleDriveClient {
500
+ /** Mirrors `drive.changes.getStartPageToken()`. */
501
+ getStartPageToken(): Promise<{
502
+ startPageToken: string;
503
+ }>;
504
+ /**
505
+ * Mirrors `drive.changes.list(...)`. We page until the response yields a
506
+ * `newStartPageToken` (i.e., no more pages). Each `change.file`, when
507
+ * present, includes the metadata we need to decide whether to ingest.
508
+ */
509
+ listChanges(args: {
510
+ pageToken: string;
511
+ pageSize: number;
512
+ }): Promise<DriveChangesPage>;
513
+ /**
514
+ * Export a Google-native doc to plaintext. Returns the body as a string.
515
+ */
516
+ exportFile(args: {
517
+ fileId: string;
518
+ mimeType: string;
519
+ }): Promise<string>;
520
+ /**
521
+ * Download a non-Google-native file as a string. Used for `text/*` MIME
522
+ * types; binary formats are filtered out before we get here.
523
+ */
524
+ getFileMedia(args: {
525
+ fileId: string;
526
+ }): Promise<string>;
527
+ }
528
+ interface DriveChangesPage {
529
+ readonly changes: readonly DriveChange[];
530
+ readonly newStartPageToken?: string;
531
+ readonly nextPageToken?: string;
532
+ }
533
+ interface DriveChange {
534
+ readonly removed?: boolean;
535
+ readonly fileId?: string;
536
+ readonly file?: DriveFileMetadata;
537
+ }
538
+ interface DriveFileMetadata {
539
+ readonly id: string;
540
+ readonly name?: string;
541
+ readonly mimeType?: string;
542
+ readonly modifiedTime?: string;
543
+ readonly trashed?: boolean;
544
+ readonly parents?: readonly string[];
545
+ readonly webViewLink?: string;
546
+ readonly size?: string | number;
547
+ }
581
548
  /**
582
- * The single gate every review-context consumer consults before consulting a
583
- * provider. Returns `true` iff the master `codingKnowledge.enabled` switch is
584
- * on AND `structuralProvider` is not `"none"`. When this returns `false`,
585
- * review-context runs its pure file-path-only ranker — byte-identical to
586
- * pre-feature behaviour on every path.
549
+ * Result of a single sync pass exposed for richer test assertions.
550
+ * Strict superset of `SyncIncrementalResult`.
587
551
  */
588
- declare function structuralProviderActive(config: PluginConfig): boolean;
552
+ interface GoogleDriveSyncResult extends SyncIncrementalResult {
553
+ readonly skippedBinary: number;
554
+ readonly skippedFolderScope: number;
555
+ readonly skippedTooLarge: number;
556
+ }
589
557
  /**
590
- * Config-only structural provider status. `remnic doctor` and xray render
591
- * this; an async sibling in `structural-subprocess-provider.ts` augments it
592
- * with a live `probed` field.
558
+ * Validate and normalise raw config. Throws with a concrete message on any
559
+ * malformed input never silently defaults (CLAUDE.md gotcha #51).
593
560
  */
594
- interface StructuralProviderStatus {
595
- readonly active: boolean;
596
- readonly mode: CodingKnowledgeConfig["structuralProvider"];
597
- /** Command path when mode is `"subprocess"` and a command is configured. */
598
- readonly command?: string;
599
- /** Provider id when one is registered for the scope. */
600
- readonly providerId?: string;
601
- /** Live probe outcome — set only by the async doctor probe. */
602
- probed?: {
603
- available: boolean;
604
- detail?: string;
605
- };
606
- }
561
+ declare function validateGoogleDriveConfig(raw: unknown): GoogleDriveConnectorConfig;
607
562
  /**
608
- * Summarise the structural provider status from config ALONE (no probe, no
609
- * I/O). Pure safe to call from any context, including the recall hot path
610
- * for xray labels.
563
+ * Construct the connector. The `clientFactory` argument is the test hook
564
+ * production callers omit it and the connector lazy-loads `googleapis`.
611
565
  */
612
- declare function describeStructuralProviderStatus(config: PluginConfig, scope?: string): StructuralProviderStatus;
566
+ declare function createGoogleDriveConnector(options?: {
567
+ clientFactory?: GoogleDriveClientFactory;
568
+ }): LiveConnector;
613
569
  /**
614
- * Render a single human-readable line for `remnic doctor`. Pure.
570
+ * Production client factory. Lazy-loads `googleapis` via a computed-specifier
571
+ * dynamic import so bundlers never statically resolve it (CLAUDE.md gotcha
572
+ * #57). Surfaces a precise install hint on miss.
615
573
  *
616
- * `structural-context provider: none (inactive review-context stays file-path-only)`
617
- * `structural-context provider: subprocess, command=/usr/local/bin/cbm, probed=available`
618
- * `structural-context provider: subprocess, command=/usr/local/bin/cbm, probed=unavailable (binary not found)`
574
+ * Exported only for the `index.ts` barrel; consumers that already inject a
575
+ * test factory don't need to touch this.
619
576
  */
620
- declare function renderStructuralProviderStatusLine(status: StructuralProviderStatus): string;
577
+ declare const defaultGoogleDriveClientFactory: GoogleDriveClientFactory;
621
578
 
622
579
  /**
623
- * Diff-aware review-context packer (issue #569 PR 4).
580
+ * @remnic/core Notion live connector (issue #683 PR 3/N)
624
581
  *
625
- * When an agent is asked "review this PR" / "what changed in this diff" /
626
- * "look at this diff", the prompt that reaches recall is short and generic
627
- * the real signal is the diff itself. This module:
582
+ * Concrete `LiveConnector` implementation that incrementally imports text
583
+ * content from Notion database pages into Remnic. Built on top of the
584
+ * framework shipped in PR 1/N (`framework.ts` / `registry.ts` /
585
+ * `state-store.ts`) and mirrors the structure of the Google Drive connector
586
+ * (PR 2/N).
628
587
  *
629
- * 1. Detects review-intent prompts via `isReviewPrompt`.
630
- * 2. Extracts the touched file list from a unified diff via
631
- * `parseTouchedFiles`.
632
- * 3. Re-ranks a set of candidate memories so that memories whose
633
- * `entityRefs` mention a touched path float to the top. The boost is
634
- * additive and bounded so it doesn't obliterate the original ranking —
635
- * it's a bias, not a filter.
588
+ * Design notes:
636
589
  *
637
- * Pure no orchestrator, no storage. Callers inject the candidate memories
638
- * they already have from their normal recall pipeline. This keeps the
639
- * module easy to test and integrates cleanly with the existing tiered-recall
640
- * code in `orchestrator.ts` (the tier itself can be wired later; the pure
641
- * surface is what PRs 5/6/7 will call).
590
+ * - **Auth.** Integration token from config (`connectors.notion.token`).
591
+ * The token is accepted at config-parse time but never logged. Operators
592
+ * must populate it from a secret store; per the repo-wide privacy policy
593
+ * no real value may appear in tests or comments.
594
+ *
595
+ * - **Scope.** `databaseIds` in config limits the import to the listed
596
+ * Notion databases. The connector queries each database for pages whose
597
+ * `last_edited_time` is after a per-page high-water mark stored in the
598
+ * cursor. When `databaseIds` is empty the connector does nothing (safe
599
+ * default — no credentials → no import).
600
+ *
601
+ * - **Cursor semantics.** The cursor is a JSON string encoding a
602
+ * `NotionCursorPayload`: a map from page-id to last-seen
603
+ * `last_edited_time` ISO string. On the first sync (cursor=null) we
604
+ * seed the payload from the current state of each database WITHOUT
605
+ * importing any content, so "first install" doesn't re-ingest history.
606
+ * Each subsequent pass only imports pages edited after the stored
607
+ * watermark.
608
+ *
609
+ * - **Block extraction.** Page content is fetched via
610
+ * `blocks.children.list` recursively up to `MAX_BLOCK_DEPTH` levels.
611
+ * Block text is extracted to Markdown-ish plain text (no raw JSON blobs).
612
+ * Only text-bearing block types are included; unsupported types are
613
+ * silently skipped.
614
+ *
615
+ * - **Raw `fetch`.** We call the Notion REST API directly rather than using
616
+ * `@notionhq/client` — there is no optional-peer-dep machinery needed and
617
+ * the API surface we consume is tiny. The `fetchFn` argument is the test
618
+ * hook allowing stubbing without network access.
619
+ *
620
+ * - **Idempotency.** `ConnectorDocument.source.externalId` is the page id
621
+ * and `externalRevision` is `last_edited_time`, so downstream dedup can
622
+ * recognise repeat fetches if the cursor is rewound.
623
+ *
624
+ * - **Privacy.** No page content is ever logged. Database ids and counts
625
+ * may be logged. The integration token is never exposed in logs, state,
626
+ * or error messages.
627
+ *
628
+ * - **Read-only.** This connector only reads. It never modifies pages,
629
+ * databases, or any other Notion resource.
642
630
  */
643
631
 
632
+ /** Stable connector id. Lives in the registry under this exact string. */
633
+ declare const NOTION_CONNECTOR_ID = "notion";
644
634
  /**
645
- * A memory candidate as fed into review-context ranking. The shape is a
646
- * deliberate subset of the core `MemorySummary` / recall result — only the
647
- * fields we actually need — so this module stays decoupled from the rest of
648
- * the codebase and can be reused by CLI tools, bench fixtures, etc.
635
+ * Cursor `kind` we emit. Opaque to the framework; documented here so
636
+ * tests can assert on it.
649
637
  */
650
- interface ReviewCandidate {
651
- /** Opaque identifier. Echoed unchanged in the output. */
652
- id: string;
653
- /**
654
- * Pre-review relevance score from the upstream recall pipeline. Higher is
655
- * better. `0` is treated as "no prior signal" and gets the full review
656
- * boost when a path match is found.
657
- */
658
- score: number;
659
- /**
660
- * References the memory mentions (file paths, entity names, etc.). Used
661
- * to decide whether any touched file appears in the memory's scope.
662
- *
663
- * Accepts `undefined`/missing so callers can pass sparse records from
664
- * legacy storage without pre-filling.
665
- */
666
- entityRefs?: string[];
667
- }
668
- interface ReviewContext {
669
- /**
670
- * Normalized file paths touched by the diff. Each entry is forward-slashed
671
- * and relative to the repo root when possible.
672
- */
673
- touchedFiles: string[];
674
- /**
675
- * Candidates re-sorted so memories whose `entityRefs` mention a touched
676
- * path are boosted. Shape matches the input `ReviewCandidate[]` — the
677
- * boost is recorded on each entry as `boost` for observability.
678
- */
679
- rankedRecall: Array<ReviewCandidate & {
680
- boost: number;
681
- }>;
682
- /**
683
- * Structural-context provider degradation observed while expanding the
684
- * diff to touched symbols (issue #1548 Track A PR 5, #1536 pattern).
685
- * Present only when a provider was consulted AND it failed — a silent
686
- * provider failure must never look like "no structural matches"
687
- * (CLAUDE.md rule 34). Undefined when no provider was consulted (the
688
- * pure {@link packReviewContext} path) or when the provider succeeded.
689
- */
690
- structuralDegradation?: StructuralContextDegradation;
691
- }
692
- /**
693
- * `true` when the prompt looks like a review / diff-explanation request.
694
- *
695
- * Empty / non-string input → `false` (the caller shouldn't branch on an
696
- * invalid prompt).
697
- */
698
- declare function isReviewPrompt(prompt: string | null | undefined): boolean;
638
+ declare const NOTION_CURSOR_KIND = "notionWatermark";
699
639
  /**
700
- * Parse a unified diff and return the set of files touched. Accepts both the
701
- * `diff --git` form (`diff --git a/foo b/bar`) and the `--- / +++` form
702
- * (`--- a/foo\n+++ b/bar`). Returns deduplicated, repo-root-relative paths
703
- * (with the conventional `a/` / `b/` prefixes stripped).
704
- *
705
- * Path entries of `/dev/null` (used in adds/deletes) are excluded.
640
+ * Default poll interval (5 minutes). Notion's API has no push capability;
641
+ * polling sub-minute wastes quota for a personal memory layer.
706
642
  */
707
- declare function parseTouchedFiles(diff: string | null | undefined): string[];
643
+ declare const NOTION_DEFAULT_POLL_INTERVAL_MS: number;
708
644
  /**
709
- * Build a review-context ranking for a set of candidate memories.
710
- *
711
- * Contract:
712
- * - `touchedFiles` is the parsed diff file list.
713
- * - `candidates` is passed through unchanged when no boost applies.
714
- * - When a boost applies, the result is sorted by `(score + boost)` desc,
715
- * with a stable secondary sort on the original `id` for determinism
716
- * (CLAUDE.md #19 — comparators must return 0 for equal items).
645
+ * Validated, frozen view of `connectors.notion.*`.
717
646
  */
718
- declare function rankReviewCandidates(candidates: ReviewCandidate[], touchedFiles: string[]): Array<ReviewCandidate & {
719
- boost: number;
720
- }>;
721
- interface PackReviewContextInput {
722
- /** Unified diff, as produced by `git diff`. */
723
- diff: string | null | undefined;
724
- /** Candidate memories from the upstream recall pipeline. */
725
- candidates: ReviewCandidate[];
647
+ interface NotionConnectorConfig {
648
+ /** Notion integration token. Starts with `secret_`. */
649
+ readonly token: string;
650
+ /** Database ids to import pages from. Empty = connector is a no-op. */
651
+ readonly databaseIds: readonly string[];
652
+ /** Poll interval surfaced to the scheduler (ms). */
653
+ readonly pollIntervalMs: number;
726
654
  }
727
655
  /**
728
- * Top-level entry point used by the orchestrator (and CLI / bench) when a
729
- * review-intent prompt is detected.
730
- *
731
- * Parses the diff, re-ranks the candidates, and returns both artefacts so
732
- * the caller can surface `touchedFiles` as context and `rankedRecall` as
733
- * the recall result.
656
+ * Minimal fetch-compatible surface we use. The real connector delegates to
657
+ * the global `fetch`; tests inject a stub factory.
734
658
  */
735
- declare function packReviewContext(input: PackReviewContextInput): ReviewContext;
659
+ type NotionFetchFn = (url: string, init: {
660
+ method: string;
661
+ headers: Record<string, string>;
662
+ body?: string;
663
+ signal?: AbortSignal;
664
+ }) => Promise<{
665
+ ok: boolean;
666
+ status: number;
667
+ json(): Promise<unknown>;
668
+ }>;
736
669
  /**
737
- * Input for the structural-context-aware review-context packer. The pure
738
- * {@link packReviewContext} stays unchanged (gate-off parity); this async
739
- * variant consults a {@link StructuralContextProvider} to expand the diff's
740
- * touched FILE paths into touched SYMBOL names, widening the match set so
741
- * memories that mention a changed symbol — not just the file path — float
742
- * up. The gate (`structuralProvider !== "none"`) is checked ONCE by the
743
- * caller, never inside this function (rule 39).
670
+ * Validate and normalise raw config. Throws with a concrete message on any
671
+ * malformed input never silently defaults (CLAUDE.md gotcha #51).
744
672
  */
745
- interface PackReviewContextStructuralInput extends PackReviewContextInput {
746
- /** A probed structural-context provider. Never undefined here. */
747
- provider: StructuralContextProvider;
748
- /** Cancellation forwarded to the provider subprocess (rule 40). */
749
- signal?: AbortSignal;
750
- }
673
+ declare function validateNotionConfig(raw: unknown): NotionConnectorConfig;
751
674
  /**
752
- * Structural-context-aware packer.
753
- *
754
- * Contract (issue #1548 Track A PR 5, prove-fail-before characterization):
755
- * - Provider returns symbols (`ok: true`) → symbol names are ADDED to the
756
- * match set alongside touched files; memories mentioning a symbol get the
757
- * same bounded additive boost as a file-path match.
758
- * - Provider fails (`ok: false`, any code) → ranking falls back to
759
- * FILE-PATH-ONLY boosting, BYTE-IDENTICAL to {@link packReviewContext}
760
- * (the match set is exactly the touched files), AND `structuralDegradation`
761
- * is populated so xray/doctor can surface the failure (rule 34).
762
- *
763
- * The underlying ranker ({@link rankReviewCandidates}) is reused unchanged,
764
- * so the deterministic ordering, boost cap, and tie-break are identical to
765
- * the pure path.
675
+ * Construct the connector. The `fetchFn` argument is the test hook —
676
+ * production callers omit it and the connector uses the global `fetch`.
766
677
  */
767
- declare function packReviewContextStructural(input: PackReviewContextStructuralInput): Promise<ReviewContext>;
678
+ declare function createNotionConnector(options?: {
679
+ fetchFn?: NotionFetchFn;
680
+ }): LiveConnector;
768
681
 
769
- /** Injectable spawn shape so tests substitute a stub (rule 33). */
770
- type StructuralSpawnFn = (command: string, argv: readonly string[], options: {
771
- timeout: number;
772
- signal?: AbortSignal;
773
- }) => Promise<{
774
- stdout: string;
775
- stderr: string;
776
- }>;
777
- interface SubprocessProviderOptions {
778
- /** Absolute path to the binary (rule 24 — statSync at probe time). */
779
- readonly command: string;
682
+ interface GitContext {
780
683
  /**
781
- * Extra argv appended after the symbols-for-diff subcommand
782
- * (rule 10 array, never a shell string).
684
+ * Stable identifier for the project. Derived from `git remote get-url origin`
685
+ * when an origin remote is configured, otherwise from the repo root path.
686
+ *
687
+ * Formatted as `origin:<hex>` or `root:<hex>` so that the source is visible
688
+ * to operators (see `remnic doctor`, issue #569 acceptance criteria).
783
689
  */
784
- readonly args?: readonly string[];
785
- /** Per-call deadline in ms. Default 5000. */
786
- readonly timeoutMs?: number;
690
+ projectId: string;
787
691
  /**
788
- * Subcommand the provider invokes for symbol expansion.
789
- * Default `"symbols-for-diff"`.
692
+ * Current branch, e.g. `main`, `feat/foo`. `null` only in detached-HEAD
693
+ * state (e.g. rebase in progress). Callers should treat `null` as "no
694
+ * branch-scope overlay applies" without erroring.
790
695
  */
791
- readonly symbolsSubcommand?: string;
792
- /** Test seam — defaults to promisified `execFile`. */
793
- readonly spawn?: StructuralSpawnFn;
696
+ branch: string | null;
697
+ /**
698
+ * Absolute path to the repository root (the directory containing `.git`).
699
+ * Tilde-expanded per CLAUDE.md #17.
700
+ */
701
+ rootPath: string;
702
+ /**
703
+ * Best-effort default branch (usually `main` or `master`). Derived from the
704
+ * `refs/remotes/origin/HEAD` symbolic ref. `null` when not available (e.g.
705
+ * fresh clone without a default branch symref, or no origin remote).
706
+ */
707
+ defaultBranch: string | null;
794
708
  }
795
709
  /**
796
- * Construct a structural-context provider backed by an external subprocess.
797
- * The instance is self-contained: callers register it via
798
- * `registerStructuralContextProvider(scope, provider)` and dispose via the
799
- * returned unregister handle.
710
+ * Injectable git-invocation surface. Only the commands `resolveGitContext`
711
+ * actually needs are exposed. Tests inject a mock implementation to avoid
712
+ * spawning a real git process.
800
713
  */
801
- declare function createSubprocessStructuralProvider(options: SubprocessProviderOptions): StructuralContextProvider;
714
+ interface GitInvoker {
715
+ /**
716
+ * Run `git <args>` with `cwd` as the working directory. Must return
717
+ * `{ stdout, exitCode }` with `stdout` trimmed by the caller as needed.
718
+ * Implementations should NOT throw for non-zero exit codes — they should
719
+ * return the exit code so the resolver can decide how to recover.
720
+ */
721
+ (cwd: string, args: string[]): {
722
+ stdout: string;
723
+ exitCode: number;
724
+ };
725
+ }
802
726
  /**
803
- * Augment the pure {@link describeStructuralProviderStatus} with a LIVE probe
804
- * so `remnic doctor` can render "configured / probed / last error code".
805
- *
806
- * - `"none"` → inactive (no probe).
807
- * - `"subprocess"` → builds a throwaway provider from
808
- * `structuralProviderCommand`, probes it, disposes it.
809
- * - `"native"` → asks `isCodingGraphInstalled()` whether the optional
810
- * @remnic/coding-graph peer is present (the adapter that
811
- * turns the engine into a provider lands in #1551).
727
+ * Non-cryptographic stable hash. Used only to derive a deterministic
728
+ * `projectId` from either the origin URL or the root path. The hash does not
729
+ * need to be collision-resistant against adversarial input — it is purely a
730
+ * namespace discriminator.
812
731
  *
813
- * Never throws a probe failure populates `probed.available = false`.
732
+ * Uses FNV-1a 32-bit so we don't pull in `node:crypto` for a simple bucket
733
+ * key. Output is lowercase hex, zero-padded to 8 characters.
814
734
  */
815
- declare function probeStructuralProviderForDoctor(config: PluginConfig): Promise<StructuralProviderStatus>;
816
-
735
+ declare function stableHash(input: string): string;
817
736
  /**
818
- * Binary file lifecycle management types.
737
+ * Normalize a git remote URL so that equivalent SSH / HTTPS forms of the
738
+ * same repo produce the same `projectId`. Handles:
739
+ * - `git@github.com:foo/bar.git` → `github.com/foo/bar`
740
+ * - `https://github.com/foo/bar` → `github.com/foo/bar`
741
+ * - `https://github.com/foo/bar.git` → `github.com/foo/bar`
742
+ * - `ssh://git@github.com/foo/bar` → `github.com/foo/bar`
743
+ * - `ssh://git@github.com:2222/foo/bar` → `github.com/foo/bar` (port stripped)
819
744
  *
820
- * Defines the configuration, manifest, and record structures for the
821
- * three-stage binary lifecycle pipeline: mirror, redirect, clean.
745
+ * Case-insensitive (remote hostnames and most repo paths on major forges are
746
+ * case-insensitive in practice).
822
747
  */
823
- interface BinaryLifecycleConfig {
824
- /** Master toggle. Default: false. */
825
- enabled: boolean;
826
- /** Days after mirror before local copy is eligible for cleanup. Default: 7. */
827
- gracePeriodDays: number;
828
- /** Files larger than this are skipped during scan. Default: 50 MB. */
829
- maxBinarySizeBytes: number;
830
- /** Glob patterns for binary file types to manage. */
831
- scanPatterns: string[];
832
- /** Backend configuration for binary storage. */
833
- backend: BinaryStorageBackendConfig;
834
- }
835
- interface BinaryStorageBackendConfig {
836
- /** Backend type. "filesystem" copies to a local directory. "none" is a no-op (dry-run/testing). */
837
- type: "filesystem" | "s3" | "none";
838
- /** Destination directory for the filesystem backend. */
839
- basePath?: string;
840
- /** S3 bucket name (future). */
841
- s3Bucket?: string;
842
- /** S3 region (future). */
843
- s3Region?: string;
844
- /** S3 key prefix (future). */
845
- s3Prefix?: string;
748
+ declare function normalizeOriginUrl(rawUrl: string): string;
749
+ interface ResolveGitContextOptions {
750
+ /** Inject a git invoker (tests). Defaults to spawning real `git`. */
751
+ invoker?: GitInvoker;
846
752
  }
847
- type BinaryAssetStatus = "pending" | "mirrored" | "redirected" | "cleaned" | "error";
848
- interface BinaryAssetRecord {
849
- /** Relative path from memoryDir to the original file. */
850
- originalPath: string;
851
- /** Path (or URL) in the backend storage. */
852
- mirroredPath: string;
853
- /** Optional user-resolvable target to write into markdown links. */
854
- redirectPath?: string;
855
- /** SHA-256 hex digest of file content. */
856
- contentHash: string;
857
- /** File size in bytes. */
858
- sizeBytes: number;
859
- /** MIME type (e.g. "image/png"). */
860
- mimeType: string;
861
- /** ISO 8601 timestamp when the file was mirrored. */
862
- mirroredAt: string;
863
- /** ISO 8601 timestamp when markdown references were rewritten. */
864
- redirectedAt?: string;
865
- /** ISO 8601 timestamp when the local copy was deleted. */
866
- cleanedAt?: string;
867
- /** Current lifecycle status. */
868
- status: BinaryAssetStatus;
869
- }
870
- interface BinaryLifecycleManifest {
871
- version: 1;
872
- assets: BinaryAssetRecord[];
873
- lastScanAt?: string;
874
- }
875
- interface PipelineResult {
876
- scanned: number;
877
- mirrored: number;
878
- redirected: number;
879
- cleaned: number;
880
- errors: string[];
881
- dryRun: boolean;
882
- }
883
- declare const DEFAULT_SCAN_PATTERNS: string[];
884
- declare const DEFAULT_MAX_BINARY_SIZE_BYTES: number;
885
- declare const DEFAULT_GRACE_PERIOD_DAYS = 7;
886
-
887
753
  /**
888
- * Binary storage backend interface and implementations.
754
+ * Detect the git project + branch for `cwd`.
889
755
  *
890
- * Backends handle the actual persistence of binary files to an external
891
- * location. The pipeline calls upload/exists/delete through this interface
892
- * so swapping storage providers requires no pipeline changes.
893
- */
894
-
895
- interface BinaryStorageBackend {
896
- /** Discriminator for the backend type. */
897
- readonly type: string;
898
- /**
899
- * Upload a local file to the backend.
900
- * @returns The backend path or URL where the file was stored.
901
- */
902
- upload(localPath: string, remotePath: string): Promise<string>;
903
- /** Check whether a remote path already exists in the backend. */
904
- exists(remotePath: string): Promise<boolean>;
905
- /** Delete a file from the backend. */
906
- delete(remotePath: string): Promise<void>;
907
- /** Return the user-resolvable markdown target for a stored backend path. */
908
- getRedirectTarget?(remotePath: string): string;
909
- }
910
- declare class FilesystemBackend implements BinaryStorageBackend {
911
- readonly type = "filesystem";
912
- private readonly basePath;
913
- constructor(basePath: string);
914
- private resolveRemotePath;
915
- private isInsideBase;
916
- private realBasePathIfExists;
917
- private ensureBaseDirectory;
918
- private ensureSafeParentDirectory;
919
- private resolveExistingRemotePath;
920
- upload(localPath: string, remotePath: string): Promise<string>;
921
- exists(remotePath: string): Promise<boolean>;
922
- delete(remotePath: string): Promise<void>;
923
- getRedirectTarget(remotePath: string): string;
924
- }
925
- declare class NoneBackend implements BinaryStorageBackend {
926
- readonly type = "none";
927
- upload(_localPath: string, remotePath: string): Promise<string>;
928
- exists(_remotePath: string): Promise<boolean>;
929
- delete(_remotePath: string): Promise<void>;
930
- }
931
- declare function createBackend(cfg: BinaryStorageBackendConfig): BinaryStorageBackend;
932
-
933
- /**
934
- * Binary file scanner.
756
+ * Returns `null` when:
757
+ * - `cwd` is not an absolute path (invalid input, CLAUDE.md #51)
758
+ * - `cwd` is not inside a git worktree
759
+ * - `git` is not available on PATH
935
760
  *
936
- * Recursively walks the memory directory, matches files against configured
937
- * glob patterns, skips files already tracked in the manifest, and respects
938
- * the max-size limit.
939
- */
940
-
941
- /**
942
- * Test whether a filename matches any of the provided glob patterns.
943
- * Supports simple `*.ext` patterns (the default scan patterns).
944
- * For more complex globs a proper library should be used; this covers
945
- * the 95% case without adding a dependency.
946
- */
947
- declare function matchesPatterns(filename: string, patterns: string[]): boolean;
948
- /**
949
- * Scan memoryDir recursively for binary files matching the configured patterns.
950
- * Returns relative paths (relative to memoryDir) for files not yet tracked.
761
+ * Never throws.
951
762
  */
952
- declare function scanForBinaries(memoryDir: string, config: BinaryLifecycleConfig, manifest: BinaryLifecycleManifest): Promise<string[]>;
763
+ declare function resolveGitContext(cwd: string, options?: ResolveGitContextOptions): Promise<GitContext | null>;
953
764
 
954
765
  /**
955
- * Binary lifecycle manifest read/write operations.
766
+ * Structural-context provider port (issue #1548 Track A PR 5).
956
767
  *
957
- * The manifest lives at `${memoryDir}/.binary-lifecycle/manifest.json`.
958
- * Writes use the atomic temp-then-rename pattern (CLAUDE.md #54).
768
+ * The architectural boundary between Track A (durable coding-memory
769
+ * features) and Track B (the native @remnic/coding-graph engine). A
770
+ * structural-context provider expands a unified diff into the SYMBOLS it
771
+ * touches (and optionally yields architecture hints), so review-intent
772
+ * recall can boost memories that mention those symbols — not just the
773
+ * bare file paths that `review-context.ts` already boosts on.
774
+ *
775
+ * Three providers satisfy this port, selected by
776
+ * `codingKnowledge.structuralProvider`:
777
+ *
778
+ * - `"none"` — no provider consulted (default; review-context stays
779
+ * file-path-only, byte-identical to pre-feature — rule 39).
780
+ * - `"subprocess"` — an external binary shelled out via `execFile` with an
781
+ * argv ARRAY (never a shell string — rule 10). The
782
+ * built-in adapter lives in `structural-subprocess-provider.ts`;
783
+ * codebase-memory-mcp's CLI mode is the canonical target,
784
+ * but the provider is command-agnostic.
785
+ * - `"native"` — the in-family @remnic/coding-graph engine, adapted to
786
+ * this port by a future PR (#1551/#1554). Only the enum
787
+ * value and this doc note land in this PR — no engine
788
+ * code. codebase-memory-mcp remains a supported
789
+ * subprocess provider through this same port regardless.
790
+ *
791
+ * Every result is a tagged outcome shaped like #1536's `SearchDegradation`
792
+ * (CLAUDE.md rule 34 — a degraded/absent provider must NEVER masquerade as
793
+ * "no structural matches"):
794
+ * - `{ ok: true, symbols }` on success
795
+ * - `{ ok: false, code }` on every failure (distinct codes per mode)
796
+ *
797
+ * No module-level mutable state (rule 11): the registry is keyed per
798
+ * orchestrator/service instance via a `Symbol.for(...)` slot on
799
+ * `globalThis`, mirroring `host-embedding-provider.ts`.
959
800
  */
960
801
 
961
- declare function manifestDir(memoryDir: string): string;
962
- declare function manifestPath(memoryDir: string): string;
963
802
  /**
964
- * Read the manifest from disk. Returns a fresh empty manifest if the file
965
- * does not exist. Existing invalid manifests fail closed so the pipeline does
966
- * not overwrite state needed for safe cleanup.
803
+ * Distinct failure codes for the structural-context port. Every code is
804
+ * load-bearing: callers (review-context, `remnic doctor`, xray) switch on
805
+ * `code` to distinguish "binary not installed" from "timed out" from
806
+ * "protocol violation" so a silent provider failure can never look like an
807
+ * empty-but-healthy result.
967
808
  */
968
- declare function readManifest(memoryDir: string): Promise<BinaryLifecycleManifest>;
809
+ type StructuralContextErrorCode =
810
+ /** Probe failed: binary missing / not executable, or native package absent. */
811
+ "provider_unavailable"
812
+ /** The provider exceeded its deadline. */
813
+ | "provider_timeout"
814
+ /** Output was present but not a valid JSON object/array (rule 18). */
815
+ | "provider_malformed"
816
+ /** Provider ran but returned an explicit error or threw. */
817
+ | "provider_error";
969
818
  /**
970
- * Write the manifest atomically: write to a temp file, then rename.
971
- * CLAUDE.md #54: never delete before write. Write temp first, rename atomically.
819
+ * A single symbol touched by a diff. The `symbol` name is the load-bearing
820
+ * field it is what review-context adds to its match set. `path`/`kind`
821
+ * are informational and surface in xray when present.
972
822
  */
973
- declare function writeManifest(memoryDir: string, manifest: BinaryLifecycleManifest): Promise<void>;
974
- declare function emptyManifest(): BinaryLifecycleManifest;
975
-
823
+ interface StructuralSymbol {
824
+ /** Qualified or bare symbol name, e.g. `"AuthService.login"` or `"login"`. */
825
+ readonly symbol: string;
826
+ /** Source file the symbol lives in (forward-slashed, repo-relative when possible). */
827
+ readonly path?: string;
828
+ /** Coarse kind if the provider knows it (`function`/`method`/`class`/...). */
829
+ readonly kind?: string;
830
+ }
831
+ interface SymbolsForDiffOk {
832
+ readonly ok: true;
833
+ readonly symbols: StructuralSymbol[];
834
+ /** Provider-reported latency in ms, when available. */
835
+ readonly latencyMs?: number;
836
+ }
837
+ interface SymbolsForDiffErr {
838
+ readonly ok: false;
839
+ readonly code: StructuralContextErrorCode;
840
+ readonly detail?: string;
841
+ }
842
+ type SymbolsForDiffResult = SymbolsForDiffOk | SymbolsForDiffErr;
843
+ interface ArchitectureHintsOk {
844
+ readonly ok: true;
845
+ readonly hints: string[];
846
+ }
847
+ interface ArchitectureHintsErr {
848
+ readonly ok: false;
849
+ readonly code: StructuralContextErrorCode;
850
+ readonly detail?: string;
851
+ }
852
+ type ArchitectureHintsResult = ArchitectureHintsOk | ArchitectureHintsErr;
976
853
  /**
977
- * Binary lifecycle pipeline mirror, redirect, clean.
978
- *
979
- * Three-stage pipeline:
980
- * 1. Mirror: upload binary to backend, record in manifest
981
- * 2. Redirect: scan markdown for inline refs, replace with redirect path
982
- * 3. Clean: after grace period, delete local copy
854
+ * The port contract. Both the built-in subprocess adapter
855
+ * (`structural-subprocess-provider.ts`) and the future native engine
856
+ * adapter (#1551) implement this. Registry callers retrieve an instance by
857
+ * scope; never construct one inline at a call site.
983
858
  */
984
-
985
- /** Minimal logger interface so we don't depend on the full logger module. */
986
- interface PipelineLogger {
987
- info(msg: string): void;
988
- warn(msg: string): void;
989
- error(msg: string): void;
859
+ interface StructuralContextProvider {
860
+ /** Stable identifier for registry logging / `remnic doctor`. */
861
+ readonly id: string;
862
+ /**
863
+ * Probe availability. Resolves to a tagged result so a missing binary
864
+ * (subprocess) or absent package (native) is surfaced, never thrown
865
+ * (rule 34). Implementations SHOULD cache the outcome per instance
866
+ * (rule 11).
867
+ */
868
+ probe(): Promise<{
869
+ available: boolean;
870
+ detail?: string;
871
+ }>;
872
+ /**
873
+ * Expand a unified diff to the symbols it touches. NEVER throws — every
874
+ * failure path returns a tagged `{ ok: false, code }`. The optional
875
+ * `signal` lets the caller cancel an in-flight subprocess (rule 40).
876
+ */
877
+ symbolsForDiff(diff: string, options?: {
878
+ signal?: AbortSignal;
879
+ }): Promise<SymbolsForDiffResult>;
880
+ /**
881
+ * Optional architecture hints for a repo root. Used by the architecture
882
+ * card composition (#1548 Track A PR 3 + #1554). Implementations MAY omit
883
+ * this; callers MUST handle its absence.
884
+ */
885
+ architectureHints?(root: string, options?: {
886
+ signal?: AbortSignal;
887
+ }): Promise<ArchitectureHintsResult>;
888
+ /** Release any resources the provider holds (subprocess handles, etc.). */
889
+ close?(): Promise<void> | void;
990
890
  }
991
- type ReadMarkdownFile = (filePath: string) => Promise<string>;
992
- type WriteMarkdownFile = (filePath: string, content: string) => Promise<void>;
993
- interface PipelineOptions {
994
- dryRun?: boolean;
995
- /** Force-clean all files past grace period, ignoring redirect status. */
996
- forceClean?: boolean;
997
- /** Test hook for deterministic markdown read failures. */
998
- readMarkdownFile?: ReadMarkdownFile;
999
- /** Test hook for deterministic markdown write failures. */
1000
- writeMarkdownFile?: WriteMarkdownFile;
891
+ /**
892
+ * Backend tag is a literal so consumers can switch on it programmatically
893
+ * alongside `SearchDegradation` (`backend: "qmd"`). `detail` is optional
894
+ * and never load-bearing — `code` is the signal.
895
+ */
896
+ interface StructuralContextDegradation {
897
+ readonly backend: "structural-context";
898
+ readonly code: StructuralContextErrorCode;
899
+ readonly detail?: string;
1001
900
  }
1002
901
  /**
1003
- * Run the binary lifecycle pipeline: scan, mirror, redirect, clean.
902
+ * Coerce a {@link SymbolsForDiffErr} into the degradation record consumers
903
+ * attach to recall snapshots / xray. Centralised so the `{ backend: ... }`
904
+ * tag is set in one place (rule 22 spirit).
1004
905
  */
1005
- declare function runBinaryLifecyclePipeline(memoryDir: string, config: BinaryLifecycleConfig, backend: BinaryStorageBackend, log: PipelineLogger, opts?: PipelineOptions): Promise<PipelineResult>;
1006
-
906
+ declare function toStructuralContextDegradation(err: SymbolsForDiffErr): StructuralContextDegradation;
1007
907
  /**
1008
- * @remnic/core Workspace Tree Projection
1009
- *
1010
- * Generates a human-readable `.engram/context-tree/` from canonical memory.
1011
- * Each node is a `.md` file with rich metadata, * (provenance, trust, confidence, source anchors).
1012
- * Manual edits are preserved in fenced blocks.
908
+ * Register a provider for a scope (typically an orchestrator `serviceId`).
909
+ * Overwriting a previous registration closes the old provider. Returns an
910
+ * unregister function callers SHOULD invoke it on service teardown so a
911
+ * hot-reloaded host does not leak subprocess handles.
1013
912
  */
1014
- interface TreeNode {
1015
- /** Relative path from context-tree root, e.g. "entities/claude.md" */
1016
- path: string;
1017
- /** Category from canonical memory */
1018
- category: string;
1019
- /** Human-readable title */
1020
- title: string;
1021
- /** File content (rendered markdown) */
1022
- content: string;
1023
- /** Source memory IDs that contributed to this node */
1024
- sourceAnchors: string[];
1025
- /** Confidence (0-1) */
1026
- confidence: number;
1027
- /** Trust zone classification */
1028
- confidenceTier: string;
1029
- /** When this node was generated */
1030
- generatedAt: string;
1031
- /** Provenance chain */
1032
- provenance: ProvenanceEntry[];
1033
- }
1034
- interface ProvenanceEntry {
1035
- memoryId: string;
1036
- source: string;
1037
- extracted: string;
1038
- }
1039
- interface GenerateOptions {
1040
- /** Memory root directory (e.g. ~/.openclaw/workspace/memory/local) */
1041
- memoryDir: string;
1042
- /** Output directory (e.g. .engram/context-tree) */
1043
- outputDir: string;
1044
- /** Categories to include (default: all) */
1045
- categories?: string[];
1046
- /** Whether to include entity graph */
1047
- includeEntities?: boolean;
1048
- /** Whether to include orphaned questions */
1049
- includeQuestions?: boolean;
1050
- /** Max nodes per category (default: unlimited) */
1051
- maxPerCategory?: number;
1052
- /** Whether to watch for changes and regenerate incrementally */
1053
- watch?: boolean;
1054
- }
1055
- interface GenerateResult {
1056
- nodesGenerated: number;
1057
- nodesSkipped: number;
1058
- categories: Record<string, number>;
1059
- durationMs: number;
1060
- outputDir: string;
913
+ declare function registerStructuralContextProvider(scope: string, provider: StructuralContextProvider): () => void;
914
+ /** Look up the provider registered for a scope, if any. */
915
+ declare function getStructuralContextProvider(scope: string): StructuralContextProvider | undefined;
916
+ /** Test-only: clear every registered provider. */
917
+ declare function clearStructuralContextProvidersForTest(): void;
918
+ /**
919
+ * The single gate every review-context consumer consults before consulting a
920
+ * provider. Returns `true` iff the master `codingKnowledge.enabled` switch is
921
+ * on AND `structuralProvider` is not `"none"`. When this returns `false`,
922
+ * review-context runs its pure file-path-only ranker byte-identical to
923
+ * pre-feature behaviour on every path.
924
+ */
925
+ declare function structuralProviderActive(config: PluginConfig): boolean;
926
+ /**
927
+ * Config-only structural provider status. `remnic doctor` and xray render
928
+ * this; an async sibling in `structural-subprocess-provider.ts` augments it
929
+ * with a live `probed` field.
930
+ */
931
+ interface StructuralProviderStatus {
932
+ readonly active: boolean;
933
+ readonly mode: CodingKnowledgeConfig["structuralProvider"];
934
+ /** Command path when mode is `"subprocess"` and a command is configured. */
935
+ readonly command?: string;
936
+ /** Provider id when one is registered for the scope. */
937
+ readonly providerId?: string;
938
+ /** Live probe outcome — set only by the async doctor probe. */
939
+ probed?: {
940
+ available: boolean;
941
+ detail?: string;
942
+ };
1061
943
  }
1062
944
  /**
1063
- * Generate a context tree from canonical memory.
945
+ * Summarise the structural provider status from config ALONE (no probe, no
946
+ * I/O). Pure — safe to call from any context, including the recall hot path
947
+ * for xray labels.
948
+ */
949
+ declare function describeStructuralProviderStatus(config: PluginConfig, scope?: string): StructuralProviderStatus;
950
+ /**
951
+ * Render a single human-readable line for `remnic doctor`. Pure.
1064
952
  *
1065
- * Reads memory `.md` files from the source directory, * and projects them into a clean, * human-readable tree structure at `outputDir`.
953
+ * `structural-context provider: none (inactive review-context stays file-path-only)`
954
+ * `structural-context provider: subprocess, command=/usr/local/bin/cbm, probed=available`
955
+ * `structural-context provider: subprocess, command=/usr/local/bin/cbm, probed=unavailable (binary not found)`
1066
956
  */
1067
- declare function generateContextTree(options: GenerateOptions): Promise<GenerateResult>;
957
+ declare function renderStructuralProviderStatusLine(status: StructuralProviderStatus): string;
1068
958
 
1069
959
  /**
1070
- * @remnic/core Onboarding
960
+ * Diff-aware review-context packer (issue #569 PR 4).
1071
961
  *
1072
- * Detects project language, shape, and documentation to produce
1073
- * an onboarding plan for memory ingestion.
962
+ * When an agent is asked "review this PR" / "what changed in this diff" /
963
+ * "look at this diff", the prompt that reaches recall is short and generic
964
+ * — the real signal is the diff itself. This module:
965
+ *
966
+ * 1. Detects review-intent prompts via `isReviewPrompt`.
967
+ * 2. Extracts the touched file list from a unified diff via
968
+ * `parseTouchedFiles`.
969
+ * 3. Re-ranks a set of candidate memories so that memories whose
970
+ * `entityRefs` mention a touched path float to the top. The boost is
971
+ * additive and bounded so it doesn't obliterate the original ranking —
972
+ * it's a bias, not a filter.
973
+ *
974
+ * Pure — no orchestrator, no storage. Callers inject the candidate memories
975
+ * they already have from their normal recall pipeline. This keeps the
976
+ * module easy to test and integrates cleanly with the existing tiered-recall
977
+ * code in `orchestrator.ts` (the tier itself can be wired later; the pure
978
+ * surface is what PRs 5/6/7 will call).
1074
979
  */
1075
- interface OnboardOptions {
1076
- /** Directory to scan (defaults to cwd) */
1077
- directory?: string;
1078
- /** Max depth to walk (default: 6) */
1079
- maxDepth?: number;
1080
- /** Directories to skip */
1081
- excludeDirs?: string[];
1082
- }
1083
- interface LanguageInfo {
1084
- /** Language name (e.g. "TypeScript", "Python") */
1085
- language: string;
1086
- /** Confidence in detection (0-1) */
1087
- confidence: number;
1088
- /** Evidence (e.g. ["package.json", "tsconfig.json", "*.ts files"]) */
1089
- evidence: string[];
1090
- }
1091
- interface DocFile {
1092
- /** Absolute path */
1093
- path: string;
1094
- /** Relative path from project root */
1095
- relativePath: string;
1096
- /** Estimated type */
1097
- kind: "readme" | "changelog" | "contributing" | "license" | "config" | "docs" | "other";
1098
- /** File size in bytes */
1099
- size: number;
1100
- }
1101
- type ProjectShape = "app" | "library" | "monorepo" | "workspace" | "script" | "unknown";
1102
- interface OnboardResult {
1103
- /** Project root */
1104
- directory: string;
1105
- /** Detected languages (sorted by confidence) */
1106
- languages: LanguageInfo[];
1107
- /** Detected project shape */
1108
- shape: ProjectShape;
1109
- /** Shape evidence */
1110
- shapeEvidence: string[];
1111
- /** Discovered documentation files */
1112
- docs: DocFile[];
1113
- /** Total files scanned */
1114
- totalFiles: number;
1115
- /** Duration in ms */
1116
- durationMs: number;
1117
- /** Suggested ingestion plan */
1118
- plan: IngestionPlan;
1119
- }
1120
- interface IngestionPlan {
1121
- /** Priority files to ingest first */
1122
- priorityFiles: DocFile[];
1123
- /** Estimated total files to ingest */
1124
- estimatedFiles: number;
1125
- /** Recommended categories */
1126
- categories: string[];
1127
- /** Suggested memory namespace */
1128
- suggestedNamespace: string;
1129
- }
1130
- declare function onboard(options: OnboardOptions): OnboardResult;
1131
980
 
1132
981
  /**
1133
- * @remnic/core Curation
1134
- *
1135
- * Deliberate ingestion of files into memory with provenance tracking.
1136
- * Supports statement-level extraction, dedup, and contradiction checks.
982
+ * A memory candidate as fed into review-context ranking. The shape is a
983
+ * deliberate subset of the core `MemorySummary` / recall result — only the
984
+ * fields we actually need so this module stays decoupled from the rest of
985
+ * the codebase and can be reused by CLI tools, bench fixtures, etc.
1137
986
  */
1138
- interface CurateOptions {
1139
- /** File or directory path to curate */
1140
- targetPath: string;
1141
- /** Memory root directory for writing */
1142
- memoryDir: string;
1143
- /** Source label (e.g. "manual", "docs", "onboarding") */
1144
- source?: string;
1145
- /** Category override (default: auto-detect) */
1146
- category?: string;
1147
- /** Confidence to assign (default: 0.9 for curated items) */
1148
- confidence?: number;
1149
- /** Entity reference to attach */
1150
- entityRef?: string;
1151
- /** Tags to add */
1152
- tags?: string[];
1153
- /** Whether to perform dedup check against existing memories */
1154
- checkDuplicates?: boolean;
1155
- /** Whether to detect contradictions */
1156
- checkContradictions?: boolean;
1157
- /** Whether to write files (default: true). False = dry run */
1158
- write?: boolean;
1159
- }
1160
- interface CuratedStatement {
1161
- /** Unique ID for this statement */
987
+ interface ReviewCandidate {
988
+ /** Opaque identifier. Echoed unchanged in the output. */
1162
989
  id: string;
1163
- /** The extracted statement text */
1164
- content: string;
1165
- /** Category */
1166
- category: string;
1167
- /** Confidence */
1168
- confidence: number;
1169
- /** Provenance info */
1170
- provenance: StatementProvenance;
1171
- /** Hash of content for dedup */
1172
- contentHash: string;
1173
- /** Tags */
1174
- tags: string[];
1175
- /** Entity reference */
1176
- entityRef?: string;
1177
- }
1178
- interface StatementProvenance {
1179
- /** Source file path */
1180
- sourcePath: string;
1181
- /** Relative path from project root */
1182
- relativePath: string;
1183
- /** Source label */
1184
- source: string;
1185
- /** Line number if extractable (0 = unknown) */
1186
- lineNumber: number;
1187
- /** Timestamp of ingestion */
1188
- ingestedAt: string;
1189
- /** Hash of the source file for diff tracking */
1190
- sourceFileHash: string;
990
+ /**
991
+ * Pre-review relevance score from the upstream recall pipeline. Higher is
992
+ * better. `0` is treated as "no prior signal" and gets the full review
993
+ * boost when a path match is found.
994
+ */
995
+ score: number;
996
+ /**
997
+ * References the memory mentions (file paths, entity names, etc.). Used
998
+ * to decide whether any touched file appears in the memory's scope.
999
+ *
1000
+ * Accepts `undefined`/missing so callers can pass sparse records from
1001
+ * legacy storage without pre-filling.
1002
+ */
1003
+ entityRefs?: string[];
1191
1004
  }
1192
- interface CurateResult {
1193
- /** Statements extracted */
1194
- statements: CuratedStatement[];
1195
- /** Files processed */
1196
- filesProcessed: number;
1197
- /** Files skipped (empty, binary, etc.) */
1198
- filesSkipped: number;
1199
- /** Duplicate statements found (if checkDuplicates) */
1200
- duplicates: DuplicateResult[];
1201
- /** Contradictions found (if checkContradictions) */
1202
- contradictions: ContradictionResult$1[];
1203
- /** Memory files written */
1204
- written: string[];
1205
- /** Duration in ms */
1206
- durationMs: number;
1207
- }
1208
- interface DuplicateResult {
1209
- /** New statement */
1210
- newStatement: CuratedStatement;
1211
- /** Existing memory ID that matches */
1212
- existingId: string;
1213
- /** Similarity score (0-1) */
1214
- similarity: number;
1215
- /** Recommended action */
1216
- action: "skip" | "merge" | "keep";
1217
- }
1218
- interface ContradictionResult$1 {
1219
- /** New statement */
1220
- newStatement: CuratedStatement;
1221
- /** Conflicting memory ID */
1222
- conflictingId: string;
1223
- /** The conflicting content */
1224
- conflictingContent: string;
1225
- /** Severity */
1226
- severity: "high" | "medium" | "low";
1005
+ interface ReviewContext {
1006
+ /**
1007
+ * Normalized file paths touched by the diff. Each entry is forward-slashed
1008
+ * and relative to the repo root when possible.
1009
+ */
1010
+ touchedFiles: string[];
1011
+ /**
1012
+ * Candidates re-sorted so memories whose `entityRefs` mention a touched
1013
+ * path are boosted. Shape matches the input `ReviewCandidate[]` — the
1014
+ * boost is recorded on each entry as `boost` for observability.
1015
+ */
1016
+ rankedRecall: Array<ReviewCandidate & {
1017
+ boost: number;
1018
+ }>;
1019
+ /**
1020
+ * Structural-context provider degradation observed while expanding the
1021
+ * diff to touched symbols (issue #1548 Track A PR 5, #1536 pattern).
1022
+ * Present only when a provider was consulted AND it failed — a silent
1023
+ * provider failure must never look like "no structural matches"
1024
+ * (CLAUDE.md rule 34). Undefined when no provider was consulted (the
1025
+ * pure {@link packReviewContext} path) or when the provider succeeded.
1026
+ */
1027
+ structuralDegradation?: StructuralContextDegradation;
1227
1028
  }
1228
- declare function curate(options: CurateOptions): Promise<CurateResult>;
1229
-
1230
1029
  /**
1231
- * @remnic/core Dedup & Contradiction Detection
1030
+ * `true` when the prompt looks like a review / diff-explanation request.
1232
1031
  *
1233
- * Statement-level deduplication and contradiction detection
1234
- * against existing memories. Can be used standalone or via curation.
1032
+ * Empty / non-string input `false` (the caller shouldn't branch on an
1033
+ * invalid prompt).
1235
1034
  */
1236
- interface MemoryEntry {
1237
- /** Memory ID */
1238
- id: string;
1239
- /** Content text */
1240
- content: string;
1241
- /** Category */
1242
- category: string;
1243
- /** File path (if known) */
1244
- filePath?: string;
1245
- }
1246
- interface DedupOptions {
1247
- /** Memory root directory */
1248
- memoryDir: string;
1249
- /** Categories to scan (default: all) */
1250
- categories?: string[];
1251
- /** Similarity threshold for fuzzy matching (0-1, default: 0.85) */
1252
- threshold?: number;
1253
- /** Max memories to load (default: 10000) */
1254
- maxLoad?: number;
1255
- }
1256
- interface DedupResult {
1257
- /** Total memories scanned */
1258
- scanned: number;
1259
- /** Duplicate pairs found */
1260
- duplicates: DuplicatePair[];
1261
- /** Duration in ms */
1262
- durationMs: number;
1263
- }
1264
- interface DuplicatePair {
1265
- /** First memory */
1266
- left: MemoryEntry;
1267
- /** Second memory */
1268
- right: MemoryEntry;
1269
- /** Similarity score */
1270
- similarity: number;
1271
- /** Recommended action */
1272
- action: "merge" | "keep_left" | "keep_right";
1273
- }
1274
- interface ContradictionOptions {
1275
- /** Memory root directory */
1276
- memoryDir: string;
1277
- /** Categories to scan (default: all) */
1278
- categories?: string[];
1279
- /** Max memories to load (default: 10000) */
1280
- maxLoad?: number;
1281
- }
1282
- interface ContradictionResult {
1283
- /** Total memories scanned */
1284
- scanned: number;
1285
- /** Contradictions found */
1286
- contradictions: ContradictionPair[];
1287
- /** Duration in ms */
1288
- durationMs: number;
1289
- }
1290
- interface ContradictionPair {
1291
- /** First statement */
1292
- left: MemoryEntry;
1293
- /** Contradicting statement */
1294
- right: MemoryEntry;
1295
- /** Severity */
1296
- severity: "high" | "medium" | "low";
1297
- /** Reason */
1298
- reason: string;
1299
- }
1300
- declare function findDuplicates(options: DedupOptions): DedupResult;
1301
- declare function findContradictions(options: ContradictionOptions): ContradictionResult;
1302
-
1035
+ declare function isReviewPrompt(prompt: string | null | undefined): boolean;
1303
1036
  /**
1304
- * @remnic/core Review Inbox
1037
+ * Parse a unified diff and return the set of files touched. Accepts both the
1038
+ * `diff --git` form (`diff --git a/foo b/bar`) and the `--- / +++` form
1039
+ * (`--- a/foo\n+++ b/bar`). Returns deduplicated, repo-root-relative paths
1040
+ * (with the conventional `a/` / `b/` prefixes stripped).
1305
1041
  *
1306
- * Manages low-confidence memories and suggestions pending review.
1307
- * Integrates with the existing review-queue system.
1042
+ * Path entries of `/dev/null` (used in adds/deletes) are excluded.
1308
1043
  */
1309
- interface ReviewItem {
1310
- /** Memory ID */
1311
- id: string;
1312
- /** Content text */
1313
- content: string;
1314
- /** Category */
1315
- category: string;
1316
- /** Confidence score (0-1) */
1317
- confidence: number;
1318
- /** Confidence tier */
1319
- confidenceTier: string;
1320
- /** Source */
1321
- source: string;
1322
- /** File path if available */
1323
- filePath?: string;
1324
- /** Created date */
1325
- created: string;
1326
- /** Reason it's in review */
1327
- reviewReason: "low_confidence" | "suggestion" | "contradiction" | "duplicate" | "tombstone_blocked";
1328
- /** Additional context */
1329
- context?: string;
1330
- }
1331
- type ReviewAction = "approve" | "dismiss" | "flag";
1332
- interface ReviewResult {
1333
- /** Item acted upon */
1334
- itemId: string;
1335
- /** Action taken */
1336
- action: ReviewAction;
1337
- /** Updated file path (if modified) */
1338
- updatedPath?: string;
1339
- /** Status message */
1340
- message: string;
1341
- /** Tombstone id cleared by an approve action (for the caller to revoke). */
1342
- clearedTombstoneId?: string;
1343
- }
1344
- interface ReviewListResult {
1345
- /** Items pending review */
1346
- items: ReviewItem[];
1347
- /** Total count */
1348
- total: number;
1349
- /** Duration in ms */
1350
- durationMs: number;
1351
- }
1352
- interface ReviewOptions {
1353
- /** Memory root directory */
1354
- memoryDir: string;
1355
- /** Filter by reason */
1356
- reason?: ReviewItem["reviewReason"];
1357
- /** Max items to return (default: 50) */
1358
- limit?: number;
1359
- /** Include items with confidence below this threshold (default: 0.7) */
1360
- confidenceThreshold?: number;
1361
- }
1362
- interface ReviewActionOptions {
1363
- /** Match the threshold used when listing review items (default: 0.7) */
1364
- confidenceThreshold?: number;
1365
- /**
1366
- * Revocation hook (issue #1579). When approving a memory whose frontmatter
1367
- * carries `blockedBy: <tombstoneId>`, the hook fires so the caller (CLI /
1368
- * orchestrator) can append a `kind: "revocation"` tombstone entry —
1369
- * re-allowing the content. Fire-and-forget: a revocation failure MUST NOT
1370
- * fail the approval (gotcha #13). The hook receives the tombstone id and
1371
- * the memory id.
1372
- */
1373
- onApproveBlockedMemory?: (tombstoneId: string, memoryId: string) => void | Promise<void>;
1044
+ declare function parseTouchedFiles(diff: string | null | undefined): string[];
1045
+ /**
1046
+ * Build a review-context ranking for a set of candidate memories.
1047
+ *
1048
+ * Contract:
1049
+ * - `touchedFiles` is the parsed diff file list.
1050
+ * - `candidates` is passed through unchanged when no boost applies.
1051
+ * - When a boost applies, the result is sorted by `(score + boost)` desc,
1052
+ * with a stable secondary sort on the original `id` for determinism
1053
+ * (CLAUDE.md #19 — comparators must return 0 for equal items).
1054
+ */
1055
+ declare function rankReviewCandidates(candidates: ReviewCandidate[], touchedFiles: string[]): Array<ReviewCandidate & {
1056
+ boost: number;
1057
+ }>;
1058
+ interface PackReviewContextInput {
1059
+ /** Unified diff, as produced by `git diff`. */
1060
+ diff: string | null | undefined;
1061
+ /** Candidate memories from the upstream recall pipeline. */
1062
+ candidates: ReviewCandidate[];
1374
1063
  }
1375
1064
  /**
1376
- * List items pending review.
1065
+ * Top-level entry point used by the orchestrator (and CLI / bench) when a
1066
+ * review-intent prompt is detected.
1067
+ *
1068
+ * Parses the diff, re-ranks the candidates, and returns both artefacts so
1069
+ * the caller can surface `touchedFiles` as context and `rankedRecall` as
1070
+ * the recall result.
1377
1071
  */
1378
- declare function listReviewItems(options: ReviewOptions): ReviewListResult;
1072
+ declare function packReviewContext(input: PackReviewContextInput): ReviewContext;
1379
1073
  /**
1380
- * Perform a review action on an item.
1074
+ * Input for the structural-context-aware review-context packer. The pure
1075
+ * {@link packReviewContext} stays unchanged (gate-off parity); this async
1076
+ * variant consults a {@link StructuralContextProvider} to expand the diff's
1077
+ * touched FILE paths into touched SYMBOL names, widening the match set so
1078
+ * memories that mention a changed symbol — not just the file path — float
1079
+ * up. The gate (`structuralProvider !== "none"`) is checked ONCE by the
1080
+ * caller, never inside this function (rule 39).
1381
1081
  */
1382
- declare function performReview(memoryDir: string, itemId: string, action: ReviewAction, options?: ReviewActionOptions): ReviewResult;
1383
-
1082
+ interface PackReviewContextStructuralInput extends PackReviewContextInput {
1083
+ /** A probed structural-context provider. Never undefined here. */
1084
+ provider: StructuralContextProvider;
1085
+ /** Cancellation forwarded to the provider subprocess (rule 40). */
1086
+ signal?: AbortSignal;
1087
+ }
1384
1088
  /**
1385
- * @remnic/core — Diff-Aware Sync
1089
+ * Structural-context-aware packer.
1386
1090
  *
1387
- * Watches source files for changes and triggers re-ingestion
1388
- * only for changed content. Uses file hashing to detect changes.
1389
- */
1390
- interface SyncOptions {
1391
- /** Source directory to watch */
1392
- sourceDir: string;
1393
- /** Memory root directory */
1394
- memoryDir: string;
1395
- /** State file path (stores hashes). Default: memoryDir/.sync-state.json */
1396
- stateFile?: string;
1397
- /** File extensions to watch (default: .md, .txt, .mdx) */
1398
- extensions?: string[];
1399
- /** Directories to exclude */
1400
- excludeDirs?: string[];
1401
- /** Poll interval for watchForChanges. Default: 5000ms */
1402
- pollIntervalMs?: number;
1403
- /** Whether to actually write changes (default: true) */
1404
- dryRun?: boolean;
1405
- }
1406
- interface SyncResult {
1407
- /** Files scanned */
1408
- scanned: number;
1409
- /** Files changed since last sync */
1410
- changed: FileChange[];
1411
- /** Files unchanged */
1412
- unchanged: number;
1413
- /** Files deleted since last sync */
1414
- deleted: string[];
1415
- /** Files newly added */
1416
- added: string[];
1417
- /** Duration in ms */
1418
- durationMs: number;
1419
- /** State file path */
1420
- stateFile: string;
1421
- }
1422
- interface FileChange {
1423
- /** Absolute file path */
1424
- filePath: string;
1425
- /** Relative path from source root */
1426
- relativePath: string;
1427
- /** Change type */
1428
- type: "added" | "modified" | "deleted";
1429
- /** Current content hash */
1430
- currentHash: string;
1431
- /** Previous content hash (if modified) */
1432
- previousHash?: string;
1433
- /** File size in bytes */
1434
- size: number;
1435
- }
1436
- interface SyncState {
1437
- /** Map of relative path → content hash */
1438
- fileHashes: Record<string, string>;
1439
- /** Last sync timestamp */
1440
- lastSyncAt: string;
1441
- /** Version of state format */
1442
- version: number;
1091
+ * Contract (issue #1548 Track A PR 5, prove-fail-before characterization):
1092
+ * - Provider returns symbols (`ok: true`) symbol names are ADDED to the
1093
+ * match set alongside touched files; memories mentioning a symbol get the
1094
+ * same bounded additive boost as a file-path match.
1095
+ * - Provider fails (`ok: false`, any code) → ranking falls back to
1096
+ * FILE-PATH-ONLY boosting, BYTE-IDENTICAL to {@link packReviewContext}
1097
+ * (the match set is exactly the touched files), AND `structuralDegradation`
1098
+ * is populated so xray/doctor can surface the failure (rule 34).
1099
+ *
1100
+ * The underlying ranker ({@link rankReviewCandidates}) is reused unchanged,
1101
+ * so the deterministic ordering, boost cap, and tie-break are identical to
1102
+ * the pure path.
1103
+ */
1104
+ declare function packReviewContextStructural(input: PackReviewContextStructuralInput): Promise<ReviewContext>;
1105
+
1106
+ /** Injectable spawn shape so tests substitute a stub (rule 33). */
1107
+ type StructuralSpawnFn = (command: string, argv: readonly string[], options: {
1108
+ timeout: number;
1109
+ signal?: AbortSignal;
1110
+ }) => Promise<{
1111
+ stdout: string;
1112
+ stderr: string;
1113
+ }>;
1114
+ interface SubprocessProviderOptions {
1115
+ /** Absolute path to the binary (rule 24 — statSync at probe time). */
1116
+ readonly command: string;
1117
+ /**
1118
+ * Extra argv appended after the symbols-for-diff subcommand
1119
+ * (rule 10 array, never a shell string).
1120
+ */
1121
+ readonly args?: readonly string[];
1122
+ /** Per-call deadline in ms. Default 5000. */
1123
+ readonly timeoutMs?: number;
1124
+ /**
1125
+ * Subcommand the provider invokes for symbol expansion.
1126
+ * Default `"symbols-for-diff"`.
1127
+ */
1128
+ readonly symbolsSubcommand?: string;
1129
+ /** Test seam defaults to promisified `execFile`. */
1130
+ readonly spawn?: StructuralSpawnFn;
1443
1131
  }
1444
- declare function syncChanges(options: SyncOptions): SyncResult;
1445
1132
  /**
1446
- * Watch for changes and call callback on file changes.
1447
- * Returns a stop function.
1133
+ * Construct a structural-context provider backed by an external subprocess.
1134
+ * The instance is self-contained: callers register it via
1135
+ * `registerStructuralContextProvider(scope, provider)` and dispose via the
1136
+ * returned unregister handle.
1448
1137
  */
1449
- declare function watchForChanges(options: SyncOptions, onChange: (changes: FileChange[]) => void | Promise<void>): {
1450
- stop: () => void;
1451
- };
1452
-
1138
+ declare function createSubprocessStructuralProvider(options: SubprocessProviderOptions): StructuralContextProvider;
1453
1139
  /**
1454
- * memory-extension-host/render-extensions-block.ts Render discovered extensions
1455
- * into a markdown block for injection into consolidation prompts.
1140
+ * Augment the pure {@link describeStructuralProviderStatus} with a LIVE probe
1141
+ * so `remnic doctor` can render "configured / probed / last error code".
1456
1142
  *
1457
- * Respects the global token budget (REMNIC_EXTENSIONS_TOTAL_TOKEN_LIMIT) and
1458
- * truncates with a footer listing omitted extensions when over budget.
1143
+ * - `"none"` → inactive (no probe).
1144
+ * - `"subprocess"` builds a throwaway provider from
1145
+ * `structuralProviderCommand`, probes it, disposes it.
1146
+ * - `"native"` → asks `isCodingGraphInstalled()` whether the optional
1147
+ * @remnic/coding-graph peer is present (the adapter that
1148
+ * turns the engine into a provider lands in #1551).
1149
+ *
1150
+ * Never throws — a probe failure populates `probed.available = false`.
1459
1151
  */
1152
+ declare function probeStructuralProviderForDoctor(config: PluginConfig): Promise<StructuralProviderStatus>;
1460
1153
 
1461
1154
  /**
1462
- * Render a markdown block containing extension instructions for injection
1463
- * into consolidation prompts.
1155
+ * Binary file lifecycle management types.
1464
1156
  *
1465
- * If the list is empty, returns "".
1466
- * Inlines extensions in name order until the token budget is exhausted.
1467
- * If the budget is exceeded, appends a truncation footer listing omitted extensions.
1157
+ * Defines the configuration, manifest, and record structures for the
1158
+ * three-stage binary lifecycle pipeline: mirror, redirect, clean.
1468
1159
  */
1469
- declare function renderExtensionsBlock(extensions: DiscoveredExtension[]): string;
1160
+ interface BinaryLifecycleConfig {
1161
+ /** Master toggle. Default: false. */
1162
+ enabled: boolean;
1163
+ /** Days after mirror before local copy is eligible for cleanup. Default: 7. */
1164
+ gracePeriodDays: number;
1165
+ /** Files larger than this are skipped during scan. Default: 50 MB. */
1166
+ maxBinarySizeBytes: number;
1167
+ /** Glob patterns for binary file types to manage. */
1168
+ scanPatterns: string[];
1169
+ /** Backend configuration for binary storage. */
1170
+ backend: BinaryStorageBackendConfig;
1171
+ }
1172
+ interface BinaryStorageBackendConfig {
1173
+ /** Backend type. "filesystem" copies to a local directory. "none" is a no-op (dry-run/testing). */
1174
+ type: "filesystem" | "s3" | "none";
1175
+ /** Destination directory for the filesystem backend. */
1176
+ basePath?: string;
1177
+ /** S3 bucket name (future). */
1178
+ s3Bucket?: string;
1179
+ /** S3 region (future). */
1180
+ s3Region?: string;
1181
+ /** S3 key prefix (future). */
1182
+ s3Prefix?: string;
1183
+ }
1184
+ type BinaryAssetStatus = "pending" | "mirrored" | "redirected" | "cleaned" | "error";
1185
+ interface BinaryAssetRecord {
1186
+ /** Relative path from memoryDir to the original file. */
1187
+ originalPath: string;
1188
+ /** Path (or URL) in the backend storage. */
1189
+ mirroredPath: string;
1190
+ /** Optional user-resolvable target to write into markdown links. */
1191
+ redirectPath?: string;
1192
+ /** SHA-256 hex digest of file content. */
1193
+ contentHash: string;
1194
+ /** File size in bytes. */
1195
+ sizeBytes: number;
1196
+ /** MIME type (e.g. "image/png"). */
1197
+ mimeType: string;
1198
+ /** ISO 8601 timestamp when the file was mirrored. */
1199
+ mirroredAt: string;
1200
+ /** ISO 8601 timestamp when markdown references were rewritten. */
1201
+ redirectedAt?: string;
1202
+ /** ISO 8601 timestamp when the local copy was deleted. */
1203
+ cleanedAt?: string;
1204
+ /** Current lifecycle status. */
1205
+ status: BinaryAssetStatus;
1206
+ }
1207
+ interface BinaryLifecycleManifest {
1208
+ version: 1;
1209
+ assets: BinaryAssetRecord[];
1210
+ lastScanAt?: string;
1211
+ }
1212
+ interface PipelineResult {
1213
+ scanned: number;
1214
+ mirrored: number;
1215
+ redirected: number;
1216
+ cleaned: number;
1217
+ errors: string[];
1218
+ dryRun: boolean;
1219
+ }
1220
+ declare const DEFAULT_SCAN_PATTERNS: string[];
1221
+ declare const DEFAULT_MAX_BINARY_SIZE_BYTES: number;
1222
+ declare const DEFAULT_GRACE_PERIOD_DAYS = 7;
1223
+
1470
1224
  /**
1471
- * Render a compact one-line footer listing active extension names.
1472
- * Used by day-summary and summary-snapshot where full instructions are not needed.
1225
+ * Binary storage backend interface and implementations.
1226
+ *
1227
+ * Backends handle the actual persistence of binary files to an external
1228
+ * location. The pipeline calls upload/exists/delete through this interface
1229
+ * so swapping storage providers requires no pipeline changes.
1473
1230
  */
1474
- declare function renderExtensionsFooter(extensions: DiscoveredExtension[]): string;
1231
+
1232
+ interface BinaryStorageBackend {
1233
+ /** Discriminator for the backend type. */
1234
+ readonly type: string;
1235
+ /**
1236
+ * Upload a local file to the backend.
1237
+ * @returns The backend path or URL where the file was stored.
1238
+ */
1239
+ upload(localPath: string, remotePath: string): Promise<string>;
1240
+ /** Check whether a remote path already exists in the backend. */
1241
+ exists(remotePath: string): Promise<boolean>;
1242
+ /** Delete a file from the backend. */
1243
+ delete(remotePath: string): Promise<void>;
1244
+ /** Return the user-resolvable markdown target for a stored backend path. */
1245
+ getRedirectTarget?(remotePath: string): string;
1246
+ }
1247
+ declare class FilesystemBackend implements BinaryStorageBackend {
1248
+ readonly type = "filesystem";
1249
+ private readonly basePath;
1250
+ constructor(basePath: string);
1251
+ private resolveRemotePath;
1252
+ private isInsideBase;
1253
+ private realBasePathIfExists;
1254
+ private ensureBaseDirectory;
1255
+ private ensureSafeParentDirectory;
1256
+ private resolveExistingRemotePath;
1257
+ upload(localPath: string, remotePath: string): Promise<string>;
1258
+ exists(remotePath: string): Promise<boolean>;
1259
+ delete(remotePath: string): Promise<void>;
1260
+ getRedirectTarget(remotePath: string): string;
1261
+ }
1262
+ declare class NoneBackend implements BinaryStorageBackend {
1263
+ readonly type = "none";
1264
+ upload(_localPath: string, remotePath: string): Promise<string>;
1265
+ exists(_remotePath: string): Promise<boolean>;
1266
+ delete(_remotePath: string): Promise<void>;
1267
+ }
1268
+ declare function createBackend(cfg: BinaryStorageBackendConfig): BinaryStorageBackend;
1475
1269
 
1476
1270
  /**
1477
- * @remnic/core Live Connectors Registry (issue #683 PR 1/N)
1271
+ * Binary file scanner.
1478
1272
  *
1479
- * Pure in-memory registry. No I/O. Concrete connectors register themselves at
1480
- * orchestrator boot (later PRs); the maintenance scheduler asks the registry
1481
- * for the active set when running due syncs.
1273
+ * Recursively walks the memory directory, matches files against configured
1274
+ * glob patterns, skips files already tracked in the manifest, and respects
1275
+ * the max-size limit.
1482
1276
  */
1483
1277
 
1484
1278
  /**
1485
- * Thrown when registering a duplicate id or an id that fails validation.
1486
- *
1487
- * Distinct error class so callers can distinguish framework-level mistakes
1488
- * (which are programmer errors) from connector-runtime failures.
1279
+ * Test whether a filename matches any of the provided glob patterns.
1280
+ * Supports simple `*.ext` patterns (the default scan patterns).
1281
+ * For more complex globs a proper library should be used; this covers
1282
+ * the 95% case without adding a dependency.
1489
1283
  */
1490
- declare class LiveConnectorRegistryError extends Error {
1491
- constructor(message: string);
1492
- }
1284
+ declare function matchesPatterns(filename: string, patterns: string[]): boolean;
1493
1285
  /**
1494
- * In-memory registry of live connectors. One instance per orchestrator. Not
1495
- * safe for cross-process sharing wire each process to its own registry.
1286
+ * Scan memoryDir recursively for binary files matching the configured patterns.
1287
+ * Returns relative paths (relative to memoryDir) for files not yet tracked.
1496
1288
  */
1497
- declare class LiveConnectorRegistry {
1498
- private readonly connectors;
1499
- /**
1500
- * Register a connector. Throws `LiveConnectorRegistryError` if the id is
1501
- * malformed or already registered.
1502
- *
1503
- * Re-registration is rejected (rather than silently overwriting) because
1504
- * silent overwrites mask plugin loading bugs in development and could let
1505
- * a malicious extension shadow a built-in connector.
1506
- */
1507
- register(connector: LiveConnector): void;
1508
- /**
1509
- * Look up a connector by id. Returns `undefined` if not registered.
1510
- */
1511
- get(id: string): LiveConnector | undefined;
1512
- /**
1513
- * Return all registered connectors, sorted by id for stable enumeration.
1514
- */
1515
- list(): LiveConnector[];
1516
- /**
1517
- * Remove a connector. Returns `true` if a connector was removed, `false`
1518
- * otherwise. The cursor / state file on disk is **not** touched — callers
1519
- * who want to fully decommission a connector must also delete its state
1520
- * file via the `state-store` module.
1521
- */
1522
- unregister(id: string): boolean;
1523
- /**
1524
- * Number of registered connectors. Cheap; safe to call frequently.
1525
- */
1526
- size(): number;
1527
- }
1289
+ declare function scanForBinaries(memoryDir: string, config: BinaryLifecycleConfig, manifest: BinaryLifecycleManifest): Promise<string[]>;
1528
1290
 
1529
1291
  /**
1530
- * @remnic/core Google Drive live connector (issue #683 PR 2/N)
1531
- *
1532
- * Concrete `LiveConnector` implementation that incrementally imports text
1533
- * content from a user's Google Drive into Remnic. Built on top of the
1534
- * framework shipped in PR 1/N (`framework.ts` / `registry.ts` /
1535
- * `state-store.ts`).
1536
- *
1537
- * Design notes:
1538
- *
1539
- * - **Cursor semantics.** We page Drive with the official `changes` API
1540
- * when a `startPageToken` is available. The cursor is opaque from the
1541
- * framework's POV (`{kind: "drivePageToken", value: ...}`), and on the
1542
- * very first sync (cursor=null) we call `changes.getStartPageToken()`
1543
- * to seed it without importing anything. This matches the documented
1544
- * Drive incremental-sync recipe and means re-runs never re-ingest the
1545
- * same file as long as the cursor file survives.
1546
- *
1547
- * - **Folder scope.** When `folderIds` is non-empty, files are filtered
1548
- * to those whose `parents` intersect the configured folder set. Drive
1549
- * does not currently support server-side parent filtering on the
1550
- * `changes.list` endpoint, so we pull the change record's `file` payload
1551
- * and apply the filter on our side. Folder ids are validated up front so
1552
- * a typo in config doesn't silently cause a broad import.
1553
- *
1554
- * - **Content extraction.** Google-native MIME types
1555
- * (`application/vnd.google-apps.{document,spreadsheet,presentation}`)
1556
- * are exported via `files.export` to plaintext. Plain-text MIME types
1557
- * are pulled with `files.get?alt=media`. Everything else is skipped —
1558
- * bytes from binary formats (images, PDFs, archives) belong in the
1559
- * binary-lifecycle pipeline, not in the textual ingestion path.
1560
- *
1561
- * - **Idempotency.** Each emitted `ConnectorDocument.source` carries
1562
- * `externalId = file.id` plus `externalRevision = file.modifiedTime`,
1563
- * so downstream dedup (CLAUDE.md gotcha #44 — never index content that
1564
- * failed to persist) can recognise repeat fetches even if the cursor is
1565
- * manually rewound.
1566
- *
1567
- * - **Privacy.** No document content is ever logged. Folder ids and
1568
- * counts may be logged. OAuth credentials (`clientId`,
1569
- * `clientSecret`, `refreshToken`) are accepted via config but the
1570
- * intent is for callers to populate them from a secret store; we never
1571
- * persist credentials through the connector state-store. Per CLAUDE.md
1572
- * repository-privacy rules, no real credentials may appear in tests,
1573
- * fixtures, or comments.
1292
+ * Binary lifecycle manifest read/write operations.
1574
1293
  *
1575
- * - **À-la-carte packaging (CLAUDE.md gotcha #57).** `googleapis` is NOT
1576
- * listed as a hard dependency of `@remnic/core`. It is loaded via a
1577
- * computed-specifier dynamic import (`await import("google" + "apis")`)
1578
- * so bundlers cannot statically resolve it, and it is declared as an
1579
- * optional peer dependency. Operators who never enable the connector
1580
- * pay nothing for it.
1294
+ * The manifest lives at `${memoryDir}/.binary-lifecycle/manifest.json`.
1295
+ * Writes use the atomic temp-then-rename pattern (CLAUDE.md #54).
1296
+ */
1297
+
1298
+ declare function manifestDir(memoryDir: string): string;
1299
+ declare function manifestPath(memoryDir: string): string;
1300
+ /**
1301
+ * Read the manifest from disk. Returns a fresh empty manifest if the file
1302
+ * does not exist. Existing invalid manifests fail closed so the pipeline does
1303
+ * not overwrite state needed for safe cleanup.
1304
+ */
1305
+ declare function readManifest(memoryDir: string): Promise<BinaryLifecycleManifest>;
1306
+ /**
1307
+ * Write the manifest atomically: write to a temp file, then rename.
1308
+ * CLAUDE.md #54: never delete before write. Write temp first, rename atomically.
1309
+ */
1310
+ declare function writeManifest(memoryDir: string, manifest: BinaryLifecycleManifest): Promise<void>;
1311
+ declare function emptyManifest(): BinaryLifecycleManifest;
1312
+
1313
+ /**
1314
+ * Binary lifecycle pipeline — mirror, redirect, clean.
1581
1315
  *
1582
- * - **Read-only.** This connector only reads. It never marks files as
1583
- * read, edits metadata, or modifies sharing settings.
1316
+ * Three-stage pipeline:
1317
+ * 1. Mirror: upload binary to backend, record in manifest
1318
+ * 2. Redirect: scan markdown for inline refs, replace with redirect path
1319
+ * 3. Clean: after grace period, delete local copy
1584
1320
  */
1585
1321
 
1322
+ /** Minimal logger interface so we don't depend on the full logger module. */
1323
+ interface PipelineLogger {
1324
+ info(msg: string): void;
1325
+ warn(msg: string): void;
1326
+ error(msg: string): void;
1327
+ }
1328
+ type ReadMarkdownFile = (filePath: string) => Promise<string>;
1329
+ type WriteMarkdownFile = (filePath: string, content: string) => Promise<void>;
1330
+ interface PipelineOptions {
1331
+ dryRun?: boolean;
1332
+ /** Force-clean all files past grace period, ignoring redirect status. */
1333
+ forceClean?: boolean;
1334
+ /** Test hook for deterministic markdown read failures. */
1335
+ readMarkdownFile?: ReadMarkdownFile;
1336
+ /** Test hook for deterministic markdown write failures. */
1337
+ writeMarkdownFile?: WriteMarkdownFile;
1338
+ }
1586
1339
  /**
1587
- * Stable connector id. Lives in the registry under this exact string.
1340
+ * Run the binary lifecycle pipeline: scan, mirror, redirect, clean.
1588
1341
  */
1589
- declare const GOOGLE_DRIVE_CONNECTOR_ID = "google-drive";
1342
+ declare function runBinaryLifecyclePipeline(memoryDir: string, config: BinaryLifecycleConfig, backend: BinaryStorageBackend, log: PipelineLogger, opts?: PipelineOptions): Promise<PipelineResult>;
1343
+
1590
1344
  /**
1591
- * Cursor `kind` we emit. Treated as opaque by the framework; documented
1592
- * here so tests can assert on it.
1345
+ * @remnic/core Workspace Tree Projection
1346
+ *
1347
+ * Generates a human-readable `.engram/context-tree/` from canonical memory.
1348
+ * Each node is a `.md` file with rich metadata, * (provenance, trust, confidence, source anchors).
1349
+ * Manual edits are preserved in fenced blocks.
1593
1350
  */
1594
- declare const GOOGLE_DRIVE_CURSOR_KIND = "drivePageToken";
1351
+ interface TreeNode {
1352
+ /** Relative path from context-tree root, e.g. "entities/claude.md" */
1353
+ path: string;
1354
+ /** Category from canonical memory */
1355
+ category: string;
1356
+ /** Human-readable title */
1357
+ title: string;
1358
+ /** File content (rendered markdown) */
1359
+ content: string;
1360
+ /** Source memory IDs that contributed to this node */
1361
+ sourceAnchors: string[];
1362
+ /** Confidence (0-1) */
1363
+ confidence: number;
1364
+ /** Trust zone classification */
1365
+ confidenceTier: string;
1366
+ /** When this node was generated */
1367
+ generatedAt: string;
1368
+ /** Provenance chain */
1369
+ provenance: ProvenanceEntry[];
1370
+ }
1371
+ interface ProvenanceEntry {
1372
+ memoryId: string;
1373
+ source: string;
1374
+ extracted: string;
1375
+ }
1376
+ interface GenerateOptions {
1377
+ /** Memory root directory (e.g. ~/.openclaw/workspace/memory/local) */
1378
+ memoryDir: string;
1379
+ /** Output directory (e.g. .engram/context-tree) */
1380
+ outputDir: string;
1381
+ /** Categories to include (default: all) */
1382
+ categories?: string[];
1383
+ /** Whether to include entity graph */
1384
+ includeEntities?: boolean;
1385
+ /** Whether to include orphaned questions */
1386
+ includeQuestions?: boolean;
1387
+ /** Max nodes per category (default: unlimited) */
1388
+ maxPerCategory?: number;
1389
+ /** Whether to watch for changes and regenerate incrementally */
1390
+ watch?: boolean;
1391
+ }
1392
+ interface GenerateResult {
1393
+ nodesGenerated: number;
1394
+ nodesSkipped: number;
1395
+ categories: Record<string, number>;
1396
+ durationMs: number;
1397
+ outputDir: string;
1398
+ }
1595
1399
  /**
1596
- * Default poll interval (5 minutes). Surfaced in `openclaw.plugin.json`
1597
- * defaults and in the documented config schema. Drive's `changes.list`
1598
- * endpoint is cheap, but polling sub-minute is wasteful for a personal
1599
- * memory layer.
1400
+ * Generate a context tree from canonical memory.
1401
+ *
1402
+ * Reads memory `.md` files from the source directory, * and projects them into a clean, * human-readable tree structure at `outputDir`.
1600
1403
  */
1601
- declare const DEFAULT_POLL_INTERVAL_MS: number;
1404
+ declare function generateContextTree(options: GenerateOptions): Promise<GenerateResult>;
1405
+
1602
1406
  /**
1603
- * Validated, frozen view of `connectors.googleDrive.*`.
1407
+ * @remnic/core Onboarding
1408
+ *
1409
+ * Detects project language, shape, and documentation to produce
1410
+ * an onboarding plan for memory ingestion.
1604
1411
  */
1605
- interface GoogleDriveConnectorConfig {
1606
- readonly clientId: string;
1607
- readonly clientSecret: string;
1608
- readonly refreshToken: string;
1609
- /** Poll interval surfaced to the scheduler. */
1610
- readonly pollIntervalMs: number;
1611
- /** Folder ids to scope import to. Empty = "all accessible". */
1612
- readonly folderIds: readonly string[];
1412
+ interface OnboardOptions {
1413
+ /** Directory to scan (defaults to cwd) */
1414
+ directory?: string;
1415
+ /** Max depth to walk (default: 6) */
1416
+ maxDepth?: number;
1417
+ /** Directories to skip */
1418
+ excludeDirs?: string[];
1419
+ }
1420
+ interface LanguageInfo {
1421
+ /** Language name (e.g. "TypeScript", "Python") */
1422
+ language: string;
1423
+ /** Confidence in detection (0-1) */
1424
+ confidence: number;
1425
+ /** Evidence (e.g. ["package.json", "tsconfig.json", "*.ts files"]) */
1426
+ evidence: string[];
1427
+ }
1428
+ interface DocFile {
1429
+ /** Absolute path */
1430
+ path: string;
1431
+ /** Relative path from project root */
1432
+ relativePath: string;
1433
+ /** Estimated type */
1434
+ kind: "readme" | "changelog" | "contributing" | "license" | "config" | "docs" | "other";
1435
+ /** File size in bytes */
1436
+ size: number;
1437
+ }
1438
+ type ProjectShape = "app" | "library" | "monorepo" | "workspace" | "script" | "unknown";
1439
+ interface OnboardResult {
1440
+ /** Project root */
1441
+ directory: string;
1442
+ /** Detected languages (sorted by confidence) */
1443
+ languages: LanguageInfo[];
1444
+ /** Detected project shape */
1445
+ shape: ProjectShape;
1446
+ /** Shape evidence */
1447
+ shapeEvidence: string[];
1448
+ /** Discovered documentation files */
1449
+ docs: DocFile[];
1450
+ /** Total files scanned */
1451
+ totalFiles: number;
1452
+ /** Duration in ms */
1453
+ durationMs: number;
1454
+ /** Suggested ingestion plan */
1455
+ plan: IngestionPlan;
1456
+ }
1457
+ interface IngestionPlan {
1458
+ /** Priority files to ingest first */
1459
+ priorityFiles: DocFile[];
1460
+ /** Estimated total files to ingest */
1461
+ estimatedFiles: number;
1462
+ /** Recommended categories */
1463
+ categories: string[];
1464
+ /** Suggested memory namespace */
1465
+ suggestedNamespace: string;
1613
1466
  }
1467
+ declare function onboard(options: OnboardOptions): OnboardResult;
1468
+
1614
1469
  /**
1615
- * Optional injection point for tests. The real connector dynamically imports
1616
- * `googleapis`; tests pass a stub here to avoid the optional-peer-dep
1617
- * machinery and to keep the test suite hermetic.
1470
+ * @remnic/core Curation
1618
1471
  *
1619
- * The shape only covers the tiny slice of the SDK we actually use.
1472
+ * Deliberate ingestion of files into memory with provenance tracking.
1473
+ * Supports statement-level extraction, dedup, and contradiction checks.
1620
1474
  */
1621
- interface GoogleDriveClientFactory {
1622
- (config: GoogleDriveConnectorConfig): Promise<GoogleDriveClient>;
1475
+ interface CurateOptions {
1476
+ /** File or directory path to curate */
1477
+ targetPath: string;
1478
+ /** Memory root directory for writing */
1479
+ memoryDir: string;
1480
+ /** Source label (e.g. "manual", "docs", "onboarding") */
1481
+ source?: string;
1482
+ /** Category override (default: auto-detect) */
1483
+ category?: string;
1484
+ /** Confidence to assign (default: 0.9 for curated items) */
1485
+ confidence?: number;
1486
+ /** Entity reference to attach */
1487
+ entityRef?: string;
1488
+ /** Tags to add */
1489
+ tags?: string[];
1490
+ /** Whether to perform dedup check against existing memories */
1491
+ checkDuplicates?: boolean;
1492
+ /** Whether to detect contradictions */
1493
+ checkContradictions?: boolean;
1494
+ /** Whether to write files (default: true). False = dry run */
1495
+ write?: boolean;
1496
+ }
1497
+ interface CuratedStatement {
1498
+ /** Unique ID for this statement */
1499
+ id: string;
1500
+ /** The extracted statement text */
1501
+ content: string;
1502
+ /** Category */
1503
+ category: string;
1504
+ /** Confidence */
1505
+ confidence: number;
1506
+ /** Provenance info */
1507
+ provenance: StatementProvenance;
1508
+ /** Hash of content for dedup */
1509
+ contentHash: string;
1510
+ /** Tags */
1511
+ tags: string[];
1512
+ /** Entity reference */
1513
+ entityRef?: string;
1514
+ }
1515
+ interface StatementProvenance {
1516
+ /** Source file path */
1517
+ sourcePath: string;
1518
+ /** Relative path from project root */
1519
+ relativePath: string;
1520
+ /** Source label */
1521
+ source: string;
1522
+ /** Line number if extractable (0 = unknown) */
1523
+ lineNumber: number;
1524
+ /** Timestamp of ingestion */
1525
+ ingestedAt: string;
1526
+ /** Hash of the source file for diff tracking */
1527
+ sourceFileHash: string;
1623
1528
  }
1529
+ interface CurateResult {
1530
+ /** Statements extracted */
1531
+ statements: CuratedStatement[];
1532
+ /** Files processed */
1533
+ filesProcessed: number;
1534
+ /** Files skipped (empty, binary, etc.) */
1535
+ filesSkipped: number;
1536
+ /** Duplicate statements found (if checkDuplicates) */
1537
+ duplicates: DuplicateResult[];
1538
+ /** Contradictions found (if checkContradictions) */
1539
+ contradictions: ContradictionResult$1[];
1540
+ /** Memory files written */
1541
+ written: string[];
1542
+ /** Duration in ms */
1543
+ durationMs: number;
1544
+ }
1545
+ interface DuplicateResult {
1546
+ /** New statement */
1547
+ newStatement: CuratedStatement;
1548
+ /** Existing memory ID that matches */
1549
+ existingId: string;
1550
+ /** Similarity score (0-1) */
1551
+ similarity: number;
1552
+ /** Recommended action */
1553
+ action: "skip" | "merge" | "keep";
1554
+ }
1555
+ interface ContradictionResult$1 {
1556
+ /** New statement */
1557
+ newStatement: CuratedStatement;
1558
+ /** Conflicting memory ID */
1559
+ conflictingId: string;
1560
+ /** The conflicting content */
1561
+ conflictingContent: string;
1562
+ /** Severity */
1563
+ severity: "high" | "medium" | "low";
1564
+ }
1565
+ declare function curate(options: CurateOptions): Promise<CurateResult>;
1566
+
1624
1567
  /**
1625
- * Minimal Drive client surface. Tests provide a fake; production wraps
1626
- * `googleapis` to fit. Method shapes mirror the upstream API where it
1627
- * matters (`startPageToken` / `nextPageToken` / `newStartPageToken`,
1628
- * `files.modifiedTime` ISO 8601 strings).
1568
+ * @remnic/core Dedup & Contradiction Detection
1569
+ *
1570
+ * Statement-level deduplication and contradiction detection
1571
+ * against existing memories. Can be used standalone or via curation.
1629
1572
  */
1630
- interface GoogleDriveClient {
1631
- /** Mirrors `drive.changes.getStartPageToken()`. */
1632
- getStartPageToken(): Promise<{
1633
- startPageToken: string;
1634
- }>;
1635
- /**
1636
- * Mirrors `drive.changes.list(...)`. We page until the response yields a
1637
- * `newStartPageToken` (i.e., no more pages). Each `change.file`, when
1638
- * present, includes the metadata we need to decide whether to ingest.
1639
- */
1640
- listChanges(args: {
1641
- pageToken: string;
1642
- pageSize: number;
1643
- }): Promise<DriveChangesPage>;
1644
- /**
1645
- * Export a Google-native doc to plaintext. Returns the body as a string.
1646
- */
1647
- exportFile(args: {
1648
- fileId: string;
1649
- mimeType: string;
1650
- }): Promise<string>;
1651
- /**
1652
- * Download a non-Google-native file as a string. Used for `text/*` MIME
1653
- * types; binary formats are filtered out before we get here.
1654
- */
1655
- getFileMedia(args: {
1656
- fileId: string;
1657
- }): Promise<string>;
1573
+ interface MemoryEntry {
1574
+ /** Memory ID */
1575
+ id: string;
1576
+ /** Content text */
1577
+ content: string;
1578
+ /** Category */
1579
+ category: string;
1580
+ /** File path (if known) */
1581
+ filePath?: string;
1582
+ }
1583
+ interface DedupOptions {
1584
+ /** Memory root directory */
1585
+ memoryDir: string;
1586
+ /** Categories to scan (default: all) */
1587
+ categories?: string[];
1588
+ /** Similarity threshold for fuzzy matching (0-1, default: 0.85) */
1589
+ threshold?: number;
1590
+ /** Max memories to load (default: 10000) */
1591
+ maxLoad?: number;
1592
+ }
1593
+ interface DedupResult {
1594
+ /** Total memories scanned */
1595
+ scanned: number;
1596
+ /** Duplicate pairs found */
1597
+ duplicates: DuplicatePair[];
1598
+ /** Duration in ms */
1599
+ durationMs: number;
1600
+ }
1601
+ interface DuplicatePair {
1602
+ /** First memory */
1603
+ left: MemoryEntry;
1604
+ /** Second memory */
1605
+ right: MemoryEntry;
1606
+ /** Similarity score */
1607
+ similarity: number;
1608
+ /** Recommended action */
1609
+ action: "merge" | "keep_left" | "keep_right";
1610
+ }
1611
+ interface ContradictionOptions {
1612
+ /** Memory root directory */
1613
+ memoryDir: string;
1614
+ /** Categories to scan (default: all) */
1615
+ categories?: string[];
1616
+ /** Max memories to load (default: 10000) */
1617
+ maxLoad?: number;
1618
+ }
1619
+ interface ContradictionResult {
1620
+ /** Total memories scanned */
1621
+ scanned: number;
1622
+ /** Contradictions found */
1623
+ contradictions: ContradictionPair[];
1624
+ /** Duration in ms */
1625
+ durationMs: number;
1626
+ }
1627
+ interface ContradictionPair {
1628
+ /** First statement */
1629
+ left: MemoryEntry;
1630
+ /** Contradicting statement */
1631
+ right: MemoryEntry;
1632
+ /** Severity */
1633
+ severity: "high" | "medium" | "low";
1634
+ /** Reason */
1635
+ reason: string;
1636
+ }
1637
+ declare function findDuplicates(options: DedupOptions): DedupResult;
1638
+ declare function findContradictions(options: ContradictionOptions): ContradictionResult;
1639
+
1640
+ /**
1641
+ * @remnic/core — Review Inbox
1642
+ *
1643
+ * Manages low-confidence memories and suggestions pending review.
1644
+ * Integrates with the existing review-queue system.
1645
+ */
1646
+ interface ReviewItem {
1647
+ /** Memory ID */
1648
+ id: string;
1649
+ /** Content text */
1650
+ content: string;
1651
+ /** Category */
1652
+ category: string;
1653
+ /** Confidence score (0-1) */
1654
+ confidence: number;
1655
+ /** Confidence tier */
1656
+ confidenceTier: string;
1657
+ /** Source */
1658
+ source: string;
1659
+ /** File path if available */
1660
+ filePath?: string;
1661
+ /** Created date */
1662
+ created: string;
1663
+ /** Reason it's in review */
1664
+ reviewReason: "low_confidence" | "suggestion" | "contradiction" | "duplicate" | "tombstone_blocked";
1665
+ /** Additional context */
1666
+ context?: string;
1667
+ }
1668
+ type ReviewAction = "approve" | "dismiss" | "flag";
1669
+ interface ReviewResult {
1670
+ /** Item acted upon */
1671
+ itemId: string;
1672
+ /** Action taken */
1673
+ action: ReviewAction;
1674
+ /** Updated file path (if modified) */
1675
+ updatedPath?: string;
1676
+ /** Status message */
1677
+ message: string;
1678
+ /** Tombstone id cleared by an approve action (for the caller to revoke). */
1679
+ clearedTombstoneId?: string;
1658
1680
  }
1659
- interface DriveChangesPage {
1660
- readonly changes: readonly DriveChange[];
1661
- readonly newStartPageToken?: string;
1662
- readonly nextPageToken?: string;
1681
+ interface ReviewListResult {
1682
+ /** Items pending review */
1683
+ items: ReviewItem[];
1684
+ /** Total count */
1685
+ total: number;
1686
+ /** Duration in ms */
1687
+ durationMs: number;
1663
1688
  }
1664
- interface DriveChange {
1665
- readonly removed?: boolean;
1666
- readonly fileId?: string;
1667
- readonly file?: DriveFileMetadata;
1689
+ interface ReviewOptions {
1690
+ /** Memory root directory */
1691
+ memoryDir: string;
1692
+ /** Filter by reason */
1693
+ reason?: ReviewItem["reviewReason"];
1694
+ /** Max items to return (default: 50) */
1695
+ limit?: number;
1696
+ /** Include items with confidence below this threshold (default: 0.7) */
1697
+ confidenceThreshold?: number;
1668
1698
  }
1669
- interface DriveFileMetadata {
1670
- readonly id: string;
1671
- readonly name?: string;
1672
- readonly mimeType?: string;
1673
- readonly modifiedTime?: string;
1674
- readonly trashed?: boolean;
1675
- readonly parents?: readonly string[];
1676
- readonly webViewLink?: string;
1677
- readonly size?: string | number;
1699
+ interface ReviewActionOptions {
1700
+ /** Match the threshold used when listing review items (default: 0.7) */
1701
+ confidenceThreshold?: number;
1702
+ /**
1703
+ * Revocation hook (issue #1579). When approving a memory whose frontmatter
1704
+ * carries `blockedBy: <tombstoneId>`, the hook fires so the caller (CLI /
1705
+ * orchestrator) can append a `kind: "revocation"` tombstone entry —
1706
+ * re-allowing the content. Fire-and-forget: a revocation failure MUST NOT
1707
+ * fail the approval (gotcha #13). The hook receives the tombstone id and
1708
+ * the memory id.
1709
+ */
1710
+ onApproveBlockedMemory?: (tombstoneId: string, memoryId: string) => void | Promise<void>;
1678
1711
  }
1679
1712
  /**
1680
- * Result of a single sync pass — exposed for richer test assertions.
1681
- * Strict superset of `SyncIncrementalResult`.
1713
+ * List items pending review.
1682
1714
  */
1683
- interface GoogleDriveSyncResult extends SyncIncrementalResult {
1684
- readonly skippedBinary: number;
1685
- readonly skippedFolderScope: number;
1686
- readonly skippedTooLarge: number;
1687
- }
1715
+ declare function listReviewItems(options: ReviewOptions): ReviewListResult;
1688
1716
  /**
1689
- * Validate and normalise raw config. Throws with a concrete message on any
1690
- * malformed input — never silently defaults (CLAUDE.md gotcha #51).
1717
+ * Perform a review action on an item.
1691
1718
  */
1692
- declare function validateGoogleDriveConfig(raw: unknown): GoogleDriveConnectorConfig;
1719
+ declare function performReview(memoryDir: string, itemId: string, action: ReviewAction, options?: ReviewActionOptions): ReviewResult;
1720
+
1693
1721
  /**
1694
- * Construct the connector. The `clientFactory` argument is the test hook —
1695
- * production callers omit it and the connector lazy-loads `googleapis`.
1722
+ * @remnic/core Diff-Aware Sync
1723
+ *
1724
+ * Watches source files for changes and triggers re-ingestion
1725
+ * only for changed content. Uses file hashing to detect changes.
1696
1726
  */
1697
- declare function createGoogleDriveConnector(options?: {
1698
- clientFactory?: GoogleDriveClientFactory;
1699
- }): LiveConnector;
1727
+ interface SyncOptions {
1728
+ /** Source directory to watch */
1729
+ sourceDir: string;
1730
+ /** Memory root directory */
1731
+ memoryDir: string;
1732
+ /** State file path (stores hashes). Default: memoryDir/.sync-state.json */
1733
+ stateFile?: string;
1734
+ /** File extensions to watch (default: .md, .txt, .mdx) */
1735
+ extensions?: string[];
1736
+ /** Directories to exclude */
1737
+ excludeDirs?: string[];
1738
+ /** Poll interval for watchForChanges. Default: 5000ms */
1739
+ pollIntervalMs?: number;
1740
+ /** Whether to actually write changes (default: true) */
1741
+ dryRun?: boolean;
1742
+ }
1743
+ interface SyncResult {
1744
+ /** Files scanned */
1745
+ scanned: number;
1746
+ /** Files changed since last sync */
1747
+ changed: FileChange[];
1748
+ /** Files unchanged */
1749
+ unchanged: number;
1750
+ /** Files deleted since last sync */
1751
+ deleted: string[];
1752
+ /** Files newly added */
1753
+ added: string[];
1754
+ /** Duration in ms */
1755
+ durationMs: number;
1756
+ /** State file path */
1757
+ stateFile: string;
1758
+ }
1759
+ interface FileChange {
1760
+ /** Absolute file path */
1761
+ filePath: string;
1762
+ /** Relative path from source root */
1763
+ relativePath: string;
1764
+ /** Change type */
1765
+ type: "added" | "modified" | "deleted";
1766
+ /** Current content hash */
1767
+ currentHash: string;
1768
+ /** Previous content hash (if modified) */
1769
+ previousHash?: string;
1770
+ /** File size in bytes */
1771
+ size: number;
1772
+ }
1773
+ interface SyncState {
1774
+ /** Map of relative path → content hash */
1775
+ fileHashes: Record<string, string>;
1776
+ /** Last sync timestamp */
1777
+ lastSyncAt: string;
1778
+ /** Version of state format */
1779
+ version: number;
1780
+ }
1781
+ declare function syncChanges(options: SyncOptions): SyncResult;
1700
1782
  /**
1701
- * Production client factory. Lazy-loads `googleapis` via a computed-specifier
1702
- * dynamic import so bundlers never statically resolve it (CLAUDE.md gotcha
1703
- * #57). Surfaces a precise install hint on miss.
1704
- *
1705
- * Exported only for the `index.ts` barrel; consumers that already inject a
1706
- * test factory don't need to touch this.
1783
+ * Watch for changes and call callback on file changes.
1784
+ * Returns a stop function.
1707
1785
  */
1708
- declare const defaultGoogleDriveClientFactory: GoogleDriveClientFactory;
1786
+ declare function watchForChanges(options: SyncOptions, onChange: (changes: FileChange[]) => void | Promise<void>): {
1787
+ stop: () => void;
1788
+ };
1709
1789
 
1710
1790
  /**
1711
- * @remnic/coreNotion live connector (issue #683 PR 3/N)
1712
- *
1713
- * Concrete `LiveConnector` implementation that incrementally imports text
1714
- * content from Notion database pages into Remnic. Built on top of the
1715
- * framework shipped in PR 1/N (`framework.ts` / `registry.ts` /
1716
- * `state-store.ts`) and mirrors the structure of the Google Drive connector
1717
- * (PR 2/N).
1718
- *
1719
- * Design notes:
1720
- *
1721
- * - **Auth.** Integration token from config (`connectors.notion.token`).
1722
- * The token is accepted at config-parse time but never logged. Operators
1723
- * must populate it from a secret store; per the repo-wide privacy policy
1724
- * no real value may appear in tests or comments.
1725
- *
1726
- * - **Scope.** `databaseIds` in config limits the import to the listed
1727
- * Notion databases. The connector queries each database for pages whose
1728
- * `last_edited_time` is after a per-page high-water mark stored in the
1729
- * cursor. When `databaseIds` is empty the connector does nothing (safe
1730
- * default — no credentials → no import).
1731
- *
1732
- * - **Cursor semantics.** The cursor is a JSON string encoding a
1733
- * `NotionCursorPayload`: a map from page-id to last-seen
1734
- * `last_edited_time` ISO string. On the first sync (cursor=null) we
1735
- * seed the payload from the current state of each database WITHOUT
1736
- * importing any content, so "first install" doesn't re-ingest history.
1737
- * Each subsequent pass only imports pages edited after the stored
1738
- * watermark.
1739
- *
1740
- * - **Block extraction.** Page content is fetched via
1741
- * `blocks.children.list` recursively up to `MAX_BLOCK_DEPTH` levels.
1742
- * Block text is extracted to Markdown-ish plain text (no raw JSON blobs).
1743
- * Only text-bearing block types are included; unsupported types are
1744
- * silently skipped.
1745
- *
1746
- * - **Raw `fetch`.** We call the Notion REST API directly rather than using
1747
- * `@notionhq/client` — there is no optional-peer-dep machinery needed and
1748
- * the API surface we consume is tiny. The `fetchFn` argument is the test
1749
- * hook allowing stubbing without network access.
1750
- *
1751
- * - **Idempotency.** `ConnectorDocument.source.externalId` is the page id
1752
- * and `externalRevision` is `last_edited_time`, so downstream dedup can
1753
- * recognise repeat fetches if the cursor is rewound.
1754
- *
1755
- * - **Privacy.** No page content is ever logged. Database ids and counts
1756
- * may be logged. The integration token is never exposed in logs, state,
1757
- * or error messages.
1791
+ * memory-extension-host/render-extensions-block.tsRender discovered extensions
1792
+ * into a markdown block for injection into consolidation prompts.
1758
1793
  *
1759
- * - **Read-only.** This connector only reads. It never modifies pages,
1760
- * databases, or any other Notion resource.
1794
+ * Respects the global token budget (REMNIC_EXTENSIONS_TOTAL_TOKEN_LIMIT) and
1795
+ * truncates with a footer listing omitted extensions when over budget.
1761
1796
  */
1762
1797
 
1763
- /** Stable connector id. Lives in the registry under this exact string. */
1764
- declare const NOTION_CONNECTOR_ID = "notion";
1765
- /**
1766
- * Cursor `kind` we emit. Opaque to the framework; documented here so
1767
- * tests can assert on it.
1768
- */
1769
- declare const NOTION_CURSOR_KIND = "notionWatermark";
1770
- /**
1771
- * Default poll interval (5 minutes). Notion's API has no push capability;
1772
- * polling sub-minute wastes quota for a personal memory layer.
1773
- */
1774
- declare const NOTION_DEFAULT_POLL_INTERVAL_MS: number;
1775
- /**
1776
- * Validated, frozen view of `connectors.notion.*`.
1777
- */
1778
- interface NotionConnectorConfig {
1779
- /** Notion integration token. Starts with `secret_`. */
1780
- readonly token: string;
1781
- /** Database ids to import pages from. Empty = connector is a no-op. */
1782
- readonly databaseIds: readonly string[];
1783
- /** Poll interval surfaced to the scheduler (ms). */
1784
- readonly pollIntervalMs: number;
1785
- }
1786
1798
  /**
1787
- * Minimal fetch-compatible surface we use. The real connector delegates to
1788
- * the global `fetch`; tests inject a stub factory.
1789
- */
1790
- type NotionFetchFn = (url: string, init: {
1791
- method: string;
1792
- headers: Record<string, string>;
1793
- body?: string;
1794
- signal?: AbortSignal;
1795
- }) => Promise<{
1796
- ok: boolean;
1797
- status: number;
1798
- json(): Promise<unknown>;
1799
- }>;
1800
- /**
1801
- * Validate and normalise raw config. Throws with a concrete message on any
1802
- * malformed input — never silently defaults (CLAUDE.md gotcha #51).
1799
+ * Render a markdown block containing extension instructions for injection
1800
+ * into consolidation prompts.
1801
+ *
1802
+ * If the list is empty, returns "".
1803
+ * Inlines extensions in name order until the token budget is exhausted.
1804
+ * If the budget is exceeded, appends a truncation footer listing omitted extensions.
1803
1805
  */
1804
- declare function validateNotionConfig(raw: unknown): NotionConnectorConfig;
1806
+ declare function renderExtensionsBlock(extensions: DiscoveredExtension[]): string;
1805
1807
  /**
1806
- * Construct the connector. The `fetchFn` argument is the test hook —
1807
- * production callers omit it and the connector uses the global `fetch`.
1808
+ * Render a compact one-line footer listing active extension names.
1809
+ * Used by day-summary and summary-snapshot where full instructions are not needed.
1808
1810
  */
1809
- declare function createNotionConnector(options?: {
1810
- fetchFn?: NotionFetchFn;
1811
- }): LiveConnector;
1811
+ declare function renderExtensionsFooter(extensions: DiscoveredExtension[]): string;
1812
1812
 
1813
1813
  /**
1814
1814
  * @remnic/core — Spaces + Collaboration