@remnic/core 9.3.701 → 9.3.702

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/dist/access-boundary.d.ts +7 -7
  2. package/dist/access-boundary.js +15 -12
  3. package/dist/access-cli.js +36 -33
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +7 -7
  6. package/dist/access-http.js +18 -15
  7. package/dist/access-mcp.d.ts +7 -7
  8. package/dist/access-mcp.js +17 -14
  9. package/dist/access-operations.d.ts +7 -7
  10. package/dist/access-operations.js +16 -13
  11. package/dist/{access-service-CGVWK6lZ.d.ts → access-service-COCzhgEL.d.ts} +397 -19
  12. package/dist/access-service.d.ts +6 -6
  13. package/dist/access-service.js +14 -11
  14. package/dist/access-surface-catalog.d.ts +7 -7
  15. package/dist/action-confidence.d.ts +1 -1
  16. package/dist/active-memory-bridge.d.ts +1 -1
  17. package/dist/active-memory-bridge.js +2 -2
  18. package/dist/active-recall.d.ts +1 -1
  19. package/dist/active-recall.js +7 -6
  20. package/dist/active-recall.js.map +1 -1
  21. package/dist/behavior-learner.d.ts +1 -1
  22. package/dist/behavior-signals.d.ts +1 -1
  23. package/dist/bootstrap.d.ts +5 -5
  24. package/dist/briefing.d.ts +1 -1
  25. package/dist/briefing.js +6 -5
  26. package/dist/buffer-surprise-report.d.ts +1 -1
  27. package/dist/buffer.d.ts +1 -1
  28. package/dist/calibration.d.ts +1 -1
  29. package/dist/capabilities.d.ts +1 -1
  30. package/dist/{catalog-DBIghceA.d.ts → catalog-DxCzhjE6.d.ts} +2 -2
  31. package/dist/causal-behavior.d.ts +1 -1
  32. package/dist/causal-consolidation.d.ts +1 -1
  33. package/dist/causal-consolidation.js +7 -6
  34. package/dist/causal-consolidation.js.map +1 -1
  35. package/dist/{chunk-YXLT4EMM.js → chunk-4463LUIE.js} +12 -2
  36. package/dist/chunk-4463LUIE.js.map +1 -0
  37. package/dist/{chunk-R5DB26G6.js → chunk-4SBW47WK.js} +9 -23
  38. package/dist/chunk-4SBW47WK.js.map +1 -0
  39. package/dist/{chunk-KF4TXW7Z.js → chunk-5VXVXJH6.js} +149 -6
  40. package/dist/{chunk-KF4TXW7Z.js.map → chunk-5VXVXJH6.js.map} +1 -1
  41. package/dist/{chunk-ODTWHSY2.js → chunk-64XXOQRW.js} +2 -2
  42. package/dist/{chunk-ZT7B64BE.js → chunk-6TAETM63.js} +2 -2
  43. package/dist/{chunk-HF4N43Q7.js → chunk-6VVP6NK7.js} +2 -2
  44. package/dist/{chunk-FN2SM5SN.js → chunk-ABCGDW3A.js} +75 -12
  45. package/dist/chunk-ABCGDW3A.js.map +1 -0
  46. package/dist/{chunk-7TAQEPLE.js → chunk-AMNLZ6SF.js} +2 -2
  47. package/dist/{chunk-KS7WQ4BZ.js → chunk-CZJ6QSKG.js} +3 -3
  48. package/dist/{chunk-27LQPUMZ.js → chunk-EOOVJK2U.js} +3 -3
  49. package/dist/{chunk-QP37KL5H.js → chunk-EYJD6KIO.js} +2 -2
  50. package/dist/{chunk-WFEZUGU5.js → chunk-EZR35XHX.js} +2 -2
  51. package/dist/{chunk-UU6MVCJ6.js → chunk-FIOYURII.js} +32 -25
  52. package/dist/chunk-FIOYURII.js.map +1 -0
  53. package/dist/{chunk-JO3E5VGS.js → chunk-FVI5B7DE.js} +2 -2
  54. package/dist/{chunk-SFMRLXIV.js → chunk-GFIARMA7.js} +15 -7
  55. package/dist/chunk-GFIARMA7.js.map +1 -0
  56. package/dist/{chunk-PONNZ54D.js → chunk-GY3SKOS4.js} +4 -4
  57. package/dist/{chunk-U33LWTQQ.js → chunk-HV57RHMD.js} +4 -4
  58. package/dist/{chunk-IKNQAGBV.js → chunk-IX3UQT4H.js} +1 -1
  59. package/dist/{chunk-IKNQAGBV.js.map → chunk-IX3UQT4H.js.map} +1 -1
  60. package/dist/{chunk-IYOPIG3E.js → chunk-IX72AAMZ.js} +2 -2
  61. package/dist/{chunk-JKOKX3PS.js → chunk-JMA4RYRN.js} +2 -2
  62. package/dist/{chunk-DEDQXIDL.js → chunk-JTKFZMZ7.js} +2 -2
  63. package/dist/{chunk-OLOYQZFB.js → chunk-KC6TCAWV.js} +5 -5
  64. package/dist/{chunk-ROZJACKP.js → chunk-KKK7YTYN.js} +4 -1
  65. package/dist/chunk-KKK7YTYN.js.map +1 -0
  66. package/dist/{chunk-EC2AYKRX.js → chunk-L6W77GWW.js} +10 -24
  67. package/dist/chunk-L6W77GWW.js.map +1 -0
  68. package/dist/{chunk-ED35D32I.js → chunk-LQ4J7ELC.js} +2 -2
  69. package/dist/chunk-LUPVCGYK.js +63 -0
  70. package/dist/chunk-LUPVCGYK.js.map +1 -0
  71. package/dist/{chunk-TFVVONWD.js → chunk-MBUM2Y3L.js} +2 -2
  72. package/dist/{chunk-T5QAZIBO.js → chunk-MOXFPLD6.js} +3 -3
  73. package/dist/{chunk-XTIRCSIH.js → chunk-MXEWQKM7.js} +2 -2
  74. package/dist/{chunk-ZYNMX6IU.js → chunk-NUIJEGVD.js} +6 -6
  75. package/dist/{chunk-FUCJAZ25.js → chunk-OXEAMU42.js} +529 -9
  76. package/dist/chunk-OXEAMU42.js.map +1 -0
  77. package/dist/{chunk-YO4MBK3I.js → chunk-QGJAGC2J.js} +2 -2
  78. package/dist/{chunk-ISLJ5WIM.js → chunk-QIMFOCSH.js} +2 -2
  79. package/dist/{chunk-IJEZMWKA.js → chunk-SFOAQQDJ.js} +3 -3
  80. package/dist/{chunk-HRUULBBV.js → chunk-SHRRWOVY.js} +81 -4
  81. package/dist/chunk-SHRRWOVY.js.map +1 -0
  82. package/dist/{chunk-NINRTFSV.js → chunk-SINGJCUR.js} +5 -5
  83. package/dist/{chunk-ZPQVJEVQ.js → chunk-SK2CR6MW.js} +146 -2
  84. package/dist/chunk-SK2CR6MW.js.map +1 -0
  85. package/dist/{chunk-6W2D6FGG.js → chunk-TGAHHCB6.js} +2 -2
  86. package/dist/{chunk-D75JXBV4.js → chunk-WN4GHSDH.js} +2 -2
  87. package/dist/{chunk-K4DWSPMW.js → chunk-WRGPE6AW.js} +2 -2
  88. package/dist/{chunk-4HIAWLA2.js → chunk-XGMCUY5P.js} +60 -114
  89. package/dist/chunk-XGMCUY5P.js.map +1 -0
  90. package/dist/{chunk-SDPDU2PM.js → chunk-YTMDF6S7.js} +2 -2
  91. package/dist/{chunk-MNU5G4TK.js → chunk-Z7XEIAV4.js} +2 -2
  92. package/dist/{chunk-HXHKLVAS.js → chunk-ZCEI242W.js} +23 -23
  93. package/dist/{cli-D3XeenwN.d.ts → cli-BM4xQPp4.d.ts} +3 -3
  94. package/dist/cli.d.ts +7 -7
  95. package/dist/cli.js +33 -32
  96. package/dist/compounding/engine.d.ts +1 -1
  97. package/dist/compounding/engine.js +6 -5
  98. package/dist/compounding/preference-consolidator.d.ts +1 -1
  99. package/dist/compression-optimizer.d.ts +1 -1
  100. package/dist/config.d.ts +1 -1
  101. package/dist/config.js +4 -2
  102. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  103. package/dist/connectors/codex-materialize-runner.js +6 -5
  104. package/dist/connectors/codex-materialize.d.ts +1 -1
  105. package/dist/connectors/index.d.ts +1 -1
  106. package/dist/connectors/index.js +7 -6
  107. package/dist/consolidation-provenance-check.d.ts +1 -1
  108. package/dist/consolidation-undo.d.ts +1 -1
  109. package/dist/contradiction/index.d.ts +2 -2
  110. package/dist/conversation-index/backend.d.ts +1 -1
  111. package/dist/conversation-index/chunker.d.ts +1 -1
  112. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  113. package/dist/conversation-index/indexer.d.ts +1 -1
  114. package/dist/conversation-index/search.d.ts +1 -1
  115. package/dist/day-summary.d.ts +1 -1
  116. package/dist/delinearize.d.ts +1 -1
  117. package/dist/direct-answer-wiring.d.ts +1 -1
  118. package/dist/direct-answer.d.ts +1 -1
  119. package/dist/embedding-fallback.d.ts +1 -1
  120. package/dist/enrichment/index.d.ts +1 -1
  121. package/dist/entity-retrieval.d.ts +1 -1
  122. package/dist/entity-retrieval.js +6 -5
  123. package/dist/entity-schema.d.ts +1 -1
  124. package/dist/event-order-recall.js +2 -1
  125. package/dist/explicit-capture.d.ts +5 -5
  126. package/dist/explicit-capture.js +2 -2
  127. package/dist/explicit-cue-recall.js +2 -1
  128. package/dist/extraction-faithfulness.d.ts +1 -1
  129. package/dist/extraction-judge-telemetry.d.ts +1 -1
  130. package/dist/extraction-judge-training.d.ts +1 -1
  131. package/dist/extraction-judge.d.ts +1 -1
  132. package/dist/extraction.d.ts +14 -1
  133. package/dist/extraction.js +5 -3
  134. package/dist/fallback-llm.d.ts +1 -1
  135. package/dist/identity-continuity.d.ts +1 -1
  136. package/dist/importance.d.ts +1 -1
  137. package/dist/index.d.ts +123 -123
  138. package/dist/index.js +53 -52
  139. package/dist/index.js.map +1 -1
  140. package/dist/intent.d.ts +1 -1
  141. package/dist/lcm/engine.d.ts +1 -1
  142. package/dist/lcm/index.d.ts +1 -1
  143. package/dist/lcm/tools.d.ts +1 -1
  144. package/dist/lifecycle.d.ts +1 -1
  145. package/dist/live-connectors-runner.d.ts +1 -1
  146. package/dist/local-llm.d.ts +1 -1
  147. package/dist/maintenance/memory-governance.d.ts +1 -1
  148. package/dist/maintenance/memory-governance.js +6 -5
  149. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +6 -5
  150. package/dist/maintenance/rebuild-memory-projection.js +7 -6
  151. package/dist/mcp-memory-inspector-app.d.ts +7 -7
  152. package/dist/memory-action-policy.d.ts +1 -1
  153. package/dist/memory-cache.d.ts +1 -1
  154. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  155. package/dist/memory-projection-store.d.ts +1 -1
  156. package/dist/memory-provenance.d.ts +1 -1
  157. package/dist/memory-worth-outcomes.d.ts +1 -1
  158. package/dist/models-json.d.ts +1 -1
  159. package/dist/namespaces/migrate.d.ts +2 -2
  160. package/dist/namespaces/migrate.js +7 -6
  161. package/dist/namespaces/principal.d.ts +1 -1
  162. package/dist/namespaces/search.d.ts +1 -1
  163. package/dist/namespaces/storage.d.ts +2 -2
  164. package/dist/namespaces/storage.js +6 -5
  165. package/dist/native-knowledge.d.ts +1 -1
  166. package/dist/operator-toolkit.d.ts +1 -1
  167. package/dist/operator-toolkit.js +12 -11
  168. package/dist/orchestration/maintenance.d.ts +2 -2
  169. package/dist/orchestration/maintenance.js +8 -7
  170. package/dist/{orchestrator-BzMCZlKn.d.ts → orchestrator-C9CDWAm6.d.ts} +4 -4
  171. package/dist/orchestrator.d.ts +5 -5
  172. package/dist/orchestrator.js +29 -27
  173. package/dist/patterns-cli.d.ts +1 -1
  174. package/dist/policy-runtime.d.ts +1 -1
  175. package/dist/provenance.d.ts +36 -2
  176. package/dist/provenance.js +5 -1
  177. package/dist/qmd-recall-cache.d.ts +1 -1
  178. package/dist/qmd.d.ts +1 -1
  179. package/dist/recall-disclosure-escalation.d.ts +1 -1
  180. package/dist/recall-explain-renderer.d.ts +1 -1
  181. package/dist/recall-explain-renderer.js +3 -3
  182. package/dist/recall-pipeline-stages.d.ts +15 -0
  183. package/dist/recall-pipeline-stages.js +3 -56
  184. package/dist/recall-pipeline-stages.js.map +1 -1
  185. package/dist/recall-planner-llm.d.ts +1 -1
  186. package/dist/recall-state.d.ts +1 -1
  187. package/dist/recall-tag-filter.d.ts +1 -1
  188. package/dist/recall-xray-cli.d.ts +1 -1
  189. package/dist/recall-xray-cli.js +4 -4
  190. package/dist/recall-xray-renderer.d.ts +1 -1
  191. package/dist/recall-xray-renderer.js +3 -3
  192. package/dist/recall-xray.d.ts +1 -1
  193. package/dist/recall-xray.js +2 -2
  194. package/dist/resolve-auth-token.d.ts +1 -1
  195. package/dist/response-guidance-recall.js +2 -1
  196. package/dist/resume-bundles.js +7 -5
  197. package/dist/retrieval-agents.d.ts +1 -1
  198. package/dist/retrieval-tiers.d.ts +1 -1
  199. package/dist/routing/engine.d.ts +1 -1
  200. package/dist/routing/store.d.ts +1 -1
  201. package/dist/schemas.d.ts +19 -0
  202. package/dist/schemas.js +1 -1
  203. package/dist/search/embed-helper.d.ts +1 -1
  204. package/dist/search/factory.d.ts +1 -1
  205. package/dist/search/index.d.ts +1 -1
  206. package/dist/search/lancedb-backend.d.ts +1 -1
  207. package/dist/search/meilisearch-backend.d.ts +1 -1
  208. package/dist/search/noop-backend.d.ts +1 -1
  209. package/dist/search/orama-backend.d.ts +1 -1
  210. package/dist/search/port.d.ts +1 -1
  211. package/dist/search/remote-backend.d.ts +1 -1
  212. package/dist/{semantic-DJR8_DMQ.d.ts → semantic-SLAa_prH.d.ts} +1 -1
  213. package/dist/{semantic-consolidation-BtUfv-AL.d.ts → semantic-consolidation-DgFXyALl.d.ts} +1 -1
  214. package/dist/semantic-consolidation.d.ts +2 -2
  215. package/dist/semantic-consolidation.js +7 -6
  216. package/dist/semantic-rule-promotion.js +6 -5
  217. package/dist/semantic-rule-verifier.d.ts +1 -1
  218. package/dist/semantic-rule-verifier.js +6 -5
  219. package/dist/session-observer-bands.d.ts +1 -1
  220. package/dist/session-observer-state.d.ts +1 -1
  221. package/dist/shared-context/manager.d.ts +1 -1
  222. package/dist/signal.d.ts +1 -1
  223. package/dist/storage.d.ts +13 -1
  224. package/dist/storage.js +5 -4
  225. package/dist/summarizer.d.ts +1 -1
  226. package/dist/summary-snapshot.d.ts +1 -1
  227. package/dist/targeted-fact-recall.js +2 -1
  228. package/dist/temporal-supersession.d.ts +1 -1
  229. package/dist/temporal-validity.d.ts +1 -1
  230. package/dist/threading.d.ts +1 -1
  231. package/dist/tier-migration.d.ts +1 -1
  232. package/dist/tier-routing.d.ts +1 -1
  233. package/dist/topics.d.ts +1 -1
  234. package/dist/transcript.d.ts +1 -1
  235. package/dist/transcript.js +2 -2
  236. package/dist/{types-PuiPZ9iE.d.ts → types-MWKPZnM0.d.ts} +25 -1
  237. package/dist/types.d.ts +1 -1
  238. package/dist/types.js +1 -1
  239. package/dist/utility-runtime.d.ts +1 -1
  240. package/dist/verified-recall.js +6 -5
  241. package/package.json +2 -2
  242. package/src/access-http.ts +162 -0
  243. package/src/access-service.ts +215 -0
  244. package/src/admin/admin-surfaces.test.ts +458 -0
  245. package/src/admin/admin-surfaces.ts +819 -0
  246. package/src/event-order-recall.test.ts +53 -0
  247. package/src/event-order-recall.ts +61 -37
  248. package/src/explicit-cue-recall.ts +21 -1
  249. package/src/extraction.ts +91 -6
  250. package/src/orchestrator.ts +46 -5
  251. package/src/provenance-extraction.test.ts +838 -0
  252. package/src/provenance.ts +413 -0
  253. package/src/recall-pipeline-parity.test.ts +288 -0
  254. package/src/recall-pipeline-stages.test.ts +43 -0
  255. package/src/recall-pipeline-stages.ts +22 -0
  256. package/src/response-guidance-recall.ts +32 -36
  257. package/src/schemas.ts +7 -0
  258. package/src/storage.ts +23 -0
  259. package/src/targeted-fact-recall.ts +25 -32
  260. package/src/types.ts +24 -0
  261. package/dist/chunk-4HIAWLA2.js.map +0 -1
  262. package/dist/chunk-EC2AYKRX.js.map +0 -1
  263. package/dist/chunk-FN2SM5SN.js.map +0 -1
  264. package/dist/chunk-FUCJAZ25.js.map +0 -1
  265. package/dist/chunk-HRUULBBV.js.map +0 -1
  266. package/dist/chunk-R5DB26G6.js.map +0 -1
  267. package/dist/chunk-ROZJACKP.js.map +0 -1
  268. package/dist/chunk-SFMRLXIV.js.map +0 -1
  269. package/dist/chunk-UU6MVCJ6.js.map +0 -1
  270. package/dist/chunk-YXLT4EMM.js.map +0 -1
  271. package/dist/chunk-ZPQVJEVQ.js.map +0 -1
  272. /package/dist/{chunk-ODTWHSY2.js.map → chunk-64XXOQRW.js.map} +0 -0
  273. /package/dist/{chunk-ZT7B64BE.js.map → chunk-6TAETM63.js.map} +0 -0
  274. /package/dist/{chunk-HF4N43Q7.js.map → chunk-6VVP6NK7.js.map} +0 -0
  275. /package/dist/{chunk-7TAQEPLE.js.map → chunk-AMNLZ6SF.js.map} +0 -0
  276. /package/dist/{chunk-KS7WQ4BZ.js.map → chunk-CZJ6QSKG.js.map} +0 -0
  277. /package/dist/{chunk-27LQPUMZ.js.map → chunk-EOOVJK2U.js.map} +0 -0
  278. /package/dist/{chunk-QP37KL5H.js.map → chunk-EYJD6KIO.js.map} +0 -0
  279. /package/dist/{chunk-WFEZUGU5.js.map → chunk-EZR35XHX.js.map} +0 -0
  280. /package/dist/{chunk-JO3E5VGS.js.map → chunk-FVI5B7DE.js.map} +0 -0
  281. /package/dist/{chunk-PONNZ54D.js.map → chunk-GY3SKOS4.js.map} +0 -0
  282. /package/dist/{chunk-U33LWTQQ.js.map → chunk-HV57RHMD.js.map} +0 -0
  283. /package/dist/{chunk-IYOPIG3E.js.map → chunk-IX72AAMZ.js.map} +0 -0
  284. /package/dist/{chunk-JKOKX3PS.js.map → chunk-JMA4RYRN.js.map} +0 -0
  285. /package/dist/{chunk-DEDQXIDL.js.map → chunk-JTKFZMZ7.js.map} +0 -0
  286. /package/dist/{chunk-OLOYQZFB.js.map → chunk-KC6TCAWV.js.map} +0 -0
  287. /package/dist/{chunk-ED35D32I.js.map → chunk-LQ4J7ELC.js.map} +0 -0
  288. /package/dist/{chunk-TFVVONWD.js.map → chunk-MBUM2Y3L.js.map} +0 -0
  289. /package/dist/{chunk-T5QAZIBO.js.map → chunk-MOXFPLD6.js.map} +0 -0
  290. /package/dist/{chunk-XTIRCSIH.js.map → chunk-MXEWQKM7.js.map} +0 -0
  291. /package/dist/{chunk-ZYNMX6IU.js.map → chunk-NUIJEGVD.js.map} +0 -0
  292. /package/dist/{chunk-YO4MBK3I.js.map → chunk-QGJAGC2J.js.map} +0 -0
  293. /package/dist/{chunk-ISLJ5WIM.js.map → chunk-QIMFOCSH.js.map} +0 -0
  294. /package/dist/{chunk-IJEZMWKA.js.map → chunk-SFOAQQDJ.js.map} +0 -0
  295. /package/dist/{chunk-NINRTFSV.js.map → chunk-SINGJCUR.js.map} +0 -0
  296. /package/dist/{chunk-6W2D6FGG.js.map → chunk-TGAHHCB6.js.map} +0 -0
  297. /package/dist/{chunk-D75JXBV4.js.map → chunk-WN4GHSDH.js.map} +0 -0
  298. /package/dist/{chunk-K4DWSPMW.js.map → chunk-WRGPE6AW.js.map} +0 -0
  299. /package/dist/{chunk-SDPDU2PM.js.map → chunk-YTMDF6S7.js.map} +0 -0
  300. /package/dist/{chunk-MNU5G4TK.js.map → chunk-Z7XEIAV4.js.map} +0 -0
  301. /package/dist/{chunk-HXHKLVAS.js.map → chunk-ZCEI242W.js.map} +0 -0
