@remnic/core 9.3.720 → 9.3.721

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 (338) hide show
  1. package/dist/access-boundary.d.ts +5 -5
  2. package/dist/access-boundary.js +35 -33
  3. package/dist/access-cli.js +69 -66
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +5 -5
  6. package/dist/access-http.js +39 -37
  7. package/dist/access-mcp.d.ts +5 -5
  8. package/dist/access-mcp.js +38 -36
  9. package/dist/access-operations-batch.js +36 -34
  10. package/dist/access-operations.d.ts +5 -5
  11. package/dist/access-operations.js +37 -35
  12. package/dist/access-schema.js +3 -3
  13. package/dist/{access-service-Dw-tK1pa.d.ts → access-service-Dj3I-VBS.d.ts} +3 -3
  14. package/dist/access-service.d.ts +5 -5
  15. package/dist/access-service.js +34 -32
  16. package/dist/access-surface-catalog.d.ts +5 -5
  17. package/dist/action-confidence.d.ts +1 -1
  18. package/dist/active-memory-bridge.d.ts +1 -1
  19. package/dist/active-recall.d.ts +1 -1
  20. package/dist/active-recall.js +4 -2
  21. package/dist/active-recall.js.map +1 -1
  22. package/dist/{auto-sync-24NNS3GU.js → auto-sync-F3KCSQFJ.js} +5 -5
  23. package/dist/behavior-learner.d.ts +1 -1
  24. package/dist/behavior-signals.d.ts +1 -1
  25. package/dist/bootstrap.d.ts +4 -4
  26. package/dist/briefing.d.ts +1 -1
  27. package/dist/briefing.js +11 -9
  28. package/dist/buffer-surprise-report.d.ts +1 -1
  29. package/dist/buffer.d.ts +1 -1
  30. package/dist/calibration.d.ts +1 -1
  31. package/dist/calibration.js +2 -2
  32. package/dist/capabilities.d.ts +1 -1
  33. package/dist/{catalog-B9SPfcsE.d.ts → catalog-F-OEVbRQ.d.ts} +1 -1
  34. package/dist/causal-behavior.d.ts +1 -1
  35. package/dist/causal-behavior.js +2 -2
  36. package/dist/causal-chain.js +2 -2
  37. package/dist/causal-consolidation.d.ts +1 -1
  38. package/dist/causal-consolidation.js +14 -12
  39. package/dist/causal-consolidation.js.map +1 -1
  40. package/dist/causal-retrieval.js +2 -2
  41. package/dist/causal-trajectory-graph.d.ts +1 -1
  42. package/dist/causal-trajectory-graph.js +2 -2
  43. package/dist/causal-trajectory.js +1 -1
  44. package/dist/{chunk-TH7WMHVK.js → chunk-3NN73CM7.js} +2 -2
  45. package/dist/{chunk-ZKUW2MJG.js → chunk-4SF232E6.js} +18 -30
  46. package/dist/chunk-4SF232E6.js.map +1 -0
  47. package/dist/{chunk-DGTDRIUO.js → chunk-4T3BMHKD.js} +2 -2
  48. package/dist/{chunk-RX7EBFFJ.js → chunk-5O42UKXI.js} +2 -2
  49. package/dist/{chunk-5YQN2XYC.js → chunk-6FAMMBQ3.js} +2 -2
  50. package/dist/{chunk-FEB7W5JG.js → chunk-6OIDDKAA.js} +27 -6
  51. package/dist/chunk-6OIDDKAA.js.map +1 -0
  52. package/dist/{chunk-67HG7KFE.js → chunk-6YKIYQU7.js} +2 -2
  53. package/dist/{chunk-V7UUIIR7.js → chunk-7CTNUVHZ.js} +2 -2
  54. package/dist/{chunk-HU4EBIXO.js → chunk-A5XNBK2D.js} +8 -8
  55. package/dist/{chunk-2RBMGTK4.js → chunk-A7TJZLPK.js} +2 -2
  56. package/dist/{chunk-BGQ7WKMT.js → chunk-B7WISIAJ.js} +5 -5
  57. package/dist/{chunk-CYJV5AYC.js → chunk-BXEOHV7K.js} +3 -3
  58. package/dist/chunk-C4X4QGIO.js +60 -0
  59. package/dist/chunk-C4X4QGIO.js.map +1 -0
  60. package/dist/{chunk-RIW3UDDY.js → chunk-D7EBTQ24.js} +2 -2
  61. package/dist/{chunk-IO5NQEGZ.js → chunk-DKT3X5C3.js} +2 -2
  62. package/dist/{chunk-H2AGJ5SV.js → chunk-EBDA5TYA.js} +2 -2
  63. package/dist/{chunk-NBYXOEO7.js → chunk-EDOQM4E4.js} +2 -2
  64. package/dist/{chunk-YMPEGBLL.js → chunk-EEJMD2L2.js} +3 -3
  65. package/dist/{chunk-XROP4ZHB.js → chunk-EHQ5HO5T.js} +4 -4
  66. package/dist/{chunk-ATGRQ2NE.js → chunk-EM2LDXJ2.js} +4 -4
  67. package/dist/{chunk-422TIKIH.js → chunk-EVPNWN7Z.js} +9 -9
  68. package/dist/{chunk-WBHQ5LP5.js → chunk-EXUCUPV2.js} +2 -2
  69. package/dist/{chunk-M3GBOKUX.js → chunk-GDUOCHCD.js} +16 -16
  70. package/dist/{chunk-3XEGHEQ2.js → chunk-GJSL4WG2.js} +2 -2
  71. package/dist/{chunk-2SXMCQEQ.js → chunk-GZQXOQQ2.js} +3 -3
  72. package/dist/{chunk-UUMYB3FR.js → chunk-HCVWF657.js} +11 -11
  73. package/dist/{chunk-ZW5I3UNP.js → chunk-ILFSEIRT.js} +1 -1
  74. package/dist/chunk-ILFSEIRT.js.map +1 -0
  75. package/dist/{chunk-HHSO6VCQ.js → chunk-J3JPB7ZI.js} +2 -2
  76. package/dist/{chunk-Y4I7SOJN.js → chunk-LA3HZKES.js} +78 -78
  77. package/dist/{chunk-LQ6JI4VH.js → chunk-LD53WPMU.js} +4 -4
  78. package/dist/{chunk-NE5WFIY6.js → chunk-LLUGAZ3B.js} +2 -2
  79. package/dist/{chunk-RVLB4E6N.js → chunk-MDBZTH7N.js} +5 -32
  80. package/dist/chunk-MDBZTH7N.js.map +1 -0
  81. package/dist/{chunk-KNN6DAZQ.js → chunk-N77ELJ2D.js} +4 -4
  82. package/dist/{chunk-HTIL3EJK.js → chunk-NXRUUDUF.js} +2 -2
  83. package/dist/{chunk-JV7Q226R.js → chunk-OHTOPFRF.js} +2 -2
  84. package/dist/{chunk-ZG2WP425.js → chunk-ORAK3LNZ.js} +2 -2
  85. package/dist/{chunk-NERBFQNY.js → chunk-PD42EYQN.js} +2 -2
  86. package/dist/chunk-PHK3HARR.js +32 -0
  87. package/dist/chunk-PHK3HARR.js.map +1 -0
  88. package/dist/{chunk-NO4DWSQE.js → chunk-PKMQK2WY.js} +7 -7
  89. package/dist/{chunk-V5EGLYXE.js → chunk-QD6QP5P6.js} +42 -42
  90. package/dist/{chunk-IPJD7WJH.js → chunk-QR67FU5G.js} +2 -2
  91. package/dist/{chunk-ZE7GJ76H.js → chunk-SQNED75R.js} +1 -1
  92. package/dist/{chunk-5CEJH5ZN.js → chunk-TAPSNIMA.js} +4 -4
  93. package/dist/{chunk-PMBWVQ7D.js → chunk-TQT3USCG.js} +2 -2
  94. package/dist/{chunk-LLLG5PTE.js → chunk-TXQCG6SE.js} +3 -3
  95. package/dist/{chunk-6S5ZI7HB.js → chunk-UIXXE6NI.js} +6 -6
  96. package/dist/{chunk-T3XSXAWK.js → chunk-UO5ZHIIH.js} +2 -2
  97. package/dist/{chunk-PPVCZPYA.js → chunk-VCLGUIFM.js} +2 -2
  98. package/dist/{chunk-5J4OL7TJ.js → chunk-VUA4S7P6.js} +4 -4
  99. package/dist/{chunk-HAPVZ4CO.js → chunk-WQYSDPMA.js} +2 -2
  100. package/dist/chunk-XGTBNVIU.js +79 -0
  101. package/dist/chunk-XGTBNVIU.js.map +1 -0
  102. package/dist/{chunk-O6SEGJNV.js → chunk-YEE5KJQQ.js} +2 -2
  103. package/dist/{chunk-WRFKZEO6.js → chunk-YQGHW5ML.js} +4 -4
  104. package/dist/{chunk-DG3ORVCU.js → chunk-Z4RU5VPM.js} +2 -2
  105. package/dist/{chunk-DCWIQFNA.js → chunk-ZUDM75KG.js} +4 -4
  106. package/dist/{cli-qcLCJlOD.d.ts → cli-P_GaYcCY.d.ts} +3 -3
  107. package/dist/cli.d.ts +6 -6
  108. package/dist/cli.js +63 -60
  109. package/dist/compounding/engine.d.ts +1 -1
  110. package/dist/compounding/engine.js +11 -9
  111. package/dist/compounding/preference-consolidator.d.ts +1 -1
  112. package/dist/compression-optimizer.d.ts +1 -1
  113. package/dist/config.d.ts +1 -1
  114. package/dist/config.js +4 -2
  115. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  116. package/dist/connectors/codex-materialize-runner.js +11 -9
  117. package/dist/connectors/codex-materialize.d.ts +1 -1
  118. package/dist/connectors/index.d.ts +1 -1
  119. package/dist/connectors/index.js +12 -10
  120. package/dist/consolidation-provenance-check.d.ts +1 -1
  121. package/dist/consolidation-undo.d.ts +1 -1
  122. package/dist/contradiction/index.d.ts +1 -1
  123. package/dist/conversation-index/backend.d.ts +1 -1
  124. package/dist/conversation-index/backend.js +2 -2
  125. package/dist/conversation-index/chunker.d.ts +1 -1
  126. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  127. package/dist/conversation-index/indexer.d.ts +1 -1
  128. package/dist/conversation-index/search.d.ts +1 -1
  129. package/dist/day-summary.d.ts +1 -1
  130. package/dist/delinearize.d.ts +1 -1
  131. package/dist/direct-answer-wiring.d.ts +1 -1
  132. package/dist/direct-answer.d.ts +1 -1
  133. package/dist/embedding-fallback.d.ts +1 -1
  134. package/dist/enrichment/index.d.ts +1 -1
  135. package/dist/entity-retrieval.d.ts +1 -1
  136. package/dist/entity-retrieval.js +11 -9
  137. package/dist/entity-schema.d.ts +1 -1
  138. package/dist/explicit-capture.d.ts +4 -4
  139. package/dist/extraction-faithfulness.d.ts +8 -2
  140. package/dist/extraction-faithfulness.js +4 -3
  141. package/dist/extraction-judge-telemetry.d.ts +1 -1
  142. package/dist/extraction-judge-training.d.ts +1 -1
  143. package/dist/extraction-judge.d.ts +1 -1
  144. package/dist/extraction-judge.js +3 -3
  145. package/dist/extraction.d.ts +1 -1
  146. package/dist/extraction.js +5 -4
  147. package/dist/faithfulness-config.d.ts +55 -0
  148. package/dist/faithfulness-config.js +11 -0
  149. package/dist/faithfulness-config.js.map +1 -0
  150. package/dist/fallback-llm.d.ts +1 -1
  151. package/dist/fallback-llm.js +2 -2
  152. package/dist/graph-dashboard-diff.d.ts +1 -1
  153. package/dist/graph-dashboard-key.d.ts +1 -1
  154. package/dist/graph-dashboard-parser.d.ts +1 -1
  155. package/dist/{graph-edge-decay-QDDC6AVV.js → graph-edge-decay-QPRJQ7DL.js} +3 -3
  156. package/dist/graph-edge-reinforcement.d.ts +1 -1
  157. package/dist/graph-snapshot.d.ts +1 -1
  158. package/dist/graph-snapshot.js +3 -3
  159. package/dist/graph.d.ts +1 -1
  160. package/dist/graph.js +2 -2
  161. package/dist/identity-continuity.d.ts +1 -1
  162. package/dist/importance.d.ts +1 -1
  163. package/dist/index.d.ts +9 -9
  164. package/dist/index.js +101 -98
  165. package/dist/index.js.map +1 -1
  166. package/dist/intent.d.ts +1 -1
  167. package/dist/lcm/engine.d.ts +1 -1
  168. package/dist/lcm/engine.js +3 -3
  169. package/dist/lcm/index.d.ts +1 -1
  170. package/dist/lcm/index.js +5 -5
  171. package/dist/lcm/tools.d.ts +1 -1
  172. package/dist/lifecycle.d.ts +1 -1
  173. package/dist/live-connectors-runner.d.ts +1 -1
  174. package/dist/local-llm.d.ts +1 -1
  175. package/dist/local-model-endpoint.d.ts +96 -0
  176. package/dist/local-model-endpoint.js +12 -0
  177. package/dist/local-model-endpoint.js.map +1 -0
  178. package/dist/maintenance/memory-governance.d.ts +1 -1
  179. package/dist/maintenance/memory-governance.js +11 -9
  180. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +11 -9
  181. package/dist/maintenance/rebuild-memory-projection.js +12 -10
  182. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  183. package/dist/memory-action-policy.d.ts +1 -1
  184. package/dist/memory-cache.d.ts +1 -1
  185. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  186. package/dist/memory-projection-store.d.ts +1 -1
  187. package/dist/memory-provenance.d.ts +1 -1
  188. package/dist/memory-worth-outcomes.d.ts +1 -1
  189. package/dist/models-json.d.ts +1 -1
  190. package/dist/namespaces/migrate.d.ts +2 -2
  191. package/dist/namespaces/migrate.js +21 -19
  192. package/dist/namespaces/principal.d.ts +1 -1
  193. package/dist/namespaces/search.d.ts +1 -1
  194. package/dist/namespaces/search.js +9 -9
  195. package/dist/namespaces/storage.d.ts +2 -2
  196. package/dist/namespaces/storage.js +11 -9
  197. package/dist/native-knowledge.d.ts +1 -1
  198. package/dist/operator-toolkit.d.ts +1 -1
  199. package/dist/operator-toolkit.js +30 -27
  200. package/dist/orchestration/maintenance.d.ts +2 -2
  201. package/dist/orchestration/maintenance.js +13 -11
  202. package/dist/{orchestrator-Bg5Fnza6.d.ts → orchestrator-f5xv19kp.d.ts} +3 -3
  203. package/dist/orchestrator.d.ts +4 -4
  204. package/dist/orchestrator.js +54 -52
  205. package/dist/patterns-cli.d.ts +1 -1
  206. package/dist/policy-runtime.d.ts +1 -1
  207. package/dist/provenance.d.ts +1 -1
  208. package/dist/provenance.js +2 -1
  209. package/dist/qmd-recall-cache.d.ts +1 -1
  210. package/dist/qmd.d.ts +1 -1
  211. package/dist/recall-disclosure-escalation.d.ts +1 -1
  212. package/dist/recall-explain-renderer.d.ts +1 -1
  213. package/dist/recall-explain-renderer.js +3 -3
  214. package/dist/recall-planner-llm.d.ts +1 -1
  215. package/dist/recall-planner-llm.js +2 -2
  216. package/dist/recall-state.d.ts +1 -1
  217. package/dist/recall-tag-filter.d.ts +1 -1
  218. package/dist/recall-xray-cli.d.ts +1 -1
  219. package/dist/recall-xray-cli.js +4 -4
  220. package/dist/recall-xray-renderer.d.ts +1 -1
  221. package/dist/recall-xray-renderer.js +3 -3
  222. package/dist/recall-xray.d.ts +1 -1
  223. package/dist/recall-xray.js +2 -2
  224. package/dist/resolve-auth-token.d.ts +1 -1
  225. package/dist/resume-bundles.js +5 -3
  226. package/dist/retrieval-agents.d.ts +1 -1
  227. package/dist/retrieval-tiers.d.ts +1 -1
  228. package/dist/routing/engine.d.ts +1 -1
  229. package/dist/routing/store.d.ts +1 -1
  230. package/dist/search/embed-helper.d.ts +1 -1
  231. package/dist/search/factory.d.ts +1 -1
  232. package/dist/search/factory.js +8 -8
  233. package/dist/search/index.d.ts +1 -1
  234. package/dist/search/index.js +13 -13
  235. package/dist/search/lancedb-backend.d.ts +1 -1
  236. package/dist/search/lancedb-backend.js +2 -2
  237. package/dist/search/meilisearch-backend.d.ts +1 -1
  238. package/dist/search/meilisearch-backend.js +2 -2
  239. package/dist/search/noop-backend.d.ts +1 -1
  240. package/dist/search/orama-backend.d.ts +1 -1
  241. package/dist/search/orama-backend.js +2 -2
  242. package/dist/search/port.d.ts +1 -1
  243. package/dist/search/remote-backend.d.ts +1 -1
  244. package/dist/{semantic-consolidation-ClOiS6KP.d.ts → semantic-consolidation-BQm1e1Pn.d.ts} +1 -1
  245. package/dist/semantic-consolidation.d.ts +2 -2
  246. package/dist/semantic-consolidation.js +12 -10
  247. package/dist/semantic-rule-promotion.js +11 -9
  248. package/dist/semantic-rule-verifier.d.ts +1 -1
  249. package/dist/semantic-rule-verifier.js +11 -9
  250. package/dist/session-observer-bands.d.ts +1 -1
  251. package/dist/session-observer-state.d.ts +1 -1
  252. package/dist/shared-context/manager.d.ts +1 -1
  253. package/dist/signal.d.ts +1 -1
  254. package/dist/storage.d.ts +1 -1
  255. package/dist/storage.js +10 -8
  256. package/dist/summarizer.d.ts +1 -1
  257. package/dist/summarizer.js +3 -3
  258. package/dist/summary-snapshot.d.ts +1 -1
  259. package/dist/temporal-supersession.d.ts +1 -1
  260. package/dist/temporal-validity.d.ts +1 -1
  261. package/dist/threading.d.ts +1 -1
  262. package/dist/tier-migration.d.ts +1 -1
  263. package/dist/tier-routing.d.ts +1 -1
  264. package/dist/topics.d.ts +1 -1
  265. package/dist/transcript.d.ts +1 -1
  266. package/dist/transfer/backup.js +2 -2
  267. package/dist/transfer/capsule-export.js +2 -2
  268. package/dist/transfer/capsule-import.js +1 -1
  269. package/dist/trust-score-stage.d.ts +1 -1
  270. package/dist/trust-score.d.ts +1 -1
  271. package/dist/{types-BSdUWePh.d.ts → types-DwO1Ot4n.d.ts} +19 -1
  272. package/dist/types.d.ts +1 -1
  273. package/dist/types.js +1 -1
  274. package/dist/utility-runtime.d.ts +1 -1
  275. package/dist/verified-recall.js +11 -9
  276. package/package.json +2 -2
  277. package/src/config.ts +9 -30
  278. package/src/extraction-faithfulness.test.ts +175 -0
  279. package/src/extraction-faithfulness.ts +53 -1
  280. package/src/faithfulness-config.test.ts +96 -0
  281. package/src/faithfulness-config.ts +138 -0
  282. package/src/local-model-endpoint.test.ts +155 -0
  283. package/src/local-model-endpoint.ts +189 -0
  284. package/src/types.ts +19 -1
  285. package/dist/chunk-FEB7W5JG.js.map +0 -1
  286. package/dist/chunk-RVLB4E6N.js.map +0 -1
  287. package/dist/chunk-ZKUW2MJG.js.map +0 -1
  288. package/dist/chunk-ZW5I3UNP.js.map +0 -1
  289. /package/dist/{auto-sync-24NNS3GU.js.map → auto-sync-F3KCSQFJ.js.map} +0 -0
  290. /package/dist/{chunk-TH7WMHVK.js.map → chunk-3NN73CM7.js.map} +0 -0
  291. /package/dist/{chunk-DGTDRIUO.js.map → chunk-4T3BMHKD.js.map} +0 -0
  292. /package/dist/{chunk-RX7EBFFJ.js.map → chunk-5O42UKXI.js.map} +0 -0
  293. /package/dist/{chunk-5YQN2XYC.js.map → chunk-6FAMMBQ3.js.map} +0 -0
  294. /package/dist/{chunk-67HG7KFE.js.map → chunk-6YKIYQU7.js.map} +0 -0
  295. /package/dist/{chunk-V7UUIIR7.js.map → chunk-7CTNUVHZ.js.map} +0 -0
  296. /package/dist/{chunk-HU4EBIXO.js.map → chunk-A5XNBK2D.js.map} +0 -0
  297. /package/dist/{chunk-2RBMGTK4.js.map → chunk-A7TJZLPK.js.map} +0 -0
  298. /package/dist/{chunk-BGQ7WKMT.js.map → chunk-B7WISIAJ.js.map} +0 -0
  299. /package/dist/{chunk-CYJV5AYC.js.map → chunk-BXEOHV7K.js.map} +0 -0
  300. /package/dist/{chunk-RIW3UDDY.js.map → chunk-D7EBTQ24.js.map} +0 -0
  301. /package/dist/{chunk-IO5NQEGZ.js.map → chunk-DKT3X5C3.js.map} +0 -0
  302. /package/dist/{chunk-H2AGJ5SV.js.map → chunk-EBDA5TYA.js.map} +0 -0
  303. /package/dist/{chunk-NBYXOEO7.js.map → chunk-EDOQM4E4.js.map} +0 -0
  304. /package/dist/{chunk-YMPEGBLL.js.map → chunk-EEJMD2L2.js.map} +0 -0
  305. /package/dist/{chunk-XROP4ZHB.js.map → chunk-EHQ5HO5T.js.map} +0 -0
  306. /package/dist/{chunk-ATGRQ2NE.js.map → chunk-EM2LDXJ2.js.map} +0 -0
  307. /package/dist/{chunk-422TIKIH.js.map → chunk-EVPNWN7Z.js.map} +0 -0
  308. /package/dist/{chunk-WBHQ5LP5.js.map → chunk-EXUCUPV2.js.map} +0 -0
  309. /package/dist/{chunk-M3GBOKUX.js.map → chunk-GDUOCHCD.js.map} +0 -0
  310. /package/dist/{chunk-3XEGHEQ2.js.map → chunk-GJSL4WG2.js.map} +0 -0
  311. /package/dist/{chunk-2SXMCQEQ.js.map → chunk-GZQXOQQ2.js.map} +0 -0
  312. /package/dist/{chunk-UUMYB3FR.js.map → chunk-HCVWF657.js.map} +0 -0
  313. /package/dist/{chunk-HHSO6VCQ.js.map → chunk-J3JPB7ZI.js.map} +0 -0
  314. /package/dist/{chunk-Y4I7SOJN.js.map → chunk-LA3HZKES.js.map} +0 -0
  315. /package/dist/{chunk-LQ6JI4VH.js.map → chunk-LD53WPMU.js.map} +0 -0
  316. /package/dist/{chunk-NE5WFIY6.js.map → chunk-LLUGAZ3B.js.map} +0 -0
  317. /package/dist/{chunk-KNN6DAZQ.js.map → chunk-N77ELJ2D.js.map} +0 -0
  318. /package/dist/{chunk-HTIL3EJK.js.map → chunk-NXRUUDUF.js.map} +0 -0
  319. /package/dist/{chunk-JV7Q226R.js.map → chunk-OHTOPFRF.js.map} +0 -0
  320. /package/dist/{chunk-ZG2WP425.js.map → chunk-ORAK3LNZ.js.map} +0 -0
  321. /package/dist/{chunk-NERBFQNY.js.map → chunk-PD42EYQN.js.map} +0 -0
  322. /package/dist/{chunk-NO4DWSQE.js.map → chunk-PKMQK2WY.js.map} +0 -0
  323. /package/dist/{chunk-V5EGLYXE.js.map → chunk-QD6QP5P6.js.map} +0 -0
  324. /package/dist/{chunk-IPJD7WJH.js.map → chunk-QR67FU5G.js.map} +0 -0
  325. /package/dist/{chunk-ZE7GJ76H.js.map → chunk-SQNED75R.js.map} +0 -0
  326. /package/dist/{chunk-5CEJH5ZN.js.map → chunk-TAPSNIMA.js.map} +0 -0
  327. /package/dist/{chunk-PMBWVQ7D.js.map → chunk-TQT3USCG.js.map} +0 -0
  328. /package/dist/{chunk-LLLG5PTE.js.map → chunk-TXQCG6SE.js.map} +0 -0
  329. /package/dist/{chunk-6S5ZI7HB.js.map → chunk-UIXXE6NI.js.map} +0 -0
  330. /package/dist/{chunk-T3XSXAWK.js.map → chunk-UO5ZHIIH.js.map} +0 -0
  331. /package/dist/{chunk-PPVCZPYA.js.map → chunk-VCLGUIFM.js.map} +0 -0
  332. /package/dist/{chunk-5J4OL7TJ.js.map → chunk-VUA4S7P6.js.map} +0 -0
  333. /package/dist/{chunk-HAPVZ4CO.js.map → chunk-WQYSDPMA.js.map} +0 -0
  334. /package/dist/{chunk-O6SEGJNV.js.map → chunk-YEE5KJQQ.js.map} +0 -0
  335. /package/dist/{chunk-WRFKZEO6.js.map → chunk-YQGHW5ML.js.map} +0 -0
  336. /package/dist/{chunk-DG3ORVCU.js.map → chunk-Z4RU5VPM.js.map} +0 -0
  337. /package/dist/{chunk-DCWIQFNA.js.map → chunk-ZUDM75KG.js.map} +0 -0
  338. /package/dist/{graph-edge-decay-QDDC6AVV.js.map → graph-edge-decay-QPRJQ7DL.js.map} +0 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Tests for the extracted faithfulness + correction-intent config parser
