@remnic/core 9.69.38 → 9.69.39

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 (245) hide show
  1. package/dist/access-admin-ops-surface.d.ts +2 -2
  2. package/dist/access-admin-ops-surface.js +18 -18
  3. package/dist/access-authorization-probe.d.ts +2 -2
  4. package/dist/access-authorization-probe.js +19 -19
  5. package/dist/access-boundary.d.ts +2 -2
  6. package/dist/access-boundary.js +18 -18
  7. package/dist/access-cli.js +67 -66
  8. package/dist/access-cli.js.map +1 -1
  9. package/dist/access-extraction-force-flush.d.ts +2 -2
  10. package/dist/access-extraction-force-flush.js +18 -18
  11. package/dist/access-http-briefing.d.ts +2 -2
  12. package/dist/access-http-briefing.js +19 -19
  13. package/dist/access-http-lcm-compaction.d.ts +2 -2
  14. package/dist/access-http-lifecycle-flush.d.ts +2 -2
  15. package/dist/access-http-offline-stream.d.ts +2 -2
  16. package/dist/access-http-query.js +20 -20
  17. package/dist/access-http.d.ts +3 -3
  18. package/dist/access-http.js +35 -35
  19. package/dist/access-identity-continuity-surface.d.ts +1 -1
  20. package/dist/access-identity-continuity-surface.js +18 -18
  21. package/dist/access-lcm-surface.d.ts +2 -2
  22. package/dist/access-lcm-surface.js +18 -18
  23. package/dist/access-mcp.d.ts +2 -2
  24. package/dist/access-mcp.js +25 -25
  25. package/dist/access-namespace-preflight.js +18 -18
  26. package/dist/access-observe-transcript.d.ts +1 -1
  27. package/dist/access-observe-write-surface.d.ts +2 -2
  28. package/dist/access-observe-write-surface.js +18 -18
  29. package/dist/access-offline-manifest.d.ts +1 -1
  30. package/dist/access-operations-batch.js +19 -19
  31. package/dist/access-operations.d.ts +2 -2
  32. package/dist/access-operations.js +24 -24
  33. package/dist/access-recall-concurrency.d.ts +2 -2
  34. package/dist/access-recall-concurrency.js +18 -18
  35. package/dist/access-recall-response.d.ts +2 -2
  36. package/dist/access-recall-response.js +18 -18
  37. package/dist/access-recall-surface.d.ts +2 -2
  38. package/dist/access-recall-surface.js +18 -18
  39. package/dist/{access-service-gBcdRUHY.d.ts → access-service-MY4bsr_Q.d.ts} +1 -1
  40. package/dist/access-service-helpers.d.ts +2 -2
  41. package/dist/access-service.d.ts +2 -2
  42. package/dist/access-service.js +18 -18
  43. package/dist/access-surface-catalog.d.ts +2 -2
  44. package/dist/access-surface-catalog.js +20 -20
  45. package/dist/active-recall.js +2 -2
  46. package/dist/adapters/index.js +4 -4
  47. package/dist/adapters/registry.js +2 -2
  48. package/dist/bootstrap.d.ts +1 -1
  49. package/dist/briefing.js +7 -7
  50. package/dist/causal-consolidation.js +8 -8
  51. package/dist/{chunk-2PTNJTBE.js → chunk-2ZTDRNCS.js} +2 -2
  52. package/dist/{chunk-BX4EOMKT.js → chunk-3EKVN2OP.js} +2 -2
  53. package/dist/{chunk-DPMTLJCT.js → chunk-3NPNBVFR.js} +2 -2
  54. package/dist/{chunk-OKO57IXB.js → chunk-4JVQRFZD.js} +4 -4
  55. package/dist/{chunk-6PRY6CLC.js → chunk-4RZLTZVM.js} +1 -1
  56. package/dist/{chunk-PPQEP4OH.js → chunk-54NX5EG4.js} +2 -2
  57. package/dist/{chunk-2OGTN37H.js → chunk-5SPCMAUC.js} +3 -3
  58. package/dist/{chunk-TUKGXQ5T.js → chunk-5YCNDC3C.js} +2 -2
  59. package/dist/chunk-6AGWBKQU.js +94 -0
  60. package/dist/chunk-6AGWBKQU.js.map +1 -0
  61. package/dist/{chunk-BJEJUOGM.js → chunk-6YAIN5XJ.js} +2 -2
  62. package/dist/{chunk-X2NFFBTO.js → chunk-A7GQBJP5.js} +4 -4
  63. package/dist/{chunk-ZKZFFWLV.js → chunk-AAZEKXUP.js} +2 -2
  64. package/dist/{chunk-6ZMNTRU3.js → chunk-AENM6GKU.js} +2 -2
  65. package/dist/{chunk-7S53BPTU.js → chunk-CZTPU65V.js} +8 -8
  66. package/dist/{chunk-UQNYPK7G.js → chunk-D545Z7UH.js} +2 -2
  67. package/dist/{chunk-GT4ZHOKT.js → chunk-ENBPQEVU.js} +2 -2
  68. package/dist/{chunk-TRHBQVVO.js → chunk-EYKIDK33.js} +11 -11
  69. package/dist/{chunk-EY5JAWZC.js → chunk-F4EHXVP7.js} +2 -2
  70. package/dist/{chunk-474DL7UB.js → chunk-FNHLI2RE.js} +3 -3
  71. package/dist/{chunk-2MT4ADLQ.js → chunk-FWWWUPOE.js} +2 -2
  72. package/dist/{chunk-M3MYMWXG.js → chunk-FXHOZGSE.js} +2 -2
  73. package/dist/{chunk-VHOSM5G4.js → chunk-GG5FHZJJ.js} +2 -2
  74. package/dist/{chunk-CBFYCDYO.js → chunk-GI4ZBWPQ.js} +3 -3
  75. package/dist/{chunk-KDFDXEXZ.js → chunk-GMIQV6DD.js} +2 -2
  76. package/dist/{chunk-XGRXQR6T.js → chunk-IL6ODG6M.js} +2 -2
  77. package/dist/{chunk-XWLLFKHM.js → chunk-IQEJIOEU.js} +7 -7
  78. package/dist/{chunk-MLWMR2GE.js → chunk-JKKII6GX.js} +2 -2
  79. package/dist/{chunk-T32B72QX.js → chunk-JTQR6L2N.js} +2 -2
  80. package/dist/{chunk-BRZWWUCJ.js → chunk-LMLGBG6H.js} +2 -2
  81. package/dist/{chunk-3OYSIVOO.js → chunk-LPYYY4AP.js} +152 -112
  82. package/dist/chunk-LPYYY4AP.js.map +1 -0
  83. package/dist/{chunk-A6CKMIPK.js → chunk-MUTVRKKY.js} +8 -8
  84. package/dist/{chunk-JPOX2AJB.js → chunk-NXGSONWM.js} +2 -2
  85. package/dist/{chunk-GGAXFTNH.js → chunk-O4M4BDIJ.js} +2 -2
  86. package/dist/{chunk-VV4ZVUE6.js → chunk-O76F5QPP.js} +2 -2
  87. package/dist/{chunk-G43PMSME.js → chunk-OPBFP4TF.js} +3 -3
  88. package/dist/{chunk-JNSENHV5.js → chunk-PCOVEDPK.js} +3 -3
  89. package/dist/{chunk-VMNCZ627.js → chunk-Q432REND.js} +2 -2
  90. package/dist/{chunk-QMWKENNK.js → chunk-Q4AUIFUA.js} +4 -4
  91. package/dist/{chunk-74AHCL56.js → chunk-Q7CLGFA7.js} +2 -2
  92. package/dist/{chunk-5AERZWJD.js → chunk-QTQ6XKGT.js} +3 -3
  93. package/dist/{chunk-K5DA5TRG.js → chunk-RLJHJQUY.js} +2 -2
  94. package/dist/{chunk-GQQVEH6V.js → chunk-TA4C3XB7.js} +2 -2
  95. package/dist/{chunk-V6G2PSXA.js → chunk-TNZNSMVJ.js} +26 -26
  96. package/dist/{chunk-JA2QJOAX.js → chunk-TSP5WH72.js} +2 -2
  97. package/dist/{chunk-NWMUSKED.js → chunk-UHRZ324M.js} +3 -3
  98. package/dist/{chunk-62QIQEUC.js → chunk-UZEHHXYC.js} +13 -13
  99. package/dist/{chunk-TTSJ7WUC.js → chunk-VBSNUH4J.js} +2 -2
  100. package/dist/{chunk-R4VAUNNC.js → chunk-VJIE4D2I.js} +2 -2
  101. package/dist/{chunk-BYZKLEWG.js → chunk-VSL63CFJ.js} +5 -5
  102. package/dist/{chunk-4B3HBIFX.js → chunk-WBM2MVBK.js} +2 -2
  103. package/dist/{chunk-ULDBGA5W.js → chunk-WIWLX65T.js} +2 -2
  104. package/dist/{chunk-XN6XT6GR.js → chunk-X4TD4O2F.js} +2 -2
  105. package/dist/{chunk-UIOIDW3L.js → chunk-XH3HGI2N.js} +2 -2
  106. package/dist/chunk-YDVAM7S4.js +221 -0
  107. package/dist/chunk-YDVAM7S4.js.map +1 -0
  108. package/dist/{chunk-VELCIWJZ.js → chunk-YKREX63Z.js} +19 -10
  109. package/dist/chunk-YKREX63Z.js.map +1 -0
  110. package/dist/{chunk-HVS7TH2Y.js → chunk-ZBW7A667.js} +2 -2
  111. package/dist/{cli-BNg-TAlz.d.ts → cli-DGq0MNpO.d.ts} +2 -2
  112. package/dist/cli.d.ts +3 -3
  113. package/dist/cli.js +49 -49
  114. package/dist/coding/export-okf-codegraph.js +6 -6
  115. package/dist/compounding/engine.js +7 -7
  116. package/dist/config.js +2 -2
  117. package/dist/connectors/codex-materialize-runner.js +7 -7
  118. package/dist/connectors/index.js +7 -7
  119. package/dist/convergence-refresh.js +9 -9
  120. package/dist/corpus-watermark.js +9 -9
  121. package/dist/deep-recall-http-glue.d.ts +2 -2
  122. package/dist/deep-recall-mcp-tools.d.ts +2 -2
  123. package/dist/entity-retrieval.js +7 -7
  124. package/dist/explicit-capture.d.ts +1 -1
  125. package/dist/external-wiki-access.d.ts +2 -2
  126. package/dist/external-wiki-access.js +19 -19
  127. package/dist/external-wiki-mcp-tools.d.ts +2 -2
  128. package/dist/extraction.js +7 -7
  129. package/dist/harmonic-retrieval.js +1 -1
  130. package/dist/index.d.ts +6 -6
  131. package/dist/index.js +78 -69
  132. package/dist/lcm/engine.js +2 -2
  133. package/dist/lcm/index.js +4 -4
  134. package/dist/location/index.js +9 -9
  135. package/dist/maintenance/memory-governance.js +7 -7
  136. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  137. package/dist/maintenance/rebuild-memory-projection.js +8 -8
  138. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  139. package/dist/memory-subject-cli.d.ts +2 -2
  140. package/dist/memory-subject.d.ts +2 -2
  141. package/dist/namespaces/migrate.js +8 -8
  142. package/dist/namespaces/search.js +4 -4
  143. package/dist/namespaces/storage.js +7 -7
  144. package/dist/operator-doctor-corpus.js +10 -10
  145. package/dist/operator-toolkit.js +17 -17
  146. package/dist/orchestration/maintenance.js +15 -15
  147. package/dist/{orchestrator-DocO0_hw.d.ts → orchestrator-ComHLm49.d.ts} +13 -2
  148. package/dist/orchestrator.d.ts +1 -1
  149. package/dist/orchestrator.js +67 -66
  150. package/dist/{public-http-C0FoTiL_.d.ts → public-http-CuK4_Cb1.d.ts} +1 -1
  151. package/dist/recall-state-view-admission.js +4 -4
  152. package/dist/recall-state-view-anchors.d.ts +85 -0
  153. package/dist/recall-state-view-anchors.js +21 -0
  154. package/dist/recall-state-view-anchors.js.map +1 -0
  155. package/dist/recall-state-view-inject.js +2 -2
  156. package/dist/recall-state-view-render.js +2 -2
  157. package/dist/recall-state-view-widen.js +2 -2
  158. package/dist/recall-state-view-wire.js +3 -3
  159. package/dist/recall-state-view.d.ts +37 -1
  160. package/dist/recall-state-view.js +11 -3
  161. package/dist/resume-bundles.js +3 -3
  162. package/dist/search/factory.js +3 -3
  163. package/dist/search/index.js +7 -7
  164. package/dist/semantic-consolidation.js +8 -8
  165. package/dist/semantic-rule-promotion.js +7 -7
  166. package/dist/semantic-rule-verifier.js +7 -7
  167. package/dist/source-agent-qualifier.js +7 -7
  168. package/dist/standup/build.js +7 -7
  169. package/dist/storage.js +6 -6
  170. package/dist/support-passport/index.d.ts +5 -5
  171. package/dist/support-passport/index.js +21 -21
  172. package/dist/transfer/export-okf.js +7 -7
  173. package/dist/verified-recall.js +7 -7
  174. package/dist/who-knows-cli.d.ts +2 -2
  175. package/dist/who-knows-cli.js +8 -8
  176. package/dist/who-knows.d.ts +2 -2
  177. package/dist/who-knows.js +7 -7
  178. package/package.json +2 -2
  179. package/src/orchestration/recall-internal.ts +91 -74
  180. package/src/orchestration/recall-rerank-coordinator.ts +41 -4
  181. package/src/orchestration/recall-search-pipeline.ts +25 -18
  182. package/src/orchestration/recall-state-view-pipeline.test.ts +89 -0
  183. package/src/recall-rerank-coordinator.test.ts +153 -0
  184. package/src/recall-state-view-anchors.ts +163 -0
  185. package/src/recall-state-view-widen.ts +21 -7
  186. package/src/recall-state-view-zero-diff.test.ts +30 -0
  187. package/src/recall-state-view.test.ts +161 -0
  188. package/src/recall-state-view.ts +206 -35
  189. package/src/trust-score-recall-paths.test.ts +1 -1
  190. package/dist/chunk-255PC37A.js +0 -120
  191. package/dist/chunk-255PC37A.js.map +0 -1
  192. package/dist/chunk-3OYSIVOO.js.map +0 -1
  193. package/dist/chunk-VELCIWJZ.js.map +0 -1
  194. /package/dist/{chunk-2PTNJTBE.js.map → chunk-2ZTDRNCS.js.map} +0 -0
  195. /package/dist/{chunk-BX4EOMKT.js.map → chunk-3EKVN2OP.js.map} +0 -0
  196. /package/dist/{chunk-DPMTLJCT.js.map → chunk-3NPNBVFR.js.map} +0 -0
  197. /package/dist/{chunk-OKO57IXB.js.map → chunk-4JVQRFZD.js.map} +0 -0
  198. /package/dist/{chunk-6PRY6CLC.js.map → chunk-4RZLTZVM.js.map} +0 -0
  199. /package/dist/{chunk-PPQEP4OH.js.map → chunk-54NX5EG4.js.map} +0 -0
  200. /package/dist/{chunk-2OGTN37H.js.map → chunk-5SPCMAUC.js.map} +0 -0
  201. /package/dist/{chunk-TUKGXQ5T.js.map → chunk-5YCNDC3C.js.map} +0 -0
  202. /package/dist/{chunk-BJEJUOGM.js.map → chunk-6YAIN5XJ.js.map} +0 -0
  203. /package/dist/{chunk-X2NFFBTO.js.map → chunk-A7GQBJP5.js.map} +0 -0
  204. /package/dist/{chunk-ZKZFFWLV.js.map → chunk-AAZEKXUP.js.map} +0 -0
  205. /package/dist/{chunk-6ZMNTRU3.js.map → chunk-AENM6GKU.js.map} +0 -0
  206. /package/dist/{chunk-7S53BPTU.js.map → chunk-CZTPU65V.js.map} +0 -0
  207. /package/dist/{chunk-UQNYPK7G.js.map → chunk-D545Z7UH.js.map} +0 -0
  208. /package/dist/{chunk-GT4ZHOKT.js.map → chunk-ENBPQEVU.js.map} +0 -0
  209. /package/dist/{chunk-TRHBQVVO.js.map → chunk-EYKIDK33.js.map} +0 -0
  210. /package/dist/{chunk-EY5JAWZC.js.map → chunk-F4EHXVP7.js.map} +0 -0
  211. /package/dist/{chunk-474DL7UB.js.map → chunk-FNHLI2RE.js.map} +0 -0
  212. /package/dist/{chunk-2MT4ADLQ.js.map → chunk-FWWWUPOE.js.map} +0 -0
  213. /package/dist/{chunk-M3MYMWXG.js.map → chunk-FXHOZGSE.js.map} +0 -0
  214. /package/dist/{chunk-VHOSM5G4.js.map → chunk-GG5FHZJJ.js.map} +0 -0
  215. /package/dist/{chunk-CBFYCDYO.js.map → chunk-GI4ZBWPQ.js.map} +0 -0
  216. /package/dist/{chunk-KDFDXEXZ.js.map → chunk-GMIQV6DD.js.map} +0 -0
  217. /package/dist/{chunk-XGRXQR6T.js.map → chunk-IL6ODG6M.js.map} +0 -0
  218. /package/dist/{chunk-XWLLFKHM.js.map → chunk-IQEJIOEU.js.map} +0 -0
  219. /package/dist/{chunk-MLWMR2GE.js.map → chunk-JKKII6GX.js.map} +0 -0
  220. /package/dist/{chunk-T32B72QX.js.map → chunk-JTQR6L2N.js.map} +0 -0
  221. /package/dist/{chunk-BRZWWUCJ.js.map → chunk-LMLGBG6H.js.map} +0 -0
  222. /package/dist/{chunk-A6CKMIPK.js.map → chunk-MUTVRKKY.js.map} +0 -0
  223. /package/dist/{chunk-JPOX2AJB.js.map → chunk-NXGSONWM.js.map} +0 -0
  224. /package/dist/{chunk-GGAXFTNH.js.map → chunk-O4M4BDIJ.js.map} +0 -0
  225. /package/dist/{chunk-VV4ZVUE6.js.map → chunk-O76F5QPP.js.map} +0 -0
  226. /package/dist/{chunk-G43PMSME.js.map → chunk-OPBFP4TF.js.map} +0 -0
  227. /package/dist/{chunk-JNSENHV5.js.map → chunk-PCOVEDPK.js.map} +0 -0
  228. /package/dist/{chunk-VMNCZ627.js.map → chunk-Q432REND.js.map} +0 -0
  229. /package/dist/{chunk-QMWKENNK.js.map → chunk-Q4AUIFUA.js.map} +0 -0
  230. /package/dist/{chunk-74AHCL56.js.map → chunk-Q7CLGFA7.js.map} +0 -0
  231. /package/dist/{chunk-5AERZWJD.js.map → chunk-QTQ6XKGT.js.map} +0 -0
  232. /package/dist/{chunk-K5DA5TRG.js.map → chunk-RLJHJQUY.js.map} +0 -0
  233. /package/dist/{chunk-GQQVEH6V.js.map → chunk-TA4C3XB7.js.map} +0 -0
  234. /package/dist/{chunk-V6G2PSXA.js.map → chunk-TNZNSMVJ.js.map} +0 -0
  235. /package/dist/{chunk-JA2QJOAX.js.map → chunk-TSP5WH72.js.map} +0 -0
  236. /package/dist/{chunk-NWMUSKED.js.map → chunk-UHRZ324M.js.map} +0 -0
  237. /package/dist/{chunk-62QIQEUC.js.map → chunk-UZEHHXYC.js.map} +0 -0
  238. /package/dist/{chunk-TTSJ7WUC.js.map → chunk-VBSNUH4J.js.map} +0 -0
  239. /package/dist/{chunk-R4VAUNNC.js.map → chunk-VJIE4D2I.js.map} +0 -0
  240. /package/dist/{chunk-BYZKLEWG.js.map → chunk-VSL63CFJ.js.map} +0 -0
  241. /package/dist/{chunk-4B3HBIFX.js.map → chunk-WBM2MVBK.js.map} +0 -0
  242. /package/dist/{chunk-ULDBGA5W.js.map → chunk-WIWLX65T.js.map} +0 -0
  243. /package/dist/{chunk-XN6XT6GR.js.map → chunk-X4TD4O2F.js.map} +0 -0
  244. /package/dist/{chunk-UIOIDW3L.js.map → chunk-XH3HGI2N.js.map} +0 -0
  245. /package/dist/{chunk-HVS7TH2Y.js.map → chunk-ZBW7A667.js.map} +0 -0
