@remnic/core 9.66.12 → 9.68.0

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 (217) hide show
  1. package/dist/access-admin-ops-surface.js +26 -26
  2. package/dist/access-audit.js +2 -2
  3. package/dist/access-authorization-probe.js +27 -27
  4. package/dist/access-boundary.js +26 -26
  5. package/dist/access-cli.js +87 -87
  6. package/dist/access-extraction-force-flush.js +26 -26
  7. package/dist/access-http-query.js +28 -28
  8. package/dist/access-http.js +44 -44
  9. package/dist/access-identity-continuity-surface.js +26 -26
  10. package/dist/access-lcm-surface.js +26 -26
  11. package/dist/access-mcp.js +34 -34
  12. package/dist/access-namespace-preflight.js +26 -26
  13. package/dist/access-observe-write-surface.js +26 -26
  14. package/dist/access-operations-batch.js +28 -28
  15. package/dist/access-operations.js +33 -33
  16. package/dist/access-recall-concurrency.js +26 -26
  17. package/dist/access-recall-response.js +26 -26
  18. package/dist/access-recall-surface.js +26 -26
  19. package/dist/access-schema.js +3 -3
  20. package/dist/access-service.js +26 -26
  21. package/dist/access-surface-catalog.js +28 -28
  22. package/dist/adapters/index.js +7 -7
  23. package/dist/adapters/registry.js +3 -3
  24. package/dist/briefing.js +11 -11
  25. package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
  26. package/dist/causal-consolidation.js +12 -12
  27. package/dist/{chunk-LEPOP46W.js → chunk-227UFSEQ.js} +4 -4
  28. package/dist/{chunk-FJP6MVRK.js → chunk-25P3LW6U.js} +2 -2
  29. package/dist/{chunk-JKXPFIPC.js → chunk-2MHMHF76.js} +2 -2
  30. package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
  31. package/dist/{chunk-QMIKXIAM.js → chunk-327JMA2N.js} +2 -2
  32. package/dist/{chunk-H6VYVOVP.js → chunk-363VZTVH.js} +2 -2
  33. package/dist/{chunk-PKRYQ222.js → chunk-3IRH5MYM.js} +6 -6
  34. package/dist/{chunk-GEHEBLVY.js → chunk-4PEZMGZD.js} +2 -2
  35. package/dist/{chunk-EU5PDHU2.js → chunk-4PMPOZK3.js} +2 -2
  36. package/dist/{chunk-6SRUPUTY.js → chunk-52GQNSLC.js} +5 -5
  37. package/dist/{chunk-SV3MDTJI.js → chunk-5UI7WQA6.js} +2 -2
  38. package/dist/{chunk-JGXMA6HL.js → chunk-6FUMA6AA.js} +2 -2
  39. package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
  40. package/dist/{chunk-WGVAEVT7.js → chunk-7KG5V4VV.js} +2 -2
  41. package/dist/{chunk-A6HVSRGM.js → chunk-A7NMS5II.js} +3 -3
  42. package/dist/{chunk-M3BFW3VX.js → chunk-ABPCF4MW.js} +1 -1
  43. package/dist/{chunk-NNBZS3DY.js → chunk-AG46P24C.js} +4 -4
  44. package/dist/{chunk-I7FVQMAA.js → chunk-BGD3T743.js} +2 -2
  45. package/dist/{chunk-QRHKJT7F.js → chunk-BHH736UQ.js} +4 -4
  46. package/dist/{chunk-4L7OKRGP.js → chunk-BS5BD3DF.js} +2 -2
  47. package/dist/{chunk-BT7AQZQX.js → chunk-BTRY6UH3.js} +2 -2
  48. package/dist/{chunk-BE4Y6GK6.js → chunk-D35XLYQ7.js} +11 -11
  49. package/dist/{chunk-CJTZO5MW.js → chunk-D6Q5EPOV.js} +2 -2
  50. package/dist/{chunk-6KGBI54C.js → chunk-DJ7TZLGU.js} +2 -2
  51. package/dist/{chunk-IDPFBRPJ.js → chunk-ERKFG4RV.js} +38 -38
  52. package/dist/{chunk-K7VTD5AZ.js → chunk-F25CL3IQ.js} +2 -2
  53. package/dist/{chunk-QZPSIQRB.js → chunk-FOT5GP2U.js} +2 -2
  54. package/dist/{chunk-OMA2B7GY.js → chunk-HACVBEAP.js} +2 -2
  55. package/dist/{chunk-IEZ6CGTW.js → chunk-HCM7ZYJD.js} +2 -2
  56. package/dist/{chunk-46XRYYOJ.js → chunk-HDQQRRCZ.js} +3 -3
  57. package/dist/{chunk-QXDICB4J.js → chunk-I6T4SYFW.js} +3 -3
  58. package/dist/{chunk-HQJM3BKH.js → chunk-J2R275FN.js} +2 -2
  59. package/dist/{chunk-STUPJFG7.js → chunk-KAY2C6VX.js} +2 -2
  60. package/dist/{chunk-I3X5NWDU.js → chunk-KJUH3QIQ.js} +2 -2
  61. package/dist/{chunk-BUKK5SWA.js → chunk-KQAFEZQX.js} +2 -2
  62. package/dist/{chunk-KZAN4JXY.js → chunk-KRNFJKMH.js} +2 -2
  63. package/dist/{chunk-SBUQ4KIE.js → chunk-L7MJJAG2.js} +2 -2
  64. package/dist/{chunk-IVKIODHO.js → chunk-MB6GLPRY.js} +4 -4
  65. package/dist/{chunk-NROLO2HY.js → chunk-ND6NL2YR.js} +2 -2
  66. package/dist/{chunk-O2HDDAUM.js → chunk-NFITGYMQ.js} +3 -3
  67. package/dist/{chunk-GRZYF6KK.js → chunk-NYQHD7H4.js} +12 -12
  68. package/dist/{chunk-REJCULWA.js → chunk-P342D5BP.js} +2 -2
  69. package/dist/{chunk-HGRRJDUH.js → chunk-PIBQIYF2.js} +2 -2
  70. package/dist/{chunk-UUGU26P2.js → chunk-QOE3EM5Z.js} +2 -2
  71. package/dist/{chunk-N37X5RU7.js → chunk-RACZB5HT.js} +4 -4
  72. package/dist/{chunk-AU7Q3LSC.js → chunk-RKNJBZ55.js} +7 -7
  73. package/dist/{chunk-F3H7VNM4.js → chunk-USQAH6PN.js} +2 -2
  74. package/dist/{chunk-ZFE7NXB7.js → chunk-VLSUKXCJ.js} +4 -4
  75. package/dist/{chunk-GHSJBVVQ.js → chunk-WFFGUFFN.js} +265 -49
  76. package/dist/chunk-WFFGUFFN.js.map +1 -0
  77. package/dist/{chunk-YWD5UIWY.js → chunk-WQCET3N3.js} +2 -2
  78. package/dist/{chunk-MUYJQ273.js → chunk-WSYACIKD.js} +2 -2
  79. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  80. package/dist/{chunk-AWMS3IFB.js → chunk-XPDWNWH6.js} +3 -3
  81. package/dist/{chunk-K5JFIA44.js → chunk-XW3G4BJI.js} +7 -7
  82. package/dist/{chunk-GIC6BASP.js → chunk-XZBXINXZ.js} +4 -4
  83. package/dist/{chunk-KDCPIXIY.js → chunk-YFK64FYK.js} +2 -2
  84. package/dist/{chunk-DDAHG5ZD.js → chunk-YYAWJRGL.js} +2 -2
  85. package/dist/{chunk-IYFL3534.js → chunk-Z2FXSE5P.js} +2 -2
  86. package/dist/{chunk-CR73YTWC.js → chunk-ZDJMCXC7.js} +11 -11
  87. package/dist/{chunk-OQT34EVH.js → chunk-ZVW724ZK.js} +2 -2
  88. package/dist/cli.js +63 -63
  89. package/dist/coding/export-okf-codegraph.js +11 -11
  90. package/dist/compounding/engine.js +11 -11
  91. package/dist/connectors/codex-materialize-runner.js +11 -11
  92. package/dist/connectors/index.js +11 -11
  93. package/dist/convergence-refresh.js +13 -13
  94. package/dist/conversation-index/backend.js +2 -2
  95. package/dist/corpus-watermark.js +13 -13
  96. package/dist/entity-retrieval.js +11 -11
  97. package/dist/external-wiki-access.js +27 -27
  98. package/dist/extraction-span-gate.d.ts +31 -0
  99. package/dist/extraction-span-gate.js +37 -0
  100. package/dist/extraction-span-gate.js.map +1 -0
  101. package/dist/extraction.js +14 -14
  102. package/dist/harmonic-retrieval.js +1 -1
  103. package/dist/index.d.ts +61 -6
  104. package/dist/index.js +106 -100
  105. package/dist/lcm/index.js +3 -3
  106. package/dist/local-llm.js +2 -2
  107. package/dist/location/index.js +13 -13
  108. package/dist/maintenance/memory-governance.js +11 -11
  109. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +11 -11
  110. package/dist/maintenance/rebuild-memory-projection.js +12 -12
  111. package/dist/namespaces/migrate.js +12 -12
  112. package/dist/namespaces/search.js +9 -9
  113. package/dist/namespaces/storage.js +11 -11
  114. package/dist/operator-doctor-corpus.js +14 -14
  115. package/dist/operator-toolkit.js +19 -19
  116. package/dist/orchestration/maintenance.js +19 -19
  117. package/dist/orchestrator.js +87 -87
  118. package/dist/recall-navigate-window.d.ts +26 -0
  119. package/dist/recall-navigate-window.js +32 -0
  120. package/dist/recall-navigate-window.js.map +1 -0
  121. package/dist/search/factory.js +8 -8
  122. package/dist/search/index.js +13 -13
  123. package/dist/search/lancedb-backend.js +2 -2
  124. package/dist/search/meilisearch-backend.js +2 -2
  125. package/dist/search/orama-backend.js +2 -2
  126. package/dist/semantic-consolidation.js +12 -12
  127. package/dist/semantic-rule-promotion.js +11 -11
  128. package/dist/semantic-rule-verifier.js +11 -11
  129. package/dist/source-agent-qualifier.js +11 -11
  130. package/dist/standup/build.js +11 -11
  131. package/dist/storage.js +10 -10
  132. package/dist/summarizer.js +3 -3
  133. package/dist/support-passport/index.js +29 -29
  134. package/dist/transfer/backup.js +2 -2
  135. package/dist/transfer/capsule-export.js +2 -2
  136. package/dist/transfer/capsule-import.js +2 -2
  137. package/dist/transfer/export-okf.js +12 -12
  138. package/dist/verified-recall.js +11 -11
  139. package/dist/who-knows-cli.js +12 -12
  140. package/dist/who-knows.js +11 -11
  141. package/package.json +2 -2
  142. package/src/activity/index.ts +2 -0
  143. package/src/activity/journal-section.ts +1 -1
  144. package/src/activity/journal-strip.test.ts +126 -0
  145. package/src/activity/journal-strip.ts +115 -0
  146. package/src/activity/privacy-delete-plan.test.ts +215 -0
  147. package/src/activity/privacy-delete-plan.ts +116 -0
  148. package/src/activity/timeline/index.ts +1 -0
  149. package/src/activity/timeline/recap-merge-edits.test.ts +181 -0
  150. package/src/activity/timeline/recap-merge-edits.ts +70 -0
  151. package/src/extraction-span-gate.test.ts +202 -0
  152. package/src/extraction-span-gate.ts +58 -0
  153. package/src/recall-navigate-window.test.ts +104 -0
  154. package/src/recall-navigate-window.ts +48 -0
  155. package/src/shared-context/supersession-chain.test.ts +158 -0
  156. package/src/shared-context/supersession-chain.ts +95 -0
  157. package/dist/chunk-GHSJBVVQ.js.map +0 -1
  158. /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
  159. /package/dist/{chunk-LEPOP46W.js.map → chunk-227UFSEQ.js.map} +0 -0
  160. /package/dist/{chunk-FJP6MVRK.js.map → chunk-25P3LW6U.js.map} +0 -0
  161. /package/dist/{chunk-JKXPFIPC.js.map → chunk-2MHMHF76.js.map} +0 -0
  162. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  163. /package/dist/{chunk-QMIKXIAM.js.map → chunk-327JMA2N.js.map} +0 -0
  164. /package/dist/{chunk-H6VYVOVP.js.map → chunk-363VZTVH.js.map} +0 -0
  165. /package/dist/{chunk-PKRYQ222.js.map → chunk-3IRH5MYM.js.map} +0 -0
  166. /package/dist/{chunk-GEHEBLVY.js.map → chunk-4PEZMGZD.js.map} +0 -0
  167. /package/dist/{chunk-EU5PDHU2.js.map → chunk-4PMPOZK3.js.map} +0 -0
  168. /package/dist/{chunk-6SRUPUTY.js.map → chunk-52GQNSLC.js.map} +0 -0
  169. /package/dist/{chunk-SV3MDTJI.js.map → chunk-5UI7WQA6.js.map} +0 -0
  170. /package/dist/{chunk-JGXMA6HL.js.map → chunk-6FUMA6AA.js.map} +0 -0
  171. /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
  172. /package/dist/{chunk-WGVAEVT7.js.map → chunk-7KG5V4VV.js.map} +0 -0
  173. /package/dist/{chunk-A6HVSRGM.js.map → chunk-A7NMS5II.js.map} +0 -0
  174. /package/dist/{chunk-M3BFW3VX.js.map → chunk-ABPCF4MW.js.map} +0 -0
  175. /package/dist/{chunk-NNBZS3DY.js.map → chunk-AG46P24C.js.map} +0 -0
  176. /package/dist/{chunk-I7FVQMAA.js.map → chunk-BGD3T743.js.map} +0 -0
  177. /package/dist/{chunk-QRHKJT7F.js.map → chunk-BHH736UQ.js.map} +0 -0
  178. /package/dist/{chunk-4L7OKRGP.js.map → chunk-BS5BD3DF.js.map} +0 -0
  179. /package/dist/{chunk-BT7AQZQX.js.map → chunk-BTRY6UH3.js.map} +0 -0
  180. /package/dist/{chunk-BE4Y6GK6.js.map → chunk-D35XLYQ7.js.map} +0 -0
  181. /package/dist/{chunk-CJTZO5MW.js.map → chunk-D6Q5EPOV.js.map} +0 -0
  182. /package/dist/{chunk-6KGBI54C.js.map → chunk-DJ7TZLGU.js.map} +0 -0
  183. /package/dist/{chunk-IDPFBRPJ.js.map → chunk-ERKFG4RV.js.map} +0 -0
  184. /package/dist/{chunk-K7VTD5AZ.js.map → chunk-F25CL3IQ.js.map} +0 -0
  185. /package/dist/{chunk-QZPSIQRB.js.map → chunk-FOT5GP2U.js.map} +0 -0
  186. /package/dist/{chunk-OMA2B7GY.js.map → chunk-HACVBEAP.js.map} +0 -0
  187. /package/dist/{chunk-IEZ6CGTW.js.map → chunk-HCM7ZYJD.js.map} +0 -0
  188. /package/dist/{chunk-46XRYYOJ.js.map → chunk-HDQQRRCZ.js.map} +0 -0
  189. /package/dist/{chunk-QXDICB4J.js.map → chunk-I6T4SYFW.js.map} +0 -0
  190. /package/dist/{chunk-HQJM3BKH.js.map → chunk-J2R275FN.js.map} +0 -0
  191. /package/dist/{chunk-STUPJFG7.js.map → chunk-KAY2C6VX.js.map} +0 -0
  192. /package/dist/{chunk-I3X5NWDU.js.map → chunk-KJUH3QIQ.js.map} +0 -0
  193. /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
  194. /package/dist/{chunk-KZAN4JXY.js.map → chunk-KRNFJKMH.js.map} +0 -0
  195. /package/dist/{chunk-SBUQ4KIE.js.map → chunk-L7MJJAG2.js.map} +0 -0
  196. /package/dist/{chunk-IVKIODHO.js.map → chunk-MB6GLPRY.js.map} +0 -0
  197. /package/dist/{chunk-NROLO2HY.js.map → chunk-ND6NL2YR.js.map} +0 -0
  198. /package/dist/{chunk-O2HDDAUM.js.map → chunk-NFITGYMQ.js.map} +0 -0
  199. /package/dist/{chunk-GRZYF6KK.js.map → chunk-NYQHD7H4.js.map} +0 -0
  200. /package/dist/{chunk-REJCULWA.js.map → chunk-P342D5BP.js.map} +0 -0
  201. /package/dist/{chunk-HGRRJDUH.js.map → chunk-PIBQIYF2.js.map} +0 -0
  202. /package/dist/{chunk-UUGU26P2.js.map → chunk-QOE3EM5Z.js.map} +0 -0
  203. /package/dist/{chunk-N37X5RU7.js.map → chunk-RACZB5HT.js.map} +0 -0
  204. /package/dist/{chunk-AU7Q3LSC.js.map → chunk-RKNJBZ55.js.map} +0 -0
  205. /package/dist/{chunk-F3H7VNM4.js.map → chunk-USQAH6PN.js.map} +0 -0
  206. /package/dist/{chunk-ZFE7NXB7.js.map → chunk-VLSUKXCJ.js.map} +0 -0
  207. /package/dist/{chunk-YWD5UIWY.js.map → chunk-WQCET3N3.js.map} +0 -0
  208. /package/dist/{chunk-MUYJQ273.js.map → chunk-WSYACIKD.js.map} +0 -0
  209. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  210. /package/dist/{chunk-AWMS3IFB.js.map → chunk-XPDWNWH6.js.map} +0 -0
  211. /package/dist/{chunk-K5JFIA44.js.map → chunk-XW3G4BJI.js.map} +0 -0
  212. /package/dist/{chunk-GIC6BASP.js.map → chunk-XZBXINXZ.js.map} +0 -0
  213. /package/dist/{chunk-KDCPIXIY.js.map → chunk-YFK64FYK.js.map} +0 -0
  214. /package/dist/{chunk-DDAHG5ZD.js.map → chunk-YYAWJRGL.js.map} +0 -0
  215. /package/dist/{chunk-IYFL3534.js.map → chunk-Z2FXSE5P.js.map} +0 -0
  216. /package/dist/{chunk-CR73YTWC.js.map → chunk-ZDJMCXC7.js.map} +0 -0
  217. /package/dist/{chunk-OQT34EVH.js.map → chunk-ZVW724ZK.js.map} +0 -0