3
+ * (issue #1576 / #1585). Pins the behavior-preserving extraction: defaults
4
+ * are byte-identical to the pre-extraction inline block, validation rejects
5
+ * the same inputs with the same messages, and the new model-lab pointer keys
6
+ * default empty (byte-identical pre-feature path, rule 39).
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+
12
+ import { parseFaithfulnessGateConfig, parseCorrectionIntentConfig } from "./faithfulness-config.js";
13
+
14
+ test("parseFaithfulnessGateConfig: empty input → byte-identical defaults (rule 39)", () => {
15
+ const out = parseFaithfulnessGateConfig({});
16
+ assert.equal(out.extractionFaithfulnessGate, "off");
17
+ assert.equal(out.extractionFaithfulnessModel, "");
18
+ assert.equal(out.extractionFaithfulnessBaseUrl, "");
19
+ assert.equal(out.extractionFaithfulnessContextChars, 400);
20
+ assert.equal(out.extractionFaithfulnessTimeoutMs, 8000);
21
+ });
22
+
23
+ test("parseFaithfulnessGateConfig: null/undefined gate → off (not rejected)", () => {
24
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessGate: undefined }).extractionFaithfulnessGate, "off");
25
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessGate: null }).extractionFaithfulnessGate, "off");
26
+ });
27
+
28
+ test("parseFaithfulnessGateConfig: valid gate modes accepted (case-insensitive, trimmed)", () => {
29
+ for (const v of ["off", "shadow", "enforce", "SHADOW", " Enforce "]) {
30
+ const mode = parseFaithfulnessGateConfig({ extractionFaithfulnessGate: v }).extractionFaithfulnessGate;
31
+ assert.ok(mode === "off" || mode === "shadow" || mode === "enforce", `${v} → ${mode}`);
32
+ }
33
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessGate: "SHADOW" }).extractionFaithfulnessGate, "shadow");
34
+ });
35
+
36
+ test("parseFaithfulnessGateConfig: present-but-invalid gate rejects (Ob4RQ — never silently off)", () => {
37
+ for (const bad of [true, 1, {}, "maybe", "on"]) {
38
+ assert.throws(
39
+ () => parseFaithfulnessGateConfig({ extractionFaithfulnessGate: bad }),
40
+ /extractionFaithfulnessGate must be one of "off" \| "shadow" \| "enforce"/,
41
+ );
42
+ }
43
+ });
44
+
45
+ test("parseFaithfulnessGateConfig: context chars clamp to [1, 4000]; invalid rejects (#1634)", () => {
46
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessContextChars: 100000 }).extractionFaithfulnessContextChars, 4000);
47
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessContextChars: "400" }).extractionFaithfulnessContextChars, 400);
48
+ for (const bad of [0, -1, 1.5, true, NaN, "abc"]) {
49
+ assert.throws(
50
+ () => parseFaithfulnessGateConfig({ extractionFaithfulnessContextChars: bad }),
51
+ /extractionFaithfulnessContextChars must be an integer/,
52
+ );
53
+ }
54
+ });
55
+
56
+ test("parseFaithfulnessGateConfig: timeout clamps to [1, 60000]; invalid rejects", () => {
57
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessTimeoutMs: 999999 }).extractionFaithfulnessTimeoutMs, 60_000);
58
+ for (const bad of [0, -5, 2.5, false, "no"]) {
59
+ assert.throws(
60
+ () => parseFaithfulnessGateConfig({ extractionFaithfulnessTimeoutMs: bad }),
61
+ /extractionFaithfulnessTimeoutMs must be an integer/,
62
+ );
63
+ }
64
+ });
65
+
66
+ test("parseFaithfulnessGateConfig: model-lab pointer keys default empty + accept strings (#1585)", () => {
67
+ // Defaults preserve the existing routing chain exactly.
68
+ assert.equal(parseFaithfulnessGateConfig({}).extractionFaithfulnessBaseUrl, "");
69
+ assert.equal(parseFaithfulnessGateConfig({}).extractionFaithfulnessModel, "");
70
+ // Non-string values are ignored → empty (not rejected; mirrors model key).
71
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessBaseUrl: 123 }).extractionFaithfulnessBaseUrl, "");
72
+ // Real pointer values round-trip.
73
+ const out = parseFaithfulnessGateConfig({
74
+ extractionFaithfulnessModel: "remnic-faithfulness-gate-v1",
75
+ extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
76
+ });
77
+ assert.equal(out.extractionFaithfulnessModel, "remnic-faithfulness-gate-v1");
78
+ assert.equal(out.extractionFaithfulnessBaseUrl, "http://localhost:11434/v1");
79
+ });
80
+
81
+ test("parseCorrectionIntentConfig: empty input → empty pointers (rule-based detector stays default)", () => {
82
+ const out = parseCorrectionIntentConfig({});
83
+ assert.equal(out.correctionIntentModel, "");
84
+ assert.equal(out.correctionIntentBaseUrl, "");
85
+ });
86
+
87
+ test("parseCorrectionIntentConfig: pointers round-trip; non-strings → empty", () => {
88
+ const out = parseCorrectionIntentConfig({
89
+ correctionIntentModel: "remnic-correction-intent-v1",
90
+ correctionIntentBaseUrl: "http://localhost:8000/v1",
91
+ });
92
+ assert.equal(out.correctionIntentModel, "remnic-correction-intent-v1");
93
+ assert.equal(out.correctionIntentBaseUrl, "http://localhost:8000/v1");
94
+ assert.equal(parseCorrectionIntentConfig({ correctionIntentModel: true }).correctionIntentModel, "");
95
+ assert.equal(parseCorrectionIntentConfig({ correctionIntentBaseUrl: 5 }).correctionIntentBaseUrl, "");
96
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Faithfulness-gate + correction-intent config parsing (issue #1576 / #1585).
3
+ *
4
+ * Extracted verbatim from `config.ts` (a watchlisted god file) so the gate
5
+ * config block can grow — the model-lab pointer keys
6
+ * `extractionFaithfulnessBaseUrl` (#1585) and the correction-intent pointers
7
+ * (`correctionIntentModel` / `correctionIntentBaseUrl`) — without growing the
8
+ * god file. This is a behavior-preserving extraction (#1526-style): the
9
+ * validation, defaults, and error messages are byte-identical to the inline
10
+ * block it replaces, which the existing extraction-faithfulness + config
11
+ * tests pin.
12
+ *
13
+ * Why a dedicated module (and not inline): the ratchet
14
+ * (`scripts/check-ratchets.mjs`) forbids `config.ts` from growing, and the
15
+ * model-lab integration needs three new optional keys. Centralizing the whole
16
+ * gate+intent parsing here keeps it unit-testable in isolation and lets
17
+ * `config.ts` spread the result in one line.
18
+ */
19
+
20
+ import { coerceNumber } from "./connectors/coerce.js";
21
+
22
+ /**
23
+ * Parse a value as a strict integer >= `min`, throwing on anything else.
24
+ *
25
+ * Mirrors `parseIntegerAtLeast` in `config.ts` byte-for-byte (issue #1634:
26
+ * reject non-numeric, <=0, non-integer, NaN, Infinity, booleans, objects —
27
+ * gotcha #51). Reimplemented locally rather than exported from `config.ts`
28
+ * so this module has no dependency on the god file (and vice versa).
29
+ */
30
+ function parseIntegerAtLeast(
31
+ value: unknown,
32
+ fallback: number,
33
+ min: number,
34
+ keyName: string,
35
+ ): number {
36
+ if (value === undefined || value === null) return fallback;
37
+ const coerced = coerceNumber(value);
38
+ if (
39
+ coerced === undefined ||
40
+ !Number.isFinite(coerced) ||
41
+ !Number.isInteger(coerced) ||
42
+ coerced < min
43
+ ) {
44
+ throw new Error(
45
+ `${keyName} must be an integer greater than or equal to ${min}; got ${JSON.stringify(value)}`,
46
+ );
47
+ }
48
+ return coerced;
49
+ }
50
+
51
+ /**
52
+ * Parse the extraction faithfulness gate config block.
53
+ *
54
+ * Returns exactly the `PluginConfig` fields this block owns. Defaults preserve
55
+ * the byte-identical pre-feature pipeline (rule 39): gate "off", empty model
56
+ * + base URL, context 400 chars, timeout 8000 ms. Invalid `gate` values reject
57
+ * listing valid options (rule 51); invalid integers throw (issue #1634).
58
+ *
59
+ * `extractionFaithfulnessBaseUrl` (issue #1585) is the model-lab pointer: when
60
+ * set together with `extractionFaithfulnessModel`, the gate routes to that
61
+ * local openai-compatible endpoint before the configured chain; empty string
62
+ * (default) preserves the existing routing exactly.
63
+ */
64
+ export function parseFaithfulnessGateConfig(cfg: Record<string, unknown>): {
65
+ extractionFaithfulnessGate: "off" | "shadow" | "enforce";
66
+ extractionFaithfulnessModel: string;
67
+ extractionFaithfulnessBaseUrl: string;
68
+ extractionFaithfulnessContextChars: number;
69
+ extractionFaithfulnessTimeoutMs: number;
70
+ } {
71
+ const gateValue = cfg.extractionFaithfulnessGate;
72
+ let extractionFaithfulnessGate: "off" | "shadow" | "enforce";
73
+ if (gateValue === undefined || gateValue === null) {
74
+ extractionFaithfulnessGate = "off";
75
+ } else {
76
+ // Present-but-invalid (true/1/{}) must reject, not silently disable the gate (Ob4RQ).
77
+ const raw = typeof gateValue === "string" ? gateValue.trim().toLowerCase() : gateValue;
78
+ if (raw === "off" || raw === "shadow" || raw === "enforce") {
79
+ extractionFaithfulnessGate = raw;
80
+ } else {
81
+ throw new Error(
82
+ `extractionFaithfulnessGate must be one of "off" | "shadow" | "enforce" (got ${JSON.stringify(gateValue)})`,
83
+ );
84
+ }
85
+ }
86
+
87
+ const extractionFaithfulnessModel =
88
+ typeof cfg.extractionFaithfulnessModel === "string" ? cfg.extractionFaithfulnessModel : "";
89
+
90
+ // Issue #1585: model-lab pointer. Empty default → gate uses its existing
91
+ // routing chain (byte-identical). Non-empty + a model name → gate prefers
92
+ // the local served endpoint, falling back to the chain on failure.
93
+ const extractionFaithfulnessBaseUrl =
94
+ typeof cfg.extractionFaithfulnessBaseUrl === "string"
95
+ ? cfg.extractionFaithfulnessBaseUrl
96
+ : "";
97
+
98
+ const extractionFaithfulnessContextChars = Math.min(
99
+ parseIntegerAtLeast(cfg.extractionFaithfulnessContextChars, 400, 1, "extractionFaithfulnessContextChars"),
100
+ 4000,
101
+ );
102
+
103
+ const extractionFaithfulnessTimeoutMs = Math.min(
104
+ parseIntegerAtLeast(cfg.extractionFaithfulnessTimeoutMs, 8000, 1, "extractionFaithfulnessTimeoutMs"),
105
+ 60_000,
106
+ );
107
+
108
+ return {
109
+ extractionFaithfulnessGate,
110
+ extractionFaithfulnessModel,
111
+ extractionFaithfulnessBaseUrl,
112
+ extractionFaithfulnessContextChars,
113
+ extractionFaithfulnessTimeoutMs,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Parse the correction-intent config block (issue #1581 / #1585).
119
+ *
120
+ * The model-lab pointer for passive-correction detection: when both
121
+ * `correctionIntentModel` and `correctionIntentBaseUrl` are set, the detection
122
+ * path can route to that local fine-tuned classifier; empty defaults (the
123
+ * common case) keep the rule-based `detectPassiveCorrections` heuristic as the
124
+ * sole detector — byte-identical to the pre-feature path. Consumption by the
125
+ * detector is the consuming child's job (#1581's detector is a pure function;
126
+ * wiring a model call into it is out of scope for this config-pointer PR).
127
+ */
128
+ export function parseCorrectionIntentConfig(cfg: Record<string, unknown>): {
129
+ correctionIntentModel: string;
130
+ correctionIntentBaseUrl: string;
131
+ } {
132
+ return {
133
+ correctionIntentModel:
134
+ typeof cfg.correctionIntentModel === "string" ? cfg.correctionIntentModel : "",
135
+ correctionIntentBaseUrl:
136
+ typeof cfg.correctionIntentBaseUrl === "string" ? cfg.correctionIntentBaseUrl : "",
137
+ };
138
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Tests for the local model-lab endpoint resolver + openai-compatible caller
3
+ * (issue #1585). Pure unit tests — the fetch caller takes an injected
4
+ * `fetchImpl`, so no network, no GPU, no live server.
5
+ */
6
+
7
+ import test from "node:test";
8
+ import assert from "node:assert/strict";
9
+
10
+ import {
11
+ resolveFaithfulnessGateEndpoint,
12
+ resolveCorrectionIntentEndpoint,
13
+ callOpenAiCompatibleChat,
14
+ type EndpointChatMessage,
15
+ } from "./local-model-endpoint.js";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Resolvers
19
+ // ---------------------------------------------------------------------------
20
+
21
+ test("resolveFaithfulnessGateEndpoint: null when either key empty (default = no pointer)", () => {
22
+ assert.equal(resolveFaithfulnessGateEndpoint({ extractionFaithfulnessModel: "", extractionFaithfulnessBaseUrl: "" }), null);
23
+ assert.equal(resolveFaithfulnessGateEndpoint({ extractionFaithfulnessModel: "m", extractionFaithfulnessBaseUrl: "" }), null);
24
+ assert.equal(resolveFaithfulnessGateEndpoint({ extractionFaithfulnessModel: "", extractionFaithfulnessBaseUrl: "http://x/v1" }), null);
25
+ // whitespace-only counts as empty.
26
+ assert.equal(resolveFaithfulnessGateEndpoint({ extractionFaithfulnessModel: " ", extractionFaithfulnessBaseUrl: "http://x/v1" }), null);
27
+ });
28
+
29
+ test("resolveFaithfulnessGateEndpoint: returns trimmed endpoint when both keys set", () => {
30
+ const ep = resolveFaithfulnessGateEndpoint({
31
+ extractionFaithfulnessModel: " remnic-faithfulness-gate-v1 ",
32
+ extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
33
+ });
34
+ assert.deepEqual(ep, { baseUrl: "http://localhost:11434/v1", model: "remnic-faithfulness-gate-v1" });
35
+ });
36
+
37
+ test("resolveCorrectionIntentEndpoint: same contract as the gate resolver", () => {
38
+ assert.equal(resolveCorrectionIntentEndpoint({ correctionIntentModel: "", correctionIntentBaseUrl: "" }), null);
39
+ assert.equal(resolveCorrectionIntentEndpoint({ correctionIntentModel: "m", correctionIntentBaseUrl: "" }), null);
40
+ assert.deepEqual(
41
+ resolveCorrectionIntentEndpoint({ correctionIntentModel: "ci-v1", correctionIntentBaseUrl: "http://localhost:8000/v1" }),
42
+ { baseUrl: "http://localhost:8000/v1", model: "ci-v1" },
43
+ );
44
+ });
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // callOpenAiCompatibleChat — injected fetch, no network
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /** Build a minimal Response-shaped object the caller reads (ok, json, .json). */
51
+ function jsonResponse(status: number, body: unknown): Response {
52
+ return new Response(JSON.stringify(body), {
53
+ status,
54
+ headers: { "content-type": "application/json" },
55
+ });
56
+ }
57
+
58
+ const MESSAGES: EndpointChatMessage[] = [
59
+ { role: "system", content: "verifier" },
60
+ { role: "user", content: "fact vs quote" },
61
+ ];
62
+
63
+ test("callOpenAiCompatibleChat: happy path — first choice content + reported model", async () => {
64
+ let calledUrl = "";
65
+ let calledBody: Record<string, unknown> | undefined;
66
+ const fakeFetch = (url: string, init: RequestInit): Promise<Response> => {
67
+ calledUrl = url;
68
+ calledBody = JSON.parse(String(init.body));
69
+ return Promise.resolve(
70
+ jsonResponse(200, {
71
+ model: "remnic-faithfulness-gate-v1",
72
+ choices: [{ message: { content: '[{"index":0,"verdict":"entailed"}]' } }],
73
+ }),
74
+ );
75
+ };
76
+ const result = await callOpenAiCompatibleChat(
77
+ { baseUrl: "http://localhost:11434/v1", model: "remnic-faithfulness-gate-v1" },
78
+ MESSAGES,
79
+ { timeoutMs: 5000, temperature: 0.1, maxTokens: 2048, responseFormatJson: true },
80
+ fakeFetch as typeof fetch,
81
+ );
82
+ assert.equal(result?.content, '[{"index":0,"verdict":"entailed"}]');
83
+ assert.equal(result?.modelUsed, "remnic-faithfulness-gate-v1");
84
+ // URL joined correctly (no double slash), body carries model + json_object format.
85
+ assert.equal(calledUrl, "http://localhost:11434/v1/chat/completions");
86
+ assert.equal(calledBody?.model, "remnic-faithfulness-gate-v1");
87
+ assert.deepEqual(calledBody?.response_format, { type: "json_object" });
88
+ assert.equal(calledBody?.max_tokens, 2048);
89
+ });
90
+
91
+ test("callOpenAiCompatibleChat: trailing slash on baseUrl is tolerated", async () => {
92
+ const seen = (await callOpenAiCompatibleChat(
93
+ { baseUrl: "http://localhost:11434/v1/", model: "m" },
94
+ MESSAGES,
95
+ { timeoutMs: 1000 },
96
+ ((url: string) => {
97
+ assert.equal(url, "http://localhost:11434/v1/chat/completions");
98
+ return Promise.resolve(jsonResponse(200, { choices: [{ message: { content: "ok" } }] }));
99
+ }) as typeof fetch,
100
+ ))!;
101
+ assert.equal(seen.content, "ok");
102
+ assert.equal(seen.modelUsed, "m"); // server reported no model → falls back to requested name
103
+ });
104
+
105
+ test("callOpenAiCompatibleChat: returns null on non-2xx (fail closed)", async () => {
106
+ const result = await callOpenAiCompatibleChat(
107
+ { baseUrl: "http://x/v1", model: "m" },
108
+ MESSAGES,
109
+ { timeoutMs: 1000 },
110
+ (() => Promise.resolve(jsonResponse(500, { error: "boom" }))) as typeof fetch,
111
+ );
112
+ assert.equal(result, null);
113
+ });
114
+
115
+ test("callOpenAiCompatibleChat: returns null on network error (fail closed)", async () => {
116
+ const result = await callOpenAiCompatibleChat(
117
+ { baseUrl: "http://x/v1", model: "m" },
118
+ MESSAGES,
119
+ { timeoutMs: 1000 },
120
+ (() => Promise.reject(new Error("ECONNREFUSED"))) as typeof fetch,
121
+ );
122
+ assert.equal(result, null);
123
+ });
124
+
125
+ test("callOpenAiCompatibleChat: returns null on malformed body (no fabricated shape)", async () => {
126
+ for (const body of [
127
+ null,
128
+ {},
129
+ { choices: [] },
130
+ { choices: [{ message: {} }] },
131
+ { choices: [{ message: { content: 42 } }] }, // non-string content
132
+ { choices: "not-an-array" },
133
+ ]) {
134
+ const result = await callOpenAiCompatibleChat(
135
+ { baseUrl: "http://x/v1", model: "m" },
136
+ MESSAGES,
137
+ { timeoutMs: 1000 },
138
+ (() => Promise.resolve(jsonResponse(200, body))) as typeof fetch,
139
+ );
140
+ assert.equal(result, null, `body ${JSON.stringify(body)} should yield null`);
141
+ }
142
+ });
143
+
144
+ test("callOpenAiCompatibleChat: returns null on .json() throw", async () => {
145
+ const result = await callOpenAiCompatibleChat(
146
+ { baseUrl: "http://x/v1", model: "m" },
147
+ MESSAGES,
148
+ { timeoutMs: 1000 },
149
+ (() =>
150
+ Promise.resolve(
151
+ new Response("not-json", { status: 200, headers: { "content-type": "application/json" } }),
152
+ )) as typeof fetch,
153
+ );
154
+ assert.equal(result, null);
155
+ });
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Local fine-tuned model endpoint resolver + openai-compatible caller
3
+ * (issue #1585 model-lab integration).
4
+ *
5
+ * Remnic's two classification workloads — the extraction faithfulness gate
6
+ * (#1576) and passive-correction intent detection (#1581) — can run on small
7
+ * fine-tuned models served locally (Ollama / vLLM) instead of prompted
8
+ * frontier LLMs. The model-lab (`model-lab/`) produces those models; this
9
+ * module is the config-side half: it resolves the optional local endpoint
10
+ * pointer from config and, when set, makes a minimal openai-compatible
11
+ * chat-completions call to it.
12
+ *
13
+ * Design:
14
+ * - **Pointers are optional.** Empty defaults preserve the existing routing
15
+ * chain exactly (rule 39 / byte-identical pre-feature). A resolver returns
16
+ * `null` when the pointer is unset, so callers short-circuit before any
17
+ * network.
18
+ * - **Graceful degradation.** A local-endpoint failure (network, non-2xx,
19
+ * malformed body, timeout) returns `null`; callers fall back to the
20
+ * configured chain (checklist §4 — never block writes on a classifier).
21
+ * - **No inline casts.** The fetch JSON is narrowed with `in` / `typeof`
22
+ * guards (project rule: never assert a shape to read one property).
23
+ * - **Injectable fetch.** `callOpenAiCompatibleChat` takes an optional
24
+ * `fetchImpl` so the caller is unit-testable without a live server.
25
+ */
26
+
27
+ import type { PluginConfig } from "./types.js";
28
+
29
+ /** A resolved local endpoint: where to call + which model to name. */
30
+ export interface LocalModelEndpoint {
31
+ /** Base URL, e.g. `http://localhost:11434/v1` (no trailing slash required). */
32
+ baseUrl: string;
33
+ /** Model name the server expects, e.g. `remnic-faithfulness-gate-v1`. */
34
+ model: string;
35
+ }
36
+
37
+ /** A chat message in the openai-compatible shape this caller sends. */
38
+ export interface EndpointChatMessage {
39
+ role: "system" | "user" | "assistant";
40
+ content: string;
41
+ }
42
+
43
+ /** Options for a single local-endpoint chat call. */
44
+ export interface EndpointChatOptions {
45
+ /** Per-call timeout in ms. The caller's batch AbortSignal is NOT forwarded
46
+ * (matches the local-LLM precedent: each attempt is bounded by its own
47
+ * timer so one slow server can't starve the batch). */
48
+ timeoutMs: number;
49
+ /** Sampling temperature — classification wants low entropy (0.0–0.2). */
50
+ temperature?: number;
51
+ /** Max output tokens. */
52
+ maxTokens?: number;
53
+ /** Request JSON-object output (the gate emits a JSON verdict array). */
54
+ responseFormatJson?: boolean;
55
+ }
56
+
57
+ /** Result of a local-endpoint chat call. */
58
+ export interface EndpointChatResult {
59
+ content: string;
60
+ /** The model the server reports it used (falls back to the requested name). */
61
+ modelUsed: string;
62
+ }
63
+
64
+ /**
65
+ * Resolve the faithfulness-gate local endpoint from config (issue #1585).
66
+ *
67
+ * Returns the endpoint only when BOTH a base URL and a model name are
68
+ * configured; otherwise `null` (gate uses its existing routing chain). The
69
+ * gate's verifier needs a model identity, so a base URL alone is intentionally
70
+ * insufficient — silently calling whatever the server's default model is would
71
+ * be an unreviewed behavioral change.
72
+ */
73
+ export function resolveFaithfulnessGateEndpoint(
74
+ config: Pick<PluginConfig, "extractionFaithfulnessModel" | "extractionFaithfulnessBaseUrl">,
75
+ ): LocalModelEndpoint | null {
76
+ const baseUrl = config.extractionFaithfulnessBaseUrl.trim();
77
+ const model = config.extractionFaithfulnessModel.trim();
78
+ if (!baseUrl || !model) return null;
79
+ return { baseUrl, model };
80
+ }
81
+
82
+ /**
83
+ * Resolve the correction-intent local endpoint from config (issue #1581/#1585).
84
+ *
85
+ * Same contract as the gate resolver. Consumption by the model-backed
86
+ * detector is the consuming child's job — the rule-based
87
+ * `detectPassiveCorrections` remains the default path; this pointer lets a
88
+ * future model-backed detector route to a fine-tuned local classifier when an
89
+ * operator configures one.
90
+ */
91
+ export function resolveCorrectionIntentEndpoint(
92
+ config: Pick<PluginConfig, "correctionIntentModel" | "correctionIntentBaseUrl">,
93
+ ): LocalModelEndpoint | null {
94
+ const baseUrl = config.correctionIntentBaseUrl.trim();
95
+ const model = config.correctionIntentModel.trim();
96
+ if (!baseUrl || !model) return null;
97
+ return { baseUrl, model };
98
+ }
99
+
100
+ type FetchLike = typeof fetch;
101
+
102
+ /**
103
+ * Make an openai-compatible chat-completions call to a local endpoint.
104
+ *
105
+ * POSTs to `${baseUrl}/chat/completions` with `{ model, messages, ... }` and
106
+ * returns the first choice's content. Any failure (non-2xx, network error,
107
+ * malformed body, timeout) returns `null` so callers fall back gracefully.
108
+ *
109
+ * The response is narrowed with runtime guards — never an unchecked cast — so
110
+ * a surprising server response fails closed (`null`) rather than reading a
111
+ * fabricated field.
112
+ */
113
+ export async function callOpenAiCompatibleChat(
114
+ endpoint: LocalModelEndpoint,
115
+ messages: EndpointChatMessage[],
116
+ options: EndpointChatOptions,
117
+ fetchImpl: FetchLike = fetch,
118
+ ): Promise<EndpointChatResult | null> {
119
+ const url = joinBaseUrl(endpoint.baseUrl, "/chat/completions");
120
+ const controller = new AbortController();
121
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs);
122
+ try {
123
+ const body: Record<string, unknown> = {
124
+ model: endpoint.model,
125
+ messages,
126
+ temperature: options.temperature ?? 0.1,
127
+ };
128
+ if (typeof options.maxTokens === "number") {
129
+ body.max_tokens = options.maxTokens;
130
+ }
131
+ if (options.responseFormatJson) {
132
+ body.response_format = { type: "json_object" };
133
+ }
134
+ let response: Response;
135
+ try {
136
+ response = await fetchImpl(url, {
137
+ method: "POST",
138
+ headers: { "content-type": "application/json" },
139
+ body: JSON.stringify(body),
140
+ signal: controller.signal,
141
+ });
142
+ } catch {
143
+ // Network error or abort — fail closed, let the caller fall back.
144
+ return null;
145
+ }
146
+ if (!response.ok) return null;
147
+ const parsed: unknown = await response.json().catch(() => null);
148
+ const content = extractFirstChoiceContent(parsed);
149
+ if (content === null) return null;
150
+ return { content, modelUsed: extractModel(parsed) ?? endpoint.model };
151
+ } finally {
152
+ clearTimeout(timer);
153
+ }
154
+ }
155
+
156
+ /** Join a base URL and a path, tolerating a trailing slash on the base. */
157
+ function joinBaseUrl(base: string, path: string): string {
158
+ return base.endsWith("/") ? `${base.slice(0, -1)}${path}` : `${base}${path}`;
159
+ }
160
+
161
+ /**
162
+ * Narrow the openai-compatible response and pull the first choice's content.
163
+ *
164
+ * `in` / `typeof` guards only — no `as` cast (project rule). Returns `null`
165
+ * for any shape that isn't `{ choices: [{ message: { content: string } }, …] }`.
166
+ */
167
+ function extractFirstChoiceContent(data: unknown): string | null {
168
+ if (!data || typeof data !== "object") return null;
169
+ // After `in`, TS narrows the property to `unknown` — read it directly, no cast.
170
+ if (!("choices" in data) || !Array.isArray(data.choices)) return null;
171
+ const choices: unknown[] = data.choices;
172
+ for (const choice of choices) {
173
+ if (!choice || typeof choice !== "object") continue;
174
+ if (!("message" in choice)) continue;
175
+ const message = choice.message;
176
+ if (!message || typeof message !== "object") continue;
177
+ if (!("content" in message)) continue;
178
+ const content = message.content;
179
+ if (typeof content === "string" && content.length > 0) return content;
180
+ }
181
+ return null;
182
+ }
183
+
184
+ /** Narrow the `model` field from the response (best-effort, may be absent). */
185
+ function extractModel(data: unknown): string | null {
186
+ if (!data || typeof data !== "object" || !("model" in data)) return null;
187
+ const model = data.model;
188
+ return typeof model === "string" && model.length > 0 ? model : null;
189
+ }
package/src/types.ts CHANGED
@@ -1207,12 +1207,30 @@ export interface PluginConfig {
1207
1207
  // extracted facts against their verified source spans from #1575.
1208
1208
  /** Gate mode: "off" (default, byte-identical pre-feature), "shadow" (record only), "enforce" (route to pending_review). */
1209
1209
  extractionFaithfulnessGate: "off" | "shadow" | "enforce";
1210
- /** Override model/endpoint; empty string = default routing chain. */
1210
+ /** Override model name; empty string = default routing chain. */
1211
1211
  extractionFaithfulnessModel: string;
1212
+ /**
1213
+ * Base URL of a local openai-compatible endpoint serving a fine-tuned
1214
+ * faithfulness gate (issue #1585 model-lab), e.g.
1215
+ * `http://localhost:11434/v1` (Ollama) or `http://localhost:8000/v1` (vLLM).
1216
+ * Empty string (default) preserves the existing routing chain exactly;
1217
+ * set together with `extractionFaithfulnessModel` to route the gate to the
1218
+ * local model first, falling back to the chain on failure.
1219
+ */
1220
+ extractionFaithfulnessBaseUrl: string;
1212
1221
  /** Context window (chars) around the quote sent to the verifier. Default 400. */
1213
1222
  extractionFaithfulnessContextChars: number;
1214
1223
  /** Per-batch timeout in ms; timeout → tagged error, never blocks writes. Default 8000. */
1215
1224
  extractionFaithfulnessTimeoutMs: number;
1225
+ /**
1226
+ * Correction-intent model-lab pointer (issue #1581 / #1585). When both this
1227
+ * and `correctionIntentBaseUrl` are set, the detection path can route to a
1228
+ * local fine-tuned correction-intent classifier. Empty default keeps the
1229
+ * rule-based passive-correction detector as the sole path (byte-identical).
1230
+ */
1231
+ correctionIntentModel: string;
1232
+ /** Base URL of a local openai-compatible endpoint serving the correction-intent classifier (issue #1585). Empty default = rule-based detection. */
1233
+ correctionIntentBaseUrl: string;
1216
1234
  // Hourly summaries
1217
1235
  hourlySummariesEnabled: boolean;
1218
1236
  daySummaryEnabled: boolean;