@@ -3,9 +3,11 @@ import test from "node:test";
3
3
 
4
4
  import {
5
5
  annotateStateView,
6
+ capStateViewPackets,
6
7
  formatSupersededPrefix,
7
8
  isChangeOrientedQuery,
8
9
  parseRecallStateViews,
10
+ reconcileStateViewPairs,
9
11
  shouldWidenSuperseded,
10
12
  type StateViewChain,
11
13
  type StateViewResult,
@@ -271,3 +273,162 @@ test("transitive orphan chains collapse: A→B→(C absent) drops both A and B",
271
273
  "the fixpoint must drop A once B is dropped, not render a dangling historical",
272
274
  );
273
275
  });
276
+
277
+ test("#2859 reverse chain derives from the successor supersedes back-pointer when supersededBy is absent", () => {
278
+ const results = [
279
+ fact("new-job", { status: "active", supersedes: "old-job" }),
280
+ // Corrected row: no supersededBy, and even no "superseded" status —
281
+ // the ONLY link is the successor's back-pointer.
282
+ fact("old-job", {}),
283
+ ];
284
+ const labeled = annotateStateView(results, "when did the job title change", [], { enabled: true });
285
+ assert.deepEqual(
286
+ labeled.map((row) => [row.id, row.stateLabel, row.supersededBy]),
287
+ [
288
+ ["new-job", "current", undefined],
289
+ ["old-job", "historical", "new-job"],
290
+ ],
291
+ "the back-pointer must link the pair, admit the predecessor, and carry derived supersededBy",
292
+ );
293
+ });
294
+
295
+ test("#2859 back-pointer pair survives the orphan fixpoint in both directions", () => {
296
+ const results = [
297
+ fact("v3", { status: "active", supersedes: "v2" }),
298
+ fact("v2", { status: "superseded", supersedes: "v1" }),
299
+ fact("v1", {}),
300
+ ];
301
+ const labeled = annotateStateView(results, "what changed about the database", [], { enabled: true });
302
+ assert.deepEqual(
303
+ labeled.map((row) => [row.id, row.stateLabel]),
304
+ [
305
+ ["v3", "current"],
306
+ ["v2", "transition"],
307
+ ["v1", "historical"],
308
+ ],
309
+ );
310
+ });
311
+
312
+ test("#2859 asOf label uses the validity boundary, not the supersededAt write time", () => {
313
+ const pin = Date.parse("2026-08-05T00:00:00.000Z");
314
+ // Write time January, validity flip September: at an August pin the row
315
+ // was still the live fact even though supersededAt PREDATES the pin.
316
+ const predecessor = fact("old-job", {
317
+ status: "superseded",
318
+ supersededBy: "new-job",
319
+ supersededAt: "2026-01-01T00:00:00.000Z",
320
+ invalidAt: "2026-09-01T00:00:00.000Z",
321
+ });
322
+ const labeled = annotateStateView([predecessor], "when did the job title change", [], {
323
+ enabled: true,
324
+ asOfMs: pin,
325
+ });
326
+ assert.equal(
327
+ labeled[0]?.stateLabel,
328
+ "current",
329
+ "the boundary is invalidAt, so a pin before the flip labels the row current-at-snapshot",
330
+ );
331
+
332
+ // Past the boundary the same row labels historical (boundary <= pin).
333
+ const afterFlip = annotateStateView([predecessor], "when did the job title change", [], {
334
+ enabled: true,
335
+ asOfMs: Date.parse("2026-09-05T00:00:00.000Z"),
336
+ });
337
+ assert.equal(
338
+ afterFlip[0]?.stateLabel,
339
+ "historical",
340
+ "a pin past the validity flip labels the row historical",
341
+ );
342
+
343
+ // Legacy row without a validity window keeps the supersededAt fallback.
344
+ const legacy = fact("old-legacy", {
345
+ status: "superseded",
346
+ supersededBy: "new-legacy",
347
+ supersededAt: "2026-01-01T00:00:00.000Z",
348
+ });
349
+ const legacyLabel = annotateStateView([legacy], "when did the job title change", [], {
350
+ enabled: true,
351
+ asOfMs: pin,
352
+ });
353
+ assert.equal(
354
+ legacyLabel[0]?.stateLabel,
355
+ "historical",
356
+ "no invalidAt → the supersededAt write time is the only recorded boundary",
357
+ );
358
+ });
359
+
360
+ test("#2859 namespace-qualified identities: same id across namespaces never cross-anchors", () => {
361
+ // ns-a has the superseded predecessor; ns-b has an UNRELATED row that
362
+ // happens to reuse the successor id. Bare-id matching would admit the
363
+ // ns-a predecessor on the ns-b anchor.
364
+ const results = [
365
+ fact("m-1", { namespace: "ns-b", status: "active" }),
366
+ fact("m-0", { namespace: "ns-a", status: "superseded", supersededBy: "m-1" }),
367
+ ];
368
+ const labeled = annotateStateView(results, "when did this change", [], { enabled: true });
369
+ assert.deepEqual(
370
+ labeled.map((row) => row.id),
371
+ ["m-1"],
372
+ "a foreign-namespace successor must not anchor the superseded row",
373
+ );
374
+
375
+ // Positive control: the same-namespace successor anchors it.
376
+ const anchored = annotateStateView(
377
+ [
378
+ fact("m-1", { namespace: "ns-a", status: "active" }),
379
+ fact("m-0", { namespace: "ns-a", status: "superseded", supersededBy: "m-1" }),
380
+ ],
381
+ "when did this change",
382
+ [],
383
+ { enabled: true },
384
+ );
385
+ assert.deepEqual(
386
+ anchored.map((row) => [row.id, row.stateLabel]),
387
+ [
388
+ ["m-1", "current"],
389
+ ["m-0", "historical"],
390
+ ],
391
+ );
392
+ });
393
+
394
+ test("#2859 namespace-qualified chains: a chain link never crosses namespaces", () => {
395
+ const chains: StateViewChain[] = [
396
+ { predecessorId: "old", successorId: "new", namespace: "ns-a" },
397
+ ];
398
+ const results = [
399
+ fact("new", { namespace: "ns-b", status: "active" }),
400
+ fact("old", { namespace: "ns-a", status: "superseded", supersededAt: "2026-02-01" }),
401
+ ];
402
+ const labeled = annotateStateView(results, "before the correction what was it", chains, {
403
+ enabled: true,
404
+ });
405
+ assert.deepEqual(
406
+ labeled.map((row) => row.id),
407
+ ["new"],
408
+ "the ns-a chain link must not pair against the ns-b successor",
409
+ );
410
+ });
411
+
412
+ test("reconcile drops a linkless superseded row so it cannot consume a packet slot", () => {
413
+ const rows = [
414
+ fact("legacy", { status: "superseded", supersededAt: "2026-01-01" }),
415
+ fact("live", { status: "active" }),
416
+ ];
417
+ assert.deepEqual(
418
+ reconcileStateViewPairs(rows).map((row) => row.id),
419
+ ["live"],
420
+ "audit/kill-switch status-only rows have no successor and must leave before the cap",
421
+ );
422
+ assert.deepEqual(
423
+ capStateViewPackets(reconcileStateViewPairs(rows), 1).map((row) => row.id),
424
+ ["live"],
425
+ );
426
+ });
427
+
428
+ test("reconcile still keeps an unlinked active row", () => {
429
+ const rows = [fact("solo", { status: "active" })];
430
+ assert.deepEqual(
431
+ reconcileStateViewPairs(rows).map((row) => row.id),
432
+ ["solo"],
433
+ );
434
+ });
@@ -8,6 +8,13 @@
8
8
  *
