@remnic/core 9.3.694 → 9.3.696

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 (332) hide show
  1. package/dist/access-boundary.d.ts +12 -11
  2. package/dist/access-boundary.js +34 -28
  3. package/dist/access-cli.js +60 -59
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +12 -11
  6. package/dist/access-http.js +38 -32
  7. package/dist/access-mcp.d.ts +12 -11
  8. package/dist/access-mcp.js +37 -31
  9. package/dist/access-operations.d.ts +12 -11
  10. package/dist/access-operations.js +36 -30
  11. package/dist/access-schema.js +3 -3
  12. package/dist/{access-service-Ceq4TdPO.d.ts → access-service-BqBQeUJd.d.ts} +15 -2
  13. package/dist/access-service.d.ts +8 -7
  14. package/dist/access-service.js +33 -27
  15. package/dist/access-surface-catalog.d.ts +12 -11
  16. package/dist/action-confidence.d.ts +1 -1
  17. package/dist/active-memory-bridge.d.ts +1 -1
  18. package/dist/active-recall.d.ts +1 -1
  19. package/dist/active-recall.js +1 -1
  20. package/dist/{auto-sync-RWXT6P5M.js → auto-sync-PFW2NJZY.js} +8 -8
  21. package/dist/behavior-learner.d.ts +1 -1
  22. package/dist/behavior-signals.d.ts +1 -1
  23. package/dist/bootstrap.d.ts +9 -8
  24. package/dist/briefing.d.ts +1 -1
  25. package/dist/briefing.js +12 -5
  26. package/dist/buffer-surprise-report.d.ts +1 -1
  27. package/dist/buffer.d.ts +1 -1
  28. package/dist/calibration.d.ts +1 -1
  29. package/dist/calibration.js +2 -2
  30. package/dist/capabilities.d.ts +1 -1
  31. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-YO5QJ6L3.js} +2 -2
  32. package/dist/{catalog-Tkb991rg.d.ts → catalog-D7YDNNF7.d.ts} +1 -1
  33. package/dist/causal-behavior.d.ts +1 -1
  34. package/dist/causal-behavior.js +2 -2
  35. package/dist/causal-chain.js +2 -2
  36. package/dist/causal-consolidation.d.ts +1 -1
  37. package/dist/causal-consolidation.js +17 -16
  38. package/dist/causal-consolidation.js.map +1 -1
  39. package/dist/causal-retrieval.js +2 -2
  40. package/dist/causal-trajectory-graph.js +2 -2
  41. package/dist/causal-trajectory.js +1 -1
  42. package/dist/{chunk-UPTZYUYJ.js → chunk-24D66MMN.js} +2 -1
  43. package/dist/chunk-24D66MMN.js.map +1 -0
  44. package/dist/{chunk-FCH766FX.js → chunk-32XTY2DQ.js} +51 -51
  45. package/dist/{chunk-YN4ZT4CW.js → chunk-3LYJIA76.js} +1 -1
  46. package/dist/{chunk-24FCDBJV.js → chunk-3P2XEQSV.js} +2 -2
  47. package/dist/{chunk-JIKBEDSB.js → chunk-4NNDJOAO.js} +8 -8
  48. package/dist/{chunk-EPVIBPVE.js → chunk-52QIAN76.js} +2 -2
  49. package/dist/chunk-5AT62NQH.js +553 -0
  50. package/dist/chunk-5AT62NQH.js.map +1 -0
  51. package/dist/{chunk-TAPSNIMA.js → chunk-5CEJH5ZN.js} +4 -4
  52. package/dist/{chunk-4RGWCRIJ.js → chunk-5TYA4OCF.js} +4 -4
  53. package/dist/{chunk-PILLH262.js → chunk-6GAUF4OU.js} +3 -3
  54. package/dist/{chunk-QXWQ5V24.js → chunk-AF6DSAWD.js} +2 -2
  55. package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
  56. package/dist/{chunk-32KD5IHZ.js → chunk-CLXTDOKV.js} +2 -2
  57. package/dist/{chunk-ARV3AUOM.js → chunk-DL6H3D7S.js} +2 -2
  58. package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
  59. package/dist/{chunk-KCRV3HVN.js → chunk-EC3A6M43.js} +2 -2
  60. package/dist/{chunk-PNW4KKJX.js → chunk-EK6FYL2E.js} +27 -1
  61. package/dist/chunk-EK6FYL2E.js.map +1 -0
  62. package/dist/{chunk-YI3KC2B5.js → chunk-FZMH66NT.js} +2 -2
  63. package/dist/{chunk-DT2ZGQT7.js → chunk-G663XUEG.js} +2 -2
  64. package/dist/{chunk-TYEOAFH3.js → chunk-G6SVZWPB.js} +10 -1
  65. package/dist/chunk-G6SVZWPB.js.map +1 -0
  66. package/dist/{chunk-CDOIDOTQ.js → chunk-HKET3ZOI.js} +4 -4
  67. package/dist/{chunk-YOI3ELXF.js → chunk-IF362TF6.js} +2 -2
  68. package/dist/{chunk-UX5QTAFE.js → chunk-J45OCIVH.js} +2 -2
  69. package/dist/{chunk-UDJLF3BO.js → chunk-JI6HWBYL.js} +2 -2
  70. package/dist/{chunk-ECZPWSB5.js → chunk-JJT3AGL4.js} +4 -4
  71. package/dist/{chunk-6C4WHMKV.js → chunk-JMAKCWJ3.js} +6 -6
  72. package/dist/{chunk-4NFVPDIL.js → chunk-LM6JB2EG.js} +2 -2
  73. package/dist/{chunk-MOGFK2DU.js → chunk-LPIWEKZ3.js} +2 -2
  74. package/dist/{chunk-LD53WPMU.js → chunk-LQ6JI4VH.js} +4 -4
  75. package/dist/{chunk-IZLNJYLX.js → chunk-MNFAGE5D.js} +2 -2
  76. package/dist/{chunk-NXNUG7IA.js → chunk-MOBRVKWE.js} +23 -12
  77. package/dist/chunk-MOBRVKWE.js.map +1 -0
  78. package/dist/{chunk-5A35EGHM.js → chunk-N4PD5IY3.js} +35 -35
  79. package/dist/{chunk-PQP6MFPL.js → chunk-NFMDHE45.js} +2 -2
  80. package/dist/{chunk-WVTNZUER.js → chunk-NLZO5NO6.js} +2 -2
  81. package/dist/{chunk-5S53ZBPH.js → chunk-Q6ZHGGNJ.js} +2 -2
  82. package/dist/{chunk-SN5DFE34.js → chunk-Q7SFJURX.js} +2 -2
  83. package/dist/{chunk-MXZWKOOR.js → chunk-QJM2XAJD.js} +11 -11
  84. package/dist/{chunk-6ZNIUGXB.js → chunk-QQW2LIEA.js} +4 -4
  85. package/dist/{chunk-ZKD3UVEO.js → chunk-QVTSLRKT.js} +3 -3
  86. package/dist/{chunk-C5AOVGVC.js → chunk-RWST6NOL.js} +166 -102
  87. package/dist/chunk-RWST6NOL.js.map +1 -0
  88. package/dist/{chunk-YXZPKBFW.js → chunk-SPVIG2R3.js} +10 -10
  89. package/dist/{chunk-B55EAZNF.js → chunk-TIVZ4MCG.js} +2 -2
  90. package/dist/{chunk-5KBNNUXS.js → chunk-TT7BJWGI.js} +2 -2
  91. package/dist/{chunk-KQAFEZQX.js → chunk-VDX2J7OX.js} +2 -2
  92. package/dist/{chunk-E6BOD3UJ.js → chunk-VPAFWWCL.js} +2 -2
  93. package/dist/{chunk-D7PQ7HZL.js → chunk-W54EAJT4.js} +2 -2
  94. package/dist/{chunk-QVG4LAQO.js → chunk-WR6KFJKA.js} +7 -7
  95. package/dist/{chunk-YQGHW5ML.js → chunk-WRFKZEO6.js} +4 -4
  96. package/dist/{chunk-SJGTY2TW.js → chunk-XUNQLJT2.js} +6 -6
  97. package/dist/{chunk-P7IDNF7H.js → chunk-XWT4JXXA.js} +2 -2
  98. package/dist/{chunk-MSUA4KAK.js → chunk-XXA6T3O4.js} +2 -2
  99. package/dist/{chunk-KF74X62T.js → chunk-YGKUAX2B.js} +4 -4
  100. package/dist/{chunk-YWYZPKK3.js → chunk-YOZNTNNA.js} +2 -2
  101. package/dist/{chunk-U62ZGWU7.js → chunk-ZXUOAFUG.js} +1 -1
  102. package/dist/chunk-ZXUOAFUG.js.map +1 -0
  103. package/dist/{cli-CxtZyBLl.d.ts → cli-DBDgdvh9.d.ts} +3 -3
  104. package/dist/cli.d.ts +11 -10
  105. package/dist/cli.js +56 -50
  106. package/dist/compounding/engine.d.ts +1 -1
  107. package/dist/compounding/engine.js +12 -5
  108. package/dist/compounding/preference-consolidator.d.ts +1 -1
  109. package/dist/compression-optimizer.d.ts +1 -1
  110. package/dist/config.d.ts +1 -1
  111. package/dist/config.js +1 -1
  112. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  113. package/dist/connectors/codex-materialize-runner.js +12 -5
  114. package/dist/connectors/codex-materialize.d.ts +1 -1
  115. package/dist/connectors/index.d.ts +1 -1
  116. package/dist/connectors/index.js +12 -5
  117. package/dist/consolidation-provenance-check.d.ts +3 -3
  118. package/dist/consolidation-undo.d.ts +2 -2
  119. package/dist/contradiction/index.d.ts +1 -1
  120. package/dist/conversation-index/backend.d.ts +1 -1
  121. package/dist/conversation-index/chunker.d.ts +1 -1
  122. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  123. package/dist/conversation-index/indexer.d.ts +1 -1
  124. package/dist/conversation-index/search.d.ts +1 -1
  125. package/dist/day-summary.d.ts +1 -1
  126. package/dist/delinearize.d.ts +1 -1
  127. package/dist/direct-answer-wiring.d.ts +1 -1
  128. package/dist/direct-answer.d.ts +1 -1
  129. package/dist/embedding-fallback.d.ts +1 -1
  130. package/dist/enrichment/index.d.ts +1 -1
  131. package/dist/entity-retrieval.d.ts +1 -1
  132. package/dist/entity-retrieval.js +13 -5
  133. package/dist/entity-schema.d.ts +1 -1
  134. package/dist/explicit-capture.d.ts +9 -8
  135. package/dist/extraction-faithfulness.d.ts +211 -0
  136. package/dist/extraction-faithfulness.js +35 -0
  137. package/dist/extraction-judge-telemetry.d.ts +1 -1
  138. package/dist/extraction-judge-training.d.ts +1 -1
  139. package/dist/extraction-judge.d.ts +1 -1
  140. package/dist/extraction-judge.js +4 -4
  141. package/dist/extraction.d.ts +1 -1
  142. package/dist/extraction.js +7 -7
  143. package/dist/fallback-llm.d.ts +1 -1
  144. package/dist/fallback-llm.js +2 -2
  145. package/dist/{graph-edge-decay-D7OESCBR.js → graph-edge-decay-JLOF3YW4.js} +3 -3
  146. package/dist/graph-snapshot.js +3 -3
  147. package/dist/graph.js +2 -2
  148. package/dist/identity-continuity.d.ts +1 -1
  149. package/dist/importance.d.ts +1 -1
  150. package/dist/index.d.ts +12 -11
  151. package/dist/index.js +96 -95
  152. package/dist/index.js.map +1 -1
  153. package/dist/intent.d.ts +1 -1
  154. package/dist/lcm/engine.d.ts +1 -1
  155. package/dist/lcm/index.d.ts +1 -1
  156. package/dist/lcm/index.js +3 -3
  157. package/dist/lcm/tools.d.ts +1 -1
  158. package/dist/lifecycle.d.ts +1 -1
  159. package/dist/live-connectors-runner.d.ts +1 -1
  160. package/dist/local-llm.d.ts +1 -1
  161. package/dist/local-llm.js +1 -1
  162. package/dist/maintenance/memory-governance.d.ts +1 -1
  163. package/dist/maintenance/memory-governance.js +13 -5
  164. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +13 -5
  165. package/dist/maintenance/rebuild-memory-projection.js +14 -6
  166. package/dist/mcp-memory-inspector-app.d.ts +12 -11
  167. package/dist/memory-action-policy.d.ts +1 -1
  168. package/dist/memory-cache.d.ts +1 -1
  169. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  170. package/dist/memory-projection-store.d.ts +1 -1
  171. package/dist/memory-provenance.d.ts +1 -1
  172. package/dist/memory-worth-outcomes.d.ts +1 -1
  173. package/dist/models-json.d.ts +1 -1
  174. package/dist/namespaces/migrate.d.ts +2 -2
  175. package/dist/namespaces/migrate.js +20 -12
  176. package/dist/namespaces/principal.d.ts +1 -1
  177. package/dist/namespaces/search.d.ts +1 -1
  178. package/dist/namespaces/search.js +6 -6
  179. package/dist/namespaces/storage.d.ts +2 -2
  180. package/dist/namespaces/storage.js +13 -5
  181. package/dist/native-knowledge.d.ts +1 -1
  182. package/dist/operator-toolkit.d.ts +1 -1
  183. package/dist/operator-toolkit.js +26 -19
  184. package/dist/orchestration/maintenance.d.ts +2 -2
  185. package/dist/orchestration/maintenance.js +15 -7
  186. package/dist/{orchestrator-CnbxRQs8.d.ts → orchestrator-Dv8JQu3-.d.ts} +20 -3
  187. package/dist/orchestrator.d.ts +6 -5
  188. package/dist/orchestrator.js +47 -46
  189. package/dist/patterns-cli.d.ts +1 -1
  190. package/dist/policy-runtime.d.ts +1 -1
  191. package/dist/provenance.d.ts +1 -1
  192. package/dist/qmd-recall-cache.d.ts +1 -1
  193. package/dist/qmd.d.ts +1 -1
  194. package/dist/recall-disclosure-escalation.d.ts +1 -1
  195. package/dist/recall-explain-renderer.d.ts +1 -1
  196. package/dist/recall-explain-renderer.js +3 -3
  197. package/dist/recall-planner-llm.d.ts +1 -1
  198. package/dist/recall-planner-llm.js +2 -2
  199. package/dist/recall-state.d.ts +1 -1
  200. package/dist/recall-tag-filter.d.ts +1 -1
  201. package/dist/recall-xray-cli.d.ts +1 -1
  202. package/dist/recall-xray-cli.js +4 -4
  203. package/dist/recall-xray-renderer.d.ts +1 -1
  204. package/dist/recall-xray-renderer.js +3 -3
  205. package/dist/recall-xray.d.ts +1 -1
  206. package/dist/recall-xray.js +2 -2
  207. package/dist/resolve-auth-token.d.ts +1 -1
  208. package/dist/resume-bundles.js +2 -2
  209. package/dist/retrieval-agents.d.ts +1 -1
  210. package/dist/retrieval-tiers.d.ts +1 -1
  211. package/dist/routing/engine.d.ts +1 -1
  212. package/dist/routing/store.d.ts +1 -1
  213. package/dist/schemas.d.ts +22 -22
  214. package/dist/search/embed-helper.d.ts +1 -1
  215. package/dist/search/factory.d.ts +1 -1
  216. package/dist/search/factory.js +5 -5
  217. package/dist/search/index.d.ts +1 -1
  218. package/dist/search/index.js +5 -5
  219. package/dist/search/lancedb-backend.d.ts +1 -1
  220. package/dist/search/lancedb-backend.js +2 -2
  221. package/dist/search/meilisearch-backend.d.ts +1 -1
  222. package/dist/search/meilisearch-backend.js +2 -2
  223. package/dist/search/noop-backend.d.ts +1 -1
  224. package/dist/search/orama-backend.d.ts +1 -1
  225. package/dist/search/orama-backend.js +2 -2
  226. package/dist/search/port.d.ts +1 -1
  227. package/dist/search/remote-backend.d.ts +1 -1
  228. package/dist/{semantic-consolidation-DXmtQ_4B.d.ts → semantic-consolidation-CgREi9E0.d.ts} +1 -1
  229. package/dist/semantic-consolidation.d.ts +2 -2
  230. package/dist/semantic-consolidation.js +13 -6
  231. package/dist/semantic-rule-promotion.js +13 -5
  232. package/dist/semantic-rule-verifier.d.ts +1 -1
  233. package/dist/semantic-rule-verifier.js +13 -5
  234. package/dist/session-observer-bands.d.ts +1 -1
  235. package/dist/session-observer-state.d.ts +1 -1
  236. package/dist/shared-context/manager.d.ts +1 -1
  237. package/dist/signal.d.ts +1 -1
  238. package/dist/{state-NCHQ4TRG.js → state-ITXJKXRE.js} +2 -2
  239. package/dist/storage.d.ts +11 -1
  240. package/dist/storage.js +12 -4
  241. package/dist/summarizer.d.ts +1 -1
  242. package/dist/summarizer.js +6 -6
  243. package/dist/summary-snapshot.d.ts +1 -1
  244. package/dist/temporal-supersession.d.ts +1 -1
  245. package/dist/temporal-validity.d.ts +1 -1
  246. package/dist/threading.d.ts +1 -1
  247. package/dist/tier-migration.d.ts +1 -1
  248. package/dist/tier-routing.d.ts +1 -1
  249. package/dist/topics.d.ts +1 -1
  250. package/dist/{trace-WM7V4CKI.js → trace-ZFVRH4GO.js} +3 -3
  251. package/dist/transcript.d.ts +1 -1
  252. package/dist/transfer/backup.js +2 -2
  253. package/dist/transfer/capsule-export.js +2 -2
  254. package/dist/transfer/capsule-import.js +2 -2
  255. package/dist/transfer/types.d.ts +12 -12
  256. package/dist/{tui-RI7P6PBS.js → tui-55UJMJX7.js} +3 -3
  257. package/dist/tui-55UJMJX7.js.map +1 -0
  258. package/dist/{types-CEWBmjCH.d.ts → types-CeZYSC-E.d.ts} +37 -1
  259. package/dist/types.d.ts +1 -1
  260. package/dist/types.js +1 -1
  261. package/dist/utility-runtime.d.ts +1 -1
  262. package/dist/verified-recall.js +13 -5
  263. package/package.json +2 -2
  264. package/src/config.test.ts +16 -0
  265. package/src/config.ts +30 -0
  266. package/src/console/state.ts +32 -0
  267. package/src/extraction-faithfulness.test.ts +1112 -0
  268. package/src/extraction-faithfulness.ts +993 -0
  269. package/src/faithfulness-graph-guard.test.ts +252 -0
  270. package/src/local-llm.ts +1 -0
  271. package/src/orchestrator.ts +169 -45
  272. package/src/storage.ts +19 -0
  273. package/src/types.ts +47 -0
  274. package/dist/chunk-C5AOVGVC.js.map +0 -1
  275. package/dist/chunk-NXNUG7IA.js.map +0 -1
  276. package/dist/chunk-PNW4KKJX.js.map +0 -1
  277. package/dist/chunk-TYEOAFH3.js.map +0 -1
  278. package/dist/chunk-U62ZGWU7.js.map +0 -1
  279. package/dist/chunk-UPTZYUYJ.js.map +0 -1
  280. /package/dist/{auto-sync-RWXT6P5M.js.map → auto-sync-PFW2NJZY.js.map} +0 -0
  281. /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
  282. /package/dist/{chunk-FCH766FX.js.map → chunk-32XTY2DQ.js.map} +0 -0
  283. /package/dist/{chunk-YN4ZT4CW.js.map → chunk-3LYJIA76.js.map} +0 -0
  284. /package/dist/{chunk-24FCDBJV.js.map → chunk-3P2XEQSV.js.map} +0 -0
  285. /package/dist/{chunk-JIKBEDSB.js.map → chunk-4NNDJOAO.js.map} +0 -0
  286. /package/dist/{chunk-EPVIBPVE.js.map → chunk-52QIAN76.js.map} +0 -0
  287. /package/dist/{chunk-TAPSNIMA.js.map → chunk-5CEJH5ZN.js.map} +0 -0
  288. /package/dist/{chunk-4RGWCRIJ.js.map → chunk-5TYA4OCF.js.map} +0 -0
  289. /package/dist/{chunk-PILLH262.js.map → chunk-6GAUF4OU.js.map} +0 -0
  290. /package/dist/{chunk-QXWQ5V24.js.map → chunk-AF6DSAWD.js.map} +0 -0
  291. /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
  292. /package/dist/{chunk-32KD5IHZ.js.map → chunk-CLXTDOKV.js.map} +0 -0
  293. /package/dist/{chunk-ARV3AUOM.js.map → chunk-DL6H3D7S.js.map} +0 -0
  294. /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.js.map} +0 -0
  295. /package/dist/{chunk-KCRV3HVN.js.map → chunk-EC3A6M43.js.map} +0 -0
  296. /package/dist/{chunk-YI3KC2B5.js.map → chunk-FZMH66NT.js.map} +0 -0
  297. /package/dist/{chunk-DT2ZGQT7.js.map → chunk-G663XUEG.js.map} +0 -0
  298. /package/dist/{chunk-CDOIDOTQ.js.map → chunk-HKET3ZOI.js.map} +0 -0
  299. /package/dist/{chunk-YOI3ELXF.js.map → chunk-IF362TF6.js.map} +0 -0
  300. /package/dist/{chunk-UX5QTAFE.js.map → chunk-J45OCIVH.js.map} +0 -0
  301. /package/dist/{chunk-UDJLF3BO.js.map → chunk-JI6HWBYL.js.map} +0 -0
  302. /package/dist/{chunk-ECZPWSB5.js.map → chunk-JJT3AGL4.js.map} +0 -0
  303. /package/dist/{chunk-6C4WHMKV.js.map → chunk-JMAKCWJ3.js.map} +0 -0
  304. /package/dist/{chunk-4NFVPDIL.js.map → chunk-LM6JB2EG.js.map} +0 -0
  305. /package/dist/{chunk-MOGFK2DU.js.map → chunk-LPIWEKZ3.js.map} +0 -0
  306. /package/dist/{chunk-LD53WPMU.js.map → chunk-LQ6JI4VH.js.map} +0 -0
  307. /package/dist/{chunk-IZLNJYLX.js.map → chunk-MNFAGE5D.js.map} +0 -0
  308. /package/dist/{chunk-5A35EGHM.js.map → chunk-N4PD5IY3.js.map} +0 -0
  309. /package/dist/{chunk-PQP6MFPL.js.map → chunk-NFMDHE45.js.map} +0 -0
  310. /package/dist/{chunk-WVTNZUER.js.map → chunk-NLZO5NO6.js.map} +0 -0
  311. /package/dist/{chunk-5S53ZBPH.js.map → chunk-Q6ZHGGNJ.js.map} +0 -0
  312. /package/dist/{chunk-SN5DFE34.js.map → chunk-Q7SFJURX.js.map} +0 -0
  313. /package/dist/{chunk-MXZWKOOR.js.map → chunk-QJM2XAJD.js.map} +0 -0
  314. /package/dist/{chunk-6ZNIUGXB.js.map → chunk-QQW2LIEA.js.map} +0 -0
  315. /package/dist/{chunk-ZKD3UVEO.js.map → chunk-QVTSLRKT.js.map} +0 -0
  316. /package/dist/{chunk-YXZPKBFW.js.map → chunk-SPVIG2R3.js.map} +0 -0
  317. /package/dist/{chunk-B55EAZNF.js.map → chunk-TIVZ4MCG.js.map} +0 -0
  318. /package/dist/{chunk-5KBNNUXS.js.map → chunk-TT7BJWGI.js.map} +0 -0
  319. /package/dist/{chunk-KQAFEZQX.js.map → chunk-VDX2J7OX.js.map} +0 -0
  320. /package/dist/{chunk-E6BOD3UJ.js.map → chunk-VPAFWWCL.js.map} +0 -0
  321. /package/dist/{chunk-D7PQ7HZL.js.map → chunk-W54EAJT4.js.map} +0 -0
  322. /package/dist/{chunk-QVG4LAQO.js.map → chunk-WR6KFJKA.js.map} +0 -0
  323. /package/dist/{chunk-YQGHW5ML.js.map → chunk-WRFKZEO6.js.map} +0 -0
  324. /package/dist/{chunk-SJGTY2TW.js.map → chunk-XUNQLJT2.js.map} +0 -0
  325. /package/dist/{chunk-P7IDNF7H.js.map → chunk-XWT4JXXA.js.map} +0 -0
  326. /package/dist/{chunk-MSUA4KAK.js.map → chunk-XXA6T3O4.js.map} +0 -0
  327. /package/dist/{chunk-KF74X62T.js.map → chunk-YGKUAX2B.js.map} +0 -0
  328. /package/dist/{chunk-YWYZPKK3.js.map → chunk-YOZNTNNA.js.map} +0 -0
  329. /package/dist/{state-NCHQ4TRG.js.map → extraction-faithfulness.js.map} +0 -0
  330. /package/dist/{graph-edge-decay-D7OESCBR.js.map → graph-edge-decay-JLOF3YW4.js.map} +0 -0
  331. /package/dist/{tui-RI7P6PBS.js.map → state-ITXJKXRE.js.map} +0 -0
  332. /package/dist/{trace-WM7V4CKI.js.map → trace-ZFVRH4GO.js.map} +0 -0
