@remnic/core 9.28.0 → 9.29.1

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 (426) hide show
  1. package/dist/access-admin-ops-surface.d.ts +10 -10
  2. package/dist/access-admin-ops-surface.js +32 -32
  3. package/dist/access-authorization-probe.d.ts +10 -10
  4. package/dist/access-authorization-probe.js +33 -33
  5. package/dist/access-boundary.d.ts +10 -10
  6. package/dist/access-boundary.js +32 -32
  7. package/dist/access-cli.js +101 -98
  8. package/dist/access-cli.js.map +1 -1
  9. package/dist/access-health-types.d.ts +1 -1
  10. package/dist/access-http-lcm-compaction.d.ts +10 -10
  11. package/dist/access-http.d.ts +10 -10
  12. package/dist/access-http.js +43 -43
  13. package/dist/access-identity-continuity-surface.d.ts +8 -8
  14. package/dist/access-identity-continuity-surface.js +32 -32
  15. package/dist/access-lcm-surface.d.ts +10 -10
  16. package/dist/access-lcm-surface.js +32 -32
  17. package/dist/access-mcp.d.ts +10 -10
  18. package/dist/access-mcp.js +38 -38
  19. package/dist/access-memory-search-fanout.d.ts +2 -2
  20. package/dist/access-memory-search-fanout.js +3 -3
  21. package/dist/{access-namespace-preflight-BiQ_-iUJ.d.ts → access-namespace-preflight-DCXBkDp0.d.ts} +1 -1
  22. package/dist/access-namespace-preflight.d.ts +3 -3
  23. package/dist/access-namespace-preflight.js +32 -32
  24. package/dist/access-observe-write-surface.d.ts +10 -10
  25. package/dist/access-observe-write-surface.js +32 -32
  26. package/dist/access-operations-batch.js +35 -35
  27. package/dist/access-operations.d.ts +10 -10
  28. package/dist/access-operations.js +37 -37
  29. package/dist/access-recall-concurrency.d.ts +10 -10
  30. package/dist/access-recall-concurrency.js +32 -32
  31. package/dist/access-recall-response.d.ts +10 -10
  32. package/dist/access-recall-response.js +32 -32
  33. package/dist/access-recall-surface.d.ts +10 -10
  34. package/dist/access-recall-surface.js +32 -32
  35. package/dist/access-schema.d.ts +7 -7
  36. package/dist/access-schema.js +4 -4
  37. package/dist/{access-service-CXkIgcGh.d.ts → access-service-DvPW_M5j.d.ts} +7 -7
  38. package/dist/access-service.d.ts +10 -10
  39. package/dist/access-service.js +32 -32
  40. package/dist/access-surface-catalog.d.ts +10 -10
  41. package/dist/access-wearables-meetings-surface.d.ts +3 -3
  42. package/dist/action-confidence.d.ts +1 -1
  43. package/dist/active-memory-bridge.d.ts +1 -1
  44. package/dist/active-memory-bridge.js +3 -3
  45. package/dist/active-recall.d.ts +1 -1
  46. package/dist/active-recall.js +1 -1
  47. package/dist/adapters/index.js +4 -4
  48. package/dist/adapters/registry.js +2 -2
  49. package/dist/behavior-learner.d.ts +1 -1
  50. package/dist/behavior-signals.d.ts +1 -1
  51. package/dist/bootstrap.d.ts +8 -8
  52. package/dist/briefing.d.ts +2 -2
  53. package/dist/briefing.js +8 -8
  54. package/dist/buffer-surprise-report.d.ts +1 -1
  55. package/dist/buffer.d.ts +2 -2
  56. package/dist/buffer.js +2 -2
  57. package/dist/bulk-import/index.d.ts +3 -3
  58. package/dist/calibration.d.ts +1 -1
  59. package/dist/capabilities.d.ts +3 -2
  60. package/dist/capabilities.js +1 -1
  61. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-GWVG7LGC.js} +2 -2
  62. package/dist/{catalog-Bouj8Gfg.d.ts → catalog-BIVgftWu.d.ts} +1 -1
  63. package/dist/causal-behavior.d.ts +1 -1
  64. package/dist/causal-consolidation.d.ts +1 -1
  65. package/dist/causal-consolidation.js +9 -9
  66. package/dist/causal-trajectory-graph.d.ts +1 -1
  67. package/dist/{chunk-GQSMYXKL.js → chunk-2AXMQWCN.js} +2 -2
  68. package/dist/{chunk-VYYNBJXH.js → chunk-2D7FEUCK.js} +6 -6
  69. package/dist/{chunk-PXQM2HOG.js → chunk-2GXM5N5A.js} +2 -2
  70. package/dist/{chunk-RDQGLJRR.js → chunk-2XRE6FLM.js} +2 -1
  71. package/dist/{chunk-RDQGLJRR.js.map → chunk-2XRE6FLM.js.map} +1 -1
  72. package/dist/{chunk-JIF3VRKQ.js → chunk-35DL7IOI.js} +10 -10
  73. package/dist/{chunk-2H7OEVPO.js → chunk-3GD2HFR6.js} +2 -2
  74. package/dist/{chunk-KKBTBUVT.js → chunk-3JNDOAQ2.js} +5 -5
  75. package/dist/{chunk-4TFIRMTE.js → chunk-4A6YGWQN.js} +2 -2
  76. package/dist/{chunk-IKMAO55B.js → chunk-55MDBZLC.js} +2 -2
  77. package/dist/{chunk-RKXZ7P4V.js → chunk-5QDRGWT2.js} +3 -3
  78. package/dist/{chunk-IPLYGWQF.js → chunk-5W7CTZ2E.js} +6 -6
  79. package/dist/{chunk-ALTP3MFS.js → chunk-5ZCUDWD4.js} +2 -2
  80. package/dist/{chunk-B7GROTXD.js → chunk-6LKEUCEM.js} +2 -2
  81. package/dist/{chunk-2AJ72VVS.js → chunk-7BZOBA5J.js} +3 -3
  82. package/dist/{chunk-44DUJETW.js → chunk-7HXCF4RE.js} +2 -2
  83. package/dist/{chunk-WVVK2RFM.js → chunk-7L45JFPW.js} +14 -14
  84. package/dist/{chunk-R372P2EH.js → chunk-ACXPX4AG.js} +2 -2
  85. package/dist/{chunk-7CPOZH6Y.js → chunk-ALSGMEJR.js} +5 -5
  86. package/dist/{chunk-JGUXPJUQ.js → chunk-AS7YMSL6.js} +2 -2
  87. package/dist/{chunk-DKQM5Q6Q.js → chunk-CW4AGY3G.js} +4 -4
  88. package/dist/{chunk-ZN4UZWMT.js → chunk-D2ANTDGO.js} +3 -3
  89. package/dist/{chunk-3UOSVTFR.js → chunk-D3LMYTBV.js} +9 -9
  90. package/dist/{chunk-FOCEI3TN.js → chunk-DRBXHRXE.js} +2 -1
  91. package/dist/chunk-DRBXHRXE.js.map +1 -0
  92. package/dist/{chunk-LIHC3T43.js → chunk-E5OBR6CJ.js} +4 -4
  93. package/dist/{chunk-XY7QPJDA.js → chunk-E6EE5B67.js} +2 -2
  94. package/dist/{chunk-TBMRQZDY.js → chunk-EP5BVCRI.js} +2 -2
  95. package/dist/{chunk-MJPV4LBH.js → chunk-FUYEVEX7.js} +83 -83
  96. package/dist/{chunk-2BEJ4XCF.js → chunk-FZRQZW4H.js} +2 -2
  97. package/dist/{chunk-X6ROIHFE.js → chunk-G5R3D5UP.js} +2 -2
  98. package/dist/{chunk-2FYNLDKJ.js → chunk-GAWOEQEQ.js} +2 -2
  99. package/dist/{chunk-TKBDJTXG.js → chunk-GBJ4RDFR.js} +94 -20
  100. package/dist/chunk-GBJ4RDFR.js.map +1 -0
  101. package/dist/{chunk-QH6CS6XJ.js → chunk-H24WNPA2.js} +2 -2
  102. package/dist/{chunk-VFNI3RPT.js → chunk-HKJCMOT6.js} +2 -2
  103. package/dist/{chunk-JQ7T5VDC.js → chunk-HUHX2OCX.js} +1 -1
  104. package/dist/chunk-HUHX2OCX.js.map +1 -0
  105. package/dist/{chunk-EZWWCN7I.js → chunk-IAEAFLF2.js} +2 -2
  106. package/dist/{chunk-NBNDVNJ2.js → chunk-IGPEJVBY.js} +3 -3
  107. package/dist/{chunk-MQVINOJQ.js → chunk-IYZ5K7MU.js} +3 -3
  108. package/dist/{chunk-Q5VJUNXA.js → chunk-JMQUBRV6.js} +105 -42
  109. package/dist/chunk-JMQUBRV6.js.map +1 -0
  110. package/dist/{chunk-2TQ2UFKH.js → chunk-JP3YPDAW.js} +57 -57
  111. package/dist/{chunk-P2CLYN6H.js → chunk-K5GB36ME.js} +2 -2
  112. package/dist/{chunk-227UFSEQ.js → chunk-LEPOP46W.js} +4 -4
  113. package/dist/{chunk-FBZTWUK5.js → chunk-OFHFH655.js} +8 -8
  114. package/dist/{chunk-GJPCP5CA.js → chunk-OP6F6L3S.js} +2 -2
  115. package/dist/{chunk-FGBJBYWP.js → chunk-OVRQLAYR.js} +3 -3
  116. package/dist/{chunk-2NLLXCJG.js → chunk-OWHERGF2.js} +2 -2
  117. package/dist/{chunk-3BSJBIZ3.js → chunk-OYJKSZIQ.js} +2 -2
  118. package/dist/chunk-P2BM76KT.js +528 -0
  119. package/dist/chunk-P2BM76KT.js.map +1 -0
  120. package/dist/{chunk-UR5SIN7H.js → chunk-PBUZNGMX.js} +30 -30
  121. package/dist/{chunk-P2OZKKYP.js → chunk-PHWUFPCO.js} +2 -2
  122. package/dist/{chunk-Q6YJW6KE.js → chunk-PKRIQL3V.js} +2 -2
  123. package/dist/{chunk-BR4UFMLO.js → chunk-PTSNLEXK.js} +3 -3
  124. package/dist/{chunk-AHFZXUZM.js → chunk-QDKK63Y7.js} +3 -3
  125. package/dist/{chunk-JBPKEARU.js → chunk-RKNJBZ55.js} +4 -4
  126. package/dist/{chunk-FCCMFVS6.js → chunk-RPSYDM6C.js} +2 -2
  127. package/dist/chunk-RPW2NOU6.js +33 -0
  128. package/dist/chunk-RPW2NOU6.js.map +1 -0
  129. package/dist/{chunk-MWTMTW2T.js → chunk-SH6YO3LT.js} +3 -3
  130. package/dist/{chunk-IYT632D5.js → chunk-UBKEQOTD.js} +3 -3
  131. package/dist/{chunk-BJJAX3IZ.js → chunk-UTRO5UJU.js} +2 -2
  132. package/dist/{chunk-X7Y7WX73.js → chunk-UVYI6VIX.js} +1 -1
  133. package/dist/{chunk-S24E4MBQ.js → chunk-VSURVXKY.js} +9 -9
  134. package/dist/chunk-WOCNN776.js +742 -0
  135. package/dist/chunk-WOCNN776.js.map +1 -0
  136. package/dist/{chunk-YOBNCD3Z.js → chunk-WRFBW2TT.js} +10 -10
  137. package/dist/{chunk-MXJ3VP6S.js → chunk-WU7TTVOD.js} +7 -7
  138. package/dist/{chunk-EG64LXKL.js → chunk-XOZ4ALGB.js} +3 -3
  139. package/dist/{chunk-2PHX3I5C.js → chunk-Z54EM5YZ.js} +10 -10
  140. package/dist/{cli-DJbkaZBU.d.ts → cli-CxoSOiG0.d.ts} +5 -5
  141. package/dist/cli.d.ts +12 -12
  142. package/dist/cli.js +65 -65
  143. package/dist/compounding/engine.d.ts +2 -2
  144. package/dist/compounding/engine.js +9 -9
  145. package/dist/compounding/preference-consolidator.d.ts +1 -1
  146. package/dist/compression-optimizer.d.ts +1 -1
  147. package/dist/config.d.ts +1 -1
  148. package/dist/config.js +1 -1
  149. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  150. package/dist/connectors/codex-materialize-runner.js +8 -8
  151. package/dist/connectors/codex-materialize.d.ts +1 -1
  152. package/dist/connectors/index.d.ts +1 -1
  153. package/dist/connectors/index.js +9 -9
  154. package/dist/consolidation-provenance-check.d.ts +2 -2
  155. package/dist/consolidation-undo.d.ts +2 -2
  156. package/dist/contradiction/index.d.ts +2 -2
  157. package/dist/contradiction/index.js +6 -6
  158. package/dist/{contradiction-scan-HMF7CNKD.js → contradiction-scan-HYLIRMJ2.js} +3 -3
  159. package/dist/conversation-index/backend.d.ts +1 -1
  160. package/dist/conversation-index/backend.js +2 -2
  161. package/dist/conversation-index/chunker.d.ts +1 -1
  162. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  163. package/dist/conversation-index/indexer.d.ts +1 -1
  164. package/dist/conversation-index/search.d.ts +1 -1
  165. package/dist/corpus-watermark.d.ts +1 -1
  166. package/dist/corpus-watermark.js +16 -16
  167. package/dist/day-summary.d.ts +1 -1
  168. package/dist/day-summary.js +2 -2
  169. package/dist/delinearize.d.ts +1 -1
  170. package/dist/direct-answer-wiring.d.ts +1 -1
  171. package/dist/direct-answer.d.ts +1 -1
  172. package/dist/embedding-fallback.d.ts +1 -1
  173. package/dist/embedding-fallback.js +2 -2
  174. package/dist/enrichment/index.d.ts +1 -1
  175. package/dist/entity-retrieval.d.ts +2 -2
  176. package/dist/entity-retrieval.js +8 -8
  177. package/dist/entity-schema.d.ts +1 -1
  178. package/dist/explicit-capture.d.ts +8 -8
  179. package/dist/explicit-capture.js +2 -2
  180. package/dist/extraction-error-classification.d.ts +1 -1
  181. package/dist/extraction-faithfulness.d.ts +1 -1
  182. package/dist/extraction-judge-telemetry.d.ts +1 -1
  183. package/dist/extraction-judge-training.d.ts +1 -1
  184. package/dist/extraction-judge.d.ts +1 -1
  185. package/dist/extraction-liveness.d.ts +1 -1
  186. package/dist/extraction-source-grounding-helpers.d.ts +34 -0
  187. package/dist/extraction-source-grounding-helpers.js +62 -0
  188. package/dist/extraction-source-grounding-helpers.js.map +1 -0
  189. package/dist/extraction-source-grounding-rules.d.ts +18 -0
  190. package/dist/extraction-source-grounding-rules.js +12 -0
  191. package/dist/extraction-source-grounding-rules.js.map +1 -0
  192. package/dist/extraction-source-grounding.d.ts +11 -0
  193. package/dist/extraction-source-grounding.js +13 -0
  194. package/dist/extraction-source-grounding.js.map +1 -0
  195. package/dist/extraction.d.ts +3 -1
  196. package/dist/extraction.js +7 -4
  197. package/dist/fallback-llm.d.ts +1 -1
  198. package/dist/{first-start-migration-BKNGOFED.js → first-start-migration-F2QJD66E.js} +5 -5
  199. package/dist/graph-dashboard-diff.d.ts +1 -1
  200. package/dist/graph-dashboard-key.d.ts +1 -1
  201. package/dist/graph-dashboard-parser.d.ts +1 -1
  202. package/dist/graph-edge-reinforcement.d.ts +1 -1
  203. package/dist/graph-snapshot.d.ts +1 -1
  204. package/dist/graph.d.ts +1 -1
  205. package/dist/importance.d.ts +1 -1
  206. package/dist/importers/index.d.ts +1 -1
  207. package/dist/in-flight-reads.d.ts +1 -1
  208. package/dist/index.d.ts +19 -19
  209. package/dist/index.js +156 -153
  210. package/dist/intent.d.ts +1 -1
  211. package/dist/lcm/engine.d.ts +1 -1
  212. package/dist/lcm/engine.js +4 -4
  213. package/dist/lcm/index.d.ts +1 -1
  214. package/dist/lcm/index.js +10 -10
  215. package/dist/lcm/tools.d.ts +1 -1
  216. package/dist/lifecycle.d.ts +1 -1
  217. package/dist/live-connectors-runner.d.ts +1 -1
  218. package/dist/local-llm.d.ts +1 -1
  219. package/dist/local-llm.js +2 -2
  220. package/dist/local-model-endpoint.d.ts +1 -1
  221. package/dist/maintenance/memory-governance.d.ts +1 -1
  222. package/dist/maintenance/memory-governance.js +8 -8
  223. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
  224. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -8
  225. package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
  226. package/dist/maintenance/rebuild-memory-projection.js +9 -9
  227. package/dist/{maintenance-DveW6azo.d.ts → maintenance-Cb-PjxQd.d.ts} +3 -3
  228. package/dist/mcp-memory-inspector-app.d.ts +10 -10
  229. package/dist/memory-action-policy.d.ts +1 -1
  230. package/dist/memory-cache.d.ts +1 -1
  231. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  232. package/dist/memory-projection-store.d.ts +1 -1
  233. package/dist/memory-provenance.d.ts +1 -1
  234. package/dist/memory-worth-outcomes.d.ts +2 -2
  235. package/dist/models-json.d.ts +1 -1
  236. package/dist/namespaces/migrate.d.ts +3 -3
  237. package/dist/namespaces/migrate.js +15 -15
  238. package/dist/namespaces/principal.d.ts +1 -1
  239. package/dist/namespaces/principal.js +2 -2
  240. package/dist/namespaces/search.d.ts +1 -1
  241. package/dist/namespaces/search.js +7 -7
  242. package/dist/namespaces/storage.d.ts +3 -3
  243. package/dist/namespaces/storage.js +8 -8
  244. package/dist/native-knowledge.d.ts +1 -1
  245. package/dist/offline-sync-impression-drain.d.ts +1 -1
  246. package/dist/offline-sync-impression-drain.js +6 -6
  247. package/dist/operator-doctor-corpus.d.ts +1 -1
  248. package/dist/operator-doctor-corpus.js +17 -17
  249. package/dist/operator-doctor-replica.d.ts +1 -1
  250. package/dist/operator-toolkit.d.ts +3 -3
  251. package/dist/operator-toolkit.js +23 -23
  252. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  253. package/dist/orchestration/compression-guideline-coordinator.js +2 -2
  254. package/dist/orchestration/maintenance.d.ts +4 -4
  255. package/dist/orchestration/maintenance.js +15 -15
  256. package/dist/{orchestrator-Cvlgpe6I.d.ts → orchestrator-5GKqb_HL.d.ts} +7 -7
  257. package/dist/orchestrator.d.ts +8 -8
  258. package/dist/orchestrator.js +103 -100
  259. package/dist/patterns-cli.d.ts +1 -1
  260. package/dist/{pipeline-BveYVaz7.d.ts → pipeline-3Qi1TpRo.d.ts} +1 -1
  261. package/dist/policy-runtime.d.ts +1 -1
  262. package/dist/proactive-contention.d.ts +1 -1
  263. package/dist/provenance.d.ts +1 -1
  264. package/dist/{qmd-CLWDsbYR.d.ts → qmd-D9JqzzRX.d.ts} +1 -1
  265. package/dist/qmd-preflight.d.ts +2 -2
  266. package/dist/qmd-recall-cache.d.ts +1 -1
  267. package/dist/qmd.d.ts +2 -2
  268. package/dist/recall-concurrency-config.d.ts +1 -1
  269. package/dist/recall-disclosure-escalation.d.ts +1 -1
  270. package/dist/recall-explain-renderer.d.ts +1 -1
  271. package/dist/recall-explain-renderer.js +3 -3
  272. package/dist/recall-memory-map.d.ts +1 -1
  273. package/dist/recall-planner-llm.d.ts +1 -1
  274. package/dist/recall-state.d.ts +1 -1
  275. package/dist/recall-tag-filter.d.ts +1 -1
  276. package/dist/recall-timings.d.ts +1 -1
  277. package/dist/recall-xray-cli.d.ts +1 -1
  278. package/dist/recall-xray-cli.js +4 -4
  279. package/dist/recall-xray-renderer.d.ts +1 -1
  280. package/dist/recall-xray-renderer.js +3 -3
  281. package/dist/recall-xray.d.ts +1 -1
  282. package/dist/recall-xray.js +2 -2
  283. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  284. package/dist/replay/normalizers/claude.d.ts +1 -1
  285. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  286. package/dist/replay/normalizers/shared.d.ts +1 -1
  287. package/dist/replay/runner.d.ts +1 -1
  288. package/dist/replay/types.d.ts +1 -1
  289. package/dist/replica-divergence.d.ts +1 -1
  290. package/dist/replica-peers-config.d.ts +1 -1
  291. package/dist/resolve-auth-token.d.ts +1 -1
  292. package/dist/resume-bundles.js +2 -2
  293. package/dist/retrieval-agents.d.ts +2 -2
  294. package/dist/retrieval-tiers.d.ts +1 -1
  295. package/dist/routing/engine.d.ts +1 -1
  296. package/dist/routing/store.d.ts +1 -1
  297. package/dist/salvage-envelope.d.ts +1 -1
  298. package/dist/{scope-profiles-BVf3DZ7z.d.ts → scope-profiles-C4g6_UdP.d.ts} +1 -1
  299. package/dist/search/embed-helper.d.ts +1 -1
  300. package/dist/search/embed-helper.js +2 -2
  301. package/dist/search/factory.d.ts +1 -1
  302. package/dist/search/factory.js +6 -6
  303. package/dist/search/index.d.ts +1 -1
  304. package/dist/search/index.js +10 -10
  305. package/dist/search/lancedb-backend.d.ts +1 -1
  306. package/dist/search/meilisearch-backend.d.ts +1 -1
  307. package/dist/search/noop-backend.d.ts +1 -1
  308. package/dist/search/orama-backend.d.ts +1 -1
  309. package/dist/search/port.d.ts +1 -1
  310. package/dist/search/remote-backend.d.ts +1 -1
  311. package/dist/{semantic-consolidation-DXLV47y2.d.ts → semantic-consolidation-BrxqnzTW.d.ts} +1 -1
  312. package/dist/semantic-consolidation.d.ts +2 -2
  313. package/dist/semantic-consolidation.js +9 -9
  314. package/dist/semantic-rule-promotion.js +8 -8
  315. package/dist/semantic-rule-verifier.d.ts +1 -1
  316. package/dist/semantic-rule-verifier.js +8 -8
  317. package/dist/{service-DbgPTeMJ.d.ts → service-Bi1JgxdP.d.ts} +2 -2
  318. package/dist/session-observer-bands.d.ts +1 -1
  319. package/dist/session-observer-state.d.ts +1 -1
  320. package/dist/shared-context/manager.d.ts +1 -1
  321. package/dist/shared-context/manager.js +2 -2
  322. package/dist/signal.d.ts +1 -1
  323. package/dist/{storage-DOzTm8wG.d.ts → storage-CcH6yPk2.d.ts} +1 -1
  324. package/dist/storage.d.ts +2 -2
  325. package/dist/storage.js +7 -7
  326. package/dist/summarizer.d.ts +1 -1
  327. package/dist/summarizer.js +3 -3
  328. package/dist/summary-snapshot.d.ts +1 -1
  329. package/dist/temporal-supersession.d.ts +2 -2
  330. package/dist/temporal-timeline-recall.d.ts +1 -1
  331. package/dist/temporal-validity.d.ts +1 -1
  332. package/dist/threading.d.ts +1 -1
  333. package/dist/tier-migration.d.ts +2 -2
  334. package/dist/tier-routing.d.ts +1 -1
  335. package/dist/{tier-stats-XKD7SXYY.js → tier-stats-OPTSBNRH.js} +2 -2
  336. package/dist/topics.d.ts +1 -1
  337. package/dist/transcript.d.ts +1 -1
  338. package/dist/transfer/autodetect.js +1 -1
  339. package/dist/transfer/backup.js +2 -2
  340. package/dist/transfer/capsule-export.js +3 -3
  341. package/dist/transfer/capsule-import.js +2 -2
  342. package/dist/trust-score-stage.d.ts +1 -1
  343. package/dist/trust-score.d.ts +1 -1
  344. package/dist/{types-BA7W-_xy.d.ts → types-Dg0w9ADr.d.ts} +1 -0
  345. package/dist/types.d.ts +1 -1
  346. package/dist/types.js +1 -1
  347. package/dist/utility-runtime.d.ts +1 -1
  348. package/dist/verified-recall.js +8 -8
  349. package/dist/write-envelope.d.ts +1 -1
  350. package/package.json +2 -2
  351. package/src/capabilities.test.ts +1 -0
  352. package/src/capabilities.ts +7 -7
  353. package/src/config.test.ts +5 -0
  354. package/src/config.ts +1 -1
  355. package/src/extraction-source-grounding-helpers.ts +603 -0
  356. package/src/extraction-source-grounding-rules.ts +1031 -0
  357. package/src/extraction-source-grounding.test.ts +2337 -0
  358. package/src/extraction-source-grounding.ts +50 -0
  359. package/src/extraction.ts +120 -15
  360. package/src/storage/entity-canonical-id-migration.ts +151 -23
  361. package/src/types.ts +2 -2
  362. package/dist/chunk-FOCEI3TN.js.map +0 -1
  363. package/dist/chunk-JQ7T5VDC.js.map +0 -1
  364. package/dist/chunk-Q5VJUNXA.js.map +0 -1
  365. package/dist/chunk-TKBDJTXG.js.map +0 -1
  366. /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
  367. /package/dist/{chunk-GQSMYXKL.js.map → chunk-2AXMQWCN.js.map} +0 -0
  368. /package/dist/{chunk-VYYNBJXH.js.map → chunk-2D7FEUCK.js.map} +0 -0
  369. /package/dist/{chunk-PXQM2HOG.js.map → chunk-2GXM5N5A.js.map} +0 -0
  370. /package/dist/{chunk-JIF3VRKQ.js.map → chunk-35DL7IOI.js.map} +0 -0
  371. /package/dist/{chunk-2H7OEVPO.js.map → chunk-3GD2HFR6.js.map} +0 -0
  372. /package/dist/{chunk-KKBTBUVT.js.map → chunk-3JNDOAQ2.js.map} +0 -0
  373. /package/dist/{chunk-4TFIRMTE.js.map → chunk-4A6YGWQN.js.map} +0 -0
  374. /package/dist/{chunk-IKMAO55B.js.map → chunk-55MDBZLC.js.map} +0 -0
  375. /package/dist/{chunk-RKXZ7P4V.js.map → chunk-5QDRGWT2.js.map} +0 -0
  376. /package/dist/{chunk-IPLYGWQF.js.map → chunk-5W7CTZ2E.js.map} +0 -0
  377. /package/dist/{chunk-ALTP3MFS.js.map → chunk-5ZCUDWD4.js.map} +0 -0
  378. /package/dist/{chunk-B7GROTXD.js.map → chunk-6LKEUCEM.js.map} +0 -0
  379. /package/dist/{chunk-2AJ72VVS.js.map → chunk-7BZOBA5J.js.map} +0 -0
  380. /package/dist/{chunk-44DUJETW.js.map → chunk-7HXCF4RE.js.map} +0 -0
  381. /package/dist/{chunk-WVVK2RFM.js.map → chunk-7L45JFPW.js.map} +0 -0
  382. /package/dist/{chunk-R372P2EH.js.map → chunk-ACXPX4AG.js.map} +0 -0
  383. /package/dist/{chunk-7CPOZH6Y.js.map → chunk-ALSGMEJR.js.map} +0 -0
  384. /package/dist/{chunk-JGUXPJUQ.js.map → chunk-AS7YMSL6.js.map} +0 -0
  385. /package/dist/{chunk-DKQM5Q6Q.js.map → chunk-CW4AGY3G.js.map} +0 -0
  386. /package/dist/{chunk-ZN4UZWMT.js.map → chunk-D2ANTDGO.js.map} +0 -0
  387. /package/dist/{chunk-3UOSVTFR.js.map → chunk-D3LMYTBV.js.map} +0 -0
  388. /package/dist/{chunk-LIHC3T43.js.map → chunk-E5OBR6CJ.js.map} +0 -0
  389. /package/dist/{chunk-XY7QPJDA.js.map → chunk-E6EE5B67.js.map} +0 -0
  390. /package/dist/{chunk-TBMRQZDY.js.map → chunk-EP5BVCRI.js.map} +0 -0
  391. /package/dist/{chunk-MJPV4LBH.js.map → chunk-FUYEVEX7.js.map} +0 -0
  392. /package/dist/{chunk-2BEJ4XCF.js.map → chunk-FZRQZW4H.js.map} +0 -0
  393. /package/dist/{chunk-X6ROIHFE.js.map → chunk-G5R3D5UP.js.map} +0 -0
  394. /package/dist/{chunk-2FYNLDKJ.js.map → chunk-GAWOEQEQ.js.map} +0 -0
  395. /package/dist/{chunk-QH6CS6XJ.js.map → chunk-H24WNPA2.js.map} +0 -0
  396. /package/dist/{chunk-VFNI3RPT.js.map → chunk-HKJCMOT6.js.map} +0 -0
  397. /package/dist/{chunk-EZWWCN7I.js.map → chunk-IAEAFLF2.js.map} +0 -0
  398. /package/dist/{chunk-NBNDVNJ2.js.map → chunk-IGPEJVBY.js.map} +0 -0
  399. /package/dist/{chunk-MQVINOJQ.js.map → chunk-IYZ5K7MU.js.map} +0 -0
  400. /package/dist/{chunk-2TQ2UFKH.js.map → chunk-JP3YPDAW.js.map} +0 -0
  401. /package/dist/{chunk-P2CLYN6H.js.map → chunk-K5GB36ME.js.map} +0 -0
  402. /package/dist/{chunk-227UFSEQ.js.map → chunk-LEPOP46W.js.map} +0 -0
  403. /package/dist/{chunk-FBZTWUK5.js.map → chunk-OFHFH655.js.map} +0 -0
  404. /package/dist/{chunk-GJPCP5CA.js.map → chunk-OP6F6L3S.js.map} +0 -0
  405. /package/dist/{chunk-FGBJBYWP.js.map → chunk-OVRQLAYR.js.map} +0 -0
  406. /package/dist/{chunk-2NLLXCJG.js.map → chunk-OWHERGF2.js.map} +0 -0
  407. /package/dist/{chunk-3BSJBIZ3.js.map → chunk-OYJKSZIQ.js.map} +0 -0
  408. /package/dist/{chunk-UR5SIN7H.js.map → chunk-PBUZNGMX.js.map} +0 -0
  409. /package/dist/{chunk-P2OZKKYP.js.map → chunk-PHWUFPCO.js.map} +0 -0
  410. /package/dist/{chunk-Q6YJW6KE.js.map → chunk-PKRIQL3V.js.map} +0 -0
  411. /package/dist/{chunk-BR4UFMLO.js.map → chunk-PTSNLEXK.js.map} +0 -0
  412. /package/dist/{chunk-AHFZXUZM.js.map → chunk-QDKK63Y7.js.map} +0 -0
  413. /package/dist/{chunk-JBPKEARU.js.map → chunk-RKNJBZ55.js.map} +0 -0
  414. /package/dist/{chunk-FCCMFVS6.js.map → chunk-RPSYDM6C.js.map} +0 -0
  415. /package/dist/{chunk-MWTMTW2T.js.map → chunk-SH6YO3LT.js.map} +0 -0
  416. /package/dist/{chunk-IYT632D5.js.map → chunk-UBKEQOTD.js.map} +0 -0
  417. /package/dist/{chunk-BJJAX3IZ.js.map → chunk-UTRO5UJU.js.map} +0 -0
  418. /package/dist/{chunk-X7Y7WX73.js.map → chunk-UVYI6VIX.js.map} +0 -0
  419. /package/dist/{chunk-S24E4MBQ.js.map → chunk-VSURVXKY.js.map} +0 -0
  420. /package/dist/{chunk-YOBNCD3Z.js.map → chunk-WRFBW2TT.js.map} +0 -0
  421. /package/dist/{chunk-MXJ3VP6S.js.map → chunk-WU7TTVOD.js.map} +0 -0
  422. /package/dist/{chunk-EG64LXKL.js.map → chunk-XOZ4ALGB.js.map} +0 -0
  423. /package/dist/{chunk-2PHX3I5C.js.map → chunk-Z54EM5YZ.js.map} +0 -0
  424. /package/dist/{contradiction-scan-HMF7CNKD.js.map → contradiction-scan-HYLIRMJ2.js.map} +0 -0
  425. /package/dist/{first-start-migration-BKNGOFED.js.map → first-start-migration-F2QJD66E.js.map} +0 -0
  426. /package/dist/{tier-stats-XKD7SXYY.js.map → tier-stats-OPTSBNRH.js.map} +0 -0