@@ -0,0 +1,288 @@
1
+ // Issue #1539 PRs 3–6 — migration parity guards.
2
+ //
3
+ // Each recall pipeline (targeted-fact, response-guidance, explicit-cue,
4
+ // event-order) was migrated from an inline dedup/score/threshold/sort block to
5
+ // a thin config object handed to `unifiedDedupeAndRank` (the spine). The
6
+ // per-tier test files (targeted-fact-recall.test.ts etc.) are the end-to-end
7
+ // characterization — they exercise buildXxxRecallSection on full fixtures. This
8
+ // file is the SEAM guard: it pins, per pipeline, that the declared config shape
9
+ // routes through the spine with the correct divergence (sort direction,
10
+ // content-dedup on/off, rank threshold, transform-applied-to-output-but-not-
11
+ // score). If a future refactor flips one of these silently, the matching test
12
+ // here fails BEFORE the end-to-end suites have a chance to mask it.
13
+ //
14
+ // These tests do NOT re-implement the per-tier scorers — those are genuinely
15
+ // per-tier policy and stay in the per-tier test files. Here we use small
16
+ // representative scorers that exercise the divergence dimension each pipeline
17
+ // declares on the spine.
18
+
19
+ import assert from "node:assert/strict";
20
+ import test from "node:test";
21
+
22
+ import type { EvidencePackItem } from "./evidence-pack.js";
23
+ import { unifiedDedupeAndRank } from "./recall-pipeline-stages.js";
24
+
25
+ function item(
26
+ turnIndex: number,
27
+ content: string,
28
+ overrides: Partial<EvidencePackItem> = {},
29
+ ): EvidencePackItem {
30
+ return {
31
+ id: `s1:${turnIndex}`,
32
+ sessionId: "s1",
33
+ turnIndex,
34
+ role: "user",
35
+ content,
36
+ ...overrides,
37
+ };
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // PR3 — targeted-fact shape: DESC, content-dedup, transform-applied-to-output,
42
+ // score-on-original. No rank threshold.
43
+ // ---------------------------------------------------------------------------
44
+
45
+ test("parity targeted-fact: DESC ordering on score ties (latest turnIndex first)", () => {
46
+ const items = [
47
+ item(1, "early turn"),
48
+ item(5, "late turn"),
49
+ item(3, "middle turn"),
50
+ ];
51
+ // Constant scorer forces score ties so turnIndex DESC is the deciding key.
52
+ const ranked = unifiedDedupeAndRank(items, {
53
+ query: "",
54
+ intents: [],
55
+ scoreEvidence: () => 10,
56
+ });
57
+ assert.equal(ranked[0].turnIndex, 5);
58
+ assert.equal(ranked[1].turnIndex, 3);
59
+ assert.equal(ranked[2].turnIndex, 1);
60
+ });
61
+
62
+ test("parity targeted-fact: content transform applied to output, NOT to scorer input", () => {
63
+ const items = [item(2, "Revenue was $500")];
64
+ // A scorer that reads ORIGINAL content must NOT see the appended cue suffix.
65
+ let scorerObserved = "";
66
+ unifiedDedupeAndRank(items, {
67
+ query: "",
68
+ intents: [],
69
+ scoreEvidence: (original) => {
70
+ scorerObserved = original.content;
71
+ return 1;
72
+ },
73
+ transformContent: (content) => `${content} [CUE]`,
74
+ });
75
+ assert.equal(scorerObserved, "Revenue was $500");
76
+ // ...and the output content carries the transform:
77
+ const ranked = unifiedDedupeAndRank(items, {
78
+ query: "",
79
+ intents: [],
80
+ scoreEvidence: () => 1,
81
+ transformContent: (content) => `${content} [CUE]`,
82
+ });
83
+ assert.equal(ranked[0].content, "Revenue was $500 [CUE]");
84
+ });
85
+
86
+ test("parity targeted-fact: duplicate normalized content collapses (default dedupByContent)", () => {
87
+ const items = [
88
+ item(1, "Same content"),
89
+ item(2, "same content"), // whitespace/case differences normalize equal
90
+ item(3, "Distinct"),
91
+ ];
92
+ const ranked = unifiedDedupeAndRank(items, {
93
+ query: "",
94
+ intents: [],
95
+ scoreEvidence: () => 10,
96
+ });
97
+ assert.equal(ranked.length, 2);
98
+ // First-seen wins: turnIndex 1 is kept, turnIndex 2 (same normalized content)
99
+ // is dropped. Output is then sorted DESC by turnIndex on the score tie.
100
+ assert.deepEqual(
101
+ ranked.map((r) => r.turnIndex),
102
+ [3, 1],
103
+ );
104
+ });
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // PR4 — response-guidance shape: same as targeted-fact (DESC + content-dedup +
108
+ // transform + score-on-original) but with guidance-cue transform. Pin the shape
109
+ // matches; the guidance-specific transform is exercised end-to-end in
110
+ // response-guidance-recall.test.ts.
111
+ // ---------------------------------------------------------------------------
112
+
113
+ test("parity response-guidance: DESC ordering on score ties", () => {
114
+ const items = [
115
+ item(2, "guidance b"),
116
+ item(7, "guidance a"),
117
+ ];
118
+ const ranked = unifiedDedupeAndRank(items, {
119
+ query: "",
120
+ intents: [],
121
+ scoreEvidence: () => 5,
122
+ });
123
+ assert.equal(ranked[0].turnIndex, 7);
124
+ assert.equal(ranked[1].turnIndex, 2);
125
+ });
126
+
127
+ test("parity response-guidance: rank primary key always DESC regardless of turnIndex", () => {
128
+ const items = [
129
+ item(1, "low rank high turnIndex"),
130
+ item(9, "high rank low turnIndex"),
131
+ ];
132
+ const ranked = unifiedDedupeAndRank(items, {
133
+ query: "",
134
+ intents: [],
135
+ scoreEvidence: (original) => (original.content.includes("high rank") ? 20 : 1),
136
+ });
137
+ assert.equal(ranked[0].turnIndex, 9);
138
+ assert.equal(ranked[1].turnIndex, 1);
139
+ });
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // PR5 — explicit-cue shape: the pipeline that OPTS OUT of scoring AND sorting.
143
+ // Its deliberate value ordering (turn references -> content cues -> lexical
144
+ // cues, by evidence type then read-key priority) is the highest-value design
145
+ // choice — preserved verbatim via preserveInsertionOrder + constant scorer +
146
+ // id-only dedup (the spine call is behavior-idempotent; items are already
147
+ // deduped by seenTurns during collection).
148
+ // ---------------------------------------------------------------------------
149
+
150
+ test("parity explicit-cue: preserveInsertionOrder keeps collection order intact", () => {
151
+ // Insertion order is 2, 4, 1. A default sort would reorder to 4, 2, 1.
152
+ // explicit-cue must NOT reorder — the collection order IS the value order.
153
+ const items = [
154
+ item(2, "explicit cue b", { id: "s:2" }),
155
+ item(4, "explicit cue a", { id: "s:4" }),
156
+ item(1, "explicit cue c", { id: "s:1" }),
157
+ ];
158
+ const ranked = unifiedDedupeAndRank(items, {
159
+ query: "",
160
+ intents: [],
161
+ scoreEvidence: () => 0,
162
+ dedupByContent: false,
163
+ preserveInsertionOrder: true,
164
+ });
165
+ assert.deepEqual(
166
+ ranked.map((r) => r.turnIndex),
167
+ [2, 4, 1],
168
+ );
169
+ });
170
+
171
+ test("parity explicit-cue: id-only dedup (dedupByContent false) keeps distinct turns", () => {
172
+ const items = [
173
+ item(1, "same body", { id: "s:1" }),
174
+ item(2, "same body", { id: "s:2" }),
175
+ ];
176
+ const ranked = unifiedDedupeAndRank(items, {
177
+ query: "",
178
+ intents: [],
179
+ scoreEvidence: () => 0,
180
+ dedupByContent: false,
181
+ preserveInsertionOrder: true,
182
+ });
183
+ assert.equal(ranked.length, 2);
184
+ });
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // PR6 — event-order shape: ASC ordering, NO content-dedup (dedupByContent:false
188
+ // keeps distinct turns even when cue-appended bodies match), declared
189
+ // rankThreshold of 6, transform applied to output.
190
+ // ---------------------------------------------------------------------------
191
+
192
+ test("parity event-order: ASC ordering on rank ties (earliest turnIndex first)", () => {
193
+ const items = [
194
+ item(5, "turn five"),
195
+ item(1, "turn one"),
196
+ item(3, "turn three"),
197
+ ];
198
+ const ranked = unifiedDedupeAndRank(items, {
199
+ query: "",
200
+ intents: [],
201
+ scoreEvidence: () => 10,
202
+ turnIndexSortDirection: "asc",
203
+ });
204
+ assert.deepEqual(
205
+ ranked.map((r) => r.turnIndex),
206
+ [1, 3, 5],
207
+ );
208
+ });
209
+
210
+ test("parity event-order: dedupByContent false keeps distinct turns with identical bodies", () => {
211
+ // Two distinct turns (different ids/turnIndex) but identical cue-appended body.
212
+ // Event-order MUST keep both — they are legitimate repeated turns in a
213
+ // chronological transcript. This is the cursor-bugbot a4299851 regression guard.
214
+ const items = [
215
+ item(1, "the user asked again"),
216
+ item(4, "the user asked again"),
217
+ ];
218
+ const ranked = unifiedDedupeAndRank(items, {
219
+ query: "",
220
+ intents: [],
221
+ scoreEvidence: () => 10,
222
+ dedupByContent: false,
223
+ turnIndexSortDirection: "asc",
224
+ });
225
+ assert.equal(ranked.length, 2);
226
+ assert.deepEqual(
227
+ ranked.map((r) => r.turnIndex),
228
+ [1, 4],
229
+ );
230
+ });
231
+
232
+ test("parity event-order: declared rankThreshold (6) drops below-threshold items", () => {
233
+ const items = [
234
+ item(1, "strong", { id: "s:1" }),
235
+ item(2, "weak", { id: "s:2" }),
236
+ item(3, "borderline", { id: "s:3" }),
237
+ ];
238
+ const ranked = unifiedDedupeAndRank(items, {
239
+ query: "",
240
+ intents: [],
241
+ scoreEvidence: (original) => {
242
+ if (original.content === "strong") return 10;
243
+ if (original.content === "borderline") return 6; // exactly threshold -> kept (>=)
244
+ return 5; // weak -> dropped
245
+ },
246
+ rankThreshold: 6,
247
+ turnIndexSortDirection: "asc",
248
+ });
249
+ // rank is ALWAYS DESC primary: strong (10) outranks borderline (6).
250
+ // turnIndex ASC only breaks ties within the same rank.
251
+ assert.deepEqual(
252
+ ranked.map((r) => r.content),
253
+ ["strong", "borderline"],
254
+ );
255
+ });
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // Cross-pipeline divergence guard: the SAME corpus produces DIFFERENT orderings
259
+ // under DESC vs ASC config. This is the core #1539 defect class — copy-pasting
260
+ // the comparator into a chronological pipeline silently inverts ordering.
261
+ // ---------------------------------------------------------------------------
262
+
263
+ test("parity divergence: DESC vs ASC configs on the same corpus invert turnIndex ordering", () => {
264
+ const corpus = [
265
+ item(1, "a", { id: "x:1" }),
266
+ item(2, "b", { id: "x:2" }),
267
+ item(3, "c", { id: "x:3" }),
268
+ ];
269
+ const desc = unifiedDedupeAndRank(corpus, {
270
+ query: "",
271
+ intents: [],
272
+ scoreEvidence: () => 10,
273
+ });
274
+ const asc = unifiedDedupeAndRank(corpus, {
275
+ query: "",
276
+ intents: [],
277
+ scoreEvidence: () => 10,
278
+ turnIndexSortDirection: "asc",
279
+ });
280
+ assert.deepEqual(
281
+ desc.map((r) => r.turnIndex),
282
+ [3, 2, 1],
283
+ );
284
+ assert.deepEqual(
285
+ asc.map((r) => r.turnIndex),
286
+ [1, 2, 3],
287
+ );
288
+ });
@@ -267,3 +267,46 @@ test("unifiedDedupeAndRank: dedupByContent true (default) still collapses identi
267
267
  });