@@ -0,0 +1,993 @@
1
+ /**
2
+ * Extraction faithfulness gate — entailment verification of extracted
3
+ * facts against their verified source spans (issue #1576).
4
+ *
5
+ * Part of #1572 (glass-box memory). Depends on #1575 (provenance spans).
6
+ * Improved later by #1585 (fine-tuned local gate model).
7
+ *
8
+ * Unlike the extraction-judge (#376), which gates on fact-worthiness
9
+ * ("is this worth remembering?"), this module gates on faithfulness
10
+ * ("is this fact actually supported by what was said?"). Hallucinated
11
+ * or mis-paraphrased extractions become durable memories, get recalled
12
+ * with confidence, and poison downstream answers — the highest-severity
13
+ * accuracy failure a memory system can have.
14
+ *
15
+ * Design constraints:
16
+ * - Tagged results only — a backend failure is distinguishable from a
17
+ * genuine verdict (rule 34 / checklist §22: never conflate "empty"
18
+ * with "failed").
19
+ * - The gate consumes the QUOTE, not the whole conversation — passing
20
+ * full turns re-introduces the hallucination surface you're guarding
21
+ * and blows the latency budget.
22
+ * - No module-level state — batch queues hang off the orchestrator
23
+ * instance (rule 11).
24
+ * - Byte-identical pre-feature pipeline when mode is "off" (rule 39).
25
+ * - Graceful degradation — a backend failure never blocks writes
26
+ * (checklist §4); the fact proceeds with verdict "unchecked".
27
+ * - Never mutate mw_*\/trust fields — faithfulness is its own
28
+ * frontmatter island consumed by #1577.
29
+ */
30
+
31
+ import { createHash } from "node:crypto";
32
+
33
+ import { log } from "./logger.js";
34
+ import type { PluginConfig, MemoryFrontmatter, FaithfulnessFrontmatter } from "./types.js";
35
+ import type { LocalLlmClient } from "./local-llm.js";
36
+ import { type FallbackLlmClient, gatewayTaskChainOptions } from "./fallback-llm.js";
37
+ import { extractJsonCandidates } from "./json-extract.js";
38
+
39
+ // Re-export for callers importing from this module.
40
+ export type { FaithfulnessFrontmatter } from "./types.js";
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Public types
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /**
47
+ * The three entailment verdicts. "unchecked" is NOT a member of this union —
48
+ * it lives only in the frontmatter to mark facts that bypassed the gate
49
+ * (backend failure, no span, mode=off). A real verdict is always one of
50
+ * these three.
51
+ */
52
+ export type FaithfulnessVerdict = "entailed" | "contradicted" | "unsupported";
53
+
54
+ /**
55
+ * Failure codes for tagged-error results. Each is distinguishable so callers
56
+ * and telemetry can tell "no span available" from "the backend was down".
57
+ */
58
+ export type FaithfulnessFailureCode =
59
+ | "no_span"
60
+ | "backend_unavailable"
61
+ | "malformed_output"
62
+ | "timeout";
63
+
64
+ /**
65
+ * Tagged result — success carries the verdict + model + optional rationale;
66
+ * failure carries only the error code. Never conflate the two (rule 34).
67
+ */
68
+ export type FaithfulnessResult =
69
+ | { ok: true; verdict: FaithfulnessVerdict; model: string; rationale?: string }
70
+ | { ok: false; error: { code: FaithfulnessFailureCode } };
71
+
72
+ /**
73
+ * Input for a single faithfulness check. The quote is the verified span from
74
+ * #1575; context is optional surrounding turn text (bounded by
75
+ * faithfulnessContextChars).
76
+ */
77
+ export interface FaithfulnessCheckInput {
78
+ factText: string;
79
+ quote: string;
80
+ context?: string;
81
+ }
82
+
83
+ /**
84
+ * Shape persisted to frontmatter (issue #1576). Serialized as a single JSON
85
+ * line so it round-trips through the existing YAML parser without special
86
+ * handling.
87
+ *
88
+ * `verdict: "unchecked"` marks a fact that entered the gate but could not be
89
+ * evaluated (backend failure, timeout). It is distinct from the field being
90
+ * absent (which means the gate was off entirely or the fact predates #1576).
91
+ */
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Sealed prompt (hash lets #1573-style caching key on it)
95
+ // ---------------------------------------------------------------------------
96
+
97
+ const FAITHFULNESS_SYSTEM_PROMPT = `You are a faithfulness verifier for a memory system. Given a QUOTE from the source conversation and an extracted FACT, determine whether the FACT is supported by the QUOTE.
98
+
99
+ Rules:
100
+ - "entailed" — the FACT directly follows from the QUOTE (paraphrase is OK).
101
+ - "contradicted" — the FACT asserts something the QUOTE directly negates.
102
+ - "unsupported" — the FACT introduces information not present in the QUOTE.
103
+
104
+ Answer with a JSON array. Each element: {"index": <int>, "verdict": "entailed"|"contradicted"|"unsupported", "rationale": "<one sentence>"}.
105
+
106
+ Examples:
107
+ QUOTE: "I've been using Vim for about ten years now."
108
+ FACT: "The user has used Vim for approximately a decade."
109
+ {"index": 0, "verdict": "entailed", "rationale": "Paraphrase of the same duration."}
110
+
111
+ QUOTE: "I've been using Vim for about ten years now."
112
+ FACT: "The user prefers Emacs."
113
+ {"index": 0, "verdict": "unsupported", "rationale": "QUOTE mentions Vim, not Emacs."}
114
+
115
+ QUOTE: "I stopped drinking coffee last month."
116
+ FACT: "The user drinks coffee regularly."
117
+ {"index": 0, "verdict": "contradicted", "rationale": "QUOTE says they stopped; FACT claims the opposite."}`;
118
+
119
+ /**
120
+ * SHA-256 hash of the sealed system prompt. Cache keys and prompt-version
121
+ * telemetry include this so a prompt change is detectable (#1573-style
122
+ * caching can invalidate stale entries).
123
+ */
124
+ export const FAITHFULNESS_PROMPT_HASH = createHash("sha256")
125
+ .update(FAITHFULNESS_SYSTEM_PROMPT)
126
+ .digest("hex");
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Prompt construction
130
+ // ---------------------------------------------------------------------------
131
+
132
+ /**
133
+ * Build the user-prompt payload for a batch of (fact, quote) pairs.
134
+ * Each pair is numbered so the LLM's response can be correlated back.
135
+ *
136
+ * Context (when provided) is included after the quote, clearly delimited,
137
+ * so the model knows it is supplementary — not the primary evidence.
138
+ */
139
+ function buildBatchPrompt(inputs: FaithfulnessCheckInput[], contextChars: number): string {
140
+ const parts: string[] = [];
141
+ for (let i = 0; i < inputs.length; i++) {
142
+ const inp = inputs[i];
143
+ if (!inp) continue;
144
+ // The QUOTE is the primary entailment evidence and is already bounded
145
+ // upstream (provenance.maxQuoteChars / locateFactQuote). Send it whole
146
+ // so the verifier never judges entailment on a truncated span. Only the
147
+ // supplementary CONTEXT is bounded by contextChars (the config's "window
148
+ // around the quote" semantics — cursor review).
149
+ parts.push(`--- Item ${i} ---
150
+ QUOTE: "${inp.quote}"`);
151
+ if (inp.context && inp.context.trim().length > 0) {
152
+ parts.push(`CONTEXT: "${inp.context.slice(0, contextChars)}"`);
153
+ }
154
+ parts.push(`FACT: "${inp.factText}"
155
+
156
+ Respond with the JSON array entry for index ${i}.`);
157
+ }
158
+ parts.push(`\nRespond now with a single JSON array covering indexes 0–${inputs.length - 1}.`);
159
+ return parts.join("\n");
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Response parsing
164
+ // ---------------------------------------------------------------------------
165
+
166
+ const VALID_VERDICTS = new Set<string>(["entailed", "contradicted", "unsupported"]);
167
+
168
+ /**
169
+ * Parsed LLM response entry — validated shape.
170
+ */
171
+ interface ParsedFaithfulnessEntry {
172
+ index: number;
173
+ verdict: FaithfulnessVerdict;
174
+ rationale?: string;
175
+ }
176
+
177
+ /**
178
+ * Parse the LLM response into a map of index → entry. Returns null when the
179
+ * response is unparseable (rules 13/18: malformed output → tagged error,
180
+ * never a crash).
181
+ *
182
+ * Accepts both a bare JSON array and a JSON array embedded in surrounding
183
+ * text (the `extractJsonCandidates` helper handles fenced blocks and
184
+ * code-block-wrapped JSON).
185
+ */
186
+ export function parseFaithfulnessResponse(
187
+ raw: string,
188
+ expectedCount: number,
189
+ ): Map<number, ParsedFaithfulnessEntry> | null {
190
+ if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
191
+ return null;
192
+ }
193
+
194
+ const candidates = extractJsonCandidates(raw);
195
+ for (const candidate of candidates) {
196
+ let parsed: unknown;
197
+ try {
198
+ parsed = JSON.parse(candidate);
199
+ } catch {
200
+ continue;
201
+ }
202
+ const map = parseEntries(parsed, expectedCount);
203
+ if (map) return map;
204
+ }
205
+
206
+ // Last resort: the model may have returned bare objects without array
207
+ // wrapping — try parsing the whole thing as a single entry.
208
+ try {
209
+ const single = JSON.parse(raw.trim());
210
+ const map = parseEntries([single], expectedCount);
211
+ if (map) return map;
212
+ } catch {
213
+ // fall through
214
+ }
215
+
216
+ return null;
217
+ }
218
+
219
+ function parseEntries(
220
+ data: unknown,
221
+ expectedCount: number,
222
+ ): Map<number, ParsedFaithfulnessEntry> | null {
223
+ if (!Array.isArray(data)) {
224
+ // Accept object-wrapped arrays — {"results":[...]}, {"verdicts":[...]},
225
+ // {"entries":[...]}, {"facts":[...]} — a common JSON-object prompt shape the
226
+ // extraction-judge parser already accepts. Rejecting them marked the whole
227
+ // batch malformed_output; in enforce mode those facts then wrote as ACTIVE
228
+ // (gate bypass — codex Ob4RO).
229
+ if (data && typeof data === "object") {
230
+ for (const key of ["results", "verdicts", "entries", "facts"]) {
231
+ const inner = (data as Record<string, unknown>)[key];
232
+ if (Array.isArray(inner)) return parseEntries(inner, expectedCount);
233
+ }
234
+ }
235
+ return null;
236
+ }
237
+ const map = new Map<number, ParsedFaithfulnessEntry>();
238
+ for (const entry of data) {
239
+ if (!entry || typeof entry !== "object") continue;
240
+ const obj = entry as Record<string, unknown>;
241
+ const idx = typeof obj.index === "number" ? obj.index : undefined;
242
+ if (idx === undefined || idx < 0 || idx >= expectedCount) continue;
243
+ const verdictRaw = typeof obj.verdict === "string" ? obj.verdict : undefined;
244
+ if (!verdictRaw || !VALID_VERDICTS.has(verdictRaw)) continue;
245
+ const rationale =
246
+ typeof obj.rationale === "string" && obj.rationale.trim().length > 0
247
+ ? obj.rationale.trim().slice(0, 500)
248
+ : undefined;
249
+ map.set(idx, {
250
+ index: idx,
251
+ verdict: verdictRaw as FaithfulnessVerdict,
252
+ rationale,
253
+ });
254
+ }
255
+ return map.size > 0 ? map : null;
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // LLM call helper
260
+ // ---------------------------------------------------------------------------
261
+
262
+ interface LlmCallResult {
263
+ content: string | null;
264
+ modelUsed: string | null;
265
+ }
266
+
267
+ /**
268
+ * Call the model routing chain (local → fallback) with the faithfulness
269
+ * classification prompt. Mirrors `callJudgeLlm` in extraction-judge.ts so
270
+ * the same routing, model-override, and gateway-chain logic applies.
271
+ *
272
+ * Returns the raw content string and the model that produced it, or
273
+ * `{ content: null, modelUsed: null }` when every backend is unavailable.
274
+ */
275
+ async function callFaithfulnessLlm(
276
+ systemPrompt: string,
277
+ userPrompt: string,
278
+ config: PluginConfig,
279
+ localLlm: LocalLlmClient | null,
280
+ fallbackLlm: FallbackLlmClient | null,
281
+ timeoutMs: number,
282
+ signal?: AbortSignal,
283
+ ): Promise<LlmCallResult> {
284
+ const messages: Array<{ role: "system" | "user"; content: string }> = [
285
+ { role: "system", content: systemPrompt },
286
+ { role: "user", content: userPrompt },
287
+ ];
288
+
289
+ const modelOverride = config.extractionFaithfulnessModel || undefined;
290
+
291
+ // Skip the local backend when (a) modelSource is "gateway", or (b) a
292
+ // faithfulness model override is set. The local client always sends
293
+ // config.localLlmModel and silently ignores options.model, so a local
294
+ // success would run the wrong model and prevent the override from ever
295
+ // reaching the gateway. Routing straight to the gateway honors the
296
+ // override (codex review PRRT_kwDORJXyws6ObYQ8).
297
+ const skipLocal = config.modelSource === "gateway" || Boolean(modelOverride);
298
+ const gatewayChain = gatewayTaskChainOptions(config);
299
+
300
+ let modelUsed: string | null = null;
301
+
302
+ // Try local LLM first (only when no override routes the call to gateway).
303
+ // The local client uses its OWN per-attempt AbortController keyed on
304
+ // `timeoutMs` (it does not read options.signal — see LocalLlmChatCompletionOptions),
305
+ // so the batch `signal` is not forwarded here; `timeoutMs` bounds each local
306
+ // attempt instead. The batch AbortController still governs the fallback path,
307
+ // which DOES honor the signal. (Matches extraction-judge.ts, which also passes
308
+ // no signal to the local client.)
309
+ if (localLlm && !skipLocal) {
310
+ try {
311
+ const result = await callLocalLlm(localLlm, messages, {
312
+ temperature: 0.1,
313
+ maxTokens: 2048,
314
+ responseFormat: { type: "json_object" },
315
+ timeoutMs,
316
+ operation: "extraction-faithfulness",
317
+ });
318
+ if (result.content) {
319
+ return { content: result.content, modelUsed: result.modelUsed ?? "local" };
320
+ }
321
+ } catch (err) {
322
+ log.debug(
323
+ `extraction-faithfulness: local LLM failed, trying fallback: ${err instanceof Error ? err.message : String(err)}`,
324
+ );
325
+ }
326
+ }
327
+
328
+ // Try fallback LLM
329
+ if (fallbackLlm) {
330
+ try {
331
+ const result = await fallbackLlm.chatCompletion(
332
+ messages as Array<{ role: "system" | "user" | "assistant"; content: string }>,
333
+ {
334
+ temperature: 0.1,
335
+ maxTokens: 2048,
336
+ timeoutMs,
337
+ ...(modelOverride ? { model: modelOverride } : {}),
338
+ ...gatewayChain,
339
+ ...(signal ? { signal } : {}),
340
+ },
341
+ );
342
+ if (result?.content) {
343
+ return { content: result.content, modelUsed: result.modelUsed ?? "fallback" };
344
+ }
345
+ } catch (err) {
346
+ log.debug(
347
+ `extraction-faithfulness: fallback LLM failed: ${err instanceof Error ? err.message : String(err)}`,
348
+ );
349
+ }
350
+ }
351
+
352
+ return { content: null, modelUsed: null };
353
+ }
354
+
355
+ /**
356
+ * Call the local LLM's chatCompletion through a narrow, typed interface.
357
+ * LocalLlmClient.chatCompletion accepts an options bag that includes an
358
+ * `operation` discriminator; we forward it so the client can route
359
+ * appropriately.
360
+ */
361
+ interface LocalLlmCallOptions {
362
+ temperature: number;
363
+ maxTokens: number;
364
+ responseFormat?: { type: string };
365
+ timeoutMs: number;
366
+ operation: string;
367
+ }
368
+
369
+ interface LocalLlmCallResponse {
370
+ content: string | null;
371
+ modelUsed?: string;
372
+ }
373
+
374
+ async function callLocalLlm(
375
+ client: LocalLlmClient,
376
+ messages: Array<{ role: "system" | "user"; content: string }>,
377
+ options: LocalLlmCallOptions,
378
+ ): Promise<LocalLlmCallResponse> {
379
+ // LocalLlmClient.chatCompletion returns { content: string } — we use a
380
+ // typed call signature so no `any` leaks. The local client does not
381
+ // expose modelUsed, so we return "local" as the model identifier.
382
+ const result = await client.chatCompletion(messages, options);
383
+ return {
384
+ content: result?.content ?? null,
385
+ };
386
+ }
387
+
388
+ // ---------------------------------------------------------------------------
389
+ // Core batch check
390
+ // ---------------------------------------------------------------------------
391
+
392
+ /**
393
+ * Result of a batch faithfulness check — per-input results plus timing.
394
+ */
395
+ export interface FaithfulnessBatchResult {
396
+ results: FaithfulnessResult[];
397
+ /** Wall-clock duration of the LLM call (0 when backend was not called). */
398
+ elapsedMs: number;
399
+ }
400
+
401
+ /**
402
+ * Evaluate a batch of (fact, quote) pairs for faithfulness.
403
+ *
404
+ * - Calls the LLM once with all pairs (bounded by `inputs.length`).
405
+ * - On timeout → every input gets `{ ok: false, code: "timeout" }`.
406
+ * - On backend unavailable → `{ ok: false, code: "backend_unavailable" }`.
407
+ * - On malformed output → `{ ok: false, code: "malformed_output" }`.
408
+ * - Inputs with empty quotes get `{ ok: false, code: "no_span" }` and are
409
+ * NOT sent to the LLM.
410
+ *
411
+ * No module-level state: the caller (orchestrator) owns any caches.
412
+ */
413
+ export async function checkFaithfulnessBatch(
414
+ inputs: FaithfulnessCheckInput[],
415
+ config: PluginConfig,
416
+ localLlm: LocalLlmClient | null,
417
+ fallbackLlm: FallbackLlmClient | null,
418
+ ): Promise<FaithfulnessBatchResult> {
419
+ const timeoutMs =
420
+ typeof config.extractionFaithfulnessTimeoutMs === "number" &&
421
+ Number.isFinite(config.extractionFaithfulnessTimeoutMs) &&
422
+ config.extractionFaithfulnessTimeoutMs > 0
423
+ ? config.extractionFaithfulnessTimeoutMs
424
+ : 8000;
425
+
426
+ const contextChars =
427
+ typeof config.extractionFaithfulnessContextChars === "number" &&
428
+ Number.isFinite(config.extractionFaithfulnessContextChars) &&
429
+ config.extractionFaithfulnessContextChars > 0
430
+ ? config.extractionFaithfulnessContextChars
431
+ : 400;
432
+
433
+ // Partition: inputs with a real quote vs. inputs without one.
434
+ const results: FaithfulnessResult[] = new Array(inputs.length);
435
+ const checkableIndices: number[] = [];
436
+ const checkableInputs: FaithfulnessCheckInput[] = [];
437
+
438
+ for (let i = 0; i < inputs.length; i++) {
439
+ const inp = inputs[i];
440
+ if (!inp || !inp.quote || inp.quote.trim().length === 0) {
441
+ results[i] = { ok: false, error: { code: "no_span" } };
442
+ } else if (!inp.factText || inp.factText.trim().length === 0) {
443
+ results[i] = { ok: false, error: { code: "no_span" } };
444
+ } else {
445
+ checkableIndices.push(i);
446
+ checkableInputs.push(inp);
447
+ }
448
+ }
449
+
450
+ if (checkableInputs.length === 0) {
451
+ return { results, elapsedMs: 0 };
452
+ }
453
+
454
+ const userPrompt = buildBatchPrompt(checkableInputs, contextChars);
455
+ const startedAt = Date.now();
456
+
457
+ let timedOut = false;
458
+ const controller = new AbortController();
459
+ // Race the LLM call against the budget so the batch fails open at
460
+ // `timeoutMs` regardless of which backend is in flight. The local backend
461
+ // ignores the batch AbortSignal (it aborts each attempt via its own
462
+ // controller keyed on `timeoutMs`), so awaiting callFaithfulnessLlm directly
463
+ // could block past the budget on a slow/retrying local verifier. The timer
464
+ // both aborts the fallback (which honors the signal) and resolves the race
465
+ // so the batch returns promptly; the in-flight local call aborts on its own
466
+ // per-attempt timeoutMs. (codex review PRRT_kwDORJXyws6ObgMJ.)
467
+ // Manual deferred instead of Promise.withResolvers (ES2024) — plugin-openclaw's
468
+ // standalone tsconfig targets ES2022 lib and this module is reachable from its
469
+ // type graph, so withResolvers would TS2550 there.
470
+ let resolveTimeout!: (value: true) => void;
471
+ const racedTimeout = new Promise<true>((resolve) => {
472
+ resolveTimeout = resolve;
473
+ });
474
+ const timer = setTimeout(() => {
475
+ timedOut = true;
476
+ controller.abort();
477
+ resolveTimeout(true);
478
+ }, timeoutMs);
479
+
480
+ try {
481
+ const callPromise = callFaithfulnessLlm(
482
+ FAITHFULNESS_SYSTEM_PROMPT,
483
+ userPrompt,
484
+ config,
485
+ localLlm,
486
+ fallbackLlm,
487
+ timeoutMs,
488
+ controller.signal,
489
+ );
490
+ const settled = await Promise.race([
491
+ callPromise.then(
492
+ (r) => ({ done: true as const, result: r }),
493
+ // An unexpected throw is treated as "no content" (backend_unavailable)
494
+ // so the race never rejects and the outer catch is the final safety net.
495
+ () => ({ done: true as const, result: { content: null, modelUsed: null } }),
496
+ ),
497
+ racedTimeout.then(() => ({ done: false as const })),
498
+ ]);
499
+ const elapsedMs = Date.now() - startedAt;
500
+
501
+ if (!settled.done) {
502
+ // The budget elapsed before any backend returned content. Fail open as
503
+ // timeout; the orphaned callPromise resolves/rejects harmlessly (the
504
+ // fallback was aborted; the local client aborts on its own timeoutMs).
505
+ for (const idx of checkableIndices) {
506
+ results[idx] = { ok: false, error: { code: "timeout" } };
507
+ }
508
+ return { results, elapsedMs };
509
+ }
510
+
511
+ const llmResult = settled.result;
512
+
513
+ // Cursor review: if the LLM returned usable content, use it even when
514
+ // the abort timer raced — a response that lands just as the timer fires
515
+ // has real verdicts. Only fall back to timeout errors when there is no
516
+ // content to parse (the call genuinely did not complete in time).
517
+ if (timedOut && !llmResult.content) {
518
+ for (const idx of checkableIndices) {
519
+ results[idx] = { ok: false, error: { code: "timeout" } };
520
+ }
521
+ return { results, elapsedMs };
522
+ }
523
+
524
+ if (!llmResult.content) {
525
+ for (const idx of checkableIndices) {
526
+ results[idx] = { ok: false, error: { code: "backend_unavailable" } };
527
+ }
528
+ return { results, elapsedMs };
529
+ }
530
+
531
+ const parsed = parseFaithfulnessResponse(llmResult.content, checkableInputs.length);
532
+ if (!parsed) {
533
+ for (const idx of checkableIndices) {
534
+ results[idx] = { ok: false, error: { code: "malformed_output" } };
535
+ }
536
+ return { results, elapsedMs };
537
+ }
538
+
539
+ for (let j = 0; j < checkableIndices.length; j++) {
540
+ const inputIdx = checkableIndices[j];
541
+ if (inputIdx === undefined) continue;
542
+ const entry = parsed.get(j);
543
+ if (entry) {
544
+ results[inputIdx] = {
545
+ ok: true,
546
+ verdict: entry.verdict,
547
+ model: llmResult.modelUsed ?? "unknown",
548
+ ...(entry.rationale ? { rationale: entry.rationale } : {}),
549
+ };
550
+ } else {
551
+ // LLM response was missing this index
552
+ results[inputIdx] = { ok: false, error: { code: "malformed_output" } };
553
+ }
554
+ }
555
+
556
+ return { results, elapsedMs };
557
+ } catch (err) {
558
+ const elapsedMs = Date.now() - startedAt;
559
+ if (timedOut || isAbortError(err)) {
560
+ for (const idx of checkableIndices) {
561
+ results[idx] = { ok: false, error: { code: "timeout" } };
562
+ }
563
+ return { results, elapsedMs };
564
+ }
565
+ log.warn(
566
+ `extraction-faithfulness: batch check threw unexpectedly: ${err instanceof Error ? err.message : String(err)}`,
567
+ );
568
+ for (const idx of checkableIndices) {
569
+ results[idx] = { ok: false, error: { code: "backend_unavailable" } };
570
+ }
571
+ return { results, elapsedMs };
572
+ } finally {
573
+ clearTimeout(timer);
574
+ }
575
+ }
576
+
577
+ function isAbortError(err: unknown): boolean {
578
+ if (!err || typeof err !== "object") return false;
579
+ const maybe = err as { name?: string; message?: string };
580
+ return (
581
+ maybe.name === "AbortError" ||
582
+ maybe.message === "This operation was aborted" ||
583
+ maybe.message === "The operation was aborted"
584
+ );
585
+ }
586
+
587
+
588
+ // ---------------------------------------------------------------------------
589
+ // Extraction-path orchestration helpers (issue #1576)
590
+ //
591
+ // These wrap checkFaithfulnessBatch + verdict application so the orchestrator
592
+ // holds only thin delegation (ground rule 4: god files gain thin wiring only).
593
+ // The core algorithm lives above; these are call-site glue.
594
+ // ---------------------------------------------------------------------------
595
+
596
+ /**
597
+ * Telemetry counters for the faithfulness gate. Hang off the orchestrator
598
+ * instance (rule 11: no module-level state) and surface via console_state so
599
+ * `remnic doctor` renders the verdict distribution.
600
+ */
601
+ export interface FaithfulnessGateCounters {
602
+ entailed: number;
603
+ contradicted: number;
604
+ unsupported: number;
605
+ unchecked: number;
606
+ skippedNoSpan: number;
607
+ }
608
+
609
+ /**
610
+ * Create a fresh zeroed counters object. Callers store it on the orchestrator
611
+ * instance and pass it by reference to the gate helpers, which mutate it.
612
+ */
613
+ export function createFaithfulnessCounters(): FaithfulnessGateCounters {
614
+ return { entailed: 0, contradicted: 0, unsupported: 0, unchecked: 0, skippedNoSpan: 0 };
615
+ }
616
+
617
+ /**
618
+ * Structural slice of an extracted fact that the gate reads. Kept loose so
619
+ * this pure module does not depend on the full `ExtractedFact` type.
620
+ */
621
+ export interface FaithfulnessGateFact {
622
+ content: string;
623
+ sources?: { quote?: string }[];
624
+ }
625
+
626
+ /**
627
+ * Run the faithfulness batch over a list of extracted facts and return a map
628
+ * keyed by the ORIGINAL fact index → result. Facts without a usable verified
629
+ * source span are omitted from the batch (they are tagged `skipped_no_span`
630
+ * at apply time, never gated — don't punish legacy data). Updates `counters`
631
+ * for console_state telemetry.
632
+ *
633
+ * Fail-open: any pipeline error is caught, logged, and an empty map returned
634
+ * so the caller records `unchecked`/`skipped_no_span` and proceeds — a gate
635
+ * outage must never block memory writes (checklist §4).
636
+ */
637
+ // Common English stopwords — filtered before overlap scoring so function
638
+ // words (the, a, is, ...) don't dilute the signal between a fact and its
639
+ // source sentence.
640
+ const STOPWORDS = new Set([
641
+ "the","a","an","and","or","but","is","are","was","were","be","been","being",
642
+ "to","of","in","on","at","for","with","from","by","as","it","its","this","that",
643
+ "these","those","i","you","he","she","we","they","my","your","his","her","our","their",
644
+ "has","have","had","do","does","did","will","would","can","could","should","not","no",
645
+ "s","very","really","just","so","than","then","there","here","about","into","over","under",
646
+ ]);
647
+
648
+ /**
649
+ * Crude stemmer: strip common suffixes so "prefers"/"prefer",
650
+ * "using"/"use", "started"/"start" collapse to one token. Not a real
651
+ * stemmer — just enough to raise recall for the interim locator (#1575 will
652
+ * replace this with NLI-verified spans).
653
+ */
654
+ function crudeStem(word: string): string {
655
+ if (word.length > 5 && word.endsWith("ing")) return word.slice(0, -3);
656
+ if (word.length > 4 && word.endsWith("ed")) return word.slice(0, -2);
657
+ if (word.length > 3 && (word.endsWith("s")) && !word.endsWith("ss")) return word.slice(0, -1);
658
+ return word;
659
+ }
660
+
661
+ /**
662
+ * Tokenize text into a lowercase, stopword-filtered, crudely-stemmed token
663
+ * set for overlap scoring.
664
+ */
665
+ function tokenize(text: string): Set<string> {
666
+ const tokens = new Set<string>();
667
+ for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
668
+ if (raw.length <= 1) continue;
669
+ if (STOPWORDS.has(raw)) continue;
670
+ tokens.add(crudeStem(raw));
671
+ }
672
+ return tokens;
673
+ }
674
+
675
+ /**
676
+ * Overlap coefficient: |A ∩ B| / min(|A|, |B|). Robust for paraphrase
677
+ * matching where a short fact paraphrases a longer source sentence — a short
678
+ * fact fully supported by a long sentence scores high, unrelated text scores 0.
679
+ */
680
+ function overlapCoefficient(a: Set<string>, b: Set<string>): number {
681
+ if (a.size === 0 || b.size === 0) return 0;
682
+ let intersection = 0;
683
+ const [small, large] = a.size <= b.size ? [a, b] : [b, a];
684
+ for (const t of small) if (large.has(t)) intersection++;
685
+ return intersection / small.size;
686
+ }
687
+
688
+ /**
689
+ * Minimum overlap for a located quote to be trusted as the fact's source span.
690
+ * Below this the fact is treated as having no located span (skipped_no_span)
691
+ * rather than judged against an unrelated sentence.
692
+ */
693
+ const LOCATE_QUOTE_MIN_OVERLAP = 0.3;
694
+
695
+ /**
696
+ * Locate the best-matching verbatim span (sentence) from the source turn text
697
+ * for an extracted fact, by token overlap. Returns the quote when a confident
698
+ * match is found, `undefined` otherwise (the gate then records
699
+ * skipped_no_span — never judged against an unrelated span).
700
+ *
701
+ * This is the interim source-text locator that makes the gate functional on
702
+ * real extraction output (where #1575 has not yet attached per-fact
703
+ * `sources`). It produces a genuine located quote per fact so the gate
704
+ * consumes a span, not the whole conversation (issue #1576 design constraint).
705
+ * #1575's NLI-verified per-fact locator will replace this when it lands.
706
+ */
707
+ /**
708
+ * Extract a bounded window of `sourceText` centered on the located `quote`,
709
+ * bounded by `contextChars` (the config's "window around the quote" semantics).
710
+ *
711
+ * Used by the fallback-locator path in `runFaithfulnessGateBatch` so the
712
+ * verifier sees surrounding turn text — without it,
713
+ * `extractionFaithfulnessContextChars` is effectively ignored in the
714
+ * production path (codex review PRRT_kwDORJXyws6OblI1). Returns undefined when
715
+ * the quote is absent from sourceText (e.g. it was truncated by maxQuoteChars).
716
+ */
717
+ export function extractContextWindow(
718
+ sourceText: string,
719
+ quote: string,
720
+ contextChars: number,
721
+ ): string | undefined {
722
+ if (!sourceText || !quote || !(contextChars > 0)) return undefined;
723
+ const idx = sourceText.indexOf(quote);
724
+ if (idx < 0) return undefined;
725
+ const quoteEnd = idx + quote.length;
726
+ const center = Math.floor((idx + quoteEnd) / 2);
727
+ const half = Math.floor(contextChars / 2);
728
+ let start = Math.max(0, center - half);
729
+ const end = Math.min(sourceText.length, start + contextChars);
730
+ // Re-anchor start so the window uses the full budget when end clamped.
731
+ start = Math.max(0, end - contextChars);
732
+ const window = sourceText.slice(start, end).trim();
733
+ return window.length > 0 ? window : undefined;
734
+ }
735
+
736
+ export function locateFactQuote(
737
+ factText: string,
738
+ sourceText: string,
739
+ maxQuoteChars = 600,
740
+ ): string | undefined {
741
+ if (!factText || !sourceText) return undefined;
742
+ const factTokens = tokenize(factText);
743
+ if (factTokens.size === 0) return undefined;
744
+ // Split source into candidate spans: sentences, then line segments as a
745
+ // fallback for transcripts without sentence punctuation.
746
+ const candidates: string[] = [];
747
+ for (const sentence of sourceText.split(/(?<=[.!?])\s+|\n+/)) {
748
+ const s = sentence.trim();
749
+ if (s.length > 0) candidates.push(s);
750
+ }
751
+ if (candidates.length === 0) return undefined;
752
+ let best: { quote: string; score: number } | null = null;
753
+ for (const candidate of candidates) {
754
+ const score = overlapCoefficient(factTokens, tokenize(candidate));
755
+ if (!best || score > best.score) best = { quote: candidate, score };
756
+ }
757
+ if (!best || best.score < LOCATE_QUOTE_MIN_OVERLAP) return undefined;
758
+ return best.quote.length > maxQuoteChars
759
+ ? best.quote.slice(0, maxQuoteChars)
760
+ : best.quote;
761
+ }
762
+ export async function runFaithfulnessGateBatch(
763
+ facts: readonly FaithfulnessGateFact[],
764
+ mode: "shadow" | "enforce",
765
+ config: PluginConfig,
766
+ localLlm: LocalLlmClient | null,
767
+ fallbackLlm: FallbackLlmClient | null,
768
+ counters: FaithfulnessGateCounters,
769
+ /**
770
+ * The verbatim source turn text the facts were extracted from. When a fact
771
+ * has no #1575 `sources`, locateFactQuote finds a fallback span here so the
772
+ * gate runs on real extraction output. May be empty (replay/import paths
773
+ * with no source turns) — facts then get skipped_no_span.
774
+ */
775
+ sourceText = "",
776
+ ): Promise<Map<number, FaithfulnessResult> | null> {
777
+ // Phase 1 — build the checkable inputs. This is pure (no LLM, no throw):
778
+ // locateFactQuote and the source/span selection never reject. Keeping it
779
+ // outside the try lets the catch walk the same inputs to tag a pipeline
780
+ // failure as "unchecked" rather than dropping it on the floor.
781
+ const inputs: { factIndex: number; input: FaithfulnessCheckInput }[] = [];
782
+ for (let fi = 0; fi < facts.length; fi++) {
783
+ const f = facts[fi];
784
+ if (!f || typeof f.content !== "string" || !f.content.trim()) continue;
785
+ // Prefer #1575 verified spans; fall back to a located quote from the
786
+ // source turn text so the gate runs even before per-fact sources are
787
+ // attached. Without either, the fact is skipped_no_span (never gated).
788
+ // A composite fact may be supported by multiple adjacent spans — collect
789
+ // every valid source quote so the verifier sees the full evidence, not
790
+ // just sources[0] (codex review PRRT_kwDORJXyws6ObYQ_).
791
+ const sources = Array.isArray(f.sources) ? f.sources : [];
792
+ const sourceQuotes = sources
793
+ .map((s) => (s && typeof s.quote === "string" ? s.quote.trim() : ""))
794
+ .filter((q) => q.length > 0);
795
+ const usingFallbackLocator = sourceQuotes.length === 0;
796
+ const quote =
797
+ sourceQuotes.length > 0
798
+ ? sourceQuotes.join("\n")
799
+ : locateFactQuote(f.content, sourceText);
800
+ if (!quote) continue; // no located span — applyFaithfulnessVerdict tags skipped_no_span
801
+ // Pass source context into the verifier so extractionFaithfulnessContextChars
802
+ // actually applies in the fallback-locator path (codex P2
803
+ // PRRT_kwDORJXyws6OblI1). #1575 verified spans already carry full evidence,
804
+ // so context is only synthesized for the fallback locator.
805
+ const fallbackContext =
806
+ usingFallbackLocator
807
+ ? extractContextWindow(
808
+ sourceText,
809
+ quote,
810
+ config.extractionFaithfulnessContextChars,
811
+ )
812
+ : undefined;
813
+ inputs.push({
814
+ factIndex: fi,
815
+ input: {
816
+ factText: f.content,
817
+ quote,
818
+ ...(fallbackContext ? { context: fallbackContext } : {}),
819
+ },
820
+ });
821
+ }
822
+ const resultsByFactIndex = new Map<number, FaithfulnessResult>();
823
+ if (inputs.length === 0) return resultsByFactIndex;
824
+ try {
825
+ const batch = await checkFaithfulnessBatch(
826
+ inputs.map((x) => x.input),
827
+ config,
828
+ localLlm,
829
+ fallbackLlm,
830
+ );
831
+ for (let j = 0; j < inputs.length; j++) {
832
+ const entry = inputs[j];
833
+ if (entry) resultsByFactIndex.set(entry.factIndex, batch.results[j]!);
834
+ }
835
+ // Verdict-distribution counters are bumped in applyFaithfulnessVerdict
836
+ // (at the per-fact apply point), NOT here, so console_state reflects
837
+ // facts that actually reached verdict application — not facts later
838
+ // dropped by dedup, importance, or judge gates (cursor review).
839
+ log.info(
840
+ `extraction-faithfulness[${mode}]: ${inputs.length} facts checked, ${batch.elapsedMs}ms`,
841
+ );
842
+ } catch (err) {
843
+ // Fail-open: a pipeline error never blocks writes (checklist §4). Tag each
844
+ // checkable fact as backend_unavailable so applyFaithfulnessVerdict records
845
+ // "unchecked" (issue spec: backend failure → unchecked). Returning null
846
+ // here would make a shadow/enforce batch failure indistinguishable from
847
+ // gate-off, losing the telemetry signal (cursor review). Facts with no
848
+ // located span stay absent → skipped_no_span at apply time.
849
+ log.warn(
850
+ `extraction-faithfulness: pipeline error, tagging ${inputs.length} checkable facts as unchecked (fail-open): ${err instanceof Error ? err.message : String(err)}`,
851
+ );
852
+ for (const { factIndex } of inputs) {
853
+ resultsByFactIndex.set(factIndex, { ok: false, error: { code: "backend_unavailable" } });
854
+ }
855
+ }
856
+ return resultsByFactIndex;
857
+ }
858
+
859
+ /**
860
+ * Apply a pre-computed faithfulness verdict to a single fact, producing the
861
+ * frontmatter record + an optional enforce-mode `pending_review` status.
862
+ *
863
+ * - `resultsByFactIndex` null (gate off) → nothing (rule 39: byte-identical).
864
+ * - fact has no entry (no verified span) → `skipped_no_span`, never gated.
865
+ * - backend failure (`ok: false`) → `unchecked`, fact proceeds (checklist §4).
866
+ * - enforce + unsupported/contradicted → `pending_review` (memory persists,
867
+ * enters the review queue, never silently dropped).
868
+ *
869
+ * Mutates `counters.skippedNoSpan` for facts without a span.
870
+ */
871
+ export function applyFaithfulnessVerdict(
872
+ resultsByFactIndex: Map<number, FaithfulnessResult> | null,
873
+ factLoopIndex: number,
874
+ mode: "off" | "shadow" | "enforce",
875
+ factContent: string,
876
+ counters: FaithfulnessGateCounters,
877
+ ): {
878
+ faithfulness: FaithfulnessFrontmatter | undefined;
879
+ enforceStatus: "pending_review" | undefined;
880
+ } {
881
+ if (!resultsByFactIndex) {
882
+ return { faithfulness: undefined, enforceStatus: undefined };
883
+ }
884
+ const result = resultsByFactIndex.get(factLoopIndex);
885
+ if (!result) {
886
+ // No result for this fact index — it had no verified source span.
887
+ counters.skippedNoSpan++;
888
+ return {
889
+ faithfulness: { verdict: "skipped_no_span", at: new Date().toISOString() },
890
+ enforceStatus: undefined,
891
+ };
892
+ }
893
+ if (result.ok) {
894
+ if (result.verdict === "entailed") counters.entailed++;
895
+ else if (result.verdict === "contradicted") counters.contradicted++;
896
+ else if (result.verdict === "unsupported") counters.unsupported++;
897
+ const fm: FaithfulnessFrontmatter = {
898
+ verdict: result.verdict,
899
+ ...(result.model ? { model: result.model } : {}),
900
+ ...(result.rationale ? { rationale: result.rationale } : {}),
901
+ at: new Date().toISOString(),
902
+ };
903
+ let enforceStatus: "pending_review" | undefined;
904
+ if (
905
+ mode === "enforce" &&
906
+ (result.verdict === "unsupported" || result.verdict === "contradicted")
907
+ ) {
908
+ enforceStatus = "pending_review";
909
+ log.info(
910
+ `extraction-faithfulness[enforce]: routing "${factContent.slice(0, 60)}…" to pending_review (verdict=${result.verdict})`,
911
+ );
912
+ }
913
+ return { faithfulness: fm, enforceStatus };
914
+ }
915
+ // Backend failure — record as unchecked, fact proceeds (graceful degradation).
916
+ counters.unchecked++;
917
+ return {
918
+ faithfulness: { verdict: "unchecked", at: new Date().toISOString() },
919
+ enforceStatus: undefined,
920
+ };
921
+ }
922
+ // ---------------------------------------------------------------------------
923
+ // Frontmatter serialization (single-line JSON, like provenance sources)
924
+ // ---------------------------------------------------------------------------
925
+
926
+ /**
927
+ * Canonical key order for the serialized `faithfulness` frontmatter field.
928
+ * Deterministic emission (rule 38).
929
+ */
930
+ const FAITHFULNESS_KEY_ORDER = ["verdict", "model", "rationale", "at"] as const;
931
+
932
+ /**
933
+ * Serialize the `faithfulness` frontmatter field as a single JSON line,
934
+ * appended to `lines`. Called from `serializeFrontmatter` in storage.ts.
935
+ *
936
+ * Contract: when `fm.faithfulness` is absent, no line is emitted — the
937
+ * memory round-trips byte-identical to pre-#1576 behavior (rule 39).
938
+ */
939
+ export function serializeFaithfulnessFields(fm: MemoryFrontmatter, lines: string[]): void {
940
+ if (!fm.faithfulness) return;
941
+ const fm2 = fm.faithfulness;
942
+ const canonical: Record<string, unknown> = {};
943
+ for (const key of FAITHFULNESS_KEY_ORDER) {
944
+ const val = fm2[key];
945
+ if (val !== undefined && val !== null && val !== "") {
946
+ canonical[key] = val;
947
+ }
948
+ }
949
+ // verdict is always present on a valid FaithfulnessFrontmatter
950
+ if (!canonical.verdict) return;
951
+ lines.push(`faithfulness: ${JSON.stringify(canonical)}`);
952
+ }
953
+
954
+ /**
955
+ * Parse the `faithfulness` frontmatter line from its single-line JSON form.
956
+ * Returns `undefined` for missing, blank, or corrupt values so a malformed
957
+ * frontmatter never poisons downstream readers (rule 34 spirit).
958
+ */
959
+ export function parseFaithfulnessField(
960
+ raw: string | undefined,
961
+ ): FaithfulnessFrontmatter | undefined {
962
+ if (!raw || typeof raw !== "string" || raw.trim().length === 0) return undefined;
963
+ try {
964
+ const parsed: unknown = JSON.parse(raw);
965
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
966
+ const obj = parsed as Record<string, unknown>;
967
+ const verdictRaw = typeof obj.verdict === "string" ? obj.verdict : undefined;
968
+ if (!verdictRaw) return undefined;
969
+ const validVerdicts = new Set([
970
+ "entailed",
971
+ "contradicted",
972
+ "unsupported",
973
+ "unchecked",
974
+ "skipped_no_span",
975
+ ]);
976
+ if (!validVerdicts.has(verdictRaw)) return undefined;
977
+ const result: FaithfulnessFrontmatter = {
978
+ verdict: verdictRaw as FaithfulnessFrontmatter["verdict"],
979
+ };
980
+ if (typeof obj.model === "string" && obj.model.length > 0) {
981
+ result.model = obj.model;
982
+ }
983
+ if (typeof obj.rationale === "string" && obj.rationale.length > 0) {
984
+ result.rationale = obj.rationale.slice(0, 500);
985
+ }
986
+ if (typeof obj.at === "string" && obj.at.length > 0) {
987
+ result.at = obj.at;
988
+ }
989
+ return result;
990
+ } catch {
991
+ return undefined;
992
+ }
993
+ }