@remnic/core 9.3.726 → 9.3.727

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 (318) hide show
  1. package/dist/access-boundary.d.ts +5 -5
  2. package/dist/access-boundary.js +20 -20
  3. package/dist/access-cli.js +42 -42
  4. package/dist/access-http.d.ts +20 -5
  5. package/dist/access-http.js +25 -25
  6. package/dist/access-mcp.d.ts +5 -5
  7. package/dist/access-mcp.js +24 -24
  8. package/dist/access-operations-batch.js +21 -21
  9. package/dist/access-operations.d.ts +5 -5
  10. package/dist/access-operations.js +23 -23
  11. package/dist/access-schema.js +3 -3
  12. package/dist/{access-service-B2PlPtxh.d.ts → access-service-Bgpjxo4f.d.ts} +36 -4
  13. package/dist/access-service.d.ts +5 -5
  14. package/dist/access-service.js +19 -19
  15. package/dist/access-surface-catalog.d.ts +5 -5
  16. package/dist/action-confidence.d.ts +1 -1
  17. package/dist/active-memory-bridge.d.ts +1 -1
  18. package/dist/active-recall.d.ts +1 -1
  19. package/dist/active-recall.js +3 -3
  20. package/dist/behavior-learner.d.ts +1 -1
  21. package/dist/behavior-signals.d.ts +1 -1
  22. package/dist/bootstrap.d.ts +4 -4
  23. package/dist/briefing.d.ts +1 -1
  24. package/dist/briefing.js +7 -7
  25. package/dist/buffer-surprise-report.d.ts +1 -1
  26. package/dist/buffer.d.ts +1 -1
  27. package/dist/calibration.d.ts +1 -1
  28. package/dist/capabilities.d.ts +1 -1
  29. package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
  30. package/dist/{catalog-CzUb-GyD.d.ts → catalog-bMczlO2G.d.ts} +1 -1
  31. package/dist/causal-behavior.d.ts +1 -1
  32. package/dist/causal-consolidation.d.ts +1 -1
  33. package/dist/causal-consolidation.js +8 -8
  34. package/dist/causal-trajectory-graph.d.ts +1 -1
  35. package/dist/{chunk-Q6D75LVH.js → chunk-27QQC7ZX.js} +75 -14
  36. package/dist/{chunk-Q6D75LVH.js.map → chunk-27QQC7ZX.js.map} +1 -1
  37. package/dist/{chunk-53EEQYE5.js → chunk-2KAYTPPT.js} +7 -7
  38. package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
  39. package/dist/{chunk-BIA7ELIB.js → chunk-36SEDFAU.js} +3 -3
  40. package/dist/{chunk-UQSVUEN2.js → chunk-37436GML.js} +2 -2
  41. package/dist/{chunk-RRHHQYJS.js → chunk-3TSTGI6F.js} +2 -2
  42. package/dist/{chunk-FO3HO52S.js → chunk-4H4F3BFL.js} +2 -2
  43. package/dist/{chunk-BEUCACTV.js → chunk-4NOI577P.js} +2 -2
  44. package/dist/{chunk-BBIE7W52.js → chunk-4NR2PDZ5.js} +2 -2
  45. package/dist/{chunk-TQQH2FTX.js → chunk-6PY5YJAV.js} +101 -15
  46. package/dist/{chunk-TQQH2FTX.js.map → chunk-6PY5YJAV.js.map} +1 -1
  47. package/dist/{chunk-PKMQK2WY.js → chunk-773DPI6T.js} +4 -4
  48. package/dist/{chunk-MQVKHFIU.js → chunk-7RLCCNHU.js} +8 -8
  49. package/dist/{chunk-XGTBNVIU.js → chunk-AAD2ZS6P.js} +11 -1
  50. package/dist/chunk-AAD2ZS6P.js.map +1 -0
  51. package/dist/{chunk-EZNDNMRH.js → chunk-AQLOP5F4.js} +2 -2
  52. package/dist/{chunk-UNNDFYUX.js → chunk-ARV3AUOM.js} +5 -5
  53. package/dist/{chunk-NPL7VDM6.js → chunk-ASKXIAHD.js} +2 -2
  54. package/dist/{chunk-K4ZNARDO.js → chunk-CBWI22JN.js} +2 -2
  55. package/dist/{chunk-ZOHMPFOB.js → chunk-CKFLLAIS.js} +3 -3
  56. package/dist/{chunk-DCKNMLEQ.js → chunk-CSRXQ6NB.js} +2 -2
  57. package/dist/{chunk-6AFVIOVZ.js → chunk-CTOQEZSN.js} +2 -2
  58. package/dist/{chunk-7NS7AWMZ.js → chunk-DKJCRGWM.js} +3 -3
  59. package/dist/{chunk-JPUT5NGH.js → chunk-DMQWV6SQ.js} +2 -2
  60. package/dist/{chunk-F3XZN24Z.js → chunk-DRUDC2UX.js} +2 -2
  61. package/dist/{chunk-KJR66XOM.js → chunk-EY6MDMHK.js} +2 -2
  62. package/dist/{chunk-3TUT4PWB.js → chunk-FVZ2ISIX.js} +2 -2
  63. package/dist/{chunk-C4X4QGIO.js → chunk-G6NKALRN.js} +5 -2
  64. package/dist/chunk-G6NKALRN.js.map +1 -0
  65. package/dist/{chunk-6SQQOJ4U.js → chunk-GKRLPHPU.js} +2 -2
  66. package/dist/{chunk-5NWYQUQC.js → chunk-HOHVIVDR.js} +2 -2
  67. package/dist/{chunk-6WAJQKBS.js → chunk-JYLFZWUX.js} +2 -2
  68. package/dist/{chunk-5W7CTZ2E.js → chunk-KQAFEZQX.js} +4 -4
  69. package/dist/{chunk-YTZ3VGC7.js → chunk-M4V2JVSZ.js} +2 -2
  70. package/dist/{chunk-PLG6JHQS.js → chunk-MJIHJ7OG.js} +38 -38
  71. package/dist/{chunk-TA3Y6GEX.js → chunk-MLKWJOAJ.js} +2 -2
  72. package/dist/{chunk-2NBP25BG.js → chunk-N3VNUM4Z.js} +171 -79
  73. package/dist/chunk-N3VNUM4Z.js.map +1 -0
  74. package/dist/{chunk-UKTDXTWX.js → chunk-NSA5ZRUL.js} +6 -6
  75. package/dist/{chunk-DQXPJ6HD.js → chunk-OEEW4KJA.js} +4 -4
  76. package/dist/{chunk-7AG54AF3.js → chunk-PUR2CKX2.js} +1 -1
  77. package/dist/chunk-PUR2CKX2.js.map +1 -0
  78. package/dist/{chunk-5OOA7SGX.js → chunk-R7IYU5DZ.js} +2 -2
  79. package/dist/{chunk-WGTWGDR5.js → chunk-SNKPUJHR.js} +5 -5
  80. package/dist/{chunk-BB57OZWC.js → chunk-UVPL565C.js} +2 -2
  81. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  82. package/dist/{chunk-OAHOL3ZC.js → chunk-XPHVAOBT.js} +111 -30
  83. package/dist/chunk-XPHVAOBT.js.map +1 -0
  84. package/dist/{chunk-V7CMKMHM.js → chunk-Y4YD5TYW.js} +3 -3
  85. package/dist/{chunk-MDBZTH7N.js → chunk-YKBHOBJK.js} +44 -10
  86. package/dist/chunk-YKBHOBJK.js.map +1 -0
  87. package/dist/{chunk-3MS3QVX6.js → chunk-YOLZS7NY.js} +2 -2
  88. package/dist/{chunk-6OIDDKAA.js → chunk-YTU7WKYN.js} +35 -9
  89. package/dist/chunk-YTU7WKYN.js.map +1 -0
  90. package/dist/{cli-IOlDHSBT.d.ts → cli-BLME420h.d.ts} +3 -3
  91. package/dist/cli.d.ts +6 -6
  92. package/dist/cli.js +47 -47
  93. package/dist/compat/codex-cli-fallback.d.ts +1 -0
  94. package/dist/compat/codex-cli-fallback.js +22 -0
  95. package/dist/compat/codex-cli-fallback.js.map +1 -0
  96. package/dist/compat/codex-thread-key.d.ts +1 -0
  97. package/dist/compat/codex-thread-key.js +18 -0
  98. package/dist/compat/codex-thread-key.js.map +1 -0
  99. package/dist/compounding/engine.d.ts +1 -1
  100. package/dist/compounding/engine.js +7 -7
  101. package/dist/compounding/preference-consolidator.d.ts +1 -1
  102. package/dist/compression-optimizer.d.ts +1 -1
  103. package/dist/config.d.ts +1 -1
  104. package/dist/config.js +3 -3
  105. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  106. package/dist/connectors/codex-materialize-runner.js +7 -7
  107. package/dist/connectors/codex-materialize.d.ts +1 -1
  108. package/dist/connectors/index.d.ts +1 -1
  109. package/dist/connectors/index.js +7 -7
  110. package/dist/consolidation-provenance-check.d.ts +1 -1
  111. package/dist/consolidation-undo.d.ts +1 -1
  112. package/dist/contradiction/index.d.ts +1 -1
  113. package/dist/conversation-index/backend.d.ts +1 -1
  114. package/dist/conversation-index/chunker.d.ts +1 -1
  115. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  116. package/dist/conversation-index/indexer.d.ts +1 -1
  117. package/dist/conversation-index/search.d.ts +1 -1
  118. package/dist/day-summary.d.ts +1 -1
  119. package/dist/delinearize.d.ts +1 -1
  120. package/dist/direct-answer-wiring.d.ts +1 -1
  121. package/dist/direct-answer.d.ts +1 -1
  122. package/dist/embedding-fallback.d.ts +1 -1
  123. package/dist/enrichment/index.d.ts +1 -1
  124. package/dist/entity-retrieval.d.ts +1 -1
  125. package/dist/entity-retrieval.js +7 -7
  126. package/dist/entity-schema.d.ts +1 -1
  127. package/dist/explicit-capture.d.ts +4 -4
  128. package/dist/extraction-faithfulness.d.ts +1 -1
  129. package/dist/extraction-faithfulness.js +2 -2
  130. package/dist/extraction-judge-telemetry.d.ts +1 -1
  131. package/dist/extraction-judge-training.d.ts +1 -1
  132. package/dist/extraction-judge.d.ts +1 -1
  133. package/dist/extraction.d.ts +1 -1
  134. package/dist/extraction.js +2 -2
  135. package/dist/faithfulness-config.d.ts +1 -0
  136. package/dist/faithfulness-config.js +1 -1
  137. package/dist/fallback-llm.d.ts +1 -1
  138. package/dist/graph-dashboard-diff.d.ts +1 -1
  139. package/dist/graph-dashboard-key.d.ts +1 -1
  140. package/dist/graph-dashboard-parser.d.ts +1 -1
  141. package/dist/graph-edge-reinforcement.d.ts +1 -1
  142. package/dist/graph-snapshot.d.ts +1 -1
  143. package/dist/graph.d.ts +1 -1
  144. package/dist/identity-continuity.d.ts +1 -1
  145. package/dist/importance.d.ts +1 -1
  146. package/dist/index.d.ts +9 -9
  147. package/dist/index.js +61 -61
  148. package/dist/intent.d.ts +1 -1
  149. package/dist/lcm/engine.d.ts +1 -1
  150. package/dist/lcm/engine.js +2 -2
  151. package/dist/lcm/index.d.ts +1 -1
  152. package/dist/lcm/index.js +7 -7
  153. package/dist/lcm/tools.d.ts +1 -1
  154. package/dist/lifecycle.d.ts +1 -1
  155. package/dist/live-connectors-runner.d.ts +1 -1
  156. package/dist/local-llm.d.ts +1 -1
  157. package/dist/local-model-endpoint.d.ts +9 -4
  158. package/dist/local-model-endpoint.js +1 -1
  159. package/dist/maintenance/memory-governance.d.ts +1 -1
  160. package/dist/maintenance/memory-governance.js +7 -7
  161. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  162. package/dist/maintenance/rebuild-memory-projection.js +8 -8
  163. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  164. package/dist/memory-action-policy.d.ts +1 -1
  165. package/dist/memory-cache.d.ts +1 -1
  166. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  167. package/dist/memory-projection-store.d.ts +1 -1
  168. package/dist/memory-provenance.d.ts +1 -1
  169. package/dist/memory-worth-outcomes.d.ts +1 -1
  170. package/dist/models-json.d.ts +1 -1
  171. package/dist/namespaces/migrate.d.ts +2 -2
  172. package/dist/namespaces/migrate.js +12 -12
  173. package/dist/namespaces/principal.d.ts +1 -1
  174. package/dist/namespaces/search.d.ts +1 -1
  175. package/dist/namespaces/search.js +4 -4
  176. package/dist/namespaces/storage.d.ts +2 -2
  177. package/dist/namespaces/storage.js +7 -7
  178. package/dist/native-knowledge.d.ts +1 -1
  179. package/dist/operator-toolkit.d.ts +1 -1
  180. package/dist/operator-toolkit.js +18 -18
  181. package/dist/orchestration/maintenance.d.ts +2 -2
  182. package/dist/orchestration/maintenance.js +9 -9
  183. package/dist/{orchestrator-C_OMSIMZ.d.ts → orchestrator-DqPAz8ot.d.ts} +3 -3
  184. package/dist/orchestrator.d.ts +4 -4
  185. package/dist/orchestrator.js +30 -30
  186. package/dist/patterns-cli.d.ts +1 -1
  187. package/dist/policy-runtime.d.ts +1 -1
  188. package/dist/provenance.d.ts +1 -1
  189. package/dist/provenance.js +1 -1
  190. package/dist/qmd-recall-cache.d.ts +1 -1
  191. package/dist/qmd.d.ts +1 -1
  192. package/dist/recall-disclosure-escalation.d.ts +1 -1
  193. package/dist/recall-explain-renderer.d.ts +1 -1
  194. package/dist/recall-explain-renderer.js +3 -3
  195. package/dist/recall-planner-llm.d.ts +1 -1
  196. package/dist/recall-state.d.ts +1 -1
  197. package/dist/recall-tag-filter.d.ts +1 -1
  198. package/dist/recall-xray-cli.d.ts +1 -1
  199. package/dist/recall-xray-cli.js +4 -4
  200. package/dist/recall-xray-renderer.d.ts +1 -1
  201. package/dist/recall-xray-renderer.js +3 -3
  202. package/dist/recall-xray.d.ts +1 -1
  203. package/dist/recall-xray.js +2 -2
  204. package/dist/resolve-auth-token.d.ts +1 -1
  205. package/dist/resume-bundles.js +4 -4
  206. package/dist/retrieval-agents.d.ts +1 -1
  207. package/dist/retrieval-tiers.d.ts +1 -1
  208. package/dist/routing/engine.d.ts +1 -1
  209. package/dist/routing/store.d.ts +1 -1
  210. package/dist/schemas.d.ts +22 -22
  211. package/dist/search/embed-helper.d.ts +1 -1
  212. package/dist/search/factory.d.ts +1 -1
  213. package/dist/search/factory.js +3 -3
  214. package/dist/search/index.d.ts +1 -1
  215. package/dist/search/index.js +7 -7
  216. package/dist/search/lancedb-backend.d.ts +1 -1
  217. package/dist/search/meilisearch-backend.d.ts +1 -1
  218. package/dist/search/noop-backend.d.ts +1 -1
  219. package/dist/search/orama-backend.d.ts +1 -1
  220. package/dist/search/port.d.ts +1 -1
  221. package/dist/search/remote-backend.d.ts +1 -1
  222. package/dist/{semantic-consolidation-B8LiYtvx.d.ts → semantic-consolidation-Cl15ksZy.d.ts} +1 -1
  223. package/dist/semantic-consolidation.d.ts +2 -2
  224. package/dist/semantic-consolidation.js +8 -8
  225. package/dist/semantic-rule-promotion.js +7 -7
  226. package/dist/semantic-rule-verifier.d.ts +1 -1
  227. package/dist/semantic-rule-verifier.js +7 -7
  228. package/dist/session-observer-bands.d.ts +1 -1
  229. package/dist/session-observer-state.d.ts +1 -1
  230. package/dist/shared-context/manager.d.ts +1 -1
  231. package/dist/signal.d.ts +1 -1
  232. package/dist/storage.d.ts +1 -1
  233. package/dist/storage.js +6 -6
  234. package/dist/summarizer.d.ts +1 -1
  235. package/dist/summary-snapshot.d.ts +1 -1
  236. package/dist/temporal-supersession.d.ts +1 -1
  237. package/dist/temporal-validity.d.ts +1 -1
  238. package/dist/threading.d.ts +1 -1
  239. package/dist/tier-migration.d.ts +1 -1
  240. package/dist/tier-routing.d.ts +1 -1
  241. package/dist/topics.d.ts +1 -1
  242. package/dist/transcript.d.ts +1 -1
  243. package/dist/transfer/backup.js +2 -2
  244. package/dist/transfer/capsule-export.js +2 -2
  245. package/dist/transfer/capsule-import.js +2 -2
  246. package/dist/transfer/types.d.ts +12 -12
  247. package/dist/trust-score-stage.d.ts +1 -1
  248. package/dist/trust-score.d.ts +1 -1
  249. package/dist/{types-DmTaqob2.d.ts → types-ChQb9D6s.d.ts} +8 -0
  250. package/dist/types.d.ts +1 -1
  251. package/dist/types.js +1 -1
  252. package/dist/utility-runtime.d.ts +1 -1
  253. package/dist/verified-recall.js +7 -7
  254. package/package.json +26 -2
  255. package/src/access-http.ts +68 -1
  256. package/src/admin/admin-surfaces.test.ts +289 -0
  257. package/src/admin/admin-surfaces.ts +138 -7
  258. package/src/bitemporal-dedup-backfill.test.ts +294 -0
  259. package/src/chat/chat-engine.test.ts +66 -1
  260. package/src/chat/chat-http.test.ts +119 -1
  261. package/src/chat/chat-http.ts +252 -97
  262. package/src/chat/chat-session.ts +143 -7
  263. package/src/compat/codex-cli-fallback.ts +23 -0
  264. package/src/compat/codex-subpath-aliases.test.ts +93 -0
  265. package/src/compat/codex-thread-key.ts +23 -0
  266. package/src/extraction-faithfulness.test.ts +240 -0
  267. package/src/extraction-faithfulness.ts +58 -6
  268. package/src/faithfulness-config.ts +16 -1
  269. package/src/local-model-endpoint.ts +24 -3
  270. package/src/orchestrator.ts +200 -8
  271. package/src/provenance-extraction.test.ts +266 -0
  272. package/src/provenance.ts +66 -16
  273. package/src/types.ts +8 -0
  274. package/dist/chunk-2NBP25BG.js.map +0 -1
  275. package/dist/chunk-6OIDDKAA.js.map +0 -1
  276. package/dist/chunk-7AG54AF3.js.map +0 -1
  277. package/dist/chunk-C4X4QGIO.js.map +0 -1
  278. package/dist/chunk-MDBZTH7N.js.map +0 -1
  279. package/dist/chunk-OAHOL3ZC.js.map +0 -1
  280. package/dist/chunk-XGTBNVIU.js.map +0 -1
  281. /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
  282. /package/dist/{chunk-53EEQYE5.js.map → chunk-2KAYTPPT.js.map} +0 -0
  283. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  284. /package/dist/{chunk-BIA7ELIB.js.map → chunk-36SEDFAU.js.map} +0 -0
  285. /package/dist/{chunk-UQSVUEN2.js.map → chunk-37436GML.js.map} +0 -0
  286. /package/dist/{chunk-RRHHQYJS.js.map → chunk-3TSTGI6F.js.map} +0 -0
  287. /package/dist/{chunk-FO3HO52S.js.map → chunk-4H4F3BFL.js.map} +0 -0
  288. /package/dist/{chunk-BEUCACTV.js.map → chunk-4NOI577P.js.map} +0 -0
  289. /package/dist/{chunk-BBIE7W52.js.map → chunk-4NR2PDZ5.js.map} +0 -0
  290. /package/dist/{chunk-PKMQK2WY.js.map → chunk-773DPI6T.js.map} +0 -0
  291. /package/dist/{chunk-MQVKHFIU.js.map → chunk-7RLCCNHU.js.map} +0 -0
  292. /package/dist/{chunk-EZNDNMRH.js.map → chunk-AQLOP5F4.js.map} +0 -0
  293. /package/dist/{chunk-UNNDFYUX.js.map → chunk-ARV3AUOM.js.map} +0 -0
  294. /package/dist/{chunk-NPL7VDM6.js.map → chunk-ASKXIAHD.js.map} +0 -0
  295. /package/dist/{chunk-K4ZNARDO.js.map → chunk-CBWI22JN.js.map} +0 -0
  296. /package/dist/{chunk-ZOHMPFOB.js.map → chunk-CKFLLAIS.js.map} +0 -0
  297. /package/dist/{chunk-DCKNMLEQ.js.map → chunk-CSRXQ6NB.js.map} +0 -0
  298. /package/dist/{chunk-6AFVIOVZ.js.map → chunk-CTOQEZSN.js.map} +0 -0
  299. /package/dist/{chunk-7NS7AWMZ.js.map → chunk-DKJCRGWM.js.map} +0 -0
  300. /package/dist/{chunk-JPUT5NGH.js.map → chunk-DMQWV6SQ.js.map} +0 -0
  301. /package/dist/{chunk-F3XZN24Z.js.map → chunk-DRUDC2UX.js.map} +0 -0
  302. /package/dist/{chunk-KJR66XOM.js.map → chunk-EY6MDMHK.js.map} +0 -0
  303. /package/dist/{chunk-3TUT4PWB.js.map → chunk-FVZ2ISIX.js.map} +0 -0
  304. /package/dist/{chunk-6SQQOJ4U.js.map → chunk-GKRLPHPU.js.map} +0 -0
  305. /package/dist/{chunk-5NWYQUQC.js.map → chunk-HOHVIVDR.js.map} +0 -0
  306. /package/dist/{chunk-6WAJQKBS.js.map → chunk-JYLFZWUX.js.map} +0 -0
  307. /package/dist/{chunk-5W7CTZ2E.js.map → chunk-KQAFEZQX.js.map} +0 -0
  308. /package/dist/{chunk-YTZ3VGC7.js.map → chunk-M4V2JVSZ.js.map} +0 -0
  309. /package/dist/{chunk-PLG6JHQS.js.map → chunk-MJIHJ7OG.js.map} +0 -0
  310. /package/dist/{chunk-TA3Y6GEX.js.map → chunk-MLKWJOAJ.js.map} +0 -0
  311. /package/dist/{chunk-UKTDXTWX.js.map → chunk-NSA5ZRUL.js.map} +0 -0
  312. /package/dist/{chunk-DQXPJ6HD.js.map → chunk-OEEW4KJA.js.map} +0 -0
  313. /package/dist/{chunk-5OOA7SGX.js.map → chunk-R7IYU5DZ.js.map} +0 -0
  314. /package/dist/{chunk-WGTWGDR5.js.map → chunk-SNKPUJHR.js.map} +0 -0
  315. /package/dist/{chunk-BB57OZWC.js.map → chunk-UVPL565C.js.map} +0 -0
  316. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  317. /package/dist/{chunk-V7CMKMHM.js.map → chunk-Y4YD5TYW.js.map} +0 -0
  318. /package/dist/{chunk-3MS3QVX6.js.map → chunk-YOLZS7NY.js.map} +0 -0