@@ -0,0 +1,158 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import {
5
+ MAX_SUPERSESSION_DEPTH,
6
+ resolveSupersessionHead,
7
+ type SupersessionItem,
8
+ } from "./supersession-chain.js";
9
+
10
+ const link = (id: string, supersedes?: string): SupersessionItem =>
11
+ supersedes === undefined ? { id } : { id, supersedes };
12
+
13
+ test("single item resolves to itself", () => {
14
+ assert.deepEqual(resolveSupersessionHead({ items: [link("a")], startId: "a" }), {
15
+ ok: true,
16
+ headId: "a",
17
+ chain: ["a"],
18
+ });
19
+ });
20
+
21
+ test("3-link chain resolves to the newest head with the full chain", () => {
22
+ const items = [link("a"), link("b", "a"), link("c", "b")];
23
+ assert.deepEqual(resolveSupersessionHead({ items, startId: "a" }), {
24
+ ok: true,
25
+ headId: "c",
26
+ chain: ["a", "b", "c"],
27
+ });
28
+ });
29
+
30
+ test("2-cycle returns cycle_detected without hanging", () => {
31
+ const items = [link("a", "b"), link("b", "a")];
32
+ assert.deepEqual(resolveSupersessionHead({ items, startId: "a" }), {
33
+ ok: false,
34
+ error: "cycle_detected",
35
+ chain: ["a", "b"],
36
+ });
37
+ });
38
+
39
+ test("longer cycle is also detected", () => {
40
+ // b supersedes a, c supersedes b, a supersedes c.
41
+ const items = [link("a", "c"), link("b", "a"), link("c", "b")];
42
+ assert.deepEqual(resolveSupersessionHead({ items, startId: "a" }), {
43
+ ok: false,
44
+ error: "cycle_detected",
45
+ chain: ["a", "b", "c"],
46
+ });
47
+ });
48
+
49
+ test("depth_exceeded with a small maxDepth", () => {
50
+ const items = [link("a"), link("b", "a"), link("c", "b"), link("d", "c")];
51
+ assert.deepEqual(resolveSupersessionHead({ items, startId: "a", maxDepth: 2 }), {
52
+ ok: false,
53
+ error: "depth_exceeded",
54
+ chain: ["a", "b"],
55
+ });
56
+ assert.deepEqual(resolveSupersessionHead({ items, startId: "a", maxDepth: 1 }), {
57
+ ok: false,
58
+ error: "depth_exceeded",
59
+ chain: ["a"],
60
+ });
61
+ // A chain exactly maxDepth ids long still resolves.
62
+ assert.deepEqual(resolveSupersessionHead({ items, startId: "b", maxDepth: 3 }), {
63
+ ok: true,
64
+ headId: "d",
65
+ chain: ["b", "c", "d"],
66
+ });
67
+ });
68
+
69
+ test("default depth is MAX_SUPERSESSION_DEPTH", () => {
70
+ assert.equal(MAX_SUPERSESSION_DEPTH, 32);
71
+ const items: SupersessionItem[] = [link("i0")];
72
+ for (let i = 1; i <= 60; i++) items.push(link(`i${i}`, `i${i - 1}`));
73
+ assert.deepEqual(resolveSupersessionHead({ items, startId: "i0" }), {
74
+ ok: false,
75
+ error: "depth_exceeded",
76
+ chain: Array.from({ length: MAX_SUPERSESSION_DEPTH }, (_, i) => `i${i}`),
77
+ });
78
+ });
79
+
80
+ test("unknown startId returns unknown_id with an empty chain", () => {
81
+ assert.deepEqual(resolveSupersessionHead({ items: [link("a")], startId: "zzz" }), {
82
+ ok: false,
83
+ error: "unknown_id",
84
+ chain: [],
85
+ });
86
+ });
87
+
88
+ test("empty and whitespace-only startId return unknown_id", () => {
89
+ const items = [link("a")];
90
+ for (const startId of ["", " "]) {
91
+ assert.deepEqual(resolveSupersessionHead({ items, startId }), {
92
+ ok: false,
93
+ error: "unknown_id",
94
+ chain: [],
95
+ });
96
+ }
97
+ });
98
+
99
+ test("startId is trimmed on the happy path", () => {
100
+ const items = [link("a"), link("b", "a")];
101
+ assert.deepEqual(resolveSupersessionHead({ items, startId: " a " }), {
102
+ ok: true,
103
+ headId: "b",
104
+ chain: ["a", "b"],
105
+ });
106
+ });
107
+
108
+ test("maxDepth 0, negative, and float throw RangeError naming maxDepth", () => {
109
+ const items = [link("a")];
110
+ for (const bad of [0, -1, -32, 2.5, Number.NaN]) {
111
+ assert.throws(
112
+ () => resolveSupersessionHead({ items, startId: "a", maxDepth: bad }),
113
+ (err: unknown) =>
114
+ err instanceof RangeError && /maxDepth/.test(String(err.message)),
115
+ );
116
+ }
117
+ });
118
+
119
+ test("fork resolves to the smallest successor id, stable across shuffles", () => {
120
+ // Both b and c supersede a; b < c, so b wins even though c appears first.
121
+ const ordered = [link("a"), link("c", "a"), link("b", "a"), link("d", "b")];
122
+ const shuffled = [link("d", "b"), link("b", "a"), link("a"), link("c", "a")];
123
+ const expected = { ok: true, headId: "d", chain: ["a", "b", "d"] };
124
+ assert.deepEqual(resolveSupersessionHead({ items: ordered, startId: "a" }), expected);
125
+ assert.deepEqual(resolveSupersessionHead({ items: shuffled, startId: "a" }), expected);
126
+ // Same shuffled input twice: identical results.
127
+ assert.deepEqual(resolveSupersessionHead({ items: shuffled, startId: "a" }), expected);
128
+ });
129
+
130
+ test("blank-id items are ignored", () => {
131
+ const items: SupersessionItem[] = [
132
+ link("a"),
133
+ link("b", "a"),
134
+ { id: "", supersedes: "b" },
135
+ { id: " ", supersedes: "b" },
136
+ { supersedes: "b" } as unknown as SupersessionItem,
137
+ link("z", " "),
138
+ ];
139
+ // The blank-id items must not make themselves successors of b.
140
+ assert.deepEqual(resolveSupersessionHead({ items, startId: "a" }), {
141
+ ok: true,
142
+ headId: "b",
143
+ chain: ["a", "b"],
144
+ });
145
+ // z has a blank supersedes pointer: it supersedes nothing.
146
+ assert.deepEqual(resolveSupersessionHead({ items, startId: "z" }), {
147
+ ok: true,
148
+ headId: "z",
149
+ chain: ["z"],
150
+ });
151
+ });
152
+
153
+ test("input is not mutated", () => {
154
+ const items = [link("c", "b"), link("a"), link("b", "a")];
155
+ const snapshot = structuredClone(items);
156
+ resolveSupersessionHead({ items, startId: "a" });
157
+ assert.deepEqual(items, snapshot);
158
+ });
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Supersession chain resolution for shared-context curation (issue #1957).
3
+ *
4
+ * A shared item may supersede another item, which may itself be superseded.
5
+ * Curation needs the live head of that chain. This module resolves the head
6
+ * deterministically and survives malformed input: cycles, forks, and depth
7
+ * blowups return errors instead of throwing or looping.
8
+ */
9
+
10
+ export interface SupersessionItem {
11
+ id: string;
12
+ /** Id of the item this one replaces, when any. */
13
+ supersedes?: string;
14
+ }
15
+
16
+ export type SupersessionChainResult =
17
+ | { ok: true; headId: string; chain: string[] }
18
+ | {
19
+ ok: false;
20
+ error: "unknown_id" | "cycle_detected" | "depth_exceeded";
21
+ chain: string[];
22
+ };
23
+
24
+ export const MAX_SUPERSESSION_DEPTH = 32;
25
+
26
+ /**
27
+ * Resolve the live head of the supersession chain that starts at `startId`.
28
+ *
29
+ * Direction: forward. The successor of id X is the item whose `supersedes`
30
+ * points at X. The head is the item that nothing supersedes.
31
+ *
32
+ * Fork policy: when two items supersede the same id, the lexicographically
33
+ * smallest successor id wins. This keeps the result dependent only on the
34
+ * item set, never on array order.
35
+ *
36
+ * Pure: reads the input, mutates nothing, performs no I/O.
37
+ */
38
+ export function resolveSupersessionHead(input: {
39
+ items: readonly SupersessionItem[];
40
+ startId: string;
41
+ maxDepth?: number;
42
+ }): SupersessionChainResult {
43
+ const { items, maxDepth } = input;
44
+ if (maxDepth !== undefined && (!Number.isInteger(maxDepth) || maxDepth < 1)) {
45
+ throw new RangeError(`maxDepth must be a positive integer, got ${maxDepth}`);
46
+ }
47
+ const depth = maxDepth ?? MAX_SUPERSESSION_DEPTH;
48
+
49
+ const start = input.startId.trim();
50
+ if (start === "") {
51
+ return { ok: false, error: "unknown_id", chain: [] };
52
+ }
53
+
54
+ // Build the id set and the successor map. Blank ids cannot participate,
55
+ // and a blank `supersedes` value cannot point at anything.
56
+ const ids = new Set<string>();
57
+ const successorOf = new Map<string, string>();
58
+ for (const item of items) {
59
+ if (typeof item?.id !== "string") continue;
60
+ const id = item.id.trim();
61
+ if (id === "") continue;
62
+ ids.add(id);
63
+ const target =
64
+ typeof item.supersedes === "string" ? item.supersedes.trim() : "";
65
+ if (target === "") continue;
66
+ // Fork resolution: keep the smallest successor id for determinism.
67
+ const best = successorOf.get(target);
68
+ if (best === undefined || id < best) {
69
+ successorOf.set(target, id);
70
+ }
71
+ }
72
+
73
+ if (!ids.has(start)) {
74
+ return { ok: false, error: "unknown_id", chain: [] };
75
+ }
76
+
77
+ const chain = [start];
78
+ const visited = new Set<string>([start]);
79
+ let current = start;
80
+ for (;;) {
81
+ const next = successorOf.get(current);
82
+ if (next === undefined) {
83
+ return { ok: true, headId: current, chain };
84
+ }
85
+ if (visited.has(next)) {
86
+ return { ok: false, error: "cycle_detected", chain };
87
+ }
88
+ if (chain.length + 1 > depth) {
89
+ return { ok: false, error: "depth_exceeded", chain };
90
+ }
91
+ chain.push(next);
92
+ visited.add(next);
93
+ current = next;
94
+ }
95
+ }