268
268
  assert.equal(resultExplicit.length, 1, "explicit dedupByContent: true collapses identical content");
269
269
  });
270
+
271
+ test("unifiedDedupeAndRank: preserveInsertionOrder skips the sort stage (explicit-cue shape)", () => {
272
+ // Insertion order is turn-index 3, 1, 2. A DESC sort would reorder to 3, 2, 1.
273
+ // preserveInsertionOrder must keep the original (post-dedup) order: 3, 1, 2.
274
+ const items: EvidencePackItem[] = [
275
+ item(3, "turn ref", { id: "s:3" }),
276
+ item(1, "content cue", { id: "s:1" }),
277
+ item(2, "lexical hit", { id: "s:2" }),
278
+ ];
279
+ const result = unifiedDedupeAndRank(items, {
280
+ query: "q",
281
+ intents: NO_INTENTS,
282
+ scoreEvidence: () => 0, // explicit-cue does not score
283
+ dedupByContent: false, // explicit-cue dedupes by id only (seenTurns)
284
+ preserveInsertionOrder: true,
285
+ });
286
+ assert.deepEqual(
287
+ result.map((r) => r.id),
288
+ ["s:3", "s:1", "s:2"],
289
+ "insertion order is preserved — no sort applied",
290
+ );
291
+ });
292
+
293
+ test("unifiedDedupeAndRank: preserveInsertionOrder still applies threshold filter", () => {
294
+ const items: EvidencePackItem[] = [
295
+ item(1, "strong", { id: "s:1" }),
296
+ item(2, "weak", { id: "s:2" }),
297
+ item(3, "medium", { id: "s:3" }),
298
+ ];
299
+ const result = unifiedDedupeAndRank(items, {
300
+ query: "q",
301
+ intents: NO_INTENTS,
302
+ scoreEvidence: (original) => (original.content === "strong" ? 10 : original.content === "medium" ? 5 : 1),
303
+ rankThreshold: 5,
304
+ dedupByContent: false,
305
+ preserveInsertionOrder: true,
306
+ });
307
+ // threshold drops "weak" (rank 1); insertion order of survivors preserved.
308
+ assert.deepEqual(
309
+ result.map((r) => r.id),
310
+ ["s:1", "s:3"],
311
+ );
312
+ });
@@ -125,6 +125,21 @@ export interface UnifiedRankConfig<TIntent> {
125
125
  * The rank primary key is ALWAYS DESC regardless of this setting.
126
126
  */
127
127
  turnIndexSortDirection?: TurnIndexSortDirection;
128
+ /**
129
+ * When `true`, skip the sort stage entirely and return items in their
130
+ * post-dedup insertion order (first-seen first). Used by explicit-cue,
131
+ * whose deliberate value ordering — turn references → content cues →
132
+ * lexical cues, gathered by evidence TYPE then read-key priority — is the
133
+ * pipeline's highest-value design choice. A rank-based sort would invert
134
+ * it (turn references are deliberately UNSCORED while lexical hits carry
135
+ * numeric scores; sorting by score would demote turn references below
136
+ * weak lexical hits — cursor[bot] / codex P2 on #1505).
137
+ *
138
+ * Score and threshold-filter still apply when declared; only the sort is
139
+ * skipped. The constant scorer (`scoreEvidence: () => 0`) makes the
140
+ * no-scoring policy explicit alongside this flag.
141
+ */
142
+ preserveInsertionOrder?: boolean;
128
143
  }