@@ -2771,16 +2771,28 @@ export class Orchestrator {
2771
2771
  dedupContent: string,
2772
2772
  bounds: {
2773
2773
  invalidAt?: string;
2774
+ // #1707 thread 2 — per-fact start bound (valid_at). Carried so a
2775
+ // re-extracted duplicate whose event time yields only a start bound
2776
+ // ("since 2024", "yesterday", an absolute date) gets the corrected
2777
+ // per-fact anchoring onto the existing copy.
2778
+ validFrom?: string;
2774
2779
  observedAt?: string;
2775
2780
  eventTimeSource?: "extracted" | "assumed";
2776
2781
  },
2777
2782
  entityRef?: string,
2778
2783
  ): Promise<void> {
2779
- // I/O gate: only scan when there is an end bound to backfill. Without
2780
- // invalidAt there is nothing that changes recall behavior observedAt
2781
- // and eventTimeSource alone do not expire a fact. This avoids a full
2782
- // readAllMemories on every dedup short-circuit under biTemporal.
2783
- if (!bounds.invalidAt) return;
2784
+ // I/O gate: scan when there is a recall-relevant bound to backfill
2785
+ // either an end bound (invalidAt, which expires the fact) or a corrected
2786
+ // EXTRACTED start bound. A start bound only changes recall when it is
2787
+ // extracted (the as-of filter excludes facts whose valid_at is after the
2788
+ // as-of instant); an "assumed" validFrom is just the ingestion anchor
2789
+ // (resolveFactEventTime sets one for every fact), so scanning on it would
2790
+ // run a full readAllMemories on every bi-temporal dedup hit for no benefit
2791
+ // (review cursor PRRT_OvHk / codex PRRT_OvHxVH). observedAt and
2792
+ // eventTimeSource alone never change recall.
2793
+ const hasExtractedStart =
2794
+ bounds.validFrom !== undefined && bounds.eventTimeSource === "extracted";
2795
+ if (!bounds.invalidAt && !hasExtractedStart) return;
2784
2796
  try {
2785
2797
  const incomingHash = ContentHashIndex.computeHash(dedupContent);
2786
2798
  const normalizedIncoming = ContentHashIndex.normalizeContent(dedupContent);
@@ -2839,6 +2851,37 @@ export class Orchestrator {
2839
2851
  ) {
2840
2852
  patch.eventTimeSource = bounds.eventTimeSource;
2841
2853
  }
2854
+ // #1707 thread 2 — per-fact-anchored start bound. A re-extracted
2855
+ // duplicate whose event time resolves a real start bound must carry
2856
+ // that anchor onto the existing copy so as-of recall uses the corrected
2857
+ // valid_at instead of a stale batch-anchored value. Only an EXTRACTED
2858
+ // bound corrects; an "assumed" bound is just the ingestion anchor.
2859
+ //
2860
+ // No-clobber via equality, not provenance inference: exact-content dedup
2861
+ // re-extracts the SAME event-time expression, which #1670 per-fact
2862
+ // anchoring resolves deterministically to the same validFrom — so for
2863
+ // stable content the incoming validFrom EQUALS the copy's valid_at and
2864
+ // we skip the redundant write (the only no-clobber that holds without a
2865
+ // fragile provenance heuristic — review codex PRRT_Ov7LKC). When they
2866
+ // differ (a prior batch/assumed anchor, end-only assumed start, or a
2867
+ // non-deterministic re-resolution), the extracted validFrom is the
2868
+ // authoritative correction and overwrites. The eventTimeSource upgrade
2869
+ // below records that the start is now extracted-anchored.
2870
+ if (
2871
+ bounds.validFrom &&
2872
+ bounds.eventTimeSource === "extracted" &&
2873
+ fm.valid_at !== bounds.validFrom
2874
+ ) {
2875
+ patch.valid_at = bounds.validFrom;
2876
+ // Mark the copy extracted-anchored in the SAME patch so its provenance
2877
+ // reflects the correction (review cursor PRRT_OvHM / codex PRRT_OvHxVD):
2878
+ // without this, a copy upgraded from "assumed" would keep "assumed"
2879
+ // provenance while carrying an extracted start. (The earlier
2880
+ // eventTimeSource block only fills an EMPTY source.)
2881
+ if (fm.eventTimeSource !== "extracted") {
2882
+ patch.eventTimeSource = "extracted";
2883
+ }
2884
+ }
2842
2885
  if (Object.keys(patch).length === 0) return;
2843
2886
  const ok = await targetStorage.writeMemoryFrontmatter(existing, patch);
2844
2887
  if (ok) {
@@ -14302,12 +14345,14 @@ export class Orchestrator {
14302
14345
  // lacks (re-extraction with a now-resolved invalidAt). Best-effort,
14303
14346
  // fail-open; the helper gates on invalidAt to avoid I/O when no
14304
14347
  // end bound is present.
14305
- if (options.invalidAt) {
14348
+ if (options.invalidAt || options.validAt) {
14306
14349
  await this.backfillTemporalBoundsOnDedupHit(
14307
14350
  targetStorage,
14308
14351
  dedupContent,
14309
14352
  {
14310
14353
  invalidAt: options.invalidAt,
14354
+ // #1707 thread 2 — carry the corrected start bound.
14355
+ validFrom: options.validAt,
14311
14356
  ...(options.observedAt ? { observedAt: options.observedAt } : {}),
14312
14357
  ...(options.eventTimeSource ? { eventTimeSource: options.eventTimeSource } : {}),
14313
14358
  },
@@ -14481,12 +14526,14 @@ export class Orchestrator {
14481
14526
  // before the supersession short-circuit. Covers all return paths below
14482
14527
  // (supersession-hit, catch-skip, and the no-supersession short-circuit)
14483
14528
  // in one shot. Best-effort / fail-open; the helper gates on invalidAt.
14484
- if (options.invalidAt) {
14529
+ if (options.invalidAt || options.validAt) {
14485
14530
  await this.backfillTemporalBoundsOnDedupHit(
14486
14531
  sharedStorage,
14487
14532
  dedupContent,
14488
14533
  {
14489
14534
  invalidAt: options.invalidAt,
14535
+ // #1707 thread 2 — carry the corrected start bound.
14536
+ validFrom: options.validAt,
14490
14537
  ...(options.observedAt ? { observedAt: options.observedAt } : {}),
14491
14538
  ...(options.eventTimeSource ? { eventTimeSource: options.eventTimeSource } : {}),
14492
14539
  },
@@ -14717,6 +14764,116 @@ export class Orchestrator {
14717
14764
  );
14718
14765
  }
14719
14766
  };
14767
+ // #1707 thread 1 — backfill temporal bounds onto promotion copies when the
14768
+ // SOURCE-namespace dedup short-circuit fires. That branch patches the
14769
+ // source copy then `continue`s before the promotion dedup paths run, so
14770
+ // promoted shared/profile copies written before the source fact carried
14771
+ // resolved bounds stay stale (cross-namespace recall surfaces an expired
14772
+ // fact). This mirrors the promotion-target resolution used by the two
14773
+ // promote closures above and calls the same fail-open helper against each
14774
+ // target storage. Backfill-only: never writes a new promoted copy.
14775
+ const backfillTemporalBoundsOnPromotionCopies = async (args: {
14776
+ sourceStorage: StorageManager;
14777
+ content: string;
14778
+ category: string;
14779
+ confidence: number;
14780
+ entityRef?: string;
14781
+ structuredAttributes?: Record<string, string>;
14782
+ bounds: {
14783
+ invalidAt?: string;
14784
+ validFrom?: string;
14785
+ observedAt?: string;
14786
+ eventTimeSource?: "extracted" | "assumed";
14787
+ };
14788
+ }): Promise<void> => {
14789
+ // Mirror the helper's I/O gate: only resolve promotion targets when
14790
+ // there is an end bound OR an EXTRACTED start bound. An "assumed"
14791
+ // validFrom is just the ingestion anchor (no recall effect), so
14792
+ // skipping it avoids resolving target storages on every bi-temporal
14793
+ // duplicate (review cursor PRRT_OvHk).
14794
+ const hasExtractedStart =
14795
+ args.bounds.validFrom !== undefined &&
14796
+ args.bounds.eventTimeSource === "extracted";
14797
+ if (!args.bounds.invalidAt && !hasExtractedStart) return;
14798
+ // Build the same dedupContent the promotion functions hash on so the
14799
+ // content-hash lookup in the helper matches what was stored.
14800
+ const rawContent =
14801
+ citationEnabled && hasCitationForTemplate(args.content, citationTemplate)
14802
+ ? stripCitationForTemplate(args.content, citationTemplate)
14803
+ : args.content;
14804
+ const sanitizedBase = sanitizeMemoryContent(rawContent);
14805
+ const dedupContent =
14806
+ args.category === "fact" &&
14807
+ args.structuredAttributes &&
14808
+ Object.keys(args.structuredAttributes).length > 0
14809
+ ? `${sanitizedBase.text}\n[Attributes: ${normalizeAttributePairs(args.structuredAttributes)}]`
14810
+ : sanitizedBase.text;
14811
+ // Profile targets. NOTE: we do NOT gate on profileAutoPromotionAllows —
14812
+ // a promoted profile copy may exist from an EARLIER extraction with
14813
+ // higher confidence or older auto-promote settings, so the current
14814
+ // duplicate's confidence must not skip backfilling an already-existing
14815
+ // promoted copy (review codex PRRT_Ov7LKF). The helper no-ops when no
14816
+ // matching copy is found, so resolving all configured auto-promote
14817
+ // targets is safe.
14818
+ if (scopeProfileWritePlan) {
14819
+ const autoTargets = new Set(
14820
+ scopeProfileWritePlan.profile.autoPromote.targets,
14821
+ );
14822
+ const profileTargets = scopeProfileWritePlan.promotionTargets.filter(
14823
+ (target) =>
14824
+ target.target !== "serverShared" &&
14825
+ autoTargets.has(target.target) &&
14826
+ target.authorized &&
14827
+ target.namespace,
14828
+ );
14829
+ for (const target of profileTargets) {
14830
+ if (!target.namespace) continue;
14831
+ try {
14832
+ const targetStorage = await this.storageRouter.storageFor(
14833
+ target.namespace,
14834
+ );
14835
+ if (targetStorage.dir === args.sourceStorage.dir) continue;
14836
+ await this.backfillTemporalBoundsOnDedupHit(
14837
+ targetStorage,
14838
+ dedupContent,
14839
+ args.bounds,
14840
+ args.entityRef,
14841
+ );
14842
+ } catch (err) {
14843
+ log.warn(
14844
+ `bitemporal-backfill: profile-target backfill failed open for ${target.target}: ${err}`,
14845
+ );
14846
+ }
14847
+ }
14848
+ }
14849
+ // Shared target. A shared copy may exist from an earlier extraction
14850
+ // regardless of the current confidence, so do not gate on
14851
+ // shouldPromoteToShared (review codex PRRT_Ov7LKF) — BUT still respect
14852
+ // shared-write AUTHORIZATION: a scoped profile that does not authorize
14853
+ // serverShared writes must not have its shared namespace backfilled
14854
+ // (review codex PRRT_Ov7dHR). profileAllowsSharedWrites is true for the
14855
+ // legacy no-scope case and encodes the readable/writable/authorized
14856
+ // checks under a scope profile.
14857
+ if (profileAllowsSharedWrites) {
14858
+ try {
14859
+ const sharedStorage = await this.storageRouter.storageFor(
14860
+ this.config.sharedNamespace,
14861
+ );
14862
+ if (sharedStorage.dir !== args.sourceStorage.dir) {
14863
+ await this.backfillTemporalBoundsOnDedupHit(
14864
+ sharedStorage,
14865
+ dedupContent,
14866
+ args.bounds,
14867
+ args.entityRef,
14868
+ );
14869
+ }
14870
+ } catch (err) {
14871
+ log.warn(
14872
+ `bitemporal-backfill: shared-target backfill failed open: ${err}`,
14873
+ );
14874
+ }
14875
+ }
14876
+ };
14720
14877
 
14721
14878
  // Defensive: validate result and facts array
14722
14879
  if (!result || !Array.isArray(result.facts)) {
@@ -15226,7 +15383,20 @@ export class Orchestrator {
15226
15383
  // Skip when the fact would be rejected/deferred/pending by downstream
15227
15384
  // gates — a non-durable candidate must not expire an active fact
15228
15385
  // (chatgpt-codex P1: faithfulness, requireSpans, extraction judge).
15229
- if (biTemporal && biTemporal.validUntil) {
15386
+ // #1671 + #1707: backfill bi-temporal bounds onto the existing
15387
+ // source-namespace copy if it lacks bounds the incoming fact now
15388
+ // carries (re-extraction with a resolved bound). #1707 thread 3:
15389
+ // gate on writeCategory === "fact" — the helper only matches facts,
15390
+ // so a non-fact duplicate must not reach the fact-only scan.
15391
+ // #1707 thread 2: also fire when only a corrected start bound
15392
+ // (validFrom) is present, not just an end bound (validUntil). The
15393
+ // downstream-gate skip still applies — a non-durable candidate must
15394
+ // not expire an active fact (chatgpt-codex P1).
15395
+ if (
15396
+ biTemporal &&
15397
+ writeCategory === "fact" &&
15398
+ (biTemporal.validUntil || biTemporal.validFrom)
15399
+ ) {
15230
15400
  const fr = faithfulnessResultsByFactIndex?.get(factLoopIndex);
15231
15401
  const faithfulnessWouldPending =
15232
15402
  faithfulnessMode === "enforce" &&
@@ -15254,11 +15424,33 @@ export class Orchestrator {
15254
15424
  contentHashDedupKey,
15255
15425
  {
15256
15426
  invalidAt: biTemporal.validUntil,
15427
+ // #1707 thread 2 — carry the corrected start bound too.
15428
+ validFrom: biTemporal.validFrom,
15257
15429
  observedAt: biTemporal.observedAt,
15258
15430
  eventTimeSource: biTemporal.eventTimeSource,
15259
15431
  },
15260
15432
  fact.entityRef,
15261
15433
  );
15434
+ // #1707 thread 1 — the source branch short-circuits (`continue`
15435
+ // below) before the promotion dedup paths run, so promoted
15436
+ // shared/profile copies written before the source fact carried
15437
+ // resolved bounds stay stale and cross-namespace recall surfaces
15438
+ // an expired fact. Backfill the promotion targets too (fail-open,
15439
+ // backfill-only — never writes a new promoted copy).
15440
+ await backfillTemporalBoundsOnPromotionCopies({
15441
+ sourceStorage: targetStorage,
15442
+ content: fact.content,
15443
+ category: writeCategory,
15444
+ confidence: fact.confidence,
15445
+ entityRef: fact.entityRef,
15446
+ structuredAttributes: fact.structuredAttributes,
15447
+ bounds: {
15448
+ invalidAt: biTemporal.validUntil,
15449
+ validFrom: biTemporal.validFrom,
15450
+ observedAt: biTemporal.observedAt,
15451
+ eventTimeSource: biTemporal.eventTimeSource,
15452
+ },
15453
+ });
15262
15454
  }
15263
15455
  }
15264
15456
  log.debug(
@@ -836,3 +836,269 @@ test("toStrictIsoTimestamp path: valid MM/DD/YYYY still normalizes (thread dH47
836
836
  /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/,
837
837
  );
838
838
  });
839
+
840
+
841
+ // ---------------------------------------------------------------------------
842
+ // Regression: issue #1657 — partial year-led timestamps must not be admitted
843
+ // as fabricated complete dates. Date.parse fills in missing calendar
844
+ // components ("2026-05" -> May 1) and the ymd guard misparses the date/time
845
+ // boundary ("2026-05T10:00" reads the hour as the day). The normalization
846
+ // path must reject any year-led shape with fewer than three complete calendar
847
+ // components rather than persisting a fabricated observedAt.
848
+ // ---------------------------------------------------------------------------
849
+
850
+ test("toStrictIsoTimestamp path: partial YYYY-MM timestamp is rejected, not fabricated (issue #1657)", () => {
851
+ // "2026-05" is Date.parse-able (-> 2026-05-01) and passes the separator
852
+ // guard, so pre-fix the write path persisted a fabricated observedAt of
853
+ // 2026-05-01T00:00:00.000Z. Reject it so the source is dropped.
854
+ const turns = [
855
+ makeTurn("We migrated the production database to pgBouncer.", {
856
+ timestamp: "2026-05" as unknown as string,
857
+ }),
858
+ ];
859
+ const result = buildFactProvenance(
860
+ "migrated the production database to pgBouncer",
861
+ turns,
862
+ DEFAULT_CONFIG,
863
+ );
864
+ // No verified source: the only matching turn has a partial timestamp.
865
+ assert.equal(result.provenance, "unverified");
866
+ assert.ok(result.sources);
867
+ assert.notEqual(
868
+ result.sources![0]!.observedAt,
869
+ "2026-05-01T00:00:00.000Z",
870
+ "partial YYYY-MM timestamp must not fabricate May 1",
871
+ );
872
+ assert.equal(
873
+ result.sources![0]!.observedAt,
874
+ new Date(0).toISOString(),
875
+ "rejected partial timestamp falls back to epoch",
876
+ );
877
+ });
878
+
879
+ test("toStrictIsoTimestamp path: partial YYYY-MM T HH:MM is rejected, not misparsed (issue #1657)", () => {
880
+ // "2026-05T10:00" is Date.parse-able; the ymd regex grabs the hour "10" as
881
+ // the day across the date/time boundary, so pre-fix the write path persisted
882
+ // a fabricated observedAt (on May 10, shifted by timezone). Reject it.
883
+ const turns = [
884
+ makeTurn("We migrated the production database to pgBouncer.", {
885
+ timestamp: "2026-05T10:00" as unknown as string,
886
+ }),
887
+ ];
888
+ const result = buildFactProvenance(
889
+ "migrated the production database to pgBouncer",
890
+ turns,
891
+ DEFAULT_CONFIG,
892
+ );
893
+ assert.equal(result.provenance, "unverified");
894
+ assert.ok(result.sources);
895
+ assert.equal(
896
+ result.sources![0]!.observedAt,
897
+ new Date(0).toISOString(),
898
+ "partial YYYY-MM T HH:MM timestamp must fall back to epoch, not a fabricated date",
899
+ );
900
+ });
901
+
902
+ test("toStrictIsoTimestamp path: complete YYYY-MM-DD (no time) still normalizes (issue #1657 non-regression)", () => {
903
+ // A complete calendar date with no time component must still round-trip
904
+ // through Date.parse to strict ISO. This is the boundary the partial-date
905
+ // guard must NOT over-reject.
906
+ const turns = [
907
+ makeTurn("We migrated the production database to pgBouncer.", {
908
+ timestamp: "2026-05-03" as unknown as string,
909
+ }),
910
+ ];
911
+ const result = buildFactProvenance(
912
+ "migrated the production database to pgBouncer",
913
+ turns,
914
+ DEFAULT_CONFIG,
915
+ );
916
+ assert.equal(result.provenance, "verified");
917
+ assert.ok(result.sources);
918
+ assert.equal(
919
+ result.sources![0]!.observedAt,
920
+ "2026-05-03T00:00:00.000Z",
921
+ "complete date-only timestamp normalizes to UTC midnight",
922
+ );
923
+ });
924
+
925
+ test("toStrictIsoTimestamp path: space-separated partial YYYY-MM HH:MM is rejected (issue #1657, codex r2)", () => {
926
+ // "2026-05 10:00" is Date.parse-able; the date separator must be a real
927
+ // date separator (- or /), not the space that precedes the time component.
928
+ // Pre-fix the guard treated the space as a date separator and captured the
929
+ // hour "10" as the day, persisting a fabricated 2026-05-(10) source.
930
+ const turns = [
931
+ makeTurn("We migrated the production database to pgBouncer.", {
932
+ timestamp: "2026-05 10:00" as unknown as string,
933
+ }),
934
+ ];
935
+ const result = buildFactProvenance(
936
+ "migrated the production database to pgBouncer",
937
+ turns,
938
+ DEFAULT_CONFIG,
939
+ );
940
+ assert.equal(result.provenance, "unverified");
941
+ assert.ok(result.sources);
942
+ assert.equal(
943
+ result.sources![0]!.observedAt,
944
+ new Date(0).toISOString(),
945
+ "space-separated partial timestamp must fall back to epoch, not a fabricated date",
946
+ );
947
+ });
948
+
949
+ test("toStrictIsoTimestamp path: leading-whitespace partial timestamp is rejected (issue #1657, codex r3)", () => {
950
+ // Date.parse trims leading whitespace, so " 2026-05" parses as May 1 —
951
+ // but the ^\d{4} guard anchored at the start would not recognize the
952
+ // string as year-led, bypassing the partial-date rejection. Trim first.
953
+ const turns = [
954
+ makeTurn("We migrated the production database to pgBouncer.", {
955
+ timestamp: " 2026-05" as unknown as string,
956
+ }),
957
+ ];
958
+ const result = buildFactProvenance(
959
+ "migrated the production database to pgBouncer",
960
+ turns,
961
+ DEFAULT_CONFIG,
962
+ );
963
+ assert.equal(result.provenance, "unverified");
964
+ assert.ok(result.sources);
965
+ assert.equal(
966
+ result.sources![0]!.observedAt,
967
+ new Date(0).toISOString(),
968
+ "leading-whitespace partial timestamp must fall back to epoch, not a fabricated date",
969
+ );
970
+ });
971
+
972
+ test("toStrictIsoTimestamp path: complete year-first textual-month date is preserved (issue #1657, codex r4)", () => {
973
+ // A complete year-first TEXTUAL-month date ("2026-Jan-15") is normalized by
974
+ // Date.parse and must stay verified — the unified guard must not reject a
975
+ // complete textual month. Date.parse parses this non-ISO form in the LOCAL
976
+ // timezone, so assert the source is preserved (verified, not the epoch
977
+ // fallback) and normalized to strict ISO — without assuming the UTC day,
978
+ // which shifts across timezones (codex r7).
979
+ const turns = [
980
+ makeTurn("We migrated the production database to pgBouncer.", {
981
+ timestamp: "2026-Jan-15" as unknown as string,
982
+ }),
983
+ ];
984
+ const result = buildFactProvenance(
985
+ "migrated the production database to pgBouncer",
986
+ turns,
987
+ DEFAULT_CONFIG,
988
+ );
989
+ assert.equal(result.provenance, "verified", "complete textual-month date must stay verified");
990
+ assert.ok(result.sources);
991
+ const observedAt = result.sources![0]!.observedAt;
992
+ assert.match(
993
+ observedAt,
994
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/,
995
+ "textual-month observedAt must normalize to strict ISO",
996
+ );
997
+ assert.notEqual(
998
+ observedAt,
999
+ new Date(0).toISOString(),
1000
+ "complete textual-month date must not fall back to the epoch (it was accepted, not rejected)",
1001
+ );
1002
+ });
1003
+
1004
+ test("toStrictIsoTimestamp path: partial textual year-month is rejected (issue #1657, codex r5)", () => {
1005
+ // "2026-Jan" is Date.parse-able (-> Jan 1) — a partial year-led TEXTUAL
1006
+ // month that must be rejected just like the numeric "2026-05", not
1007
+ // fabricated. The unified year-led guard covers textual months too.
1008
+ const turns = [
1009
+ makeTurn("We migrated the production database to pgBouncer.", {
1010
+ timestamp: "2026-Jan" as unknown as string,
1011
+ }),
1012
+ ];
1013
+ const result = buildFactProvenance(
1014
+ "migrated the production database to pgBouncer",
1015
+ turns,
1016
+ DEFAULT_CONFIG,
1017
+ );
1018
+ assert.equal(result.provenance, "unverified");
1019
+ assert.equal(
1020
+ result.sources![0]!.observedAt,
1021
+ new Date(0).toISOString(),
1022
+ "partial textual year-month must fall back to epoch, not a fabricated Jan 1",
1023
+ );
1024
+ });
1025
+
1026
+ test("toStrictIsoTimestamp path: partial textual year-month with time is rejected (issue #1657, codex r5)", () => {
1027
+ // "2026-Jan 10:00" — the hour must not be captured as the day across the
1028
+ // date/time boundary (textual analogue of "2026-05 10:00").
1029
+ const turns = [
1030
+ makeTurn("We migrated the production database to pgBouncer.", {
1031
+ timestamp: "2026-Jan 10:00" as unknown as string,
1032
+ }),
1033
+ ];
1034
+ const result = buildFactProvenance(
1035
+ "migrated the production database to pgBouncer",
1036
+ turns,
1037
+ DEFAULT_CONFIG,
1038
+ );
1039
+ assert.equal(result.provenance, "unverified");
1040
+ assert.equal(
1041
+ result.sources![0]!.observedAt,
1042
+ new Date(0).toISOString(),
1043
+ "partial textual year-month with time must fall back to epoch",
1044
+ );
1045
+ });
1046
+
1047
+ test("toStrictIsoTimestamp path: textual-month calendar overflow is rejected (issue #1657, codex r5)", () => {
1048
+ // "2026-Feb-30" is a COMPLETE textual date but Feb 30 overflows — Date.parse
1049
+ // shifts it to March 2. The unified guard validates the textual month's
1050
+ // calendar day too, so this is rejected rather than fabricated (textual
1051
+ // analogue of the numeric dANc overflow).
1052
+ const turns = [
1053
+ makeTurn("We migrated the production database to pgBouncer.", {
1054
+ timestamp: "2026-Feb-30" as unknown as string,
1055
+ }),
1056
+ ];
1057
+ const result = buildFactProvenance(
1058
+ "migrated the production database to pgBouncer",
1059
+ turns,
1060
+ DEFAULT_CONFIG,
1061
+ );
1062
+ assert.equal(result.provenance, "unverified");
1063
+ assert.notEqual(
1064
+ result.sources![0]!.observedAt,
1065
+ "2026-03-02T00:00:00.000Z",
1066
+ "textual-month overflow must not be silently shifted to March 2",
1067
+ );
1068
+ assert.equal(
1069
+ result.sources![0]!.observedAt,
1070
+ new Date(0).toISOString(),
1071
+ "textual-month overflow falls back to epoch",
1072
+ );
1073
+ });
1074
+
1075
+ test("toStrictIsoTimestamp path: full month name is preserved (issue #1657, codex r6)", () => {
1076
+ // Date.parse accepts full month names ("2026-January-15"), so the textual
1077
+ // month map must include both abbreviations and full names — otherwise a
1078
+ // complete, valid full-name date is dropped to an unverified epoch source.
1079
+ // Date.parse parses this non-ISO form in the LOCAL timezone, so assert the
1080
+ // source is preserved (verified, not epoch) without a UTC-day assumption.
1081
+ const turns = [
1082
+ makeTurn("We migrated the production database to pgBouncer.", {
1083
+ timestamp: "2026-January-15" as unknown as string,
1084
+ }),
1085
+ ];
1086
+ const result = buildFactProvenance(
1087
+ "migrated the production database to pgBouncer",
1088
+ turns,
1089
+ DEFAULT_CONFIG,
1090
+ );
1091
+ assert.equal(result.provenance, "verified", "full-name textual month must stay verified");
1092
+ assert.ok(result.sources);
1093
+ const observedAt = result.sources![0]!.observedAt;
1094
+ assert.match(
1095
+ observedAt,
1096
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/,
1097
+ "full-name textual month observedAt must normalize to strict ISO",
1098
+ );
1099
+ assert.notEqual(
1100
+ observedAt,
1101
+ new Date(0).toISOString(),
1102
+ "full-name textual month must not fall back to the epoch (it was accepted, not rejected)",
1103
+ );
1104
+ });
package/src/provenance.ts CHANGED
@@ -188,33 +188,39 @@ function isStrictIsoTimestamp(s: string): boolean {
188
188
  */
189
189
  function toStrictIsoTimestamp(ts: string | undefined | null): string | undefined {
190
190
  if (typeof ts !== "string" || ts.length === 0) return undefined;
191
- if (isStrictIsoTimestamp(ts)) return ts;
192
- const parsed = Date.parse(ts);
191
+ // Trim surrounding whitespace: Date.parse accepts leading/trailing
192
+ // whitespace, so an imported " 2026-05" would otherwise bypass the
193
+ // year-led partial-date guard below and fabricate May 1
194
+ // (chatgpt-codex-connector review on #1714, issue #1657).
195
+ const value = ts.trim();
196
+ if (value.length === 0) return undefined;
197
+ if (isStrictIsoTimestamp(value)) return value;
198
+ const parsed = Date.parse(value);
193
199
  if (Number.isNaN(parsed)) return undefined;
194
200
  // Reject bare-year / numeric-only strings that Date.parse accepts (e.g.
195
201
  // "123") — isStrictIsoTimestamp already rejected them, and the round-trip
196
202
  // below would otherwise resurrect them. Require at least one date/time
197
203
  // separator so a plain number never round-trips into a fake epoch.
198
- if (!/[-:T]/.test(ts)) return undefined;
199
- // Reject calendar-overflow dates (chatgpt-codex-connector thread dANc):
200
- // Date.parse silently rolls over invalid components "2026-02-30" becomes
201
- // 2026-03-02 fabricating the observation date. For strings that carry an
202
- // explicit numeric Y/M/D prefix (the common import/provider shape), validate
203
- // the calendar components are in range before accepting the shifted result.
204
- // isStrictIsoTimestamp already does this for full ISO strings; this closes
205
- // the gap for the non-ISO normalization path.
206
- const ymd = /^(\d{4})\D(\d{1,2})\D(\d{1,2})/.exec(ts);
207
- if (ymd) {
208
- const [_, ys, ms, ds] = ymd;
209
- const y = Number(ys), mo = Number(ms), da = Number(ds);
210
- if (!isValidCalendarDate(y, mo, da)) return undefined;
204
+ if (!/[-:T]/.test(value)) return undefined;
205
+ // Reject year-led timestamps that are not a COMPLETE, calendar-valid date
206
+ // (issue #1657). Date.parse silently fills missing components ("2026-05" ->
207
+ // May 1, "2026-Jan" -> Jan 1) and rolls overflow ("2026-02-30" and
208
+ // "2026-Feb-30" -> March 2), fabricating observedAt. `parseYearLedDatePrefix`
209
+ // requires a complete three-component date numeric OR textual month — with
210
+ // real date separators (not T/:, and not the space that precedes a time
211
+ // component, so the hour is never captured as the day) and a calendar-valid
212
+ // day, in one rule (subsumes the prior partial-numeric and numeric-overflow
213
+ // checks, and extends them to textual months). Non-year-led shapes
214
+ // ("Jan 15 2026", "02/30/2026") fall through to Date.parse / the mdy check.
215
+ if (/^\d{4}\D/.test(value) && parseYearLedDatePrefix(value) === null) {
216
+ return undefined;
211
217
  }
212
218
  // Also validate M/D/Y or D/M/Y formats (provider/import common shapes:
213
219
  // 02/30/2026, 30-02-2026, etc.). Date.parse silently shifts overflow in
214
220
  // these too. Try both month-first and day-first interpretations; if
215
221
  // neither is a valid calendar date, reject (chatgpt-codex-connector thread
216
222
  // dH47 — non-YMD overflow timestamps).
217
- const mdy = /^(\d{1,2})\D(\d{1,2})\D(\d{4})/.exec(ts);
223
+ const mdy = /^(\d{1,2})\D(\d{1,2})\D(\d{4})/.exec(value);
218
224
  if (mdy) {
219
225
  const a = Number(mdy[1]), b = Number(mdy[2]), yr = Number(mdy[3]);
220
226
  if (!isValidCalendarDate(yr, a, b) && !isValidCalendarDate(yr, b, a)) {
@@ -238,6 +244,50 @@ function isValidCalendarDate(y: number, mo: number, da: number): boolean {
238
244
  return da <= daysInMonth[mo - 1]!;
239
245
  }
240
246
 
247
+ /**
248
+ * Recognized month-name → number map for year-led textual-month dates
249
+ * (issue #1657). Both the 3-letter abbreviations and the full names that
250
+ * Date.parse accepts for year-first shapes (e.g. "2026-Jan-15",
251
+ * "2026-January-15", "2026 February 2").
252
+ */
253
+ const MONTH_NAMES: Record<string, number> = {
254
+ jan: 1, january: 1,
255
+ feb: 2, february: 2,
256
+ mar: 3, march: 3,
257
+ apr: 4, april: 4,
258
+ may: 5,
259
+ jun: 6, june: 6,
260
+ jul: 7, july: 7,
261
+ aug: 8, august: 8,
262
+ sep: 9, september: 9,
263
+ sept: 9,
264
+ oct: 10, october: 10,
265
+ nov: 11, november: 11,
266
+ dec: 12, december: 12,
267
+ };
268
+
269
+ /**
270
+ * Parse a complete, calendar-valid year-led date prefix from `s`
271
+ * (issue #1657): `YYYY <sep> (MM | MonthName) <sep> DD` where the separators
272
+ * are real date separators (non-digit, non-T, non-colon) and the day is a
273
+ * true calendar day — not the hour before a `:` time separator (the
274
+ * `(?![\d:])` lookahead rejects "2026-05 10:00" capturing "10" as the day).
275
+ * Returns the numeric components for a complete, valid date; `null` for an
276
+ * incomplete ("2026-05", "2026-Jan"), overflowed ("2026-02-30",
277
+ * "2026-Feb-30"), or non-year-led prefix. Centralizes the fabricated-date
278
+ * rejection for the toStrictIsoTimestamp normalization path across numeric
279
+ * and textual months in one rule.
280
+ */
281
+ function parseYearLedDatePrefix(s: string): { y: number; mo: number; da: number } | null {
282
+ const m = /^(\d{4})[^0-9T](\d{1,2}|[A-Za-z]{3,})[^0-9T:](\d{1,2})(?![\d:])/.exec(s);
283
+ if (!m) return null;
284
+ const y = Number(m[1]);
285
+ const da = Number(m[3]);
286
+ const mo = /^\d+$/.test(m[2]) ? Number(m[2]) : (MONTH_NAMES[m[2]!.toLowerCase()] ?? -1);
287
+ if (!isValidCalendarDate(y, mo, da)) return null;
288
+ return { y, mo, da };
289
+ }
290
+
241
291
  /**
242
292
  * Zod schema for a single `ProvenanceSource` entry (issue #1575). Parsed
243
293
  * JSON from frontmatter is external data, so each entry is validated here
package/src/types.ts CHANGED
@@ -1222,6 +1222,14 @@ export interface PluginConfig {
1222
1222
  extractionFaithfulnessContextChars: number;
1223
1223
  /** Per-batch timeout in ms; timeout → tagged error, never blocks writes. Default 8000. */
1224
1224
  extractionFaithfulnessTimeoutMs: number;
1225
+ /**
1226
+ * Resilient-fallback toggle for the local model-lab endpoint (issue #1700
1227
+ * nit #6). When the local endpoint returns 200 with non-verdict JSON, the
1228
+ * default (false) surfaces `malformed_output` so a misconfigured endpoint is
1229
+ * visible to the operator. Set true to instead fall back to the configured
1230
+ * chain on local parse failure (pre-validated via parseFaithfulnessResponse).
1231
+ */
1232
+ extractionFaithfulnessLocalParseFallback: boolean;
1225
1233
  /**
1226
1234
  * Correction-intent model-lab pointer (issue #1581 / #1585). When both this
1227
1235
  * and `correctionIntentBaseUrl` are set, the detection path can route to a