@@ -0,0 +1,2337 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { parseConfig } from "./config.js";
5
+ import { ExtractionEngine } from "./extraction.js";
6
+ import {
7
+ applyExtractionSourceGrounding,
8
+ filterExtractionResultBySource,
9
+ } from "./extraction-source-grounding.js";
10
+ import type { ExtractionResult } from "./types.js";
11
+
12
+ const OBSERVED_TURN = {
13
+ role: "user" as const,
14
+ content: "Moonlight's theme color is green, not blue. The deployment finished yesterday.",
15
+ timestamp: "2026-07-25T12:00:00.000Z",
16
+ };
17
+
18
+ const CONTEXT_ONLY_TURN = {
19
+ role: "user" as const,
20
+ content: "The user prefers dark mode in all editors.",
21
+ timestamp: "2026-07-25T12:00:01.000Z",
22
+ extractionContextOnly: true as const,
23
+ };
24
+
25
+ const MODEL_RESULT: ExtractionResult = {
26
+ facts: [
27
+ {
28
+ category: "correction",
29
+ content: "Moonlight uses green rather than blue for its theme color.",
30
+ confidence: 0.95,
31
+ tags: [],
32
+ },
33
+ {
34
+ category: "fact",
35
+ content: "The user prefers dark mode in all editors.",
36
+ confidence: 0.9,
37
+ tags: [],
38
+ },
39
+ {
40
+ category: "fact",
41
+ content: "The deployment finished yesterday.",
42
+ confidence: 0.9,
43
+ tags: [],
44
+ },
45
+ {
46
+ category: "fact",
47
+ content: "Moonlight uses blue for its theme color.",
48
+ confidence: 0.9,
49
+ tags: [],
50
+ },
51
+ ],
52
+ profileUpdates: [
53
+ "The user corrected Moonlight's theme color to green.",
54
+ "User prefers dark mode in all editors",
55
+ "Moonlight's theme color is blue.",
56
+ ],
57
+ entities: [],
58
+ questions: [
59
+ {
60
+ question: "What is Moonlight's theme color?",
61
+ context: "Theme color discussion",
62
+ priority: 0.5,
63
+ },
64
+ {
65
+ question: "Which cloud provider hosts the staging environment?",
66
+ context: "Deployment discussion",
67
+ priority: 0.5,
68
+ },
69
+ {
70
+ question: "What is Moonlight's theme color?",
71
+ context: "Deployment discussion",
72
+ priority: 0.5,
73
+ },
74
+ ],
75
+ };
76
+
77
+ function assertGroundedResult(result: ExtractionResult): void {
78
+ assert.deepEqual(
79
+ result.facts.map((fact) => fact.content),
80
+ [
81
+ "Moonlight uses green rather than blue for its theme color.",
82
+ "The deployment finished on 2026-07-24.",
83
+ ],
84
+ );
85
+
86
+ assert.deepEqual(
87
+ result.profileUpdates,
88
+ ["The user corrected Moonlight's theme color to green."],
89
+ );
90
+ assert.deepEqual(result.questions, []);
91
+ }
92
+
93
+ test("contradictory source polarity is rejected instead of passing token overlap", () => {
94
+ const result = filterExtractionResultBySource(
95
+ {
96
+ facts: [
97
+ {
98
+ category: "fact",
99
+ content: "Moonlight uses blue for its theme color.",
100
+ confidence: 0.9,
101
+ tags: [],
102
+ },
103
+ {
104
+ category: "fact",
105
+ content: "blue",
106
+ confidence: 0.9,
107
+ tags: [],
108
+ },
109
+ ],
110
+ profileUpdates: ["Moonlight's theme color is blue."],
111
+ entities: [],
112
+ questions: [],
113
+ },
114
+ OBSERVED_TURN.content,
115
+ );
116
+
117
+ assert.deepEqual(result.facts, []);
118
+ assert.deepEqual(result.profileUpdates, []);
119
+ });
120
+
121
+ test("source-language corrections remain grounded when the model copies a source span", () => {
122
+ const result = filterExtractionResultBySource(
123
+ {
124
+ facts: [
125
+ {
126
+ category: "correction",
127
+ content: "Moonlightのテーマカラーは青ではなく緑です。",
128
+ confidence: 0.95,
129
+ tags: [],
130
+ },
131
+ {
132
+ category: "fact",
133
+ content: "The user prefers dark mode in all editors.",
134
+ confidence: 0.9,
135
+ tags: [],
136
+ },
137
+ ],
138
+ profileUpdates: [
139
+ "Moonlightのテーマカラーは青ではなく緑です。",
140
+ "User prefers dark mode in all editors",
141
+ ],
142
+ entities: [],
143
+ questions: [],
144
+ },
145
+ "Moonlightのテーマカラーは青です。訂正します。Moonlightのテーマカラーは青ではなく緑です。",
146
+ );
147
+
148
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["Moonlightのテーマカラーは青ではなく緑です。"]);
149
+ assert.deepEqual(result.profileUpdates, ["Moonlightのテーマカラーは青ではなく緑です。"]);
150
+ });
151
+
152
+ function fixtureEngine(configInput: Record<string, unknown>): ExtractionEngine {
153
+ return new ExtractionEngine(parseConfig(configInput));
154
+ }
155
+
156
+ test("local extraction drops unsupported facts, profile updates, and questions but keeps paraphrases", async () => {
157
+ const engine = fixtureEngine({
158
+ localLlmEnabled: true,
159
+ localLlmModel: "fixture-local",
160
+ localLlmFallback: false,
161
+ });
162
+ Object.assign(engine, {
163
+ localLlm: {
164
+ async chatCompletion() {
165
+ return { content: JSON.stringify(MODEL_RESULT) };
166
+ },
167
+ },
168
+ modelRegistry: {
169
+ calculateContextSizes: () => ({
170
+ maxInputChars: 8_000,
171
+ maxOutputTokens: 1_000,
172
+ description: "fixture",
173
+ }),
174
+ },
175
+ });
176
+
177
+ const result = await engine.extract([OBSERVED_TURN, CONTEXT_ONLY_TURN]);
178
+
179
+ assertGroundedResult(result);
180
+ });
181
+
182
+ test("extraction uses context-only turns to resolve asserted target references", async () => {
183
+ const engine = fixtureEngine({
184
+ localLlmEnabled: true,
185
+ localLlmModel: "fixture-local",
186
+ localLlmFallback: false,
187
+ });
188
+ Object.assign(engine, {
189
+ localLlm: {
190
+ async chatCompletion() {
191
+ return {
192
+ content: JSON.stringify({
193
+ facts: [
194
+ {
195
+ category: "fact",
196
+ content: "The database uses PostgreSQL.",
197
+ confidence: 0.9,
198
+ tags: [],
199
+ },
200
+ {
201
+ category: "fact",
202
+ content: "The user prefers dark mode in all editors.",
203
+ confidence: 0.9,
204
+ tags: [],
205
+ },
206
+ ],
207
+ profileUpdates: [],
208
+ entities: [],
209
+ questions: [],
210
+ }),
211
+ };
212
+ },
213
+ },
214
+ modelRegistry: {
215
+ calculateContextSizes: () => ({
216
+ maxInputChars: 8_000,
217
+ maxOutputTokens: 1_000,
218
+ description: "fixture",
219
+ }),
220
+ },
221
+ });
222
+
223
+ const result = await engine.extract([
224
+ {
225
+ role: "user",
226
+ content: "Should the database use PostgreSQL?",
227
+ timestamp: "2026-07-25T12:00:00.000Z",
228
+ extractionContextOnly: true,
229
+ },
230
+ {
231
+ role: "user",
232
+ content: "Yes, use that.",
233
+ timestamp: "2026-07-25T12:00:01.000Z",
234
+ },
235
+ ]);
236
+
237
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["The database uses PostgreSQL."]);
238
+ });
239
+
240
+ test("source grounding can be disabled for legacy extraction behavior", async () => {
241
+ const engine = fixtureEngine({
242
+ extractionSourceGroundingEnabled: "false",
243
+ localLlmEnabled: true,
244
+ localLlmModel: "fixture-local",
245
+ localLlmFallback: false,
246
+ });
247
+ Object.assign(engine, {
248
+ localLlm: {
249
+ async chatCompletion() {
250
+ return { content: JSON.stringify(MODEL_RESULT) };
251
+ },
252
+ },
253
+ modelRegistry: {
254
+ calculateContextSizes: () => ({
255
+ maxInputChars: 8_000,
256
+ maxOutputTokens: 1_000,
257
+ description: "fixture",
258
+ }),
259
+ },
260
+ });
261
+
262
+ const result = await engine.extract([OBSERVED_TURN, CONTEXT_ONLY_TURN]);
263
+
264
+ assert.equal(result.facts.length, MODEL_RESULT.facts.length);
265
+ assert.ok(result.facts.some((fact) => fact.content === "Moonlight uses blue for its theme color."));
266
+ assert.ok(result.profileUpdates.includes("Moonlight's theme color is blue."));
267
+ assert.equal(result.questions.length, MODEL_RESULT.questions.length);
268
+ });
269
+
270
+ test("direct extraction applies source grounding to the same output contract", async () => {
271
+ const engine = fixtureEngine({ openaiApiKey: "fixture-key" });
272
+ Object.assign(engine, {
273
+ client: {
274
+ chat: {
275
+ completions: {
276
+ async create() {
277
+ return { choices: [{ message: { content: JSON.stringify(MODEL_RESULT) } }] };
278
+ },
279
+ },
280
+ },
281
+ },
282
+ });
283
+
284
+ const result = await engine.extract([OBSERVED_TURN, CONTEXT_ONLY_TURN]);
285
+
286
+ assertGroundedResult(result);
287
+ });
288
+
289
+ test("gateway extraction applies source grounding to the same output contract", async () => {
290
+ const engine = fixtureEngine({ modelSource: "gateway" });
291
+ Object.assign(engine, {
292
+ fallbackLlm: {
293
+ async parseWithSchemaDetailed() {
294
+ return { modelUsed: "fixture-gateway", result: MODEL_RESULT };
295
+ },
296
+ },
297
+ });
298
+
299
+ const result = await engine.extract([OBSERVED_TURN, CONTEXT_ONLY_TURN]);
300
+
301
+ assertGroundedResult(result);
302
+ });
303
+ test("grounding matches the relevant repeated source occurrence", () => {
304
+ const result = filterExtractionResultBySource(
305
+ {
306
+ facts: [
307
+ {
308
+ category: "fact",
309
+ content: "Blue is supported for accents.",
310
+ confidence: 0.9,
311
+ tags: [],
312
+ },
313
+ {
314
+ category: "fact",
315
+ content: "Blue is supported for backgrounds.",
316
+ confidence: 0.9,
317
+ tags: [],
318
+ },
319
+ {
320
+ category: "fact",
321
+ content: "Blue is not supported for backgrounds.",
322
+ confidence: 0.9,
323
+ tags: [],
324
+ },
325
+ ],
326
+ profileUpdates: [],
327
+ entities: [],
328
+ questions: [],
329
+ },
330
+ "Blue is supported for accents. Blue is not supported for backgrounds.",
331
+ );
332
+
333
+ assert.deepEqual(
334
+ result.facts.map((fact) => fact.content),
335
+ ["Blue is supported for accents.", "Blue is not supported for backgrounds."],
336
+ );
337
+ });
338
+
339
+ test("grounding filters unsupported durable fact fields and nested entity facts", () => {
340
+ const result = filterExtractionResultBySource(
341
+ {
342
+ facts: [
343
+ {
344
+ category: "fact",
345
+ content: "The deployment finished yesterday.",
346
+ confidence: 0.9,
347
+ tags: [],
348
+ structuredAttributes: {
349
+ status: "complete",
350
+ provider: "unknown-cloud",
351
+ },
352
+ eventTime: "yesterday",
353
+ },
354
+ {
355
+ category: "procedure",
356
+ content: "How to deploy staging",
357
+ confidence: 0.9,
358
+ tags: [],
359
+ procedureSteps: [
360
+ {
361
+ order: 1,
362
+ intent: "first run tests",
363
+ expectedOutcome: "tests pass",
364
+ toolCall: { kind: "shell", signature: "run-tests" },
365
+ },
366
+ {
367
+ order: 2,
368
+ intent: "invent a secret deployment step",
369
+ },
370
+ {
371
+ order: 3,
372
+ intent: "then deploy service",
373
+ expectedOutcome: "secret outcome",
374
+ toolCall: { kind: "shell", signature: "invent-secret-command" },
375
+ },
376
+ ],
377
+ },
378
+ {
379
+ category: "reasoning_trace",
380
+ content: "How I debugged latency",
381
+ confidence: 0.9,
382
+ tags: [],
383
+ reasoningTrace: {
384
+ steps: [
385
+ { order: 1, description: "first inspect logs" },
386
+ { order: 2, description: "then compare traces" },
387
+ ],
388
+ finalAnswer: "increase timeout",
389
+ observedOutcome: "the service recovered",
390
+ },
391
+ },
392
+ ],
393
+ profileUpdates: [],
394
+ entities: [
395
+ {
396
+ name: "Moonlight",
397
+ type: "project",
398
+ facts: ["Moonlight's theme color is green.", "Moonlight has a secret plan."],
399
+ structuredSections: [
400
+ {
401
+ key: "appearance",
402
+ title: "Appearance",
403
+ facts: ["Moonlight's theme color is green.", "Moonlight runs on Mars."],
404
+ },
405
+ ],
406
+ },
407
+ ],
408
+ questions: [],
409
+ },
410
+ [
411
+ "The deployment finished yesterday; its status is complete.",
412
+ "To deploy staging, first run tests, then deploy the service. Tests pass. The shell command is run-tests.",
413
+ "How I debugged latency: first inspect logs, then compare traces. The answer is increase timeout.",
414
+ "Moonlight's theme color is green.",
415
+ ].join(" "),
416
+ );
417
+
418
+ assert.deepEqual(result.facts[0]?.structuredAttributes, { status: "complete" });
419
+ assert.equal(result.facts[0]?.eventTime, "yesterday");
420
+ assert.deepEqual(result.facts[1]?.procedureSteps, [
421
+ {
422
+ order: 1,
423
+ intent: "first run tests",
424
+ expectedOutcome: "tests pass",
425
+ toolCall: { kind: "shell", signature: "run-tests" },
426
+ },
427
+ {
428
+ order: 3,
429
+ intent: "then deploy service",
430
+ },
431
+ ]);
432
+ assert.deepEqual(result.facts[2]?.reasoningTrace, {
433
+ steps: [
434
+ { order: 1, description: "first inspect logs" },
435
+ { order: 2, description: "then compare traces" },
436
+ ],
437
+
438
+ finalAnswer: "increase timeout",
439
+ });
440
+ assert.deepEqual(result.entities, [
441
+ {
442
+ name: "Moonlight",
443
+ type: "project",
444
+ facts: ["Moonlight's theme color is green."],
445
+ structuredSections: [
446
+ {
447
+ key: "appearance",
448
+ title: "Appearance",
449
+ facts: ["Moonlight's theme color is green."],
450
+ },
451
+ ],
452
+ },
453
+ ]);
454
+ });
455
+
456
+ test("grounding does not let context questions establish unsupported facts", () => {
457
+ const result = filterExtractionResultBySource(
458
+ {
459
+ facts: [{
460
+ category: "fact",
461
+ content: "Alice works at Acme.",
462
+ confidence: 0.9,
463
+ tags: [],
464
+ }],
465
+ profileUpdates: [],
466
+ entities: [],
467
+ questions: [],
468
+ },
469
+ "Does Alice work at Acme?\n\nAlice joined the call.",
470
+ "Alice joined the call.",
471
+ );
472
+
473
+ assert.deepEqual(result.facts, []);
474
+ });
475
+
476
+ test("grounding splits unpunctuated turns before evaluating facts", () => {
477
+ const result = filterExtractionResultBySource(
478
+ {
479
+ facts: [{
480
+ category: "fact",
481
+ content: "PostgreSQL is the selected database.",
482
+ confidence: 0.9,
483
+ tags: [],
484
+ }],
485
+ profileUpdates: [],
486
+ entities: [],
487
+ questions: [],
488
+ },
489
+ "What database should we select\n\nPostgreSQL is the selected database.",
490
+ "PostgreSQL is the selected database.",
491
+ );
492
+
493
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["PostgreSQL is the selected database."]);
494
+ });
495
+
496
+ test("grounding excludes auxiliary fields sourced only from questions", () => {
497
+ const result = filterExtractionResultBySource(
498
+ {
499
+ facts: [{
500
+ category: "fact",
501
+ content: "The deployment finished.",
502
+ confidence: 0.9,
503
+ tags: [],
504
+ structuredAttributes: { owner: "Alice" },
505
+ }],
506
+ profileUpdates: [],
507
+ entities: [],
508
+ questions: [],
509
+ },
510
+ "The deployment finished.\nIs Alice the owner?",
511
+ "The deployment finished.",
512
+ );
513
+
514
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["The deployment finished."]);
515
+ assert.equal(result.facts[0]?.structuredAttributes, undefined);
516
+ });
517
+
518
+ test("grounding rejects swapped role-normalized fact arguments", () => {
519
+ const result = filterExtractionResultBySource(
520
+ {
521
+ facts: [{
522
+ category: "fact",
523
+ content: "The user supports Acme.",
524
+ confidence: 0.9,
525
+ tags: [],
526
+ }],
527
+ profileUpdates: [],
528
+ entities: [],
529
+ questions: [],
530
+ },
531
+ "Acme supports me.",
532
+ "Acme supports me.",
533
+ { profile: "Acme supports me." },
534
+ );
535
+
536
+ assert.deepEqual(result.facts, []);
537
+ });
538
+
539
+ test("grounding drops entities with no grounded facts", () => {
540
+ const result = filterExtractionResultBySource(
541
+ {
542
+ facts: [],
543
+ profileUpdates: [],
544
+ entities: [
545
+ {
546
+ name: "Moonlight",
547
+ type: "project",
548
+ facts: ["Moonlight has an unsupported secret plan."],
549
+ structuredSections: [
550
+ {
551
+ key: "details",
552
+ title: "Details",
553
+ facts: ["Moonlight runs on Mars."],
554
+ },
555
+ ],
556
+ },
557
+ ],
558
+ questions: [],
559
+ },
560
+ "Moonlight is a project.",
561
+ );
562
+
563
+ assert.deepEqual(result.entities, []);
564
+ });
565
+
566
+ test("grounding rejects an unsupported appended clause", () => {
567
+ const result = filterExtractionResultBySource(
568
+ {
569
+ facts: [
570
+ {
571
+ category: "fact",
572
+ content: "Alice works at Acme and secretly owns Mars.",
573
+ confidence: 0.9,
574
+ tags: [],
575
+ },
576
+ ],
577
+ profileUpdates: [],
578
+ entities: [],
579
+ questions: [],
580
+ },
581
+ "Alice works at Acme.",
582
+ );
583
+
584
+ assert.deepEqual(result.facts, []);
585
+ });
586
+
587
+ test("grounding matches normalized entity identifiers", () => {
588
+ const result = filterExtractionResultBySource(
589
+ {
590
+ facts: [],
591
+ profileUpdates: [],
592
+ entities: [
593
+ {
594
+ name: "acme-corp",
595
+ type: "company",
596
+ facts: ["Acme Corp uses PostgreSQL."],
597
+ },
598
+ {
599
+ name: "art",
600
+ type: "project",
601
+ facts: ["Cart is shipped."],
602
+ },
603
+ ],
604
+ relationships: [
605
+ {
606
+ source: "company-acme-corp",
607
+ target: "tool-postgresql",
608
+ label: "uses",
609
+ },
610
+ ],
611
+
612
+ questions: [],
613
+ },
614
+ "Acme Corp uses PostgreSQL. Cart is shipped.",
615
+ );
616
+
617
+ assert.deepEqual(result.entities.map((entity) => entity.name), ["acme-corp"]);
618
+ assert.deepEqual(result.relationships, [
619
+ {
620
+ source: "company-acme-corp",
621
+ target: "tool-postgresql",
622
+ label: "uses",
623
+ },
624
+ ]);
625
+ });
626
+
627
+ test("grounding rejects a relationship label unsupported by the source span", () => {
628
+ const result = filterExtractionResultBySource(
629
+ {
630
+ facts: [],
631
+ profileUpdates: [],
632
+ entities: [],
633
+ relationships: [
634
+ {
635
+ source: "person-alice",
636
+ target: "company-acme",
637
+ label: "works at",
638
+ },
639
+ ],
640
+ questions: [],
641
+ },
642
+ "Alice criticized Acme.",
643
+ );
644
+
645
+ assert.deepEqual(result.relationships, []);
646
+ });
647
+
648
+ test("grounding does not treat questions or hypotheticals as factual evidence", () => {
649
+ const questionResult = filterExtractionResultBySource(
650
+ {
651
+ facts: [
652
+ {
653
+ category: "fact",
654
+ content: "Alice is the CEO at Acme.",
655
+ confidence: 0.9,
656
+ tags: [],
657
+ },
658
+ ],
659
+ profileUpdates: ["Alice is the CEO at Acme."],
660
+ entities: [],
661
+ questions: [{ question: "Is Alice the CEO at Acme?", context: "", priority: 0.5 }],
662
+ },
663
+ "Is Alice the CEO at Acme?",
664
+ "Is Alice the CEO at Acme?",
665
+ );
666
+ assert.deepEqual(questionResult.facts, []);
667
+ assert.deepEqual(questionResult.profileUpdates, []);
668
+ assert.deepEqual(questionResult.questions, [
669
+ { question: "Is Alice the CEO at Acme?", context: "", priority: 0.5 },
670
+ ]);
671
+
672
+ const affirmativeFactResult = filterExtractionResultBySource(
673
+ {
674
+ facts: [
675
+ {
676
+ category: "fact",
677
+ content: "Alice works at Acme.",
678
+ confidence: 0.9,
679
+ tags: [],
680
+ },
681
+ ],
682
+ profileUpdates: [],
683
+ entities: [],
684
+ questions: [],
685
+ },
686
+ "Does Alice work at Acme?\nYes.",
687
+ );
688
+ assert.deepEqual(affirmativeFactResult.facts, [
689
+ {
690
+ category: "fact",
691
+ content: "Alice works at Acme.",
692
+ confidence: 0.9,
693
+ tags: [],
694
+ },
695
+ ]);
696
+
697
+ const answeredResult = filterExtractionResultBySource(
698
+ {
699
+ facts: [],
700
+ profileUpdates: [],
701
+ entities: [],
702
+ questions: [{ question: "Does Alice work at Acme?", context: "", priority: 0.5 }],
703
+ },
704
+ "Alice works at Acme.",
705
+ );
706
+ assert.deepEqual(answeredResult.questions, []);
707
+
708
+ const bareAnswerResult = filterExtractionResultBySource(
709
+ {
710
+ facts: [],
711
+ profileUpdates: [],
712
+ entities: [],
713
+ questions: [{ question: "Does Alice work at Acme?", context: "", priority: 0.5 }],
714
+ },
715
+ "Does Alice work at Acme?\nYes.",
716
+ );
717
+ assert.deepEqual(bareAnswerResult.questions, []);
718
+
719
+ const partialOverlapResult = filterExtractionResultBySource(
720
+ {
721
+ facts: [],
722
+ profileUpdates: [],
723
+ entities: [],
724
+ questions: [{ question: "Does Alice work at Globex?", context: "", priority: 0.5 }],
725
+ },
726
+ "Alice works at Acme. Globex is a company.",
727
+ );
728
+ assert.deepEqual(partialOverlapResult.questions, [
729
+ { question: "Does Alice work at Globex?", context: "", priority: 0.5 },
730
+ ]);
731
+
732
+
733
+ const nonAnswerResult = filterExtractionResultBySource(
734
+ {
735
+ facts: [],
736
+ profileUpdates: [],
737
+ entities: [],
738
+ questions: [{ question: "What is the Acme deployment deadline?", context: "", priority: 0.5 }],
739
+ },
740
+ "Acme deployment is delayed.",
741
+ );
742
+ assert.deepEqual(nonAnswerResult.questions, [
743
+ { question: "What is the Acme deployment deadline?", context: "", priority: 0.5 },
744
+ ]);
745
+ const unknownAnswerResult = filterExtractionResultBySource(
746
+ {
747
+ facts: [],
748
+ profileUpdates: [],
749
+ entities: [],
750
+ questions: [{ question: "What is the deployment deadline?", context: "", priority: 0.5 }],
751
+ },
752
+ "The deployment deadline is unknown.",
753
+ );
754
+ assert.deepEqual(unknownAnswerResult.questions, [
755
+ { question: "What is the deployment deadline?", context: "", priority: 0.5 },
756
+ ]);
757
+
758
+ const hypotheticalResult = filterExtractionResultBySource(
759
+ {
760
+ facts: [
761
+ {
762
+ category: "fact",
763
+ content: "Alice is the CEO at Acme.",
764
+ confidence: 0.9,
765
+ tags: [],
766
+ },
767
+ ],
768
+ profileUpdates: [],
769
+ entities: [],
770
+ questions: [],
771
+ },
772
+ "If Alice were the CEO at Acme, we could proceed.",
773
+ );
774
+ assert.deepEqual(hypotheticalResult.facts, []);
775
+ });
776
+
777
+ test("grounding preserves exact dotted identifiers", () => {
778
+ const result = filterExtractionResultBySource(
779
+ {
780
+ facts: [
781
+ {
782
+ category: "fact",
783
+ content: "The package is com.example.platform.billing.service.",
784
+ confidence: 0.9,
785
+ tags: [],
786
+ },
787
+ ],
788
+ profileUpdates: [],
789
+ entities: [],
790
+ questions: [],
791
+ },
792
+ "The package is com.example.platform.billing.service.",
793
+ );
794
+
795
+ assert.equal(result.facts.length, 1);
796
+ });
797
+
798
+ test("grounding joins context questions to asserted target turns", () => {
799
+ const source = "Should the database use PostgreSQL? Yes, use that.";
800
+ const result = filterExtractionResultBySource(
801
+ {
802
+ facts: [
803
+ {
804
+ category: "fact",
805
+ content: "The database uses PostgreSQL.",
806
+ confidence: 0.9,
807
+ tags: [],
808
+ },
809
+ ],
810
+ profileUpdates: [],
811
+ entities: [],
812
+ questions: [],
813
+ },
814
+ source,
815
+ "Yes, use that.",
816
+ );
817
+ assert.equal(result.facts.length, 1);
818
+
819
+ const unsupported = filterExtractionResultBySource(
820
+ {
821
+ facts: [
822
+ {
823
+ category: "fact",
824
+ content: "The database uses PostgreSQL.",
825
+ confidence: 0.9,
826
+ tags: [],
827
+ },
828
+ ],
829
+ profileUpdates: [],
830
+ entities: [],
831
+ questions: [],
832
+ },
833
+ source,
834
+ "I do not know.",
835
+ );
836
+ assert.deepEqual(unsupported.facts, []);
837
+ });
838
+
839
+ test("grounding splits turn-delimited questions before affirmative answers", () => {
840
+ const result = filterExtractionResultBySource(
841
+ {
842
+ facts: [{
843
+ category: "fact",
844
+ content: "The database uses PostgreSQL.",
845
+ confidence: 0.9,
846
+ tags: [],
847
+ }],
848
+ profileUpdates: [],
849
+ entities: [],
850
+ questions: [],
851
+ },
852
+ "Should the database use PostgreSQL\n\nYes, use that.",
853
+ "Yes, use that.",
854
+ );
855
+
856
+ assert.equal(result.facts.length, 1);
857
+ });
858
+
859
+ test("proactive extraction grounds before delinearization", async () => {
860
+ const engine = fixtureEngine({
861
+ localLlmEnabled: true,
862
+ localLlmModel: "fixture-local",
863
+ localLlmFallback: false,
864
+ proactiveExtractionEnabled: true,
865
+ maxProactiveQuestionsPerExtraction: 1,
866
+ });
867
+ Object.assign(engine, {
868
+ localLlm: {
869
+ async chatCompletion(messages: Array<{ role: string; content: string }>) {
870
+ const prompt = messages[1]?.content ?? "";
871
+ if (prompt.startsWith("You are doing a proactive second-pass")) {
872
+ return {
873
+ content: JSON.stringify({
874
+ questions: [{ question: "When did the deployment finish?", context: "", priority: 0.5 }],
875
+ }),
876
+ };
877
+ }
878
+ if (prompt.startsWith("You are answering proactive memory follow-up questions")) {
879
+ return {
880
+ content: JSON.stringify({
881
+ facts: [
882
+ {
883
+ category: "fact",
884
+ content: "The deployment finished yesterday.",
885
+ confidence: 0.95,
886
+ tags: [],
887
+ },
888
+ ],
889
+ profileUpdates: [],
890
+ entities: [],
891
+ relationships: [],
892
+ }),
893
+ };
894
+ }
895
+ return { content: JSON.stringify({ facts: [], profileUpdates: [], entities: [], questions: [] }) };
896
+ },
897
+ isBackgroundLaneContended: () => false,
898
+ },
899
+ modelRegistry: {
900
+ calculateContextSizes: () => ({
901
+ maxInputChars: 8_000,
902
+ maxOutputTokens: 1_000,
903
+ description: "fixture",
904
+ }),
905
+ },
906
+ });
907
+
908
+ const result = await engine.extract([{
909
+ ...OBSERVED_TURN,
910
+ content: "The deployment finished yesterday.",
911
+ }]);
912
+
913
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["The deployment finished on 2026-07-24."]);
914
+ });
915
+
916
+ test("grounding evaluates exact matches in their source sentence", () => {
917
+ const result = filterExtractionResultBySource(
918
+ {
919
+ facts: [
920
+ {
921
+ category: "fact",
922
+ content: "Alice is the CEO at Acme.",
923
+ confidence: 0.9,
924
+ tags: [],
925
+ },
926
+ ],
927
+ profileUpdates: ["Alice is the CEO at Acme."],
928
+ entities: [],
929
+ questions: [],
930
+ },
931
+ "We discussed staffing. Do we know that Alice is the CEO at Acme? Bob joined the call.",
932
+ );
933
+
934
+ assert.deepEqual(result.facts, []);
935
+ assert.deepEqual(result.profileUpdates, []);
936
+ });
937
+
938
+ test("grounding rejects embedded whether questions as factual evidence", () => {
939
+ const result = filterExtractionResultBySource(
940
+ {
941
+ facts: [
942
+ {
943
+ category: "fact",
944
+ content: "Alice uses PostgreSQL.",
945
+ confidence: 0.9,
946
+ tags: [],
947
+ },
948
+ ],
949
+ profileUpdates: ["Alice uses PostgreSQL."],
950
+ entities: [],
951
+ questions: [],
952
+ },
953
+ "We need to determine whether Alice uses PostgreSQL.",
954
+ );
955
+
956
+ assert.deepEqual(result.facts, []);
957
+ assert.deepEqual(result.profileUpdates, []);
958
+ });
959
+ test("grounding rejects comma-spliced unsupported claims", () => {
960
+ const result = filterExtractionResultBySource(
961
+ {
962
+ facts: [{
963
+ category: "fact",
964
+ content: "Alice works at Acme, Bob owns Mars.",
965
+ confidence: 0.9,
966
+ tags: [],
967
+ }],
968
+ profileUpdates: ["Alice works at Acme, Bob owns Mars."],
969
+ entities: [],
970
+ questions: [],
971
+ },
972
+ "Alice works at Acme.",
973
+ );
974
+
975
+ assert.deepEqual(result.facts, []);
976
+ assert.deepEqual(result.profileUpdates, []);
977
+ });
978
+
979
+ test("grounding preserves a supported question while clearing unsupported context", () => {
980
+ const result = filterExtractionResultBySource(
981
+ {
982
+ facts: [],
983
+ profileUpdates: [],
984
+ entities: [],
985
+ questions: [{
986
+ question: "Which database should we use?",
987
+ context: "This blocks the launch schedule.",
988
+ priority: 0.5,
989
+ }],
990
+ },
991
+ "Which database should we use?",
992
+ );
993
+
994
+ assert.deepEqual(result.questions, [{
995
+ question: "Which database should we use?",
996
+ context: "",
997
+ priority: 0.5,
998
+ }]);
999
+ });
1000
+
1001
+ test("grounding keeps affirmative facts answered by context-only questions", () => {
1002
+ const result = filterExtractionResultBySource(
1003
+ {
1004
+ facts: [
1005
+ {
1006
+ category: "fact",
1007
+ content: "The database uses PostgreSQL.",
1008
+ confidence: 0.9,
1009
+ tags: [],
1010
+ },
1011
+ ],
1012
+ profileUpdates: [],
1013
+ entities: [],
1014
+ questions: [],
1015
+ },
1016
+ "Should the database use PostgreSQL?\nYes.",
1017
+ "Yes.",
1018
+ );
1019
+
1020
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["The database uses PostgreSQL."]);
1021
+ });
1022
+
1023
+ test("speaker-aware grounding keeps profile and identity evidence from their own roles", async () => {
1024
+ const engine = fixtureEngine({
1025
+ localLlmEnabled: true,
1026
+ localLlmModel: "fixture-local",
1027
+ localLlmFallback: false,
1028
+ });
1029
+ Object.assign(engine, {
1030
+ localLlm: {
1031
+ async chatCompletion() {
1032
+ return {
1033
+ content: JSON.stringify({
1034
+ facts: [],
1035
+ profileUpdates: [
1036
+ "The user works at Acme.",
1037
+ "The user works at Globex.",
1038
+ ],
1039
+ entities: [],
1040
+ questions: [],
1041
+ identityReflection: "The assistant works at Acme.",
1042
+ }),
1043
+ };
1044
+ },
1045
+ },
1046
+ modelRegistry: {
1047
+ calculateContextSizes: () => ({
1048
+ maxInputChars: 8_000,
1049
+ maxOutputTokens: 1_000,
1050
+ description: "fixture",
1051
+ }),
1052
+ },
1053
+ });
1054
+
1055
+ const result = await engine.extract([
1056
+ {
1057
+ role: "assistant",
1058
+ content: "I work at Acme.",
1059
+ timestamp: "2026-07-25T12:00:00.000Z",
1060
+ },
1061
+ {
1062
+ role: "user",
1063
+ content: "I work at Globex.",
1064
+ timestamp: "2026-07-25T12:00:01.000Z",
1065
+ },
1066
+ ]);
1067
+
1068
+ assert.deepEqual(result.profileUpdates, ["The user works at Globex."]);
1069
+ assert.equal(result.identityReflection, "The assistant works at Acme.");
1070
+ });
1071
+
1072
+ test("grounding rejects substring matches that do not preserve token boundaries", () => {
1073
+ const result = filterExtractionResultBySource(
1074
+ {
1075
+ facts: [
1076
+ {
1077
+ category: "fact",
1078
+ content: "Art is archived.",
1079
+ confidence: 0.9,
1080
+ tags: [],
1081
+ },
1082
+ ],
1083
+ profileUpdates: [],
1084
+ entities: [],
1085
+ questions: [],
1086
+ },
1087
+ "Cart is archived.",
1088
+ );
1089
+
1090
+ assert.deepEqual(result.facts, []);
1091
+ });
1092
+ test("grounding rejects subject-swapped overlap claims", () => {
1093
+ const result = filterExtractionResultBySource(
1094
+ {
1095
+ facts: [
1096
+ {
1097
+ category: "fact",
1098
+ content: "Bob works at Acme.",
1099
+ confidence: 0.9,
1100
+ tags: [],
1101
+ },
1102
+ ],
1103
+ profileUpdates: ["Bob works at Acme."],
1104
+ entities: [],
1105
+ questions: [],
1106
+ },
1107
+ "Alice works at Acme.",
1108
+ );
1109
+
1110
+ assert.deepEqual(result.facts, []);
1111
+ assert.deepEqual(result.profileUpdates, []);
1112
+ });
1113
+
1114
+ test("grounding rejects claims with unsupported object values", () => {
1115
+ const result = filterExtractionResultBySource(
1116
+ {
1117
+ facts: [{
1118
+ category: "fact",
1119
+ content: "Alice uses Windows.",
1120
+ confidence: 0.9,
1121
+ tags: [],
1122
+ }],
1123
+ profileUpdates: [],
1124
+ entities: [],
1125
+ questions: [],
1126
+ },
1127
+ "Alice uses Linux.",
1128
+ );
1129
+
1130
+ assert.deepEqual(result.facts, []);
1131
+ });
1132
+
1133
+ test("grounding rejects claims with reversed argument order", () => {
1134
+ const result = filterExtractionResultBySource(
1135
+ {
1136
+ facts: [{
1137
+ category: "fact",
1138
+ content: "Alice supports Acme.",
1139
+ confidence: 0.9,
1140
+ tags: [],
1141
+ }],
1142
+ profileUpdates: [],
1143
+ entities: [],
1144
+ questions: [],
1145
+ },
1146
+ "Acme supports Alice.",
1147
+ );
1148
+
1149
+ assert.deepEqual(result.facts, []);
1150
+ });
1151
+
1152
+ test("grounding compares polarity within one source occurrence", () => {
1153
+ const result = filterExtractionResultBySource(
1154
+ {
1155
+ facts: [{
1156
+ category: "fact",
1157
+ content: "Alice works at Acme.",
1158
+ confidence: 0.9,
1159
+ tags: [],
1160
+ }],
1161
+ profileUpdates: [],
1162
+ entities: [],
1163
+ questions: [],
1164
+ },
1165
+ "Alice does not work at Acme, but Alice works at Globex.",
1166
+ );
1167
+
1168
+ assert.deepEqual(result.facts, []);
1169
+ });
1170
+
1171
+ test("grounding rejects overlap with a differing object", () => {
1172
+ const result = filterExtractionResultBySource(
1173
+ {
1174
+ facts: [{
1175
+ category: "fact",
1176
+ content: "Alice works at Acme.",
1177
+ confidence: 0.9,
1178
+ tags: [],
1179
+ }],
1180
+ profileUpdates: [],
1181
+ entities: [],
1182
+ questions: [],
1183
+ },
1184
+ "Alice works at Globex.",
1185
+ );
1186
+
1187
+ assert.deepEqual(result.facts, []);
1188
+ });
1189
+
1190
+ test("grounding rejects structured attributes sourced only from unanswered questions", () => {
1191
+ const result = filterExtractionResultBySource(
1192
+ {
1193
+ facts: [{
1194
+ category: "fact",
1195
+ content: "The deployment finished.",
1196
+ confidence: 0.9,
1197
+ tags: [],
1198
+ structuredAttributes: { owner: "Alice" },
1199
+ }],
1200
+ profileUpdates: [],
1201
+ entities: [],
1202
+ questions: [],
1203
+ },
1204
+ "The deployment finished. Is Alice the owner?",
1205
+ );
1206
+
1207
+ assert.deepEqual(result.facts, [{
1208
+ category: "fact",
1209
+ content: "The deployment finished.",
1210
+ confidence: 0.9,
1211
+ tags: [],
1212
+ }]);
1213
+ });
1214
+
1215
+ test("grounding removes unsupported fact entity references", () => {
1216
+ const result = filterExtractionResultBySource(
1217
+ {
1218
+ facts: [{
1219
+ category: "fact",
1220
+ content: "Alice works at Acme.",
1221
+ confidence: 0.9,
1222
+ tags: [],
1223
+ entityRef: "person-bob",
1224
+ }],
1225
+ profileUpdates: [],
1226
+ entities: [],
1227
+ questions: [],
1228
+ },
1229
+ "Alice works at Acme.",
1230
+ );
1231
+
1232
+ assert.deepEqual(result.facts, [{
1233
+ category: "fact",
1234
+ content: "Alice works at Acme.",
1235
+ confidence: 0.9,
1236
+ tags: [],
1237
+ }]);
1238
+ });
1239
+
1240
+ test("grounding preserves short role-normalized profile traits", () => {
1241
+ const result = filterExtractionResultBySource(
1242
+ {
1243
+ facts: [],
1244
+ profileUpdates: ["The user is vegan."],
1245
+ entities: [],
1246
+ questions: [],
1247
+ },
1248
+ "I am vegan.",
1249
+ );
1250
+
1251
+ assert.deepEqual(result.profileUpdates, ["The user is vegan."]);
1252
+ });
1253
+
1254
+ test("grounding rejects role-normalized profile claims with swapped arguments", () => {
1255
+ const result = filterExtractionResultBySource(
1256
+ {
1257
+ facts: [],
1258
+ profileUpdates: ["The user supports Acme."],
1259
+ entities: [],
1260
+ questions: [],
1261
+ },
1262
+ "Acme supports me.",
1263
+ );
1264
+
1265
+ assert.deepEqual(result.profileUpdates, []);
1266
+ });
1267
+
1268
+ test("grounding rejects punctuation-delimited appended claims", () => {
1269
+ const result = filterExtractionResultBySource(
1270
+ {
1271
+ facts: [{
1272
+ category: "fact",
1273
+ content: "Alice works at Acme. Bob owns Mars.",
1274
+ confidence: 0.9,
1275
+ tags: [],
1276
+ }],
1277
+ profileUpdates: [],
1278
+ entities: [],
1279
+ questions: [],
1280
+ },
1281
+ "Alice works at Acme.",
1282
+ );
1283
+
1284
+ assert.deepEqual(result.facts, []);
1285
+ });
1286
+
1287
+ test("grounding rejects context-only claims with a subject-only assertion anchor", () => {
1288
+ const result = filterExtractionResultBySource(
1289
+ {
1290
+ facts: [{
1291
+ category: "fact",
1292
+ content: "Alice works at Acme.",
1293
+ confidence: 0.9,
1294
+ tags: [],
1295
+ }],
1296
+ profileUpdates: [],
1297
+ entities: [],
1298
+ questions: [],
1299
+ },
1300
+ "Alice works at Acme. Alice called Bob.",
1301
+ "Alice called Bob.",
1302
+ );
1303
+
1304
+ assert.deepEqual(result.facts, []);
1305
+ });
1306
+ test("grounding keeps bare negative answers as negative facts", () => {
1307
+ const result = filterExtractionResultBySource(
1308
+ {
1309
+ facts: [{
1310
+ category: "fact",
1311
+ content: "The database does not use PostgreSQL.",
1312
+ confidence: 0.9,
1313
+ tags: [],
1314
+ }],
1315
+ profileUpdates: [],
1316
+ entities: [],
1317
+ questions: [],
1318
+ },
1319
+ "Does the database use PostgreSQL?\nNo.",
1320
+ "No.",
1321
+ );
1322
+
1323
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["The database does not use PostgreSQL."]);
1324
+ });
1325
+
1326
+ test("grounding scopes fact role normalization to the matching speaker", () => {
1327
+ const result = filterExtractionResultBySource(
1328
+ {
1329
+ facts: [
1330
+ {
1331
+ category: "fact",
1332
+ content: "The user is vegan.",
1333
+ confidence: 0.9,
1334
+ tags: [],
1335
+ },
1336
+ {
1337
+ category: "fact",
1338
+ content: "The user is a cyclist.",
1339
+ confidence: 0.9,
1340
+ tags: [],
1341
+ },
1342
+ ],
1343
+ profileUpdates: [],
1344
+ entities: [],
1345
+ questions: [],
1346
+ },
1347
+ "I am vegan.\nI am a cyclist.",
1348
+ undefined,
1349
+ {
1350
+ profile: "I am vegan.",
1351
+ identity: "I am a cyclist.",
1352
+ },
1353
+ );
1354
+
1355
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["The user is vegan."]);
1356
+ });
1357
+
1358
+ test("grounding drops fact entity refs found only in context source", () => {
1359
+ const result = filterExtractionResultBySource(
1360
+ {
1361
+ facts: [{
1362
+ category: "fact",
1363
+ content: "Alice works at Acme.",
1364
+ confidence: 0.9,
1365
+ tags: [],
1366
+ entityRef: "company-globex",
1367
+ }],
1368
+ profileUpdates: [],
1369
+ entities: [],
1370
+ questions: [],
1371
+ },
1372
+ "Alice works at Acme.\nWhich company is Globex?",
1373
+ "Alice works at Acme.",
1374
+ );
1375
+
1376
+ assert.deepEqual(result.facts, [{
1377
+ category: "fact",
1378
+ content: "Alice works at Acme.",
1379
+ confidence: 0.9,
1380
+ tags: [],
1381
+ }]);
1382
+ });
1383
+
1384
+ test("grounding preserves identifier terminal s instead of stemming names", () => {
1385
+ const result = filterExtractionResultBySource(
1386
+ {
1387
+ facts: [
1388
+ {
1389
+ category: "fact",
1390
+ content: "Alice uses Redi.",
1391
+ confidence: 0.9,
1392
+ tags: [],
1393
+ },
1394
+ {
1395
+ category: "fact",
1396
+ content: "Jame works at Acme.",
1397
+ confidence: 0.9,
1398
+ tags: [],
1399
+ },
1400
+ ],
1401
+ profileUpdates: [],
1402
+ entities: [],
1403
+ questions: [],
1404
+ },
1405
+ "Alice uses Redis. James works at Acme.",
1406
+ );
1407
+
1408
+ assert.deepEqual(result.facts, []);
1409
+ });
1410
+ test("grounding rejects exact propositions embedded in explicit denials", () => {
1411
+ const result = filterExtractionResultBySource(
1412
+ {
1413
+ facts: [{
1414
+ category: "fact",
1415
+ content: "Alice works at Acme.",
1416
+ confidence: 0.9,
1417
+ tags: [],
1418
+ }],
1419
+ profileUpdates: [],
1420
+ entities: [],
1421
+ questions: [],
1422
+ },
1423
+ "The claim that Alice works at Acme is false.",
1424
+ );
1425
+
1426
+ assert.deepEqual(result.facts, []);
1427
+ });
1428
+
1429
+ test("grounding preserves wh-questions when an assertion reverses their roles", () => {
1430
+ const result = filterExtractionResultBySource(
1431
+ {
1432
+ facts: [],
1433
+ profileUpdates: [],
1434
+ entities: [],
1435
+ questions: [{ question: "Who employs Alice?", context: "", priority: 0.5 }],
1436
+ },
1437
+ "Who employs Alice? Alice employs Acme.",
1438
+ );
1439
+
1440
+ assert.deepEqual(result.questions, [{
1441
+ question: "Who employs Alice?",
1442
+ context: "",
1443
+ priority: 0.5,
1444
+ }]);
1445
+ });
1446
+
1447
+ test("grounding clears question context that is supported only by the question", () => {
1448
+ const result = filterExtractionResultBySource(
1449
+ {
1450
+ facts: [],
1451
+ profileUpdates: [],
1452
+ entities: [],
1453
+ questions: [{
1454
+ question: "Does Alice use PostgreSQL?",
1455
+ context: "Alice uses PostgreSQL.",
1456
+ priority: 0.5,
1457
+ }],
1458
+ },
1459
+ "Does Alice use PostgreSQL?",
1460
+ );
1461
+
1462
+ assert.deepEqual(result.questions, [{
1463
+ question: "Does Alice use PostgreSQL?",
1464
+ context: "",
1465
+ priority: 0.5,
1466
+ }]);
1467
+ });
1468
+ test("grounding rejects predicate mismatches with shared subject and object", () => {
1469
+ const result = filterExtractionResultBySource(
1470
+ {
1471
+ facts: [{
1472
+ category: "fact",
1473
+ content: "Alice supports Acme.",
1474
+ confidence: 0.9,
1475
+ tags: [],
1476
+ }],
1477
+ profileUpdates: [],
1478
+ entities: [],
1479
+ questions: [],
1480
+ },
1481
+ "Alice criticized Acme.",
1482
+ );
1483
+
1484
+ assert.deepEqual(result.facts, []);
1485
+ });
1486
+
1487
+ test("grounding keeps eventTime tied to the fact's supporting sentence", () => {
1488
+ const result = filterExtractionResultBySource(
1489
+ {
1490
+ facts: [{
1491
+ category: "fact",
1492
+ content: "Alice deployed Acme.",
1493
+ confidence: 0.9,
1494
+ tags: [],
1495
+ eventTime: "yesterday",
1496
+ }],
1497
+ profileUpdates: [],
1498
+ entities: [],
1499
+ questions: [],
1500
+ },
1501
+ "Alice deployed Acme today. Bob left yesterday.",
1502
+ );
1503
+
1504
+ assert.deepEqual(result.facts, [{
1505
+ category: "fact",
1506
+ content: "Alice deployed Acme.",
1507
+ confidence: 0.9,
1508
+ tags: [],
1509
+ }]);
1510
+ });
1511
+
1512
+ test("grounding rejects colon-suffixed unresolved questions", () => {
1513
+ const result = filterExtractionResultBySource(
1514
+ {
1515
+ facts: [{
1516
+ category: "fact",
1517
+ content: "Alice works at Acme.",
1518
+ confidence: 0.9,
1519
+ tags: [],
1520
+ }],
1521
+ profileUpdates: [],
1522
+ entities: [],
1523
+ questions: [],
1524
+ },
1525
+ "Alice works at Acme: yes or no?",
1526
+ );
1527
+
1528
+ assert.deepEqual(result.facts, []);
1529
+ });
1530
+ test("grounding accepts doubled-consonant verb inflection paraphrases", () => {
1531
+ const result = filterExtractionResultBySource(
1532
+ {
1533
+ facts: [{
1534
+ category: "fact",
1535
+ content: "Alice runs Redis.",
1536
+ confidence: 0.9,
1537
+ tags: [],
1538
+ }],
1539
+ profileUpdates: [],
1540
+ entities: [],
1541
+ questions: [],
1542
+ },
1543
+ "Alice is running Redis.",
1544
+ );
1545
+
1546
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["Alice runs Redis."]);
1547
+ });
1548
+
1549
+ test("grounding accepts qualified affirmative answers", () => {
1550
+ const result = filterExtractionResultBySource(
1551
+ {
1552
+ facts: [{
1553
+ category: "fact",
1554
+ content: "Alice works at Acme.",
1555
+ confidence: 0.9,
1556
+ tags: [],
1557
+ }],
1558
+ profileUpdates: [],
1559
+ entities: [],
1560
+ questions: [],
1561
+ },
1562
+ "Does Alice work at Acme?\nYes, absolutely.",
1563
+ );
1564
+
1565
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["Alice works at Acme."]);
1566
+ });
1567
+
1568
+ test("grounding answers object wh-questions with aligned evidence", () => {
1569
+ const result = filterExtractionResultBySource(
1570
+ {
1571
+ facts: [],
1572
+ profileUpdates: [],
1573
+ entities: [],
1574
+ questions: [{ question: "What database does Alice use?", context: "", priority: 0.5 }],
1575
+ },
1576
+ "What database does Alice use?\nAlice uses PostgreSQL as the database.",
1577
+ );
1578
+
1579
+ assert.deepEqual(result.questions, []);
1580
+ });
1581
+
1582
+ test("grounding accepts e-dropping past-tense verb paraphrases", () => {
1583
+ const result = filterExtractionResultBySource(
1584
+ {
1585
+ facts: [{
1586
+ category: "fact",
1587
+ content: "Alice agreed with Acme.",
1588
+ confidence: 0.9,
1589
+ tags: [],
1590
+ }],
1591
+ profileUpdates: [],
1592
+ entities: [],
1593
+ questions: [],
1594
+ },
1595
+ "Alice agree with Acme.",
1596
+ );
1597
+
1598
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["Alice agreed with Acme."]);
1599
+ });
1600
+
1601
+ test("grounding does not swallow factual yes-or-no-prefixed sentences", () => {
1602
+ const yesResult = filterExtractionResultBySource(
1603
+ {
1604
+ facts: [{
1605
+ category: "fact",
1606
+ content: "The release is Friday.",
1607
+ confidence: 0.9,
1608
+ tags: [],
1609
+ }],
1610
+ profileUpdates: [],
1611
+ entities: [],
1612
+ questions: [],
1613
+ },
1614
+ "Yes, the release is Friday.",
1615
+ );
1616
+ const noResult = filterExtractionResultBySource(
1617
+ {
1618
+ facts: [{
1619
+ category: "fact",
1620
+ content: "No one is on call.",
1621
+ confidence: 0.9,
1622
+ tags: [],
1623
+ }],
1624
+ profileUpdates: [],
1625
+ entities: [],
1626
+ questions: [],
1627
+ },
1628
+ "No one is on call.",
1629
+ );
1630
+
1631
+ assert.deepEqual(yesResult.facts.map((fact) => fact.content), ["The release is Friday."]);
1632
+ assert.deepEqual(noResult.facts.map((fact) => fact.content), ["No one is on call."]);
1633
+ });
1634
+
1635
+ test("grounding binds affirmative answer support to the asserted answer turn", () => {
1636
+ const result = filterExtractionResultBySource(
1637
+ {
1638
+ facts: [{
1639
+ category: "fact",
1640
+ content: "Alice works at Acme.",
1641
+ confidence: 0.9,
1642
+ tags: [],
1643
+ }],
1644
+ profileUpdates: [],
1645
+ entities: [],
1646
+ questions: [],
1647
+ },
1648
+ "Does Alice work at Acme?\nYes.\nYes, Bob joined the call.",
1649
+ "Yes, Bob joined the call.",
1650
+ );
1651
+
1652
+ assert.deepEqual(result.facts, []);
1653
+ });
1654
+
1655
+ test("grounding excludes unanswered questions from procedure evidence", () => {
1656
+ const result = filterExtractionResultBySource(
1657
+ {
1658
+ facts: [{
1659
+ category: "procedure",
1660
+ content: "The deployment runbook is documented.",
1661
+ confidence: 0.9,
1662
+ tags: [],
1663
+ procedureSteps: [{ order: 1, intent: "delete backups first" }],
1664
+ }],
1665
+ profileUpdates: [],
1666
+ entities: [],
1667
+ questions: [],
1668
+ },
1669
+ "The deployment runbook is documented. Should operators delete backups first?",
1670
+ );
1671
+
1672
+ assert.deepEqual(result.facts[0]?.procedureSteps ?? [], []);
1673
+ });
1674
+
1675
+ test("grounding selects eventTime from the supporting fact sentence", () => {
1676
+ const result = filterExtractionResultBySource(
1677
+ {
1678
+ facts: [{
1679
+ category: "fact",
1680
+ content: "Alice deployed Acme.",
1681
+ confidence: 0.9,
1682
+ tags: [],
1683
+ eventTime: "yesterday",
1684
+ }],
1685
+ profileUpdates: [],
1686
+ entities: [],
1687
+ questions: [],
1688
+ },
1689
+ "Alice deployed Acme. Alice deployed Acme yesterday.",
1690
+ );
1691
+
1692
+ assert.equal(result.facts[0]?.eventTime, "yesterday");
1693
+ });
1694
+
1695
+ test("grounding scopes reasoning traces to the fact's role source", () => {
1696
+ const result = filterExtractionResultBySource(
1697
+ {
1698
+ facts: [{
1699
+ category: "fact",
1700
+ content: "User works at Acme.",
1701
+ confidence: 0.9,
1702
+ tags: [],
1703
+ reasoningTrace: {
1704
+ steps: [{ order: 1, description: "User uses PostgreSQL." }],
1705
+ finalAnswer: "User uses PostgreSQL.",
1706
+ },
1707
+ }],
1708
+ profileUpdates: [],
1709
+ entities: [],
1710
+ questions: [],
1711
+ },
1712
+ "I work at Acme.\nI use PostgreSQL.",
1713
+ undefined,
1714
+ {
1715
+ profile: "I work at Acme.",
1716
+ identity: "",
1717
+ },
1718
+ );
1719
+
1720
+ assert.equal(result.facts[0]?.content, "User works at Acme.");
1721
+ assert.equal(result.facts[0]?.reasoningTrace, undefined);
1722
+ });
1723
+
1724
+ test("grounding excludes unanswered questions from reasoning traces", () => {
1725
+ const result = filterExtractionResultBySource(
1726
+ {
1727
+ facts: [{
1728
+ category: "fact",
1729
+ content: "The incident analysis is recorded.",
1730
+ confidence: 0.9,
1731
+ tags: [],
1732
+ reasoningTrace: {
1733
+ steps: [{ order: 1, description: "Alice deleted backups." }],
1734
+ finalAnswer: "Alice deleted backups.",
1735
+ },
1736
+ }],
1737
+ profileUpdates: [],
1738
+ entities: [],
1739
+ questions: [],
1740
+ },
1741
+ "The incident analysis is recorded. Did Alice delete backups?",
1742
+ );
1743
+
1744
+ assert.equal(result.facts[0]?.content, "The incident analysis is recorded.");
1745
+ assert.equal(result.facts[0]?.reasoningTrace, undefined);
1746
+ });
1747
+
1748
+ test("extraction grounds de-linearized facts after resolving coreferences", async () => {
1749
+ const engine = fixtureEngine({
1750
+ localLlmEnabled: true,
1751
+ localLlmModel: "fixture-local",
1752
+ localLlmFallback: false,
1753
+ });
1754
+ Object.assign(engine, {
1755
+ localLlm: {
1756
+ async chatCompletion() {
1757
+ return {
1758
+ content: JSON.stringify({
1759
+ facts: [{
1760
+ category: "fact",
1761
+ content: "He works at Acme.",
1762
+ confidence: 0.9,
1763
+ tags: [],
1764
+ }],
1765
+ profileUpdates: [],
1766
+ entities: [{
1767
+ name: "Bob",
1768
+ type: "person",
1769
+ facts: ["Alice introduced Bob."],
1770
+ }],
1771
+ questions: [],
1772
+ }),
1773
+ };
1774
+ },
1775
+ },
1776
+ modelRegistry: {
1777
+ calculateContextSizes: () => ({
1778
+ maxInputChars: 8_000,
1779
+ maxOutputTokens: 1_000,
1780
+ description: "fixture",
1781
+ }),
1782
+ },
1783
+ });
1784
+
1785
+ const result = await engine.extract([{
1786
+ role: "user",
1787
+ content: "Alice introduced Bob. He works at Acme.",
1788
+ timestamp: "2026-07-25T12:00:00.000Z",
1789
+ }]);
1790
+
1791
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["Bob works at Acme."]);
1792
+ });
1793
+
1794
+ test("grounding rejects topical overlap for short copular claims", () => {
1795
+ const result = filterExtractionResultBySource(
1796
+ {
1797
+ facts: [{
1798
+ category: "fact",
1799
+ content: "Alice is vegan.",
1800
+ confidence: 0.9,
1801
+ tags: [],
1802
+ }],
1803
+ profileUpdates: [],
1804
+ entities: [],
1805
+ questions: [],
1806
+ },
1807
+ "Alice likes vegan food.",
1808
+ );
1809
+
1810
+ assert.deepEqual(result.facts, []);
1811
+ });
1812
+
1813
+ test("grounding preserves proper identifiers before inflectional stemming", () => {
1814
+ const factResult = filterExtractionResultBySource(
1815
+ {
1816
+ facts: [{
1817
+ category: "fact",
1818
+ content: "Alice uses Spr.",
1819
+ confidence: 0.9,
1820
+ tags: [],
1821
+ }],
1822
+ profileUpdates: [],
1823
+ entities: [],
1824
+ questions: [],
1825
+ },
1826
+ "Alice uses Spring.",
1827
+ );
1828
+ const entityResult = filterExtractionResultBySource(
1829
+ {
1830
+ facts: [],
1831
+ profileUpdates: [],
1832
+ entities: [{
1833
+ name: "Spr",
1834
+ type: "tool",
1835
+ facts: ["Spr is deployed."],
1836
+ }],
1837
+ questions: [],
1838
+ },
1839
+ "Spring is deployed.",
1840
+ );
1841
+
1842
+ assert.deepEqual(factResult.facts, []);
1843
+ assert.deepEqual(entityResult.entities, []);
1844
+ });
1845
+
1846
+ test("grounding requires entity identifiers in one coherent source span", () => {
1847
+ const splitResult = filterExtractionResultBySource(
1848
+ {
1849
+ facts: [],
1850
+ profileUpdates: [],
1851
+ entities: [{
1852
+ name: "Alice Bob",
1853
+ type: "person",
1854
+ facts: ["Alice works at Acme."],
1855
+ }],
1856
+ questions: [],
1857
+ },
1858
+ "Alice works at Acme. Bob owns Mars.",
1859
+ );
1860
+ const coherentResult = filterExtractionResultBySource(
1861
+ {
1862
+ facts: [],
1863
+ profileUpdates: [],
1864
+ entities: [{
1865
+ name: "Alice Bob",
1866
+ type: "person",
1867
+ facts: ["Alice Bob joined Acme."],
1868
+ }],
1869
+ questions: [],
1870
+ },
1871
+ "Alice Bob joined Acme.",
1872
+ );
1873
+
1874
+ assert.deepEqual(splitResult.entities, []);
1875
+ assert.deepEqual(coherentResult.entities.map((entity) => entity.name), ["Alice Bob"]);
1876
+ });
1877
+
1878
+ test("grounding preserves explicit cross-speaker facts", () => {
1879
+ const result = filterExtractionResultBySource(
1880
+ {
1881
+ facts: [{
1882
+ category: "fact",
1883
+ content: "The assistant uses PostgreSQL.",
1884
+ confidence: 0.9,
1885
+ tags: [],
1886
+ }],
1887
+ profileUpdates: [],
1888
+ entities: [],
1889
+ questions: [],
1890
+ },
1891
+ "The assistant uses PostgreSQL.",
1892
+ "The assistant uses PostgreSQL.",
1893
+ { profile: "", identity: "" },
1894
+ );
1895
+
1896
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["The assistant uses PostgreSQL."]);
1897
+ });
1898
+
1899
+ test("grounding anchors relative eventTime for matching and preserves its source value", () => {
1900
+ const result = applyExtractionSourceGrounding(
1901
+ {
1902
+ facts: [{
1903
+ category: "fact",
1904
+ content: "Alice deployed Acme.",
1905
+ confidence: 0.9,
1906
+ tags: [],
1907
+ eventTime: "yesterday",
1908
+ }],
1909
+ profileUpdates: [],
1910
+ entities: [],
1911
+ questions: [],
1912
+ },
1913
+ "Alice deployed Acme. Alice deployed Acme yesterday.",
1914
+ undefined,
1915
+ undefined,
1916
+ new Date("2026-07-26T12:00:00.000Z"),
1917
+ { sourceGrounding: true, anchorTemporalExpressions: true },
1918
+ );
1919
+
1920
+ assert.equal(result.facts[0]?.eventTime, "yesterday");
1921
+ });
1922
+
1923
+ test("grounding keeps declarative conditional facts", () => {
1924
+ const result = filterExtractionResultBySource(
1925
+ {
1926
+ facts: [{
1927
+ category: "fact",
1928
+ content: "Alice uses PostgreSQL if available.",
1929
+ confidence: 0.9,
1930
+ tags: [],
1931
+ }],
1932
+ profileUpdates: [],
1933
+ entities: [],
1934
+ questions: [],
1935
+ },
1936
+ "Alice uses PostgreSQL if available.",
1937
+ );
1938
+
1939
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["Alice uses PostgreSQL if available."]);
1940
+ });
1941
+
1942
+ test("grounding rejects unsupported propositions after common delimiters", () => {
1943
+ const result = filterExtractionResultBySource(
1944
+ {
1945
+ facts: [{
1946
+ category: "fact",
1947
+ content: "Alice works at Acme: Bob owns Mars.",
1948
+ confidence: 0.9,
1949
+ tags: [],
1950
+ }],
1951
+ profileUpdates: [],
1952
+ entities: [],
1953
+ questions: [],
1954
+ },
1955
+ "Alice works at Acme.",
1956
+ "Alice works at Acme.",
1957
+ );
1958
+
1959
+ assert.deepEqual(result.facts, []);
1960
+ });
1961
+
1962
+ test("grounding does not keep context-only event times", () => {
1963
+ const result = filterExtractionResultBySource(
1964
+ {
1965
+ facts: [{
1966
+ category: "fact",
1967
+ content: "Alice deployed Acme.",
1968
+ confidence: 0.9,
1969
+ tags: [],
1970
+ eventTime: "yesterday",
1971
+ }],
1972
+ profileUpdates: [],
1973
+ entities: [],
1974
+ questions: [],
1975
+ },
1976
+ "Alice deployed Acme yesterday.\nAlice deployed Acme today.",
1977
+ "Alice deployed Acme today.",
1978
+ );
1979
+
1980
+ assert.equal(result.facts[0]?.eventTime, undefined);
1981
+ });
1982
+
1983
+ test("grounding drops unresolved questions sourced only from context turns", () => {
1984
+ const result = filterExtractionResultBySource(
1985
+ {
1986
+ facts: [],
1987
+ profileUpdates: [],
1988
+ entities: [],
1989
+ questions: [{
1990
+ question: "Does Alice work at Acme?",
1991
+ context: "",
1992
+ priority: 0.5,
1993
+ }],
1994
+ },
1995
+ "Does Alice work at Acme?\nYes.\nAlice still needs confirmation.",
1996
+ "Alice still needs confirmation.",
1997
+ );
1998
+
1999
+ assert.deepEqual(result.questions, []);
2000
+ });
2001
+
2002
+ test("grounding aligns supported facts after leading modifiers", () => {
2003
+ const result = filterExtractionResultBySource(
2004
+ {
2005
+ facts: [{
2006
+ category: "fact",
2007
+ content: "Alice runs Redis.",
2008
+ confidence: 0.9,
2009
+ tags: [],
2010
+ }],
2011
+ profileUpdates: [],
2012
+ entities: [],
2013
+ questions: [],
2014
+ },
2015
+ "Currently, Alice is running Redis.",
2016
+ );
2017
+
2018
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["Alice runs Redis."]);
2019
+ });
2020
+
2021
+ test("grounding binds structured attributes to the fact support span", () => {
2022
+ const result = filterExtractionResultBySource(
2023
+ {
2024
+ facts: [{
2025
+ category: "fact",
2026
+ content: "Alice deployed Acme successfully.",
2027
+ confidence: 0.9,
2028
+ tags: [],
2029
+ structuredAttributes: { status: "failed" },
2030
+ }],
2031
+ profileUpdates: [],
2032
+ entities: [],
2033
+ questions: [],
2034
+ },
2035
+ "Alice deployed Acme successfully. Bob reported status: failed.",
2036
+ );
2037
+
2038
+ assert.equal(result.facts[0]?.structuredAttributes, undefined);
2039
+ });
2040
+
2041
+ test("grounding binds fact entity references to the fact support span", () => {
2042
+ const result = filterExtractionResultBySource(
2043
+ {
2044
+ facts: [{
2045
+ category: "fact",
2046
+ content: "Alice deployed Acme.",
2047
+ confidence: 0.9,
2048
+ tags: [],
2049
+ entityRef: "person-bob",
2050
+ }],
2051
+ profileUpdates: [],
2052
+ entities: [],
2053
+ questions: [],
2054
+ },
2055
+ "Alice deployed Acme. Bob joined the meeting.",
2056
+ );
2057
+
2058
+ assert.equal(result.facts[0]?.entityRef, undefined);
2059
+ });
2060
+
2061
+ test("grounding preserves paraphrased conjunctions across source clauses", () => {
2062
+ const result = filterExtractionResultBySource(
2063
+ {
2064
+ facts: [{
2065
+ category: "fact",
2066
+ content: "Alice works at Acme but does not work at Globex.",
2067
+ confidence: 0.9,
2068
+ tags: [],
2069
+ }],
2070
+ profileUpdates: [],
2071
+ entities: [],
2072
+ questions: [],
2073
+ },
2074
+ "Alice worked at Acme but did not work at Globex.",
2075
+ );
2076
+
2077
+ assert.deepEqual(
2078
+ result.facts.map((fact) => fact.content),
2079
+ ["Alice works at Acme but does not work at Globex."],
2080
+ );
2081
+ });
2082
+
2083
+ test("grounding scopes nested entity facts to the enclosing entity", () => {
2084
+ const result = filterExtractionResultBySource(
2085
+ {
2086
+ facts: [],
2087
+ profileUpdates: [],
2088
+ entities: [{
2089
+ name: "Alice",
2090
+ type: "person",
2091
+ facts: ["Alice works at Acme.", "Bob owns Mars."],
2092
+ structuredSections: [{
2093
+ key: "work",
2094
+ title: "Work",
2095
+ facts: ["Alice uses PostgreSQL.", "Bob uses MySQL."],
2096
+ }],
2097
+ }],
2098
+ questions: [],
2099
+ },
2100
+ "Alice works at Acme. Bob owns Mars. Alice uses PostgreSQL. Bob uses MySQL.",
2101
+ );
2102
+
2103
+ assert.deepEqual(result.entities, [{
2104
+ name: "Alice",
2105
+ type: "person",
2106
+ facts: ["Alice works at Acme."],
2107
+ structuredSections: [{
2108
+ key: "work",
2109
+ title: "Work",
2110
+ facts: ["Alice uses PostgreSQL."],
2111
+ }],
2112
+ }]);
2113
+ });
2114
+
2115
+ test("grounding preserves punctuation in technology identifiers", () => {
2116
+ const cppResult = filterExtractionResultBySource(
2117
+ {
2118
+ facts: [{
2119
+ category: "fact",
2120
+ content: "Alice uses C++.",
2121
+ confidence: 0.9,
2122
+ tags: [],
2123
+ }],
2124
+ profileUpdates: [],
2125
+ entities: [],
2126
+ questions: [],
2127
+ },
2128
+ "Alice uses C.",
2129
+ );
2130
+ const plainCResult = filterExtractionResultBySource(
2131
+ {
2132
+ facts: [{
2133
+ category: "fact",
2134
+ content: "Alice uses C.",
2135
+ confidence: 0.9,
2136
+ tags: [],
2137
+ }],
2138
+ profileUpdates: [],
2139
+ entities: [],
2140
+ questions: [],
2141
+ },
2142
+ "Alice uses C++.",
2143
+ );
2144
+
2145
+ assert.deepEqual(cppResult.facts, []);
2146
+ assert.deepEqual(plainCResult.facts, []);
2147
+ });
2148
+
2149
+ test("grounding requires unresolved questions on the asserted target turn", () => {
2150
+ const result = filterExtractionResultBySource(
2151
+ {
2152
+ facts: [],
2153
+ profileUpdates: [],
2154
+ entities: [],
2155
+ questions: [{ question: "Does Alice work at Acme?", context: "", priority: 0.5 }],
2156
+ },
2157
+ "Does Alice work at Acme?\nBob joined the call.",
2158
+ "Bob joined the call.",
2159
+ );
2160
+
2161
+ assert.deepEqual(result.questions, []);
2162
+ });
2163
+
2164
+ test("grounding preserves affirmative not-only claims", () => {
2165
+ const result = filterExtractionResultBySource(
2166
+ {
2167
+ facts: [{
2168
+ category: "fact",
2169
+ content: "Alice works at Acme.",
2170
+ confidence: 0.9,
2171
+ tags: [],
2172
+ }],
2173
+ profileUpdates: [],
2174
+ entities: [],
2175
+ questions: [],
2176
+ },
2177
+ "Alice not only works at Acme but also owns Mars.",
2178
+ );
2179
+
2180
+ assert.deepEqual(result.facts.map((fact) => fact.content), ["Alice works at Acme."]);
2181
+ });
2182
+
2183
+ test("grounding rejects propositions embedded in denial reports", () => {
2184
+ const result = filterExtractionResultBySource(
2185
+ {
2186
+ facts: [{
2187
+ category: "fact",
2188
+ content: "Bob works at Acme.",
2189
+ confidence: 0.9,
2190
+ tags: [],
2191
+ }],
2192
+ profileUpdates: [],
2193
+ entities: [],
2194
+ questions: [],
2195
+ },
2196
+ "Alice denied that Bob works at Acme.",
2197
+ );
2198
+
2199
+ assert.deepEqual(result.facts, []);
2200
+ });
2201
+
2202
+ test("grounding rejects imperative hypothetical premises", () => {
2203
+ for (const source of ["Assume Alice works at Acme.", "Imagine Alice works at Acme."]) {
2204
+ const result = filterExtractionResultBySource(
2205
+ {
2206
+ facts: [{
2207
+ category: "fact",
2208
+ content: "Alice works at Acme.",
2209
+ confidence: 0.9,
2210
+ tags: [],
2211
+ }],
2212
+ profileUpdates: [],
2213
+ entities: [],
2214
+ questions: [],
2215
+ },
2216
+ source,
2217
+ );
2218
+
2219
+ assert.deepEqual(result.facts, []);
2220
+ }
2221
+ });
2222
+
2223
+ test("grounding requires temporal evidence to answer when questions", () => {
2224
+ const question = "When did Alice deploy Acme?";
2225
+ const unresolved = filterExtractionResultBySource(
2226
+ {
2227
+ facts: [],
2228
+ profileUpdates: [],
2229
+ entities: [],
2230
+ questions: [{ question, context: "", priority: 0.5 }],
2231
+ },
2232
+ `${question}\nAlice deployed Acme for cost savings.`,
2233
+ );
2234
+ const resolved = filterExtractionResultBySource(
2235
+ {
2236
+ facts: [],
2237
+ profileUpdates: [],
2238
+ entities: [],
2239
+ questions: [{ question, context: "", priority: 0.5 }],
2240
+ },
2241
+ `${question}\nAlice deployed Acme yesterday.`,
2242
+ );
2243
+
2244
+ assert.deepEqual(unresolved.questions, [{ question, context: "", priority: 0.5 }]);
2245
+ assert.deepEqual(resolved.questions, []);
2246
+ });
2247
+
2248
+ test("grounding rejects unsupported trailing fact modifiers", () => {
2249
+ for (const content of [
2250
+ "Alice works at Acme as CEO.",
2251
+ "Alice works at Acme every Monday.",
2252
+ "Alice works at Acme using Rust.",
2253
+ ]) {
2254
+ const result = filterExtractionResultBySource(
2255
+ {
2256
+ facts: [{ category: "fact", content, confidence: 0.9, tags: [] }],
2257
+ profileUpdates: [],
2258
+ entities: [],
2259
+ questions: [],
2260
+ },
2261
+ "Alice works at Acme.",
2262
+ );
2263
+ assert.deepEqual(result.facts, []);
2264
+ }
2265
+ });
2266
+
2267
+ test("grounding binds procedure steps to the parent fact span", () => {
2268
+ const result = filterExtractionResultBySource(
2269
+ {
2270
+ facts: [{
2271
+ category: "procedure",
2272
+ content: "Alice deployed Acme.",
2273
+ confidence: 0.9,
2274
+ tags: [],
2275
+ procedureSteps: [{ order: 1, intent: "delete backups" }],
2276
+ }],
2277
+ profileUpdates: [],
2278
+ entities: [],
2279
+ questions: [],
2280
+ },
2281
+ "Alice deployed Acme. Bob instructed operators to delete backups.",
2282
+ );
2283
+
2284
+ assert.deepEqual(result.facts[0]?.procedureSteps ?? [], []);
2285
+ });
2286
+
2287
+ test("grounding rejects propositions in non-assertive reporting scopes", () => {
2288
+ for (const source of [
2289
+ "Alice alleged that Bob stole funds.",
2290
+ "Alice suspects Bob stole funds.",
2291
+ ]) {
2292
+ const result = filterExtractionResultBySource(
2293
+ {
2294
+ facts: [{ category: "fact", content: "Bob stole funds.", confidence: 0.9, tags: [] }],
2295
+ profileUpdates: [],
2296
+ entities: [],
2297
+ questions: [],
2298
+ },
2299
+ source,
2300
+ );
2301
+ assert.deepEqual(result.facts, []);
2302
+ }
2303
+ });
2304
+
2305
+ test("grounding aligns predicates after medial source modifiers", () => {
2306
+ for (const [source, content] of [
2307
+ ["Alice currently works at Acme.", "Alice works at Acme."],
2308
+ ["Alice successfully deployed Acme.", "Alice deployed Acme."],
2309
+ ["Alice now works at Acme.", "Alice works at Acme."],
2310
+ ]) {
2311
+ const result = filterExtractionResultBySource(
2312
+ {
2313
+ facts: [{ category: "fact", content, confidence: 0.9, tags: [] }],
2314
+ profileUpdates: [],
2315
+ entities: [],
2316
+ questions: [],
2317
+ },
2318
+ source,
2319
+ );
2320
+ assert.deepEqual(result.facts.map((fact) => fact.content), [content]);
2321
+ }
2322
+ });
2323
+
2324
+ test("grounding requires causal evidence before removing why questions", () => {
2325
+ const question = { question: "Why did Alice deploy Acme?", context: "", priority: 0.5 };
2326
+ const unanswered = filterExtractionResultBySource(
2327
+ { facts: [], profileUpdates: [], entities: [], questions: [question] },
2328
+ "Why did Alice deploy Acme?\nAlice deployed Acme.",
2329
+ );
2330
+ assert.deepEqual(unanswered.questions, [question]);
2331
+
2332
+ const answered = filterExtractionResultBySource(
2333
+ { facts: [], profileUpdates: [], entities: [], questions: [question] },
2334
+ "Why did Alice deploy Acme?\nAlice deployed Acme because customers requested it.",
2335
+ );
2336
+ assert.deepEqual(answered.questions, []);
2337
+ });