129
144
 
130
145
  /**
@@ -232,6 +247,13 @@ export function unifiedDedupeAndRank<TIntent>(
232
247
  // turnIndex direction is configurable: DESC (relevance) or ASC (chronology).
233
248
  // The tertiary tiebreaker follows the direction:
234
249
  // DESC → score DESC; ASC → content.localeCompare.
250
+ //
251
+ // explicit-cue opts out of sorting entirely (preserveInsertionOrder) — its
252
+ // deliberate value ordering is the collection order (turn references →
253
+ // content cues → lexical cues), and a rank-based sort would invert it.
254
+ if (config.preserveInsertionOrder) {
255
+ return filtered;
256
+ }
235
257
  return filtered.sort(makeComparator(direction));
236
258
  }
237
259
 
@@ -5,6 +5,11 @@ import {
5
5
  resolveLcmReadSessionIds,
6
6
  } from "./lcm-fallback-read.js";
7
7
 
8
+ import {
9
+ unifiedDedupeAndRank,
10
+ type RankedEvidenceItem,
11
+ } from "./recall-pipeline-stages.js";
12
+
8
13
  export interface ResponseGuidanceRecallOptions {
9
14
  engine: ExplicitCueRecallEngine | null | undefined;
10
15
  sessionId?: string;
@@ -24,10 +29,6 @@ export interface ResponseGuidanceRecallOptions {
24
29
  forceGeneric?: boolean;
25
30
  }
26
31
 
27
- interface RankedGuidanceItem extends EvidencePackItem {
28
- rank: number;
29
- }
30
-
31
32
  type GuidanceIntent =
32
33
  | "dates"
33
34
  | "editing"
@@ -341,42 +342,37 @@ async function collectGuidanceScanItems(
341
342
  return items;
342
343
  }
343
344
 
345
+ /**
346
+ * Issue #1539 PR4: response-guidance now routes dedup/score/threshold/sort through
347
+ * the unified spine (`unifiedDedupeAndRank`). The divergence that lived inline
348
+ * here — append-guidance-cues content transform (two-arg: content + intents),
349
+ * relevance scoring on ORIGINAL content, default DESC turn-index ordering with
350
+ * -1 sentinel for missing turns — is now declared config on the spine. The score
351
+ * and appendGuidanceCues helpers stay in this module because they are genuinely
352
+ * per-tier policy; only the duplicated dedup/sort/threshold machinery moved out.
353
+ *
354
+ * Behavior is byte-for-byte identical to the inline implementation it replaces
355
+ * (the spine was extracted to match it exactly; see recall-pipeline-stages.ts).
356
+ * The response-guidance-recall.test.ts suite is the characterization.
357
+ *
358
+ * Note: response-guidance (like the pre-migration code) dedups on the TRANSFORMED
359
+ * (cue-appended) content, not the original — this matches the spine's default
360
+ * (transform-then-dedup) exactly. No divergence from prior behavior.
361
+ */
344
362
  function rankAndDedupeGuidanceItems(
345
363
  items: EvidencePackItem[],
346
364
  query: string,
347
365
  intents: readonly GuidanceIntent[],
348
- ): RankedGuidanceItem[] {
349
- const seenIds = new Set<string>();
350
- const seenContent = new Set<string>();
351
- const ranked: RankedGuidanceItem[] = [];
352
-
353
- for (const item of items) {
354
- const id = item.id ?? (
355
- item.sessionId && typeof item.turnIndex === "number"
356
- ? `${item.sessionId}:${item.turnIndex}`
357
- : undefined
358
- );
359
- if (id && seenIds.has(id)) continue;
360
-
361
- const enhancedContent = appendGuidanceCues(item.content, intents);
362
- const contentKey = enhancedContent.toLowerCase().replace(/\s+/g, " ").trim();
363
- if (seenContent.has(contentKey)) continue;
364
-
365
- if (id) seenIds.add(id);
366
- seenContent.add(contentKey);
367
- ranked.push({
368
- ...item,
369
- content: enhancedContent,
370
- rank: scoreGuidanceEvidence(item, query, intents),
371
- });
372
- }
373
-
374
- return ranked.sort((left, right) => {
375
- if (right.rank !== left.rank) return right.rank - left.rank;
376
- const leftTurn = typeof left.turnIndex === "number" ? left.turnIndex : -1;
377
- const rightTurn = typeof right.turnIndex === "number" ? right.turnIndex : -1;
378
- if (rightTurn !== leftTurn) return rightTurn - leftTurn;
379
- return (right.score ?? 0) - (left.score ?? 0);
366
+ ): RankedEvidenceItem[] {
367
+ // The spine's config.intents is typed TIntent[] (readability contract — it is
368
+ // never mutated). Cast the readonly array through; safe because the spine only
369
+ // forwards intents to the score/transform callbacks.
370
+ const mutableIntents = intents as GuidanceIntent[];
371
+ return unifiedDedupeAndRank(items, {
372
+ query,
373
+ intents: mutableIntents,
374
+ scoreEvidence: (item, q, intentArr) => scoreGuidanceEvidence(item, q, intentArr),
375
+ transformContent: (content, intentArr) => appendGuidanceCues(content, intentArr),
380
376
  });
381
377
  }
382
378
 
package/src/schemas.ts CHANGED
@@ -93,6 +93,13 @@ export const ExtractedFactSchema = z.object({
93
93
  .describe(
94
94
  'Scope classification: "global" for cross-project knowledge (framework bugs, library behavior, API patterns, user preferences, tool configs, general coding patterns); "project" for project-specific knowledge (file paths, env configs, deployment details, project workarounds). Defaults to "project" when a coding context is active.',
95
95
  ),
96
+ quote: z
97
+ .string()
98
+ .optional()
99
+ .nullable()
100
+ .describe(
101
+ "The EXACT verbatim words from the conversation that support this fact. Copy a contiguous span from a single speaker turn (not a paraphrase). Cap at ~300 characters. This is the grounding evidence for downstream faithfulness verification (issue #1575).",
102
+ ),
96
103
  structuredAttributes: z
97
104
  .record(z.string(), z.string())
98
105
  .optional()
package/src/storage.ts CHANGED
@@ -88,6 +88,7 @@ import type {
88
88
  ScoredEntity,
89
89
  TopicScore,
90
90
  FileHygieneConfig,
91
+ ProvenanceSource,
91
92
  } from "./types.js";
92
93
  import { confidenceTier, SPECULATIVE_TTL_DAYS } from "./types.js";
93
94
  import {
@@ -3608,6 +3609,14 @@ export class StorageManager {
3608
3609
  * can consume it. Absent = gate was off or fact predates #1576.
3609
3610
  */
3610
3611
  faithfulness?: import("./types.js").FaithfulnessFrontmatter;
3612
+ /**
3613
+ * Claim-level provenance spans (issue #1575 PR 2). When provided,
3614
+ * persisted to frontmatter so downstream readers (memory_get, x-ray,
3615
+ * faithfulness gate #1576) can consume them. Absent = fact predates
3616
+ * #1575 or provenance was disabled.
3617
+ */
3618
+ sources?: ProvenanceSource[];
3619
+ provenance?: "verified" | "unverified" | "none";
3611
3620
  } = {},
3612
3621
  ): Promise<string> {
3613
3622
  await this.ensureDirectories();
@@ -3681,6 +3690,14 @@ export class StorageManager {
3681
3690
  if (options.faithfulness !== undefined) {
3682
3691
  fm.faithfulness = options.faithfulness;
3683
3692
  }
3693
+ // Claim-level provenance (issue #1575 PR 2). Wire through to frontmatter
3694
+ // so verified spans survive extraction → storage → memory_get end-to-end.
3695
+ if (options.sources !== undefined) {
3696
+ fm.sources = options.sources;
3697
+ }
3698
+ if (options.provenance !== undefined) {
3699
+ fm.provenance = options.provenance;
3700
+ }
3684
3701
 
3685
3702
  // Append structured attributes as searchable suffix so QMD indexes them.
3686
3703
  // normalizeAttributePairs sorts and lowercases keys so the enriched content
@@ -7338,6 +7355,10 @@ export class StorageManager {
7338
7355
  status?: import("./types.js").MemoryStatus;
7339
7356
  /** Faithfulness gate verdict (issue #1576), propagated from the parent fact. */
7340
7357
  faithfulness?: import("./types.js").FaithfulnessFrontmatter;
7358
+ /** Claim-level provenance spans (issue #1575 PR 2), propagated from the parent fact so a chunk surfaced independently (memory_get/x-ray) preserves them (chatgpt-codex-connector thread Ocvmo). */
7359
+ sources?: ProvenanceSource[];
7360
+ /** Coarse provenance tag (issue #1575 PR 2), propagated from the parent. */
7361
+ provenance?: "verified" | "unverified" | "none";
7341
7362
  } = {},
7342
7363
  ): Promise<string> {
7343
7364
  await this.ensureDirectories();
@@ -7369,6 +7390,8 @@ export class StorageManager {
7369
7390
  valid_at: validAt,
7370
7391
  ...(options.status ? { status: options.status } : {}),
7371
7392
  ...(options.faithfulness ? { faithfulness: options.faithfulness } : {}),
7393
+ ...(options.sources ? { sources: options.sources } : {}),
7394
+ ...(options.provenance ? { provenance: options.provenance } : {}),
7372
7395
  };
7373
7396
 
7374
7397
  const sanitized = sanitizeMemoryContent(content);
@@ -9,6 +9,11 @@ import {
9
9
  resolveLcmReadSessionIds,
10
10
  } from "./lcm-fallback-read.js";
11
11
 
12
+ import {
13
+ unifiedDedupeAndRank,
14
+ type RankedEvidenceItem,
15
+ } from "./recall-pipeline-stages.js";
16
+
12
17
  export interface TargetedFactRecallOptions {
13
18
  engine: ExplicitCueRecallEngine | null | undefined;
14
19
  sessionId?: string;
@@ -27,10 +32,6 @@ export interface TargetedFactRecallOptions {
27
32
  title?: string;
28
33
  }
29
34
 
30
- interface RankedEvidenceItem extends EvidencePackItem {
31
- rank: number;
32
- }
33
-
34
35
  const DEFAULT_MAX_SEARCH_RESULTS = 48;
35
36
  const DEFAULT_SCAN_WINDOW_TURNS = 8;
36
37
  const DEFAULT_SCAN_WINDOW_TOKENS = 12_000;
@@ -210,38 +211,30 @@ async function collectTargetedFactScanItems(
210
211
  return items;
211
212
  }
212
213
 
214
+ /**
215
+ * Issue #1539 PR3: targeted-fact now routes dedup/score/threshold/sort through
216
+ * the unified spine (`unifiedDedupeAndRank`). The divergence that lived inline
217
+ * here — append-normalized-numeric-cues content transform, relevance scoring on
218
+ * ORIGINAL content, default DESC turn-index ordering with -1 sentinel for
219
+ * missing turns — is now declared config on the spine. The score and
220
+ * appendNormalizedNumericCues helpers stay in this module because they are
221
+ * genuinely per-tier policy; only the duplicated dedup/sort/threshold machinery
222
+ * moved out.
223
+ *
224
+ * Behavior is byte-for-byte identical to the inline implementation it replaces
225
+ * (the spine was extracted to match it exactly; see recall-pipeline-stages.ts).
226
+ * The targeted-fact-recall.test.ts suite (42 cases, incl. the search/scan
227
+ * ordering and summary insertion tests) is the characterization.
228
+ */
213
229
  function rankAndDedupeTargetedFactItems(
214
230
  items: EvidencePackItem[],
215
231
  query: string,
216
232
  ): RankedEvidenceItem[] {
217
- const seenIds = new Set<string>();
218
- const seenContent = new Set<string>();
219
- const ranked: RankedEvidenceItem[] = [];
220
-
221
- for (const item of items) {
222
- const id = item.id ?? (
223
- item.sessionId && typeof item.turnIndex === "number"
224
- ? `${item.sessionId}:${item.turnIndex}`
225
- : undefined
226
- );
227
- if (id && seenIds.has(id)) continue;
228
- const contentKey = item.content.toLowerCase().replace(/\s+/g, " ").trim();
229
- if (seenContent.has(contentKey)) continue;
230
- if (id) seenIds.add(id);
231
- seenContent.add(contentKey);
232
- ranked.push({
233
- ...item,
234
- content: appendNormalizedNumericCues(item.content),
235
- rank: scoreTargetedFactEvidence(item, query),
236
- });
237
- }
238
-
239
- return ranked.sort((left, right) => {
240
- if (right.rank !== left.rank) return right.rank - left.rank;
241
- const leftTurn = typeof left.turnIndex === "number" ? left.turnIndex : -1;
242
- const rightTurn = typeof right.turnIndex === "number" ? right.turnIndex : -1;
243
- if (rightTurn !== leftTurn) return rightTurn - leftTurn;
244
- return (right.score ?? 0) - (left.score ?? 0);
233
+ return unifiedDedupeAndRank(items, {
234
+ query,
235
+ intents: [],
236
+ scoreEvidence: (item, q) => scoreTargetedFactEvidence(item, q),
237
+ transformContent: (content) => appendNormalizedNumericCues(content),
245
238
  });
246
239
  }
247
240
 
package/src/types.ts CHANGED
@@ -2852,6 +2852,30 @@ export interface ExtractedFact {
2852
2852
  * "no verified span" and skip gracefully.
2853
2853
  */
2854
2854
  sources?: ProvenanceSource[];
2855
+ /**
2856
+ * Coarse provenance strength tag (issue #1575 PR 2). Set by the extraction
2857
+ * validator: "verified" when the quote was located in source turns,
2858
+ * "unverified" when the quote survived but no offsets were recoverable.
2859
+ * Absent when `provenance.enabled` is false or no quote was provided —
2860
+ * readers treat absent as "none" (rule 34 spirit).
2861
+ */
2862
+ provenance?: "verified" | "unverified" | "none";
2863
+ /**
2864
+ * Transient LLM-provided supporting quote (issue #1575 PR 2). Consumed by
2865
+ * the post-parse validator to build `sources`, then stripped before the
2866
+ * fact reaches persistence. NEVER included in content-hash dedup (rule 23 /
2867
+ * checklist §13). Absent on facts that have already passed the validator.
2868
+ */
2869
+ quote?: string;
2870
+ /**
2871
+ * Transient requireSpans signal (issue #1575 PR 2). Set by the provenance
2872
+ * validator (`ProvenanceBuildResult.requireSpansPending`) when
2873
+ * `provenance.requireSpans` is enabled and the quote could not be located
2874
+ * in any source turn. Read by the persist path to route the fact to
2875
+ * `pending_review` instead of `active`. Stripped before persistence —
2876
+ * it never reaches frontmatter (chatgpt-codex-connector thread 4xB).
2877
+ */
2878
+ requireSpansPending?: boolean;
2855
2879
  promptedByQuestion?: string;
2856
2880
  /**
2857
2881
  * Whether this fact is project-scoped or globally applicable.