9
9
  * Change-intent morphology lives here (do not grow intent.ts). A superseded
10
10
  * memory is admitted only when its successor is also in the candidate set.
11
+ *
12
+ * #2859 — chain identities are namespace-qualified (`namespace\0id`): the
13
+ * same id in two namespaces never cross-anchors. The reverse chain derives
14
+ * from the successor's `supersedes` back-pointer when a predecessor lacks
15
+ * `supersededBy`, and asOf snapshot labels use the temporal validity
16
+ * boundary (predecessor `invalidAt` / successor `validAt`), not the
17
+ * `supersededAt` write time.
11
18
  */
12
19
  import { coerceBooleanLike } from "./connectors/coerce.js";
13
20
 
@@ -17,14 +24,24 @@ export interface StateViewChain {
17
24
  predecessorId: string;
18
25
  successorId: string;
19
26
  supersededAt?: string;
27
+ /** #2859 — namespace scope; a chain link never crosses namespaces. */
28
+ namespace?: string;
20
29
  }
21
30
 
22
31
  export interface StateViewResult {
23
32
  id?: string;
24
33
  docid?: string;
25
34
  status?: string;
35
+ /** #2859 — namespace that owns this row; qualifies every chain identity. */
36
+ namespace?: string;
37
+ /** #2859 — back-pointer: id of the row this one replaced. */
38
+ supersedes?: string;
26
39
  supersededBy?: string;
27
40
  supersededAt?: string;
41
+ /** #2859 — fact validity start (frontmatter `valid_at`). */
42
+ validAt?: string;
43
+ /** #2859 — fact validity end (frontmatter `invalid_at`). */
44
+ invalidAt?: string;
28
45
  stateLabel?: StateLabel;
29
46
  }
30
47
 
@@ -66,32 +83,94 @@ export function resultStateViewId(result: StateViewResult): string {
66
83
  return result.id ?? result.docid ?? "";
67
84
  }
68
85
 
69
- function buildSuccessorMap(results: readonly StateViewResult[], chains: readonly StateViewChain[]): Map<string, string> {
86
+ /**
87
+ * #2859 — namespace-qualified chain identity. Namespace fanout can surface
88
+ * the same id from two namespaces; every successor/admission match runs on
89
+ * `namespace\0id` keys so a row in one namespace can never anchor (or be
90
+ * anchored by) an identically-named row in another.
91
+ */
92
+ export function stateViewKey(namespace: string | undefined, id: string): string {
93
+ return `${typeof namespace === "string" ? namespace : ""}\u0000${id}`;
94
+ }
95
+
96
+ export function resultStateViewKey(result: StateViewResult): string {
97
+ const id = resultStateViewId(result);
98
+ return id ? stateViewKey(result.namespace, id) : "";
99
+ }
100
+
101
+ /** Inverse of {@link stateViewKey}: the id after the namespace separator. */
102
+ function stateViewIdFromKey(key: string): string {
103
+ const sep = key.indexOf("\u0000");
104
+ return sep === -1 ? key : key.slice(sep + 1);
105
+ }
106
+
107
+ /**
108
+ * predecessorKey → successorKey, namespace-qualified. Precedence: explicit
109
+ * chains, then successor rows' `supersedes` back-pointers (#2859 — derives
110
+ * the reverse chain when the predecessor lacks `supersededBy`), then the
111
+ * predecessor's own `supersededBy` (authoritative when present).
112
+ */
113
+ function buildSuccessorMap(
114
+ results: readonly StateViewResult[],
115
+ chains: readonly StateViewChain[],
116
+ ): Map<string, string> {
70
117
  const byPred = new Map<string, string>();
118
+ const link = (predKey: string, succKey: string, overwrite: boolean): void => {
119
+ if (!predKey || !succKey) return;
120
+ if (overwrite || !byPred.has(predKey)) byPred.set(predKey, succKey);
121
+ };
71
122
  for (const chain of chains) {
72
123
  if (chain.predecessorId && chain.successorId) {
73
- byPred.set(chain.predecessorId, chain.successorId);
124
+ link(
125
+ stateViewKey(chain.namespace, chain.predecessorId),
126
+ stateViewKey(chain.namespace, chain.successorId),
127
+ false,
128
+ );
129
+ }
130
+ }
131
+ for (const result of results) {
132
+ const key = resultStateViewKey(result);
133
+ if (key && result.supersedes) {
134
+ link(stateViewKey(result.namespace, result.supersedes), key, false);
74
135
  }
75
136
  }
76
137
  for (const result of results) {
77
- const id = resultStateViewId(result);
78
- if (id && result.supersededBy) byPred.set(id, result.supersededBy);
138
+ const key = resultStateViewKey(result);
139
+ if (key && result.supersededBy) {
140
+ link(key, stateViewKey(result.namespace, result.supersededBy), true);
141
+ }
79
142
  }
80
143
  return byPred;
81
144
  }
82
145
 
146
+ function successorLinkOf(
147
+ result: StateViewResult,
148
+ byPred: ReadonlyMap<string, string>,
149
+ ): { superseded: boolean; successorKey: string | undefined } {
150
+ const key = resultStateViewKey(result);
151
+ return {
152
+ superseded:
153
+ result.status === "superseded" ||
154
+ Boolean(result.supersededBy) ||
155
+ byPred.has(key),
156
+ successorKey: result.supersededBy
157
+ ? stateViewKey(result.namespace, result.supersededBy)
158
+ : byPred.get(key),
159
+ };
160
+ }
161
+
83
162
  function labelFor(
84
- id: string,
163
+ key: string,
85
164
  result: StateViewResult,
86
165
  byPred: Map<string, string>,
87
166
  admittedIds: ReadonlySet<string>,
88
167
  asOfMs: number | null,
89
168
  ): StateLabel {
90
- const successorId = result.supersededBy ?? byPred.get(id);
91
- const isPred = shouldWidenSuperseded(successorId, admittedIds);
169
+ const { successorKey } = successorLinkOf(result, byPred);
170
+ const isPred = shouldWidenSuperseded(successorKey, admittedIds);
92
171
  let isSucc = false;
93
172
  for (const [predId, succId] of byPred) {
94
- if (succId === id && admittedIds.has(predId)) {
173
+ if (succId === key && admittedIds.has(predId)) {
95
174
  isSucc = true;
96
175
  break;
97
176
  }
@@ -101,19 +180,25 @@ function labelFor(
101
180
  if (isSucc) return "current";
102
181
  if (result.status === "superseded") {
103
182
  // #1952 asOf mode: the successor is legitimately absent under the
104
- // historical pin. The row was the live fact at the snapshot when the
105
- // recorded supersession postdates the pin ("current" relative to
106
- // the snapshot); already replaced at the pin is "historical".
183
+ // historical pin (a linked successor takes the pair branches above).
184
+ // The row was the live fact at the snapshot when the temporal
185
+ // validity boundary postdates the pin ("current" relative to the
186
+ // snapshot); already replaced at the pin is "historical". #2859: the
187
+ // boundary is the fact's validity flip — `invalidAt` (the supersession
188
+ // pipeline sets it to the successor's valid_at) — not the
189
+ // `supersededAt` write time; legacy rows without a window fall back
190
+ // to supersededAt.
107
191
  return asOfMs === null ? "historical" : snapshotStateLabel(result, asOfMs);
108
192
  }
109
193
  return "current";
110
194
  }
111
195
 
112
196
  function snapshotStateLabel(result: StateViewResult, asOfMs: number): StateLabel {
113
- const supersededAtMs = Date.parse(result.supersededAt ?? "");
114
- return Number.isFinite(supersededAtMs) && supersededAtMs <= asOfMs
115
- ? "historical"
116
- : "current";
197
+ const invalidAtMs = Date.parse(result.invalidAt ?? "");
198
+ const boundaryMs = Number.isFinite(invalidAtMs)
199
+ ? invalidAtMs
200
+ : Date.parse(result.supersededAt ?? "");
201
+ return Number.isFinite(boundaryMs) && boundaryMs <= asOfMs ? "historical" : "current";
117
202
  }
118
203
 
119
204
  export function annotateStateView<T extends StateViewResult>(
@@ -136,26 +221,17 @@ export function annotateStateView<T extends StateViewResult>(
136
221
  const byPred = buildSuccessorMap(results, chains);
137
222
  const candidateIds = new Set<string>();
138
223
  for (const result of results) {
139
- const id = resultStateViewId(result);
140
- if (id) candidateIds.add(id);
224
+ const key = resultStateViewKey(result);
225
+ if (key) candidateIds.add(key);
141
226
  }
142
-
143
- const isSupersededRow = (result: T): { superseded: boolean; successorId: string | undefined } => {
144
- const id = resultStateViewId(result);
145
- return {
146
- superseded: result.status === "superseded" || Boolean(result.supersededBy) || byPred.has(id),
147
- successorId: result.supersededBy ?? byPred.get(id),
148
- };
149
- };
150
-
151
227
  let admitted: T[];
152
228
  if (asOfMs !== null) {
153
229
  admitted = results.slice();
154
230
  } else {
155
231
  admitted = [];
156
232
  for (const result of results) {
157
- const { superseded, successorId } = isSupersededRow(result);
158
- if (!superseded || shouldWidenSuperseded(successorId, candidateIds)) {
233
+ const { superseded, successorKey } = successorLinkOf(result, byPred);
234
+ if (!superseded || shouldWidenSuperseded(successorKey, candidateIds)) {
159
235
  admitted.push(result);
160
236
  }
161
237
  }
@@ -163,24 +239,119 @@ export function annotateStateView<T extends StateViewResult>(
163
239
  // #1952 contract: superseded never appears without its successor. A
164
240
  // successor can itself be dropped (filtered elsewhere, or transitively
165
241
  // orphaned), so reconcile to a fixpoint instead of trusting the first pass.
166
- let admittedIds = new Set(admitted.map(resultStateViewId).filter(Boolean));
242
+ let admittedIds = new Set(admitted.map(resultStateViewKey).filter(Boolean));
167
243
  for (let changed = true; changed; ) {
168
244
  changed = false;
169
245
  for (let i = admitted.length - 1; i >= 0; i -= 1) {
170
246
  const row = admitted[i]!;
171
- const { superseded, successorId } = isSupersededRow(row);
172
- if (superseded && !shouldWidenSuperseded(successorId, admittedIds)) {
247
+ const { superseded, successorKey } = successorLinkOf(row, byPred);
248
+ if (superseded && !shouldWidenSuperseded(successorKey, admittedIds)) {
173
249
  admitted.splice(i, 1);
174
250
  changed = true;
175
251
  }
176
252
  }
177
- if (changed) admittedIds = new Set(admitted.map(resultStateViewId).filter(Boolean));
253
+ if (changed) admittedIds = new Set(admitted.map(resultStateViewKey).filter(Boolean));
178
254
  }
179
255
  }
180
256
 
181
- const admittedIds = new Set(admitted.map(resultStateViewId).filter(Boolean));
257
+ const admittedIds = new Set(admitted.map(resultStateViewKey).filter(Boolean));
182
258
  return admitted.map((result) => {
183
- const id = resultStateViewId(result);
184
- return { ...result, stateLabel: labelFor(id, result, byPred, admittedIds, asOfMs) };
259
+ const key = resultStateViewKey(result);
260
+ const { successorKey } = successorLinkOf(result, byPred);
261
+ const stateLabel = labelFor(key, result, byPred, admittedIds, asOfMs);
262
+ if (result.supersededBy || !successorKey) return { ...result, stateLabel };
263
+ return { ...result, stateLabel, supersededBy: stateViewIdFromKey(successorKey) };
185
264
  });
186
265
  }
266
+
267
+ /**
268
+ * #2859 — drop superseded rows whose successor is not in the pool BEFORE
269
+ * the user cap/MMR runs, so post-cap orphan removal can never underfill
270
+ * the result set. Reconciles to a fixpoint (a dropped successor orphans
271
+ * its own predecessors). Rows with no successor links pass through in
272
+ * order. ponytail: repeated full-pool passes are O(n²) on one long chain;
273
+ * recall pools stay small, memoize per-key if pools ever grow.
274
+ */
275
+ export function reconcileStateViewPairs<T extends StateViewResult>(results: readonly T[]): T[] {
276
+ const byPred = buildSuccessorMap(results, []);
277
+ let kept = results.slice();
278
+ for (let changed = true; changed; ) {
279
+ changed = false;
280
+ const keptKeys = new Set(kept.map(resultStateViewKey).filter(Boolean));
281
+ const next: T[] = [];
282
+ for (const row of kept) {
283
+ const { superseded, successorKey } = successorLinkOf(row, byPred);
284
+ if (superseded && !shouldWidenSuperseded(successorKey, keptKeys)) {
285
+ changed = true;
286
+ continue;
287
+ }
288
+ next.push(row);
289
+ }
290
+ if (changed) kept = next;
291
+ }
292
+ return kept;
293
+ }
294
+
295
+ /**
296
+ * #2859 — packet-counting cap. A predecessor/successor chain admitted
297
+ * together is ONE complete evidence packet: it consumes a single slot of
298
+ * the user cap, and the slice never splits a packet at the boundary (no
299
+ * underfill from post-cap orphan removal). Rows keep their incoming
300
+ * (MMR) order; packets are ordered by first appearance. Call
301
+ * reconcileStateViewPairs first so orphans never consume a slot.
302
+ */
303
+ export function capStateViewPackets<T extends StateViewResult>(
304
+ ordered: readonly T[],
305
+ limit: number,
306
+ ): T[] {
307
+ const safeLimit =
308
+ typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 0;
309
+ if (safeLimit === 0) return [];
310
+ if (ordered.length === 0) return [];
311
+
312
+ const byPred = buildSuccessorMap(ordered, []);
313
+ // Union-find over row keys; blank-key rows become per-index singletons.
314
+ const parent = new Map<string, string>();
315
+ const root = (key: string): string => {
316
+ let current = key;
317
+ for (;;) {
318
+ const up = parent.get(current);
319
+ if (up === undefined || up === current) {
320
+ parent.set(current, current);
321
+ return current;
322
+ }
323
+ current = up;
324
+ }
325
+ };
326
+ const union = (a: string, b: string): void => {
327
+ const ra = root(a);
328
+ const rb = root(b);
329
+ if (ra !== rb) parent.set(ra, rb);
330
+ };
331
+ const keys: string[] = [];
332
+ const keyToIndex = new Map<string, number>();
333
+ for (let i = 0; i < ordered.length; i += 1) {
334
+ // ponytail: blank-key rows must stay distinct singletons; index-keyed.
335
+ const key = resultStateViewKey(ordered[i]!) || `#idx${i}`;
336
+ keys.push(key);
337
+ if (!parent.has(key)) parent.set(key, key);
338
+ const first = keyToIndex.get(key);
339
+ if (first !== undefined) union(keys[first]!, key);
340
+ else keyToIndex.set(key, i);
341
+ }
342
+ for (const [predKey, succKey] of byPred) {
343
+ const predIdx = keyToIndex.get(predKey);
344
+ const succIdx = keyToIndex.get(succKey);
345
+ if (predIdx !== undefined && succIdx !== undefined) union(keys[predIdx]!, keys[succIdx]!);
346
+ }
347
+ // Packets ordered by first appearance; admit whole packets until the cap.
348
+ const selectedRoots = new Set<string>();
349
+ for (let i = 0; i < ordered.length && selectedRoots.size < safeLimit; i += 1) {
350
+ selectedRoots.add(root(keys[i]!));
351
+ }
352
+ const selected: T[] = [];
353
+ for (let i = 0; i < ordered.length; i += 1) {
354
+ if (selectedRoots.has(root(keys[i]!))) selected.push(ordered[i]!);
355
+ }
356
+ return selected;
357
+ }
@@ -175,7 +175,7 @@ test("TrustScore quarantine excludes a contradicted memory from injection but su
175
175
 
176
176
  test("X-ray capture preserves recent-scan serving while recording post-trust applied and pre-trust headroom", async () => {
177
177
  const { orchestrator, memoryDir } = await makeOrchestrator({
178
- qmdMaxResults: 2,
178
+ qmdMaxResults: 1,
179
179
  });
180
180
  const now = Date.now();
181
181
  try {
@@ -1,120 +0,0 @@
1
- import {
2
- coerceBooleanLike
3
- } from "./chunk-PQVCH2AE.js";
4
-
5
- // src/recall-state-view.ts
6
- var DEFAULT_RECALL_STATE_VIEWS = false;
7
- var CHANGE_WORDS = ["switch", "switches", "switched", "switching", "change", "changes", "changed", "changing"];
8
- var CHANGE_PHRASE_RE = /\bwhen\s+did\b|\bused\s+to\b/;
9
- var CHANGE_WORD_RE = new RegExp(`\\b(?:${CHANGE_WORDS.join("|")})\\b`);
10
- var EVENT_POINTER_RE = /\b(?:before|after)\s+(?:the|a|an|this|that|these|those)\b/;
11
- function parseRecallStateViews(raw) {
12
- return coerceBooleanLike(raw, "recallStateViews") === true;
13
- }
14
- function isChangeOrientedQuery(query) {
15
- const lower = query.toLowerCase();
16
- return CHANGE_PHRASE_RE.test(lower) || CHANGE_WORD_RE.test(lower) || EVENT_POINTER_RE.test(lower);
17
- }
18
- function shouldWidenSuperseded(successorId, candidateIds) {
19
- return typeof successorId === "string" && successorId.length > 0 && candidateIds.has(successorId);
20
- }
21
- function formatSupersededPrefix(date, successorId) {
22
- return `[superseded ${date} by ${successorId}]`;
23
- }
24
- function resultStateViewId(result) {
25
- return result.id ?? result.docid ?? "";
26
- }
27
- function buildSuccessorMap(results, chains) {
28
- const byPred = /* @__PURE__ */ new Map();
29
- for (const chain of chains) {
30
- if (chain.predecessorId && chain.successorId) {
31
- byPred.set(chain.predecessorId, chain.successorId);
32
- }
33
- }
34
- for (const result of results) {
35
- const id = resultStateViewId(result);
36
- if (id && result.supersededBy) byPred.set(id, result.supersededBy);
37
- }
38
- return byPred;
39
- }
40
- function labelFor(id, result, byPred, admittedIds, asOfMs) {
41
- const successorId = result.supersededBy ?? byPred.get(id);
42
- const isPred = shouldWidenSuperseded(successorId, admittedIds);
43
- let isSucc = false;
44
- for (const [predId, succId] of byPred) {
45
- if (succId === id && admittedIds.has(predId)) {
46
- isSucc = true;
47
- break;
48
- }
49
- }
50
- if (isPred && isSucc) return "transition";
51
- if (isPred) return "historical";
52
- if (isSucc) return "current";
53
- if (result.status === "superseded") {
54
- return asOfMs === null ? "historical" : snapshotStateLabel(result, asOfMs);
55
- }
56
- return "current";
57
- }
58
- function snapshotStateLabel(result, asOfMs) {
59
- const supersededAtMs = Date.parse(result.supersededAt ?? "");
60
- return Number.isFinite(supersededAtMs) && supersededAtMs <= asOfMs ? "historical" : "current";
61
- }
62
- function annotateStateView(results, query, chains, options = {}) {
63
- const enabled = options.enabled === true;
64
- if (!enabled || !isChangeOrientedQuery(query)) return results;
65
- const asOfMs = typeof options.asOfMs === "number" && Number.isFinite(options.asOfMs) ? options.asOfMs : null;
66
- const byPred = buildSuccessorMap(results, chains);
67
- const candidateIds = /* @__PURE__ */ new Set();
68
- for (const result of results) {
69
- const id = resultStateViewId(result);
70
- if (id) candidateIds.add(id);
71
- }
72
- const isSupersededRow = (result) => {
73
- const id = resultStateViewId(result);
74
- return {
75
- superseded: result.status === "superseded" || Boolean(result.supersededBy) || byPred.has(id),
76
- successorId: result.supersededBy ?? byPred.get(id)
77
- };
78
- };
79
- let admitted;
80
- if (asOfMs !== null) {
81
- admitted = results.slice();
82
- } else {
83
- admitted = [];
84
- for (const result of results) {
85
- const { superseded, successorId } = isSupersededRow(result);
86
- if (!superseded || shouldWidenSuperseded(successorId, candidateIds)) {
87
- admitted.push(result);
88
- }
89
- }
90
- let admittedIds2 = new Set(admitted.map(resultStateViewId).filter(Boolean));
91
- for (let changed = true; changed; ) {
92
- changed = false;
93
- for (let i = admitted.length - 1; i >= 0; i -= 1) {
94
- const row = admitted[i];
95
- const { superseded, successorId } = isSupersededRow(row);
96
- if (superseded && !shouldWidenSuperseded(successorId, admittedIds2)) {
97
- admitted.splice(i, 1);
98
- changed = true;
99
- }
100
- }
101
- if (changed) admittedIds2 = new Set(admitted.map(resultStateViewId).filter(Boolean));
102
- }
103
- }
104
- const admittedIds = new Set(admitted.map(resultStateViewId).filter(Boolean));
105
- return admitted.map((result) => {
106
- const id = resultStateViewId(result);
107
- return { ...result, stateLabel: labelFor(id, result, byPred, admittedIds, asOfMs) };
108
- });
109
- }
110
-
111
- export {
112
- DEFAULT_RECALL_STATE_VIEWS,
113
- parseRecallStateViews,
114
- isChangeOrientedQuery,
115
- shouldWidenSuperseded,
116
- formatSupersededPrefix,
117
- resultStateViewId,
118
- annotateStateView
119
- };
120
- //# sourceMappingURL=chunk-255PC37A.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/recall-state-view.ts"],"sourcesContent":["/**\n * Recall state views (issue #1952).\n *\n * Config key: `recallStateViews` (default false). When false, annotateStateView\n * is identity: same array reference, no `stateLabel`. config.ts is at its\n * fileSizeGrandfather ceiling, so parseConfig wiring waits for a later PR.\n * Call `parseRecallStateViews` at the recall seam when that lands.\n *\n * Change-intent morphology lives here (do not grow intent.ts). A superseded\n * memory is admitted only when its successor is also in the candidate set.\n */\nimport { coerceBooleanLike } from \"./connectors/coerce.js\";\n\nexport type StateLabel = \"current\" | \"historical\" | \"transition\";\n\nexport interface StateViewChain {\n predecessorId: string;\n successorId: string;\n supersededAt?: string;\n}\n\nexport interface StateViewResult {\n id?: string;\n docid?: string;\n status?: string;\n supersededBy?: string;\n supersededAt?: string;\n stateLabel?: StateLabel;\n}\n\nexport const DEFAULT_RECALL_STATE_VIEWS = false;\n\nconst CHANGE_WORDS = [\"switch\", \"switches\", \"switched\", \"switching\", \"change\", \"changes\", \"changed\", \"changing\"];\n\n// Phrase matches are token-bounded: \"when Didi\" must not fire the\n// \"when did\" phrase, and \"confused to\" must not fire \"used to\".\nconst CHANGE_PHRASE_RE = /\\bwhen\\s+did\\b|\\bused\\s+to\\b/;\nconst CHANGE_WORD_RE = new RegExp(`\\\\b(?:${CHANGE_WORDS.join(\"|\")})\\\\b`);\n// Bare \"before\"/\"after\" are ordinary sequencing (\"before lunch\",\n// \"after install\"). They carry change intent only when they point at a\n// specific event through a determiner (\"before the move\", \"after the\n// cutover\").\nconst EVENT_POINTER_RE = /\\b(?:before|after)\\s+(?:the|a|an|this|that|these|those)\\b/;\n\nexport function parseRecallStateViews(raw: unknown): boolean {\n return coerceBooleanLike(raw, \"recallStateViews\") === true;\n}\n\nexport function isChangeOrientedQuery(query: string): boolean {\n const lower = query.toLowerCase();\n return CHANGE_PHRASE_RE.test(lower) || CHANGE_WORD_RE.test(lower) || EVENT_POINTER_RE.test(lower);\n}\n\nexport function shouldWidenSuperseded(\n successorId: string | undefined,\n candidateIds: ReadonlySet<string>,\n): boolean {\n return typeof successorId === \"string\" && successorId.length > 0 && candidateIds.has(successorId);\n}\n\nexport function formatSupersededPrefix(date: string, successorId: string): string {\n return `[superseded ${date} by ${successorId}]`;\n}\n\nexport function resultStateViewId(result: StateViewResult): string {\n return result.id ?? result.docid ?? \"\";\n}\n\nfunction buildSuccessorMap(results: readonly StateViewResult[], chains: readonly StateViewChain[]): Map<string, string> {\n const byPred = new Map<string, string>();\n for (const chain of chains) {\n if (chain.predecessorId && chain.successorId) {\n byPred.set(chain.predecessorId, chain.successorId);\n }\n }\n for (const result of results) {\n const id = resultStateViewId(result);\n if (id && result.supersededBy) byPred.set(id, result.supersededBy);\n }\n return byPred;\n}\n\nfunction labelFor(\n id: string,\n result: StateViewResult,\n byPred: Map<string, string>,\n admittedIds: ReadonlySet<string>,\n asOfMs: number | null,\n): StateLabel {\n const successorId = result.supersededBy ?? byPred.get(id);\n const isPred = shouldWidenSuperseded(successorId, admittedIds);\n let isSucc = false;\n for (const [predId, succId] of byPred) {\n if (succId === id && admittedIds.has(predId)) {\n isSucc = true;\n break;\n }\n }\n if (isPred && isSucc) return \"transition\";\n if (isPred) return \"historical\";\n if (isSucc) return \"current\";\n if (result.status === \"superseded\") {\n // #1952 asOf mode: the successor is legitimately absent under the\n // historical pin. The row was the live fact at the snapshot when the\n // recorded supersession postdates the pin (\"current\" relative to\n // the snapshot); already replaced at the pin is \"historical\".\n return asOfMs === null ? \"historical\" : snapshotStateLabel(result, asOfMs);\n }\n return \"current\";\n}\n\nfunction snapshotStateLabel(result: StateViewResult, asOfMs: number): StateLabel {\n const supersededAtMs = Date.parse(result.supersededAt ?? \"\");\n return Number.isFinite(supersededAtMs) && supersededAtMs <= asOfMs\n ? \"historical\"\n : \"current\";\n}\n\nexport function annotateStateView<T extends StateViewResult>(\n results: T[],\n query: string,\n chains: readonly StateViewChain[],\n options: { enabled?: boolean; asOfMs?: number } = {},\n): T[] {\n const enabled = options.enabled === true;\n if (!enabled || !isChangeOrientedQuery(query)) return results;\n // #1952 asOf mode: under a historical pin the pipeline's isValidAsOf\n // gate already decided validity at the pinned instant, so a predecessor\n // must not be discarded merely because its successor is absent under\n // the same filter. A valid asOf result is never emptied here.\n const asOfMs =\n typeof options.asOfMs === \"number\" && Number.isFinite(options.asOfMs)\n ? options.asOfMs\n : null;\n\n const byPred = buildSuccessorMap(results, chains);\n const candidateIds = new Set<string>();\n for (const result of results) {\n const id = resultStateViewId(result);\n if (id) candidateIds.add(id);\n }\n\n const isSupersededRow = (result: T): { superseded: boolean; successorId: string | undefined } => {\n const id = resultStateViewId(result);\n return {\n superseded: result.status === \"superseded\" || Boolean(result.supersededBy) || byPred.has(id),\n successorId: result.supersededBy ?? byPred.get(id),\n };\n };\n\n let admitted: T[];\n if (asOfMs !== null) {\n admitted = results.slice();\n } else {\n admitted = [];\n for (const result of results) {\n const { superseded, successorId } = isSupersededRow(result);\n if (!superseded || shouldWidenSuperseded(successorId, candidateIds)) {\n admitted.push(result);\n }\n }\n\n // #1952 contract: superseded never appears without its successor. A\n // successor can itself be dropped (filtered elsewhere, or transitively\n // orphaned), so reconcile to a fixpoint instead of trusting the first pass.\n let admittedIds = new Set(admitted.map(resultStateViewId).filter(Boolean));\n for (let changed = true; changed; ) {\n changed = false;\n for (let i = admitted.length - 1; i >= 0; i -= 1) {\n const row = admitted[i]!;\n const { superseded, successorId } = isSupersededRow(row);\n if (superseded && !shouldWidenSuperseded(successorId, admittedIds)) {\n admitted.splice(i, 1);\n changed = true;\n }\n }\n if (changed) admittedIds = new Set(admitted.map(resultStateViewId).filter(Boolean));\n }\n }\n\n const admittedIds = new Set(admitted.map(resultStateViewId).filter(Boolean));\n return admitted.map((result) => {\n const id = resultStateViewId(result);\n return { ...result, stateLabel: labelFor(id, result, byPred, admittedIds, asOfMs) };\n });\n}\n"],"mappings":"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","names":["admittedIds"]}