@nicia-ai/typegraph 0.30.0 → 0.32.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 (187) hide show
  1. package/README.md +39 -0
  2. package/dist/backend/postgres/index.cjs +19 -18
  3. package/dist/backend/postgres/index.d.cts +6 -6
  4. package/dist/backend/postgres/index.d.ts +6 -6
  5. package/dist/backend/postgres/index.js +9 -8
  6. package/dist/backend/postgres/pglite.cjs +13 -12
  7. package/dist/backend/postgres/pglite.cjs.map +1 -1
  8. package/dist/backend/postgres/pglite.d.cts +3 -3
  9. package/dist/backend/postgres/pglite.d.ts +3 -3
  10. package/dist/backend/postgres/pglite.js +9 -8
  11. package/dist/backend/postgres/pglite.js.map +1 -1
  12. package/dist/backend/sqlite/index.cjs +16 -16
  13. package/dist/backend/sqlite/index.d.cts +7 -7
  14. package/dist/backend/sqlite/index.d.ts +7 -7
  15. package/dist/backend/sqlite/index.js +7 -7
  16. package/dist/backend/sqlite/libsql.cjs +12 -12
  17. package/dist/backend/sqlite/libsql.d.cts +3 -3
  18. package/dist/backend/sqlite/libsql.d.ts +3 -3
  19. package/dist/backend/sqlite/libsql.js +8 -8
  20. package/dist/backend/sqlite/local.cjs +13 -13
  21. package/dist/backend/sqlite/local.d.cts +3 -3
  22. package/dist/backend/sqlite/local.d.ts +3 -3
  23. package/dist/backend/sqlite/local.js +8 -8
  24. package/dist/{chunk-3JLRHQ67.js → chunk-2BVHPOIG.js} +7 -6
  25. package/dist/chunk-2BVHPOIG.js.map +1 -0
  26. package/dist/chunk-2LAT5BIK.cjs +16277 -0
  27. package/dist/chunk-2LAT5BIK.cjs.map +1 -0
  28. package/dist/chunk-2YMGNYXY.js +40 -0
  29. package/dist/chunk-2YMGNYXY.js.map +1 -0
  30. package/dist/chunk-3EUKJEQ3.js +77 -0
  31. package/dist/chunk-3EUKJEQ3.js.map +1 -0
  32. package/dist/{chunk-WFCECD6Q.cjs → chunk-3HQDMOBK.cjs} +73 -68
  33. package/dist/chunk-3HQDMOBK.cjs.map +1 -0
  34. package/dist/chunk-3QRBXYT5.cjs +94 -0
  35. package/dist/chunk-3QRBXYT5.cjs.map +1 -0
  36. package/dist/chunk-6G35I3VW.cjs +40 -0
  37. package/dist/chunk-6G35I3VW.cjs.map +1 -0
  38. package/dist/{chunk-4BXHVEFB.cjs → chunk-6RMLO5O4.cjs} +6 -51
  39. package/dist/chunk-6RMLO5O4.cjs.map +1 -0
  40. package/dist/chunk-6ZR4QC3O.cjs +12 -0
  41. package/dist/{chunk-LUHU4T2F.cjs.map → chunk-6ZR4QC3O.cjs.map} +1 -1
  42. package/dist/chunk-73KASJGO.cjs +52 -0
  43. package/dist/chunk-73KASJGO.cjs.map +1 -0
  44. package/dist/{chunk-IUIJJYPI.js → chunk-7WW3WL6E.js} +6 -5
  45. package/dist/chunk-7WW3WL6E.js.map +1 -0
  46. package/dist/chunk-A3W7QG7K.js +45 -0
  47. package/dist/chunk-A3W7QG7K.js.map +1 -0
  48. package/dist/chunk-AW4A3P5A.cjs +51 -0
  49. package/dist/chunk-AW4A3P5A.cjs.map +1 -0
  50. package/dist/{chunk-AI3RNREM.js → chunk-C4C4Q7A6.js} +94 -496
  51. package/dist/chunk-C4C4Q7A6.js.map +1 -0
  52. package/dist/{chunk-4LBO6KIF.cjs → chunk-CAMMC3E4.cjs} +60 -59
  53. package/dist/chunk-CAMMC3E4.cjs.map +1 -0
  54. package/dist/{chunk-3IRMLJEK.js → chunk-CEF2QOTO.js} +3 -3
  55. package/dist/{chunk-3IRMLJEK.js.map → chunk-CEF2QOTO.js.map} +1 -1
  56. package/dist/{chunk-UESTHN47.cjs → chunk-CFNT3ITG.cjs} +95 -507
  57. package/dist/chunk-CFNT3ITG.cjs.map +1 -0
  58. package/dist/chunk-CWFPJ3RC.js +282 -0
  59. package/dist/chunk-CWFPJ3RC.js.map +1 -0
  60. package/dist/{chunk-7JGLPAFV.cjs → chunk-EEYV6UOE.cjs} +16 -16
  61. package/dist/chunk-EEYV6UOE.cjs.map +1 -0
  62. package/dist/{chunk-FWBVPATK.cjs → chunk-FDN43KXV.cjs} +19 -19
  63. package/dist/{chunk-FWBVPATK.cjs.map → chunk-FDN43KXV.cjs.map} +1 -1
  64. package/dist/chunk-HFBRAOF7.js +610 -0
  65. package/dist/chunk-HFBRAOF7.js.map +1 -0
  66. package/dist/chunk-HTGDWIDE.cjs +89 -0
  67. package/dist/chunk-HTGDWIDE.cjs.map +1 -0
  68. package/dist/{chunk-R4JOBEJU.js → chunk-IJORSAC3.js} +4 -40
  69. package/dist/chunk-IJORSAC3.js.map +1 -0
  70. package/dist/{chunk-WEMZM7J7.cjs → chunk-JKRKIEVA.cjs} +5 -41
  71. package/dist/chunk-JKRKIEVA.cjs.map +1 -0
  72. package/dist/{chunk-SGPXKJ4A.cjs → chunk-KTIEB7VH.cjs} +26 -16
  73. package/dist/chunk-KTIEB7VH.cjs.map +1 -0
  74. package/dist/{chunk-K4N35E7B.cjs → chunk-LAZGHOZW.cjs} +72 -71
  75. package/dist/chunk-LAZGHOZW.cjs.map +1 -0
  76. package/dist/{chunk-VFX3LC76.cjs → chunk-LM2IC4ZJ.cjs} +14 -14
  77. package/dist/{chunk-VFX3LC76.cjs.map → chunk-LM2IC4ZJ.cjs.map} +1 -1
  78. package/dist/{chunk-K5VREXMZ.js → chunk-NDM5PXEH.js} +7 -6
  79. package/dist/chunk-NDM5PXEH.js.map +1 -0
  80. package/dist/chunk-PIHXFF73.js +90 -0
  81. package/dist/chunk-PIHXFF73.js.map +1 -0
  82. package/dist/{chunk-FVIGJTS7.js → chunk-PRMKPFN5.js} +4 -74
  83. package/dist/chunk-PRMKPFN5.js.map +1 -0
  84. package/dist/chunk-SAJSMR2B.js +35 -0
  85. package/dist/chunk-SAJSMR2B.js.map +1 -0
  86. package/dist/chunk-SB65GJNY.cjs +624 -0
  87. package/dist/chunk-SB65GJNY.cjs.map +1 -0
  88. package/dist/{chunk-N2OEOGBB.js → chunk-TUA3MEJJ.js} +13 -8
  89. package/dist/chunk-TUA3MEJJ.js.map +1 -0
  90. package/dist/{chunk-MAQJYNJR.js → chunk-W4ZX2GCP.js} +14 -16
  91. package/dist/chunk-W4ZX2GCP.js.map +1 -0
  92. package/dist/chunk-WNORITK6.js +16231 -0
  93. package/dist/chunk-WNORITK6.js.map +1 -0
  94. package/dist/chunk-WWVV44DY.cjs +287 -0
  95. package/dist/chunk-WWVV44DY.cjs.map +1 -0
  96. package/dist/{chunk-IX7RE7CG.js → chunk-WYTUF3QO.js} +3 -3
  97. package/dist/{chunk-IX7RE7CG.js.map → chunk-WYTUF3QO.js.map} +1 -1
  98. package/dist/{chunk-2QA5EHEN.js → chunk-XKNAK27R.js} +3 -3
  99. package/dist/{chunk-2QA5EHEN.js.map → chunk-XKNAK27R.js.map} +1 -1
  100. package/dist/{chunk-633N7DSE.js → chunk-XQFAJVYW.js} +4 -36
  101. package/dist/chunk-XQFAJVYW.js.map +1 -0
  102. package/dist/{chunk-ENDUY2CD.js → chunk-YCV72E6L.js} +23 -17
  103. package/dist/chunk-YCV72E6L.js.map +1 -0
  104. package/dist/{chunk-26HWIIVS.cjs → chunk-YL4F3CMD.cjs} +63 -62
  105. package/dist/chunk-YL4F3CMD.cjs.map +1 -0
  106. package/dist/{chunk-IE6GN7L3.cjs → chunk-YW7GHMF4.cjs} +17 -98
  107. package/dist/chunk-YW7GHMF4.cjs.map +1 -0
  108. package/dist/{ddl-EdkIiFVU.d.ts → ddl-C8e8zDAE.d.ts} +3 -3
  109. package/dist/{ddl-BSFlbGQF.d.cts → ddl-CwyG4HoQ.d.cts} +3 -3
  110. package/dist/{extension-types-CwHV-Uk4.d.cts → extension-types-D1k0inWU.d.cts} +1 -1
  111. package/dist/{extension-types-CwHV-Uk4.d.ts → extension-types-D1k0inWU.d.ts} +1 -1
  112. package/dist/graph-extension/index.cjs +17 -17
  113. package/dist/graph-extension/index.d.cts +5 -16
  114. package/dist/graph-extension/index.d.ts +5 -16
  115. package/dist/graph-extension/index.js +2 -2
  116. package/dist/graph-merge/index.cjs +3966 -0
  117. package/dist/graph-merge/index.cjs.map +1 -0
  118. package/dist/graph-merge/index.d.cts +1191 -0
  119. package/dist/graph-merge/index.d.ts +1191 -0
  120. package/dist/graph-merge/index.js +3935 -0
  121. package/dist/graph-merge/index.js.map +1 -0
  122. package/dist/{index-DzeIiMO3.d.ts → index-BEK0VtNB.d.ts} +2 -2
  123. package/dist/{index-Ln_KHTGA.d.cts → index-C8sSTErx.d.cts} +2 -2
  124. package/dist/index.cjs +291 -15919
  125. package/dist/index.cjs.map +1 -1
  126. package/dist/index.d.cts +12 -11
  127. package/dist/index.d.ts +12 -11
  128. package/dist/index.js +24 -15791
  129. package/dist/index.js.map +1 -1
  130. package/dist/indexes/index.cjs +30 -26
  131. package/dist/indexes/index.d.cts +5 -5
  132. package/dist/indexes/index.d.ts +5 -5
  133. package/dist/indexes/index.js +6 -2
  134. package/dist/interchange/index.cjs +49 -592
  135. package/dist/interchange/index.cjs.map +1 -1
  136. package/dist/interchange/index.d.cts +9 -9
  137. package/dist/interchange/index.d.ts +9 -9
  138. package/dist/interchange/index.js +2 -588
  139. package/dist/interchange/index.js.map +1 -1
  140. package/dist/{manager-BbdyNhkv.d.ts → manager-8htmuGr-.d.ts} +3 -3
  141. package/dist/{manager-wTDgjoXG.d.cts → manager-aVgxbGt-.d.cts} +3 -3
  142. package/dist/{materialize-indexes-lkawaDor.d.ts → materialize-indexes-CAGhqPHQ.d.ts} +1 -1
  143. package/dist/{materialize-indexes-oanE3RU0.d.cts → materialize-indexes-DEYkU-wr.d.cts} +1 -1
  144. package/dist/{postgres-CK2ByP1q.d.cts → postgres-Dhom7iiX.d.ts} +1 -1
  145. package/dist/{postgres-ymXYC6Rr.d.ts → postgres-jgXmD45C.d.cts} +1 -1
  146. package/dist/profiler/index.d.cts +7 -7
  147. package/dist/profiler/index.d.ts +7 -7
  148. package/dist/result-DWo6mUVz.d.cts +37 -0
  149. package/dist/result-DWo6mUVz.d.ts +37 -0
  150. package/dist/schema/index.cjs +30 -29
  151. package/dist/schema/index.cjs.map +1 -1
  152. package/dist/schema/index.d.cts +7 -7
  153. package/dist/schema/index.d.ts +7 -7
  154. package/dist/schema/index.js +7 -6
  155. package/dist/schema/index.js.map +1 -1
  156. package/dist/{sqlite-R8YGOqVQ.d.ts → sqlite-BfLsCEqO.d.ts} +1 -1
  157. package/dist/{sqlite-DxH1lrwD.d.cts → sqlite-DcQ_BCUW.d.cts} +1 -1
  158. package/dist/{store-CNKM334w.d.ts → store-view-lTcb_cnp.d.cts} +694 -366
  159. package/dist/{store--z1Tmk7U.d.cts → store-view-pBOUrQte.d.ts} +694 -366
  160. package/dist/{types-D5B6Be5c.d.cts → types-5RQwzWXx.d.cts} +2 -2
  161. package/dist/{types-CMvxx2kd.d.ts → types-DGDiXAYc.d.ts} +2 -2
  162. package/dist/{types-BBiDqJCU.d.ts → types-Dk8r09KY.d.ts} +16 -2
  163. package/dist/{types-dL-clxO3.d.cts → types-Dvmn-eal.d.cts} +16 -2
  164. package/dist/{where-CAo6wsBZ.d.ts → where-CHZFELX0.d.ts} +2 -2
  165. package/dist/{where-bJjuOCeV.d.cts → where-Cx_glEEB.d.cts} +2 -2
  166. package/package.json +7 -2
  167. package/dist/chunk-26HWIIVS.cjs.map +0 -1
  168. package/dist/chunk-3JLRHQ67.js.map +0 -1
  169. package/dist/chunk-4BXHVEFB.cjs.map +0 -1
  170. package/dist/chunk-4LBO6KIF.cjs.map +0 -1
  171. package/dist/chunk-633N7DSE.js.map +0 -1
  172. package/dist/chunk-7JGLPAFV.cjs.map +0 -1
  173. package/dist/chunk-AI3RNREM.js.map +0 -1
  174. package/dist/chunk-ENDUY2CD.js.map +0 -1
  175. package/dist/chunk-FVIGJTS7.js.map +0 -1
  176. package/dist/chunk-IE6GN7L3.cjs.map +0 -1
  177. package/dist/chunk-IUIJJYPI.js.map +0 -1
  178. package/dist/chunk-K4N35E7B.cjs.map +0 -1
  179. package/dist/chunk-K5VREXMZ.js.map +0 -1
  180. package/dist/chunk-LUHU4T2F.cjs +0 -12
  181. package/dist/chunk-MAQJYNJR.js.map +0 -1
  182. package/dist/chunk-N2OEOGBB.js.map +0 -1
  183. package/dist/chunk-R4JOBEJU.js.map +0 -1
  184. package/dist/chunk-SGPXKJ4A.cjs.map +0 -1
  185. package/dist/chunk-UESTHN47.cjs.map +0 -1
  186. package/dist/chunk-WEMZM7J7.cjs.map +0 -1
  187. package/dist/chunk-WFCECD6Q.cjs.map +0 -1
@@ -0,0 +1,3935 @@
1
+ import { createStoreWithSchema, generateId, computeUniqueKey } from '../chunk-WNORITK6.js';
2
+ import '../chunk-3EUKJEQ3.js';
3
+ import { exportGraph, importGraph } from '../chunk-HFBRAOF7.js';
4
+ import { defineNode, computeSchemaHash, serializeSchema } from '../chunk-2BVHPOIG.js';
5
+ import { getNodeKinds, getEdgeKinds, defineGraph } from '../chunk-W4ZX2GCP.js';
6
+ import '../chunk-PIHXFF73.js';
7
+ import '../chunk-IJORSAC3.js';
8
+ import '../chunk-MXE2U3EX.js';
9
+ import '../chunk-454I6GDF.js';
10
+ import { ok, err, isErr, computeTransitiveClosure, isReachable } from '../chunk-YCV72E6L.js';
11
+ export { isErr, isOk, unwrap } from '../chunk-YCV72E6L.js';
12
+ import '../chunk-SAJSMR2B.js';
13
+ import { sortedReplacer } from '../chunk-M6OC455F.js';
14
+ import '../chunk-C4C4Q7A6.js';
15
+ import { sha256Hex } from '../chunk-2YMGNYXY.js';
16
+ import { TypeGraphError } from '../chunk-TQOYM4LN.js';
17
+ import '../chunk-K5P5OZXS.js';
18
+ import { z } from 'zod';
19
+
20
+ // src/graph-merge/canonical-props.ts
21
+ function canonicalizeProps(props) {
22
+ return JSON.stringify(props, sortedReplacer);
23
+ }
24
+ function canonicalValueKey(value) {
25
+ return JSON.stringify(value, sortedReplacer);
26
+ }
27
+ function parseRowProps(props) {
28
+ let parsed;
29
+ try {
30
+ parsed = JSON.parse(props);
31
+ } catch {
32
+ return {};
33
+ }
34
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
35
+ return {};
36
+ }
37
+ return parsed;
38
+ }
39
+
40
+ // src/graph-merge/node-key.ts
41
+ var SEPARATOR = String.fromCharCode(0);
42
+ function mergeKey(kind, id) {
43
+ if (kind.includes(SEPARATOR)) {
44
+ throw new Error(
45
+ `Node kind ${JSON.stringify(kind)} contains a NUL byte, which collides the (kind, id) merge-identity separator.`
46
+ );
47
+ }
48
+ return `${kind}${SEPARATOR}${id}`;
49
+ }
50
+ function compareStrings(left, right) {
51
+ return left < right ? -1 : left > right ? 1 : 0;
52
+ }
53
+ function mergeKeyOf(node) {
54
+ return mergeKey(node.kind, node.id);
55
+ }
56
+ function kindOf(key) {
57
+ const separator = key.indexOf(SEPARATOR);
58
+ return separator === -1 ? key : key.slice(0, separator);
59
+ }
60
+ function idOf(key) {
61
+ const separator = key.indexOf(SEPARATOR);
62
+ return separator === -1 ? key : key.slice(separator + 1);
63
+ }
64
+ function compareMergeKeys(left, right) {
65
+ const leftId = idOf(left);
66
+ const rightId = idOf(right);
67
+ if (leftId !== rightId) {
68
+ return leftId < rightId ? -1 : 1;
69
+ }
70
+ const leftKind = kindOf(left);
71
+ const rightKind = kindOf(right);
72
+ return leftKind < rightKind ? -1 : leftKind > rightKind ? 1 : 0;
73
+ }
74
+
75
+ // src/graph-merge/state-diff.ts
76
+ var ENUMERATION_PAGE_SIZE = 1e3;
77
+ async function enumerateAllNodes(backend, graphId, kind) {
78
+ const collected = [];
79
+ let after = void 0;
80
+ for (; ; ) {
81
+ const page = await backend.findNodesByKind({
82
+ graphId,
83
+ kind,
84
+ excludeDeleted: false,
85
+ orderBy: "id",
86
+ limit: ENUMERATION_PAGE_SIZE,
87
+ ...after === void 0 ? {} : { after }
88
+ });
89
+ for (const row of page) {
90
+ collected.push(row);
91
+ }
92
+ if (page.length < ENUMERATION_PAGE_SIZE) {
93
+ break;
94
+ }
95
+ after = page.at(-1).id;
96
+ }
97
+ return collected;
98
+ }
99
+ async function enumerateAllEdges(backend, graphId, kind) {
100
+ const collected = [];
101
+ let after = void 0;
102
+ for (; ; ) {
103
+ const page = await backend.findEdgesByKind({
104
+ graphId,
105
+ kind,
106
+ excludeDeleted: false,
107
+ orderBy: "id",
108
+ limit: ENUMERATION_PAGE_SIZE,
109
+ ...after === void 0 ? {} : { after }
110
+ });
111
+ for (const row of page) {
112
+ collected.push(row);
113
+ }
114
+ if (page.length < ENUMERATION_PAGE_SIZE) {
115
+ break;
116
+ }
117
+ after = page.at(-1).id;
118
+ }
119
+ return collected;
120
+ }
121
+ function isLive(row) {
122
+ return row.deleted_at === void 0;
123
+ }
124
+ function indexById(rows) {
125
+ const index = /* @__PURE__ */ new Map();
126
+ for (const row of rows) {
127
+ index.set(row.id, row);
128
+ }
129
+ return index;
130
+ }
131
+ function diffNodeKind(kind, baseRows, forkRows) {
132
+ const baseIndex = indexById(baseRows);
133
+ const forkIndex = indexById(forkRows);
134
+ const created = [];
135
+ const modified = [];
136
+ const deleted = [];
137
+ for (const forkRow of forkRows) {
138
+ if (!isLive(forkRow)) {
139
+ continue;
140
+ }
141
+ const baseRow = baseIndex.get(forkRow.id);
142
+ const forkProps = parseRowProps(forkRow.props);
143
+ if (baseRow === void 0 || !isLive(baseRow)) {
144
+ created.push({
145
+ id: forkRow.id,
146
+ kind,
147
+ props: forkProps,
148
+ row: forkRow
149
+ });
150
+ continue;
151
+ }
152
+ const baseProps = parseRowProps(baseRow.props);
153
+ if (canonicalizeProps(baseProps) !== canonicalizeProps(forkProps)) {
154
+ modified.push({
155
+ id: forkRow.id,
156
+ kind,
157
+ baseProps,
158
+ forkProps,
159
+ row: forkRow
160
+ });
161
+ }
162
+ }
163
+ for (const baseRow of baseRows) {
164
+ if (!isLive(baseRow)) {
165
+ continue;
166
+ }
167
+ const forkRow = forkIndex.get(baseRow.id);
168
+ if (forkRow === void 0 || !isLive(forkRow)) {
169
+ deleted.push({ id: baseRow.id, kind });
170
+ }
171
+ }
172
+ return { new: created, modified, deleted };
173
+ }
174
+ function diffEdgeKind(kind, baseRows, forkRows) {
175
+ const baseIndex = indexById(baseRows);
176
+ const forkIndex = indexById(forkRows);
177
+ const created = [];
178
+ const modified = [];
179
+ const deleted = [];
180
+ for (const forkRow of forkRows) {
181
+ if (!isLive(forkRow)) {
182
+ continue;
183
+ }
184
+ const baseRow = baseIndex.get(forkRow.id);
185
+ const forkProps = parseRowProps(forkRow.props);
186
+ if (baseRow === void 0 || !isLive(baseRow)) {
187
+ created.push({
188
+ id: forkRow.id,
189
+ kind,
190
+ fromId: forkRow.from_id,
191
+ toId: forkRow.to_id,
192
+ fromKind: forkRow.from_kind,
193
+ toKind: forkRow.to_kind,
194
+ props: forkProps,
195
+ row: forkRow
196
+ });
197
+ continue;
198
+ }
199
+ const baseProps = parseRowProps(baseRow.props);
200
+ if (canonicalizeProps(baseProps) !== canonicalizeProps(forkProps)) {
201
+ modified.push({
202
+ id: forkRow.id,
203
+ kind,
204
+ fromId: forkRow.from_id,
205
+ toId: forkRow.to_id,
206
+ fromKind: forkRow.from_kind,
207
+ toKind: forkRow.to_kind,
208
+ baseProps,
209
+ forkProps,
210
+ row: forkRow
211
+ });
212
+ }
213
+ }
214
+ for (const baseRow of baseRows) {
215
+ if (!isLive(baseRow)) {
216
+ continue;
217
+ }
218
+ const forkRow = forkIndex.get(baseRow.id);
219
+ if (forkRow === void 0 || !isLive(forkRow)) {
220
+ deleted.push({ id: baseRow.id, kind });
221
+ }
222
+ }
223
+ return { new: created, modified, deleted };
224
+ }
225
+ function byId(left, right) {
226
+ return compareStrings(left.id, right.id);
227
+ }
228
+ async function diffAgainstBase(baseStore, forkStore) {
229
+ const graph = baseStore.graph;
230
+ const nodeKinds = getNodeKinds(graph);
231
+ const edgeKinds = getEdgeKinds(graph);
232
+ const newNodes = [];
233
+ const modifiedNodes = [];
234
+ const deletedNodes = [];
235
+ for (const kind of nodeKinds) {
236
+ const baseRows = await enumerateAllNodes(
237
+ baseStore.backend,
238
+ baseStore.graphId,
239
+ kind
240
+ );
241
+ const forkRows = await enumerateAllNodes(
242
+ forkStore.backend,
243
+ forkStore.graphId,
244
+ kind
245
+ );
246
+ const delta = diffNodeKind(kind, baseRows, forkRows);
247
+ for (const entry of delta.new) {
248
+ newNodes.push(entry);
249
+ }
250
+ for (const entry of delta.modified) {
251
+ modifiedNodes.push(entry);
252
+ }
253
+ for (const entry of delta.deleted) {
254
+ deletedNodes.push(entry);
255
+ }
256
+ }
257
+ const newEdges = [];
258
+ const modifiedEdges = [];
259
+ const deletedEdges = [];
260
+ for (const kind of edgeKinds) {
261
+ const baseRows = await enumerateAllEdges(
262
+ baseStore.backend,
263
+ baseStore.graphId,
264
+ kind
265
+ );
266
+ const forkRows = await enumerateAllEdges(
267
+ forkStore.backend,
268
+ forkStore.graphId,
269
+ kind
270
+ );
271
+ const delta = diffEdgeKind(kind, baseRows, forkRows);
272
+ for (const entry of delta.new) {
273
+ newEdges.push(entry);
274
+ }
275
+ for (const entry of delta.modified) {
276
+ modifiedEdges.push(entry);
277
+ }
278
+ for (const entry of delta.deleted) {
279
+ deletedEdges.push(entry);
280
+ }
281
+ }
282
+ return {
283
+ nodes: {
284
+ new: newNodes.sort((left, right) => byId(left, right)),
285
+ modified: modifiedNodes.sort((left, right) => byId(left, right)),
286
+ deleted: deletedNodes.sort((left, right) => byId(left, right))
287
+ },
288
+ edges: {
289
+ new: newEdges.sort((left, right) => byId(left, right)),
290
+ modified: modifiedEdges.sort((left, right) => byId(left, right)),
291
+ deleted: deletedEdges.sort((left, right) => byId(left, right))
292
+ }
293
+ };
294
+ }
295
+
296
+ // src/graph-merge/types.ts
297
+ function asBranchId(value) {
298
+ return value;
299
+ }
300
+ function asBaseVersion(value) {
301
+ return value;
302
+ }
303
+
304
+ // src/graph-merge/base-version.ts
305
+ var TOKEN_SEPARATOR = "\0";
306
+ var FALLBACK_SCHEMA_VERSION = 1;
307
+ async function readActiveSchemaVersion(backend, graphId) {
308
+ const active = await backend.getActiveSchema(graphId);
309
+ return active?.version ?? FALLBACK_SCHEMA_VERSION;
310
+ }
311
+ async function computeSchemaComponent(store) {
312
+ const version = await readActiveSchemaVersion(store.backend, store.graphId);
313
+ return computeSchemaHash(serializeSchema(store.graph, version));
314
+ }
315
+ function byDigestEntry(left, right) {
316
+ const byKind = compareStrings(left.kind, right.kind);
317
+ return byKind === 0 ? compareStrings(left.id, right.id) : byKind;
318
+ }
319
+ var CONTENT_FINGERPRINT_BYTES = 16;
320
+ async function computeContentComponent(backend, graphId, graph) {
321
+ const nodeKinds = getNodeKinds(graph);
322
+ const edgeKinds = getEdgeKinds(graph);
323
+ const nodeDigest = [];
324
+ for (const kind of nodeKinds) {
325
+ const rows = await enumerateAllNodes(backend, graphId, kind);
326
+ for (const row of rows) {
327
+ if (row.deleted_at === void 0) {
328
+ nodeDigest.push({
329
+ id: row.id,
330
+ kind: row.kind,
331
+ updatedAt: row.updated_at,
332
+ validFrom: row.valid_from,
333
+ validTo: row.valid_to,
334
+ props: canonicalizeProps(parseRowProps(row.props))
335
+ });
336
+ }
337
+ }
338
+ }
339
+ const edgeDigest = [];
340
+ for (const kind of edgeKinds) {
341
+ const rows = await enumerateAllEdges(backend, graphId, kind);
342
+ for (const row of rows) {
343
+ if (row.deleted_at === void 0) {
344
+ edgeDigest.push({
345
+ id: row.id,
346
+ kind: row.kind,
347
+ fromId: row.from_id,
348
+ toId: row.to_id,
349
+ updatedAt: row.updated_at,
350
+ validFrom: row.valid_from,
351
+ validTo: row.valid_to,
352
+ props: canonicalizeProps(parseRowProps(row.props))
353
+ });
354
+ }
355
+ }
356
+ }
357
+ return sha256Hex(
358
+ canonicalizeProps({
359
+ nodes: nodeDigest.sort((left, right) => byDigestEntry(left, right)),
360
+ edges: edgeDigest.sort((left, right) => byDigestEntry(left, right))
361
+ }),
362
+ CONTENT_FINGERPRINT_BYTES
363
+ );
364
+ }
365
+ async function computeBaseVersion(store) {
366
+ const [schemaComponent, contentComponent] = await Promise.all([
367
+ computeSchemaComponent(store),
368
+ computeContentComponent(store.backend, store.graphId, store.graph)
369
+ ]);
370
+ return asBaseVersion(
371
+ `${schemaComponent}${TOKEN_SEPARATOR}${contentComponent}`
372
+ );
373
+ }
374
+ function contentComponentOf(version) {
375
+ const separatorIndex = version.indexOf(TOKEN_SEPARATOR);
376
+ return separatorIndex === -1 ? version : version.slice(separatorIndex + 1);
377
+ }
378
+
379
+ // src/graph-merge/errors.ts
380
+ var MERGE_ERROR_CODES = {
381
+ merge: "GRAPH_MERGE_ERROR",
382
+ branch: "GRAPH_MERGE_BRANCH_ERROR",
383
+ similarityUnavailable: "GRAPH_MERGE_SIMILARITY_UNAVAILABLE",
384
+ conflict: "GRAPH_MERGE_CONFLICT",
385
+ baseVersionMismatch: "GRAPH_MERGE_BASE_VERSION_MISMATCH"
386
+ };
387
+ function toTypeGraphErrorOptions(category, options) {
388
+ return {
389
+ category,
390
+ ...options.details === void 0 ? {} : { details: options.details },
391
+ ...options.suggestion === void 0 ? {} : { suggestion: options.suggestion },
392
+ ...options.cause === void 0 ? {} : { cause: options.cause }
393
+ };
394
+ }
395
+ var MergeError = class extends TypeGraphError {
396
+ constructor(message, options = {}) {
397
+ super(
398
+ message,
399
+ MERGE_ERROR_CODES.merge,
400
+ toTypeGraphErrorOptions("system", options)
401
+ );
402
+ this.name = "MergeError";
403
+ }
404
+ };
405
+ var BranchError = class extends TypeGraphError {
406
+ constructor(message, options = {}) {
407
+ super(
408
+ message,
409
+ MERGE_ERROR_CODES.branch,
410
+ toTypeGraphErrorOptions("system", options)
411
+ );
412
+ this.name = "BranchError";
413
+ }
414
+ };
415
+ var SimilarityUnavailableError = class extends MergeError {
416
+ code = MERGE_ERROR_CODES.similarityUnavailable;
417
+ constructor(message, options = {}) {
418
+ super(message, {
419
+ ...options,
420
+ suggestion: options.suggestion ?? "Pass MergeOptions.embedder (a local model), or use a fulltext/custom similarity strategy."
421
+ });
422
+ this.name = "SimilarityUnavailableError";
423
+ }
424
+ };
425
+ var MergeConflictError = class extends MergeError {
426
+ code = MERGE_ERROR_CODES.conflict;
427
+ constructor(message, options = {}) {
428
+ super(message, options);
429
+ this.name = "MergeConflictError";
430
+ }
431
+ };
432
+ var BaseVersionMismatchError = class extends MergeError {
433
+ code = MERGE_ERROR_CODES.baseVersionMismatch;
434
+ constructor(message, options = {}) {
435
+ super(message, {
436
+ ...options,
437
+ suggestion: options.suggestion ?? "Re-branch from the current target so the branch base matches before merging."
438
+ });
439
+ this.name = "BaseVersionMismatchError";
440
+ }
441
+ };
442
+
443
+ // src/graph-merge/working-copy.ts
444
+ var CLONE_IMPORT_BATCH_SIZE = 1e3;
445
+ function cloneWorkingCopyStrategy(makeBackend) {
446
+ return {
447
+ create: async (baseStore) => {
448
+ const data = await exportGraph(baseStore, {
449
+ includeMeta: true,
450
+ includeDeleted: false
451
+ });
452
+ const backend = await makeBackend();
453
+ try {
454
+ const [freshStore] = await createStoreWithSchema(
455
+ baseStore.graph,
456
+ backend
457
+ );
458
+ const result = await importGraph(freshStore, data, {
459
+ onConflict: "error",
460
+ onUnknownProperty: "error",
461
+ validateReferences: true,
462
+ batchSize: CLONE_IMPORT_BATCH_SIZE
463
+ });
464
+ if (!result.success) {
465
+ throw new BranchError(
466
+ "Clone import failed: the working copy could not be seeded from the base store. The backend returned by makeBackend() must be empty.",
467
+ { details: { errors: result.errors } }
468
+ );
469
+ }
470
+ return freshStore;
471
+ } catch (error) {
472
+ try {
473
+ await backend.close();
474
+ } catch {
475
+ }
476
+ throw error;
477
+ }
478
+ }
479
+ };
480
+ }
481
+
482
+ // src/graph-merge/branch.ts
483
+ async function branch(baseStore, makeBackend, options, strategy) {
484
+ try {
485
+ const base = await computeBaseVersion(baseStore);
486
+ const id = options?.id ?? asBranchId(generateId());
487
+ const workingCopyStrategy = strategy ?? cloneWorkingCopyStrategy(makeBackend);
488
+ const store = await workingCopyStrategy.create(baseStore);
489
+ return ok({ id, base, store });
490
+ } catch (error) {
491
+ return err(
492
+ new BranchError("Failed to create working-copy branch of base store", {
493
+ cause: error
494
+ })
495
+ );
496
+ }
497
+ }
498
+
499
+ // src/graph-merge/blocking.ts
500
+ var UNBLOCKED_BUCKET_KEY = "unblocked";
501
+ var KEY_PART_SEPARATOR = "\0";
502
+ var BLOCK_KEY_PREFIX = "b";
503
+ var UNIQUE_KEY_PREFIX = "u";
504
+ function isUniqueBucketKey(key) {
505
+ return key.startsWith(`${UNIQUE_KEY_PREFIX}${KEY_PART_SEPARATOR}`);
506
+ }
507
+ function constraintSignature(node, constraint) {
508
+ const props = node;
509
+ for (const field of constraint.fields) {
510
+ const value = props[field];
511
+ if (value === void 0 || value === null) {
512
+ return void 0;
513
+ }
514
+ }
515
+ return `${constraint.name}${KEY_PART_SEPARATOR}${computeUniqueKey(
516
+ props,
517
+ constraint.fields,
518
+ constraint.collation
519
+ )}`;
520
+ }
521
+ function bucketKeysFor(node, block, constraints) {
522
+ const keys = [];
523
+ const blockKey = block?.(node);
524
+ if (blockKey !== void 0) {
525
+ keys.push(`${BLOCK_KEY_PREFIX}${KEY_PART_SEPARATOR}${blockKey}`);
526
+ }
527
+ for (const constraint of constraints) {
528
+ const signature = constraintSignature(node, constraint);
529
+ if (signature !== void 0) {
530
+ keys.push(`${UNIQUE_KEY_PREFIX}${KEY_PART_SEPARATOR}${signature}`);
531
+ }
532
+ }
533
+ return keys.length === 0 ? [UNBLOCKED_BUCKET_KEY] : keys;
534
+ }
535
+ function blockNodes(newNodes, resolveConfig, uniqueConstraints = []) {
536
+ const buckets = /* @__PURE__ */ new Map();
537
+ for (const node of newNodes) {
538
+ for (const key of bucketKeysFor(
539
+ node,
540
+ resolveConfig.block,
541
+ uniqueConstraints
542
+ )) {
543
+ const bucket = buckets.get(key);
544
+ if (bucket === void 0) {
545
+ buckets.set(key, [node]);
546
+ } else {
547
+ bucket.push(node);
548
+ }
549
+ }
550
+ }
551
+ const sortedKeys = [...buckets.keys()].sort(
552
+ (left, right) => compareStrings(left, right)
553
+ );
554
+ const ordered = /* @__PURE__ */ new Map();
555
+ for (const key of sortedKeys) {
556
+ const members = [...buckets.get(key)].sort(
557
+ (left, right) => compareStrings(left.id, right.id)
558
+ );
559
+ ordered.set(key, members);
560
+ }
561
+ return ordered;
562
+ }
563
+
564
+ // src/graph-merge/conflict-policy.ts
565
+ function buildBranchRank(branchOrder, allBranchIds) {
566
+ const rank = /* @__PURE__ */ new Map();
567
+ let next = 0;
568
+ for (const branchId of branchOrder) {
569
+ if (!rank.has(branchId)) {
570
+ rank.set(branchId, next);
571
+ next += 1;
572
+ }
573
+ }
574
+ const remaining = allBranchIds.filter((branchId) => !rank.has(branchId)).sort((left, right) => compareStrings(left, right));
575
+ for (const branchId of remaining) {
576
+ if (!rank.has(branchId)) {
577
+ rank.set(branchId, next);
578
+ next += 1;
579
+ }
580
+ }
581
+ return rank;
582
+ }
583
+ function pickByPriority(values, branchRank) {
584
+ let chosen = values[0];
585
+ let chosenRank = branchRank.get(chosen.branchId) ?? Number.MAX_SAFE_INTEGER;
586
+ for (const candidate of values.slice(1)) {
587
+ const candidateRank = branchRank.get(candidate.branchId) ?? Number.MAX_SAFE_INTEGER;
588
+ if (candidateRank < chosenRank) {
589
+ chosen = candidate;
590
+ chosenRank = candidateRank;
591
+ } else if (candidateRank === chosenRank && canonicalValueKey(candidate.value) < canonicalValueKey(chosen.value)) {
592
+ chosen = candidate;
593
+ }
594
+ }
595
+ return chosen.value;
596
+ }
597
+ function pickByWeight(values, weights, branchRank) {
598
+ let chosen = values[0];
599
+ let chosenWeight = weights.get(chosen.branchId) ?? 0;
600
+ let chosenRank = branchRank.get(chosen.branchId) ?? Number.MAX_SAFE_INTEGER;
601
+ for (const candidate of values.slice(1)) {
602
+ const candidateWeight = weights.get(candidate.branchId) ?? 0;
603
+ const candidateRank = branchRank.get(candidate.branchId) ?? Number.MAX_SAFE_INTEGER;
604
+ if (candidateWeight > chosenWeight) {
605
+ chosen = candidate;
606
+ chosenWeight = candidateWeight;
607
+ chosenRank = candidateRank;
608
+ continue;
609
+ }
610
+ if (candidateWeight === chosenWeight) {
611
+ if (candidateRank < chosenRank) {
612
+ chosen = candidate;
613
+ chosenRank = candidateRank;
614
+ } else if (candidateRank === chosenRank && canonicalValueKey(candidate.value) < canonicalValueKey(chosen.value)) {
615
+ chosen = candidate;
616
+ }
617
+ }
618
+ }
619
+ return chosen.value;
620
+ }
621
+ function allValuesEqual(values) {
622
+ if (values.length <= 1) {
623
+ return true;
624
+ }
625
+ const first = canonicalValueKey(values[0].value);
626
+ return values.every(
627
+ (candidate) => canonicalValueKey(candidate.value) === first
628
+ );
629
+ }
630
+ function collectConflictingValues(property, contributions) {
631
+ const seen = /* @__PURE__ */ new Set();
632
+ const values = [];
633
+ for (const { branchId, props } of contributions) {
634
+ if (!(property in props)) {
635
+ continue;
636
+ }
637
+ const value = props[property];
638
+ const dedupeKey2 = `${branchId}\0${canonicalValueKey(value)}`;
639
+ if (seen.has(dedupeKey2)) {
640
+ continue;
641
+ }
642
+ seen.add(dedupeKey2);
643
+ values.push({ branchId, value });
644
+ }
645
+ return [...values].sort((left, right) => {
646
+ const byBranch = compareStrings(left.branchId, right.branchId);
647
+ return byBranch === 0 ? compareStrings(
648
+ canonicalValueKey(left.value),
649
+ canonicalValueKey(right.value)
650
+ ) : byBranch;
651
+ });
652
+ }
653
+ function resolveConflictValue(input, context, branchRank, makeConflict) {
654
+ if (allValuesEqual(input.values)) {
655
+ return {
656
+ value: input.values[0]?.value ?? input.canonicalValue,
657
+ conflicted: false
658
+ };
659
+ }
660
+ const { policy } = context;
661
+ if (policy === "flag") {
662
+ return { value: input.canonicalValue, conflicted: true };
663
+ }
664
+ if (policy === "lastWriteWins") {
665
+ return {
666
+ value: pickByPriority(input.values, branchRank),
667
+ conflicted: true
668
+ };
669
+ }
670
+ if (policy === "provenanceWeighted") {
671
+ const weights = context.weights ?? /* @__PURE__ */ new Map();
672
+ return {
673
+ value: pickByWeight(input.values, weights, branchRank),
674
+ conflicted: true
675
+ };
676
+ }
677
+ const provisional = pickByPriority(input.values, branchRank);
678
+ const resolved = policy(makeConflict(provisional));
679
+ return { value: resolved, conflicted: true };
680
+ }
681
+ function resolvePropertyUnion(input, context, branchRank) {
682
+ const makeRecord = (resolution) => ({
683
+ entityId: input.entityId,
684
+ kind: input.kind,
685
+ property: input.property,
686
+ values: input.reportedValues,
687
+ resolution
688
+ });
689
+ const resolved = resolveConflictValue(
690
+ {
691
+ property: input.property,
692
+ values: input.values,
693
+ canonicalValue: input.canonicalValue
694
+ },
695
+ context,
696
+ branchRank,
697
+ (resolution) => makeRecord(resolution)
698
+ );
699
+ return {
700
+ value: resolved.value,
701
+ conflict: resolved.conflicted ? makeRecord(resolved.value) : void 0
702
+ };
703
+ }
704
+
705
+ // src/graph-merge/canonicalize.ts
706
+ var BASE_PROVENANCE_BRANCH = asBranchId("__committed_base__");
707
+ function pickClusterSurvivor(members, cluster, canonicalOverride, preferKind) {
708
+ const byKey = (left, right) => compareMergeKeys(mergeKeyOf(left), mergeKeyOf(right));
709
+ const baseMembers = members.filter((member) => member.origin === "base").sort(byKey);
710
+ if (baseMembers.length > 0) {
711
+ return baseMembers[0];
712
+ }
713
+ let canonicalId;
714
+ if (canonicalOverride !== void 0) {
715
+ const chosen = canonicalOverride({ members: cluster.members.map(idOf) });
716
+ if (members.some((member) => member.id === chosen)) {
717
+ canonicalId = chosen;
718
+ }
719
+ }
720
+ if (canonicalId === void 0) {
721
+ const minIdentity = [...cluster.members].sort(
722
+ (left, right) => compareMergeKeys(left, right)
723
+ )[0];
724
+ canonicalId = minIdentity === void 0 ? members[0].id : idOf(minIdentity);
725
+ }
726
+ const membersAtId = members.filter((member) => member.id === canonicalId);
727
+ if (membersAtId.length === 0) {
728
+ return [...members].sort(byKey)[0];
729
+ }
730
+ const kinds = [...new Set(membersAtId.map((member) => member.kind))];
731
+ const chosenKind = preferKind !== void 0 && kinds.length > 1 ? preferKind(kinds) : void 0;
732
+ return (chosenKind === void 0 ? void 0 : membersAtId.find((member) => member.kind === chosenKind)) ?? [...membersAtId].sort(byKey)[0];
733
+ }
734
+ function findPreferredMember(members, property, preferredBranchId) {
735
+ if (preferredBranchId === void 0) {
736
+ return void 0;
737
+ }
738
+ return members.find(
739
+ (member) => member.branchId === preferredBranchId && property in member.props
740
+ );
741
+ }
742
+ function memberPropertyValue(member, property) {
743
+ if (!(property in member.props)) {
744
+ return void 0;
745
+ }
746
+ return member.props[property];
747
+ }
748
+ function pickCanonicalPropertyValue(canonicalMember, members, property, values, preferredBranchId) {
749
+ const preferredMember = findPreferredMember(
750
+ members,
751
+ property,
752
+ preferredBranchId
753
+ );
754
+ const preferredValue = preferredMember === void 0 ? void 0 : memberPropertyValue(preferredMember, property);
755
+ if (preferredValue !== void 0) {
756
+ return preferredValue;
757
+ }
758
+ const canonicalValue = memberPropertyValue(canonicalMember, property);
759
+ return canonicalValue === void 0 ? values[0].value : canonicalValue;
760
+ }
761
+ function unionProperties(canonicalMember, members, context, baseContext, branchRank, preferredBranchId) {
762
+ const canonicalId = canonicalMember.id;
763
+ const kind = canonicalMember.kind;
764
+ const hasBaseMember = members.some((member) => member.origin === "base");
765
+ const propertyNames = /* @__PURE__ */ new Set();
766
+ for (const member of members) {
767
+ for (const name of Object.keys(member.props)) {
768
+ propertyNames.add(name);
769
+ }
770
+ }
771
+ const props = {};
772
+ const conflicts = [];
773
+ for (const property of [...propertyNames].sort(
774
+ (left, right) => compareStrings(left, right)
775
+ )) {
776
+ const values = collectConflictingValues(property, members);
777
+ if (values.length === 0) {
778
+ continue;
779
+ }
780
+ const canonicalValue = pickCanonicalPropertyValue(
781
+ canonicalMember,
782
+ members,
783
+ property,
784
+ values,
785
+ preferredBranchId
786
+ );
787
+ const baseInvolved = hasBaseMember && members.some(
788
+ (member) => member.origin === "base" && property in member.props
789
+ );
790
+ const reportedValues = values.filter(
791
+ (value2) => value2.branchId !== BASE_PROVENANCE_BRANCH && value2.branchId !== preferredBranchId
792
+ );
793
+ const { value, conflict } = resolvePropertyUnion(
794
+ {
795
+ entityId: canonicalId,
796
+ kind,
797
+ property,
798
+ values,
799
+ reportedValues,
800
+ canonicalValue
801
+ },
802
+ baseInvolved ? baseContext : context,
803
+ branchRank
804
+ );
805
+ props[property] = value;
806
+ if (conflict !== void 0) {
807
+ conflicts.push(conflict);
808
+ }
809
+ }
810
+ return { props, conflicts };
811
+ }
812
+ function canonicalizeCluster(cluster, members, policy, branchRank, weights, canonicalOverride, basePolicy = "flag", preferKind, preferredBranchId) {
813
+ const survivor = pickClusterSurvivor(
814
+ members,
815
+ cluster,
816
+ canonicalOverride,
817
+ preferKind
818
+ );
819
+ const canonicalId = survivor.id;
820
+ const kind = survivor.kind;
821
+ const context = {
822
+ policy,
823
+ ...weights === void 0 ? {} : { weights }
824
+ };
825
+ const baseContext = {
826
+ policy: basePolicy,
827
+ ...weights === void 0 ? {} : { weights }
828
+ };
829
+ const { props, conflicts } = unionProperties(
830
+ survivor,
831
+ members,
832
+ context,
833
+ baseContext,
834
+ branchRank,
835
+ preferredBranchId
836
+ );
837
+ const memberIds = [
838
+ ...new Set(cluster.members.map((member) => idOf(member)))
839
+ ].sort((left, right) => compareStrings(left, right));
840
+ const branchOrigins = [
841
+ ...new Set(
842
+ members.map((member) => member.branchId).filter(
843
+ (branchId) => branchId !== BASE_PROVENANCE_BRANCH && branchId !== preferredBranchId
844
+ )
845
+ )
846
+ ].sort((left, right) => compareStrings(left, right));
847
+ const resolution = {
848
+ canonicalId,
849
+ memberIds,
850
+ kind,
851
+ branchOrigins
852
+ };
853
+ return { canonicalId, kind, props, resolution, conflicts };
854
+ }
855
+
856
+ // src/graph-merge/union-find.ts
857
+ var UnionFind = class {
858
+ constructor(compare) {
859
+ this.compare = compare;
860
+ }
861
+ parent = /* @__PURE__ */ new Map();
862
+ /** Adds `value` as its own singleton set if it is not already present. */
863
+ add(value) {
864
+ if (!this.parent.has(value)) {
865
+ this.parent.set(value, value);
866
+ }
867
+ }
868
+ /** Returns the representative of `value`'s set, compressing the path to it. */
869
+ find(value) {
870
+ this.add(value);
871
+ let root = value;
872
+ while (this.parent.get(root) !== root) {
873
+ root = this.parent.get(root);
874
+ }
875
+ let cursor = value;
876
+ while (cursor !== root) {
877
+ const next = this.parent.get(cursor);
878
+ this.parent.set(cursor, root);
879
+ cursor = next;
880
+ }
881
+ return root;
882
+ }
883
+ /** Merges the sets of `left` and `right`; the `compare`-minimal root wins. */
884
+ union(left, right) {
885
+ const leftRoot = this.find(left);
886
+ const rightRoot = this.find(right);
887
+ if (leftRoot === rightRoot) {
888
+ return;
889
+ }
890
+ if (this.compare(leftRoot, rightRoot) <= 0) {
891
+ this.parent.set(rightRoot, leftRoot);
892
+ } else {
893
+ this.parent.set(leftRoot, rightRoot);
894
+ }
895
+ }
896
+ /** Every value seen by {@link add}/{@link find}/{@link union}, insertion order. */
897
+ members() {
898
+ return [...this.parent.keys()];
899
+ }
900
+ };
901
+
902
+ // src/graph-merge/closures.ts
903
+ var SUB_CLASS_OF_META_EDGE = "subClassOf";
904
+ var EQUIVALENT_TO_META_EDGE = "equivalentTo";
905
+ function buildSubClassClosure(ontology) {
906
+ const unionFind = new UnionFind(compareStrings);
907
+ for (const relation of ontology) {
908
+ unionFind.add(relation.from);
909
+ unionFind.add(relation.to);
910
+ if (relation.metaEdge === EQUIVALENT_TO_META_EDGE) {
911
+ unionFind.union(relation.from, relation.to);
912
+ }
913
+ }
914
+ const canonicalOf = /* @__PURE__ */ new Map();
915
+ for (const value of unionFind.members()) {
916
+ canonicalOf.set(value, unionFind.find(value));
917
+ }
918
+ const relationKeys = /* @__PURE__ */ new Set();
919
+ const relations = [];
920
+ for (const relation of ontology) {
921
+ if (relation.metaEdge !== SUB_CLASS_OF_META_EDGE) {
922
+ continue;
923
+ }
924
+ const child = canonicalOf.get(relation.from) ?? relation.from;
925
+ const parent = canonicalOf.get(relation.to) ?? relation.to;
926
+ if (child === parent) {
927
+ continue;
928
+ }
929
+ const key = `${child}\0${parent}`;
930
+ if (relationKeys.has(key)) {
931
+ continue;
932
+ }
933
+ relationKeys.add(key);
934
+ relations.push([child, parent]);
935
+ }
936
+ return {
937
+ closure: computeTransitiveClosure(relations),
938
+ canonicalOf
939
+ };
940
+ }
941
+ function canonicalType(closure, type) {
942
+ return closure.canonicalOf.get(type) ?? type;
943
+ }
944
+ function isReachable2(closure, from, to) {
945
+ const fromRep = canonicalType(closure, from);
946
+ const toRep = canonicalType(closure, to);
947
+ if (fromRep === toRep) {
948
+ return from !== to;
949
+ }
950
+ return isReachable(closure.closure, fromRep, toRep);
951
+ }
952
+
953
+ // src/graph-merge/similarity.ts
954
+ var MIN_SCORE = 0;
955
+ var MAX_SCORE = 1;
956
+ var NO_EVIDENCE_SCORE = -1;
957
+ var TRIGRAM_LENGTH = 3;
958
+ var TRIGRAM_PAD = " ";
959
+ function clampScore(value) {
960
+ if (Number.isNaN(value)) {
961
+ return MIN_SCORE;
962
+ }
963
+ if (value < MIN_SCORE) {
964
+ return MIN_SCORE;
965
+ }
966
+ if (value > MAX_SCORE) {
967
+ return MAX_SCORE;
968
+ }
969
+ return value;
970
+ }
971
+ function readField(node, field) {
972
+ return node[field];
973
+ }
974
+ function fieldText(node, fields) {
975
+ const parts = [];
976
+ for (const field of fields) {
977
+ const value = readField(node, field);
978
+ if (value === void 0 || value === null) {
979
+ continue;
980
+ }
981
+ parts.push(stringifyFieldValue(value));
982
+ }
983
+ return parts.join(TRIGRAM_PAD).toLowerCase();
984
+ }
985
+ function stringifyFieldValue(value) {
986
+ if (typeof value === "string") {
987
+ return value;
988
+ }
989
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
990
+ return String(value);
991
+ }
992
+ return canonicalValueKey(value);
993
+ }
994
+ function trigramMultiset(text) {
995
+ const counts = /* @__PURE__ */ new Map();
996
+ if (text.length === 0) {
997
+ return counts;
998
+ }
999
+ const padded = `${TRIGRAM_PAD}${text}${TRIGRAM_PAD}`;
1000
+ for (let index = 0; index + TRIGRAM_LENGTH <= padded.length; index += 1) {
1001
+ const gram = padded.slice(index, index + TRIGRAM_LENGTH);
1002
+ counts.set(gram, (counts.get(gram) ?? 0) + 1);
1003
+ }
1004
+ return counts;
1005
+ }
1006
+ function multisetSize(counts) {
1007
+ let total = 0;
1008
+ for (const count of counts.values()) {
1009
+ total += count;
1010
+ }
1011
+ return total;
1012
+ }
1013
+ function multisetIntersectionSize(left, right) {
1014
+ let shared = 0;
1015
+ for (const [gram, leftCount] of left) {
1016
+ const rightCount = right.get(gram);
1017
+ if (rightCount !== void 0) {
1018
+ shared += Math.min(leftCount, rightCount);
1019
+ }
1020
+ }
1021
+ return shared;
1022
+ }
1023
+ function diceFromMultisets(left, right) {
1024
+ const leftSize = multisetSize(left);
1025
+ const rightSize = multisetSize(right);
1026
+ if (leftSize === 0 && rightSize === 0) {
1027
+ return MAX_SCORE;
1028
+ }
1029
+ if (leftSize === 0 || rightSize === 0) {
1030
+ return MIN_SCORE;
1031
+ }
1032
+ const shared = multisetIntersectionSize(left, right);
1033
+ return 2 * shared / (leftSize + rightSize);
1034
+ }
1035
+ function embeddingFields(strategy) {
1036
+ switch (strategy.kind) {
1037
+ case "vector": {
1038
+ return [strategy.field];
1039
+ }
1040
+ case "hybrid": {
1041
+ return strategy.fields;
1042
+ }
1043
+ case "fulltext":
1044
+ case "custom": {
1045
+ return void 0;
1046
+ }
1047
+ default: {
1048
+ const exhaustive = strategy;
1049
+ throw new Error(
1050
+ `Unhandled similarity strategy: ${JSON.stringify(exhaustive)}`
1051
+ );
1052
+ }
1053
+ }
1054
+ }
1055
+ function cosineSimilarity(left, right) {
1056
+ if (left.length !== right.length || left.length === 0) {
1057
+ return MIN_SCORE;
1058
+ }
1059
+ let dot = 0;
1060
+ let leftMagnitude = 0;
1061
+ let rightMagnitude = 0;
1062
+ for (let index = 0; index < left.length; index += 1) {
1063
+ const leftComponent = left[index];
1064
+ const rightComponent = right[index];
1065
+ dot += leftComponent * rightComponent;
1066
+ leftMagnitude += leftComponent * leftComponent;
1067
+ rightMagnitude += rightComponent * rightComponent;
1068
+ }
1069
+ if (leftMagnitude === 0 || rightMagnitude === 0) {
1070
+ return MIN_SCORE;
1071
+ }
1072
+ return dot / (Math.sqrt(leftMagnitude) * Math.sqrt(rightMagnitude));
1073
+ }
1074
+ function cosineFromLookup(embeddings, leftText, rightText) {
1075
+ const leftVector = embeddings.get(leftText);
1076
+ const rightVector = embeddings.get(rightText);
1077
+ if (leftVector === void 0 || rightVector === void 0) {
1078
+ return MIN_SCORE;
1079
+ }
1080
+ return cosineSimilarity(leftVector, rightVector);
1081
+ }
1082
+ var DEFAULT_HYBRID_WEIGHT = 0.5;
1083
+ function nonNegativeFinite(value) {
1084
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
1085
+ }
1086
+ function normalizeHybridWeights(weights) {
1087
+ const vector = nonNegativeFinite(weights?.vector ?? DEFAULT_HYBRID_WEIGHT);
1088
+ const fulltext = nonNegativeFinite(
1089
+ weights?.fulltext ?? DEFAULT_HYBRID_WEIGHT
1090
+ );
1091
+ const total = vector + fulltext;
1092
+ if (total === 0) {
1093
+ return { vector: DEFAULT_HYBRID_WEIGHT, fulltext: DEFAULT_HYBRID_WEIGHT };
1094
+ }
1095
+ return { vector: vector / total, fulltext: fulltext / total };
1096
+ }
1097
+ function embedderUnavailable(kind, ctx) {
1098
+ return new SimilarityUnavailableError(
1099
+ `Similarity strategy "${kind}" requires a configured embedder (MergeOptions.embedder); none was provided.`,
1100
+ { details: { strategy: kind, dialect: ctx.backend.dialect } }
1101
+ );
1102
+ }
1103
+ function scoringFields(strategy) {
1104
+ switch (strategy.kind) {
1105
+ case "fulltext":
1106
+ case "hybrid": {
1107
+ return strategy.fields;
1108
+ }
1109
+ case "vector": {
1110
+ return [strategy.field];
1111
+ }
1112
+ case "custom": {
1113
+ return [];
1114
+ }
1115
+ default: {
1116
+ const exhaustive = strategy;
1117
+ throw new Error(
1118
+ `Unhandled similarity strategy: ${JSON.stringify(exhaustive)}`
1119
+ );
1120
+ }
1121
+ }
1122
+ }
1123
+ var NO_GRAMS = /* @__PURE__ */ new Map();
1124
+ function prepareNode(node, strategy) {
1125
+ const text = fieldText(node, scoringFields(strategy));
1126
+ const grams = strategy.kind === "fulltext" || strategy.kind === "hybrid" ? trigramMultiset(text) : NO_GRAMS;
1127
+ return { node, text, grams };
1128
+ }
1129
+ function scorePrepared(left, right, strategy, ctx) {
1130
+ switch (strategy.kind) {
1131
+ case "custom": {
1132
+ const raw = strategy.score(left.node, right.node);
1133
+ return ok(clampScore(raw));
1134
+ }
1135
+ case "fulltext": {
1136
+ if (left.text.length === 0 || right.text.length === 0) {
1137
+ return ok(NO_EVIDENCE_SCORE);
1138
+ }
1139
+ return ok(clampScore(diceFromMultisets(left.grams, right.grams)));
1140
+ }
1141
+ case "vector": {
1142
+ if (ctx.embeddings === void 0) {
1143
+ return err(embedderUnavailable("vector", ctx));
1144
+ }
1145
+ if (left.text.length === 0 || right.text.length === 0) {
1146
+ return ok(NO_EVIDENCE_SCORE);
1147
+ }
1148
+ const cosine = cosineFromLookup(ctx.embeddings, left.text, right.text);
1149
+ return ok(clampScore(cosine));
1150
+ }
1151
+ case "hybrid": {
1152
+ if (ctx.embeddings === void 0) {
1153
+ return err(embedderUnavailable("hybrid", ctx));
1154
+ }
1155
+ if (left.text.length === 0 || right.text.length === 0) {
1156
+ return ok(NO_EVIDENCE_SCORE);
1157
+ }
1158
+ const vectorScore = clampScore(
1159
+ cosineFromLookup(ctx.embeddings, left.text, right.text)
1160
+ );
1161
+ const fulltextScore = diceFromMultisets(left.grams, right.grams);
1162
+ const weights = normalizeHybridWeights(strategy.weights);
1163
+ return ok(
1164
+ clampScore(
1165
+ weights.vector * vectorScore + weights.fulltext * fulltextScore
1166
+ )
1167
+ );
1168
+ }
1169
+ default: {
1170
+ const exhaustive = strategy;
1171
+ throw new Error(
1172
+ `Unhandled similarity strategy: ${JSON.stringify(exhaustive)}`
1173
+ );
1174
+ }
1175
+ }
1176
+ }
1177
+ function createPairScorer(strategy, ctx) {
1178
+ const prepared = /* @__PURE__ */ new Map();
1179
+ const prepare = (key, node) => {
1180
+ const cached = prepared.get(key);
1181
+ if (cached !== void 0) {
1182
+ return cached;
1183
+ }
1184
+ const fresh = prepareNode(node, strategy);
1185
+ prepared.set(key, fresh);
1186
+ return fresh;
1187
+ };
1188
+ return (a, left, b, right) => scorePrepared(prepare(a, left), prepare(b, right), strategy, ctx);
1189
+ }
1190
+
1191
+ // src/graph-merge/scoring.ts
1192
+ var FORCED_MATCH_SCORE = 1;
1193
+ function compareCandidateEdges(left, right) {
1194
+ const byA = compareMergeKeys(left.a, right.a);
1195
+ return byA === 0 ? compareMergeKeys(left.b, right.b) : byA;
1196
+ }
1197
+ function endpointKey(a, b) {
1198
+ return JSON.stringify([a, b]);
1199
+ }
1200
+ function scoreCandidates(input, resolveConfig, ctx, ceilingPolicy, maxComparisonsPerKind) {
1201
+ const { similarity, threshold } = resolveConfig;
1202
+ const seen = /* @__PURE__ */ new Set();
1203
+ const forced = [];
1204
+ for (const edge of input.forcedEdges) {
1205
+ const key = endpointKey(edge.a, edge.b);
1206
+ if (!seen.has(key)) {
1207
+ seen.add(key);
1208
+ forced.push(edge);
1209
+ }
1210
+ }
1211
+ const fuzzy = [];
1212
+ for (const pair of input.pairs) {
1213
+ const key = endpointKey(pair.a, pair.b);
1214
+ if (!seen.has(key)) {
1215
+ seen.add(key);
1216
+ fuzzy.push(pair);
1217
+ }
1218
+ }
1219
+ if (maxComparisonsPerKind !== void 0 && fuzzy.length > maxComparisonsPerKind) {
1220
+ if (ceilingPolicy === "error") {
1221
+ return err(
1222
+ new MergeError(
1223
+ `Comparison ceiling exceeded: ${fuzzy.length} candidate pairs > maxComparisonsPerKind ${maxComparisonsPerKind}.`,
1224
+ {
1225
+ details: {
1226
+ comparisons: fuzzy.length,
1227
+ limit: maxComparisonsPerKind
1228
+ },
1229
+ suggestion: `Tighten the kind's block() to shrink buckets, or set onComparisonCeiling: "mergeByIdOnly".`
1230
+ }
1231
+ )
1232
+ );
1233
+ }
1234
+ return ok({
1235
+ edges: [...forced].sort(
1236
+ (left, right) => compareCandidateEdges(left, right)
1237
+ ),
1238
+ warnings: [
1239
+ {
1240
+ kind: "comparisonCeiling",
1241
+ comparisons: fuzzy.length,
1242
+ limit: maxComparisonsPerKind,
1243
+ message: `Skipped similarity for this kind: ${fuzzy.length} candidate pairs exceeded maxComparisonsPerKind ${maxComparisonsPerKind}; nodes will merge by id and exact unique match only.`
1244
+ }
1245
+ ]
1246
+ });
1247
+ }
1248
+ const scorer = createPairScorer(similarity, ctx);
1249
+ const edges = [...forced];
1250
+ for (const { left, right, a, b } of fuzzy) {
1251
+ const scored = scorer(a, left, b, right);
1252
+ if (isErr(scored)) {
1253
+ return err(scored.error);
1254
+ }
1255
+ if (scored.data >= threshold) {
1256
+ edges.push({ a, b, score: scored.data });
1257
+ }
1258
+ }
1259
+ return ok({
1260
+ edges: edges.sort((left, right) => compareCandidateEdges(left, right)),
1261
+ warnings: []
1262
+ });
1263
+ }
1264
+
1265
+ // src/graph-merge/clustering.ts
1266
+ function buildComponents(edges, nodeIds, edgesSorted = false) {
1267
+ const forest = new UnionFind(compareMergeKeys);
1268
+ const seed = new Set(nodeIds);
1269
+ for (const id of nodeIds) {
1270
+ forest.add(id);
1271
+ }
1272
+ const orderedEdges = edgesSorted ? edges : [...edges].sort((left, right) => compareCandidateEdges(left, right));
1273
+ for (const edge of orderedEdges) {
1274
+ if (!seed.has(edge.a) || !seed.has(edge.b)) {
1275
+ continue;
1276
+ }
1277
+ forest.union(edge.a, edge.b);
1278
+ }
1279
+ const byRoot = /* @__PURE__ */ new Map();
1280
+ for (const id of forest.members()) {
1281
+ const root = forest.find(id);
1282
+ const bucket = byRoot.get(root);
1283
+ if (bucket === void 0) {
1284
+ byRoot.set(root, [id]);
1285
+ } else {
1286
+ bucket.push(id);
1287
+ }
1288
+ }
1289
+ const clusters = [];
1290
+ for (const members of byRoot.values()) {
1291
+ clusters.push({
1292
+ members: [...members].sort(
1293
+ (left, right) => compareMergeKeys(left, right)
1294
+ )
1295
+ });
1296
+ }
1297
+ clusters.sort(
1298
+ (left, right) => compareMergeKeys(left.members[0], right.members[0])
1299
+ );
1300
+ return clusters;
1301
+ }
1302
+ function adjacencyOf(members, edges) {
1303
+ const memberSet = new Set(members);
1304
+ const adjacency = /* @__PURE__ */ new Map();
1305
+ for (const id of members) {
1306
+ adjacency.set(id, /* @__PURE__ */ new Set());
1307
+ }
1308
+ for (const edge of edges) {
1309
+ if (memberSet.has(edge.a) && memberSet.has(edge.b)) {
1310
+ adjacency.get(edge.a).add(edge.b);
1311
+ adjacency.get(edge.b).add(edge.a);
1312
+ }
1313
+ }
1314
+ return adjacency;
1315
+ }
1316
+ function exceedsDiameter(members, edges, maxDiameter) {
1317
+ if (members.length <= 1) {
1318
+ return false;
1319
+ }
1320
+ const adjacency = adjacencyOf(members, edges);
1321
+ for (const source of members) {
1322
+ const distance = /* @__PURE__ */ new Map([[source, 0]]);
1323
+ const queue = [source];
1324
+ let head = 0;
1325
+ while (head < queue.length) {
1326
+ const current = queue[head];
1327
+ head += 1;
1328
+ const currentDistance = distance.get(current);
1329
+ for (const neighbor of adjacency.get(current)) {
1330
+ if (!distance.has(neighbor)) {
1331
+ const neighborDistance = currentDistance + 1;
1332
+ if (neighborDistance > maxDiameter) {
1333
+ return true;
1334
+ }
1335
+ distance.set(neighbor, neighborDistance);
1336
+ queue.push(neighbor);
1337
+ }
1338
+ }
1339
+ }
1340
+ if (distance.size < members.length) {
1341
+ return true;
1342
+ }
1343
+ }
1344
+ return false;
1345
+ }
1346
+ function internalEdges(members, edges) {
1347
+ const memberSet = new Set(members);
1348
+ return edges.filter((edge) => memberSet.has(edge.a) && memberSet.has(edge.b)).sort((left, right) => compareCandidateEdges(left, right));
1349
+ }
1350
+ function weakestEdge(edges) {
1351
+ return edges.reduce((current, candidate) => {
1352
+ if (candidate.score !== current.score) {
1353
+ return candidate.score < current.score ? candidate : current;
1354
+ }
1355
+ return compareCandidateEdges(candidate, current) < 0 ? candidate : current;
1356
+ });
1357
+ }
1358
+ function splitUntil(members, edges, isOffending) {
1359
+ let surviving = internalEdges(members, edges);
1360
+ for (; ; ) {
1361
+ const components = buildComponents(surviving, members, true);
1362
+ const offending = components.filter(
1363
+ (component) => isOffending(component.members, surviving)
1364
+ );
1365
+ if (offending.length === 0) {
1366
+ return components;
1367
+ }
1368
+ let splittable;
1369
+ for (const component of offending) {
1370
+ const componentEdges = internalEdges(component.members, surviving);
1371
+ if (componentEdges.length > 0) {
1372
+ splittable = componentEdges;
1373
+ break;
1374
+ }
1375
+ }
1376
+ if (splittable === void 0) {
1377
+ const offendingSet = new Set(offending);
1378
+ return components.flatMap(
1379
+ (component) => offendingSet.has(component) ? component.members.map((id) => ({ members: [id] })) : [component]
1380
+ );
1381
+ }
1382
+ const weakest = weakestEdge(splittable);
1383
+ surviving = surviving.filter((edge) => edge !== weakest);
1384
+ }
1385
+ }
1386
+ function splitByDropWeakest(members, edges, maxDiameter) {
1387
+ return splitUntil(
1388
+ members,
1389
+ edges,
1390
+ (componentMembers, surviving) => exceedsDiameter(componentMembers, surviving, maxDiameter)
1391
+ );
1392
+ }
1393
+ function connectedComponents(candidateEdges, nodeIds) {
1394
+ return buildComponents(candidateEdges, nodeIds);
1395
+ }
1396
+ function enforceDiameter(clusters, candidateEdges, clusterMaxDiameter) {
1397
+ const guarded = [];
1398
+ for (const component of clusters) {
1399
+ if (exceedsDiameter(component.members, candidateEdges, clusterMaxDiameter)) {
1400
+ guarded.push(
1401
+ ...splitByDropWeakest(
1402
+ component.members,
1403
+ candidateEdges,
1404
+ clusterMaxDiameter
1405
+ )
1406
+ );
1407
+ } else {
1408
+ guarded.push(component);
1409
+ }
1410
+ }
1411
+ guarded.sort(
1412
+ (left, right) => compareMergeKeys(left.members[0], right.members[0])
1413
+ );
1414
+ return guarded;
1415
+ }
1416
+ function countBaseMembers(members, baseIds) {
1417
+ let count = 0;
1418
+ for (const id of members) {
1419
+ if (baseIds.has(id)) {
1420
+ count += 1;
1421
+ }
1422
+ }
1423
+ return count;
1424
+ }
1425
+ function splitByBaseMultiplicity(members, edges, baseIds) {
1426
+ return splitUntil(
1427
+ members,
1428
+ edges,
1429
+ (componentMembers) => countBaseMembers(componentMembers, baseIds) >= 2
1430
+ );
1431
+ }
1432
+ function enforceBaseGuard(clusters, candidateEdges, baseIds) {
1433
+ if (baseIds.size === 0) {
1434
+ return { clusters, events: [] };
1435
+ }
1436
+ const result = [];
1437
+ const events = [];
1438
+ for (const cluster of clusters) {
1439
+ const clusterBaseIds = cluster.members.filter((id) => baseIds.has(id));
1440
+ if (clusterBaseIds.length < 2) {
1441
+ result.push(cluster);
1442
+ continue;
1443
+ }
1444
+ events.push({
1445
+ baseIds: [...clusterBaseIds].sort(
1446
+ (left, right) => compareMergeKeys(left, right)
1447
+ ),
1448
+ memberIds: [...cluster.members].sort(
1449
+ (left, right) => compareMergeKeys(left, right)
1450
+ )
1451
+ });
1452
+ result.push(
1453
+ ...splitByBaseMultiplicity(cluster.members, candidateEdges, baseIds)
1454
+ );
1455
+ }
1456
+ result.sort(
1457
+ (left, right) => compareMergeKeys(left.members[0], right.members[0])
1458
+ );
1459
+ return { clusters: result, events };
1460
+ }
1461
+
1462
+ // src/graph-merge/delete-modify.ts
1463
+ var DELETED_NODE_DROP_REASON = "delete-modify:deleteWins";
1464
+ function pickHighestPriorityBranch(branchIds, branchRank) {
1465
+ let chosen = branchIds[0];
1466
+ let chosenRank = branchRank.get(chosen) ?? Number.MAX_SAFE_INTEGER;
1467
+ for (const candidate of branchIds.slice(1)) {
1468
+ const candidateRank = branchRank.get(candidate) ?? Number.MAX_SAFE_INTEGER;
1469
+ if (candidateRank < chosenRank || candidateRank === chosenRank && compareStrings(candidate, chosen) < 0) {
1470
+ chosen = candidate;
1471
+ chosenRank = candidateRank;
1472
+ }
1473
+ }
1474
+ return chosen;
1475
+ }
1476
+ function effectiveDeleteModifyPolicy(policy, deletions, modifications, preferredBranchId) {
1477
+ if (preferredBranchId !== void 0 && deletions.some((deletion) => deletion.branchId === preferredBranchId)) {
1478
+ return "deleteWins";
1479
+ }
1480
+ if (policy === "deleteWins" && preferredBranchId !== void 0 && modifications.some(
1481
+ (modification) => modification.branchId === preferredBranchId
1482
+ )) {
1483
+ return "modifyWins";
1484
+ }
1485
+ return policy;
1486
+ }
1487
+ function groupByEntity(items, entityOf) {
1488
+ const grouped = /* @__PURE__ */ new Map();
1489
+ for (const item of items) {
1490
+ const entity = entityOf(item);
1491
+ const key = mergeKey(entity.kind, entity.id);
1492
+ const bucket = grouped.get(key);
1493
+ if (bucket === void 0) {
1494
+ grouped.set(key, [item]);
1495
+ } else {
1496
+ bucket.push(item);
1497
+ }
1498
+ }
1499
+ return grouped;
1500
+ }
1501
+ function resolveDeleteModifyOver(modified, deleted, modifiedEntity, deletedEntity, droppedKind, policy, branchRank, preferredBranchId) {
1502
+ const modifiedByKey = groupByEntity(modified, modifiedEntity);
1503
+ const deletedByKey = groupByEntity(deleted, deletedEntity);
1504
+ const survivingModifications = [];
1505
+ const deletions = [];
1506
+ const conflicts = [];
1507
+ const dropped = [];
1508
+ const conflictedKeys = /* @__PURE__ */ new Set();
1509
+ for (const key of deletedByKey.keys()) {
1510
+ if (modifiedByKey.has(key)) {
1511
+ conflictedKeys.add(key);
1512
+ }
1513
+ }
1514
+ for (const [key, modifications] of modifiedByKey) {
1515
+ if (!conflictedKeys.has(key)) {
1516
+ for (const modification of modifications) {
1517
+ survivingModifications.push(modification);
1518
+ }
1519
+ continue;
1520
+ }
1521
+ const deletionsForKey = deletedByKey.get(key);
1522
+ const { id, kind } = modifiedEntity(modifications[0]);
1523
+ const deletedBy = pickHighestPriorityBranch(
1524
+ deletionsForKey.map((deletion) => deletion.branchId),
1525
+ branchRank
1526
+ );
1527
+ const modifiedBy = pickHighestPriorityBranch(
1528
+ modifications.map((modification) => modification.branchId),
1529
+ branchRank
1530
+ );
1531
+ const effectivePolicy = effectiveDeleteModifyPolicy(
1532
+ policy,
1533
+ deletionsForKey,
1534
+ modifications,
1535
+ preferredBranchId
1536
+ );
1537
+ conflicts.push({
1538
+ entityId: id,
1539
+ kind,
1540
+ deletedBy,
1541
+ modifiedBy,
1542
+ resolution: effectivePolicy
1543
+ });
1544
+ if (effectivePolicy === "deleteWins") {
1545
+ deletions.push({ id, kind });
1546
+ dropped.push({ kind: droppedKind, id, reason: DELETED_NODE_DROP_REASON });
1547
+ continue;
1548
+ }
1549
+ for (const modification of modifications) {
1550
+ survivingModifications.push(modification);
1551
+ }
1552
+ }
1553
+ for (const [key, deletionsForKey] of deletedByKey) {
1554
+ if (conflictedKeys.has(key)) {
1555
+ continue;
1556
+ }
1557
+ const { id, kind } = deletedEntity(deletionsForKey[0]);
1558
+ deletions.push({ id, kind });
1559
+ }
1560
+ return {
1561
+ survivingModifications: [...survivingModifications].sort((left, right) => {
1562
+ const byId2 = compareStrings(
1563
+ modifiedEntity(left).id,
1564
+ modifiedEntity(right).id
1565
+ );
1566
+ return byId2 === 0 ? compareStrings(left.branchId, right.branchId) : byId2;
1567
+ }),
1568
+ deletions: [...deletions].sort(
1569
+ (left, right) => compareStrings(left.id, right.id)
1570
+ ),
1571
+ conflicts: [...conflicts].sort(
1572
+ (left, right) => compareStrings(left.entityId, right.entityId)
1573
+ ),
1574
+ dropped: [...dropped].sort(
1575
+ (left, right) => compareStrings(left.id, right.id)
1576
+ )
1577
+ };
1578
+ }
1579
+ function reconcileOver(survivingModifications, entityOf, rebuild, policy, branchRank, weights, preferredBranchId) {
1580
+ const context = {
1581
+ policy,
1582
+ ...weights === void 0 ? {} : { weights }
1583
+ };
1584
+ const grouped = groupByEntity(survivingModifications, entityOf);
1585
+ const out = [];
1586
+ const conflicts = [];
1587
+ for (const modifications of grouped.values()) {
1588
+ if (modifications.length === 1) {
1589
+ out.push(modifications[0]);
1590
+ continue;
1591
+ }
1592
+ const first = entityOf(modifications[0]);
1593
+ const { props, conflicts: propertyConflicts } = threeWayMergeProps(
1594
+ first.id,
1595
+ first.kind,
1596
+ first.baseProps,
1597
+ modifications.map((modification) => ({
1598
+ branchId: modification.branchId,
1599
+ forkProps: entityOf(modification).forkProps
1600
+ })),
1601
+ context,
1602
+ branchRank,
1603
+ preferredBranchId
1604
+ );
1605
+ const chosenBranch = pickHighestPriorityBranch(
1606
+ modifications.map((modification) => modification.branchId),
1607
+ branchRank
1608
+ );
1609
+ const representative = modifications.find(
1610
+ (modification) => modification.branchId === chosenBranch
1611
+ ) ?? modifications[0];
1612
+ out.push(rebuild(representative, chosenBranch, props));
1613
+ for (const conflict of propertyConflicts) {
1614
+ conflicts.push(conflict);
1615
+ }
1616
+ }
1617
+ return {
1618
+ survivingModifications: [...out].sort((left, right) => {
1619
+ const byId2 = compareStrings(entityOf(left).id, entityOf(right).id);
1620
+ return byId2 === 0 ? compareStrings(left.branchId, right.branchId) : byId2;
1621
+ }),
1622
+ conflicts: conflicts.sort(
1623
+ (left, right) => compareStrings(
1624
+ `${left.entityId}|${left.property}`,
1625
+ `${right.entityId}|${right.property}`
1626
+ )
1627
+ )
1628
+ };
1629
+ }
1630
+ function resolveDeleteModify(staging, policy, branchRank, preferredBranchId) {
1631
+ const resolved = resolveDeleteModifyOver(
1632
+ staging.modifiedNodes,
1633
+ staging.deletedNodes,
1634
+ (item) => item.node,
1635
+ (item) => item.node,
1636
+ "node",
1637
+ policy,
1638
+ branchRank,
1639
+ preferredBranchId
1640
+ );
1641
+ return {
1642
+ survivingModifications: resolved.survivingModifications,
1643
+ nodeDeletions: resolved.deletions,
1644
+ conflicts: resolved.conflicts,
1645
+ dropped: resolved.dropped
1646
+ };
1647
+ }
1648
+ function threeWayMergeProps(entityId, kind, baseProps, contributions, context, branchRank, preferredBranchId) {
1649
+ const merged = {};
1650
+ const conflicts = [];
1651
+ const propertyNames = new Set(Object.keys(baseProps));
1652
+ for (const contribution of contributions) {
1653
+ for (const name of Object.keys(contribution.forkProps)) {
1654
+ propertyNames.add(name);
1655
+ }
1656
+ }
1657
+ for (const property of [...propertyNames].sort(compareStrings)) {
1658
+ const baseHas = property in baseProps;
1659
+ const baseKey = baseHas ? canonicalValueKey(baseProps[property]) : void 0;
1660
+ const changed = contributions.filter((contribution) => {
1661
+ if (!(property in contribution.forkProps)) {
1662
+ return false;
1663
+ }
1664
+ const value2 = contribution.forkProps[property];
1665
+ return !baseHas || canonicalValueKey(value2) !== baseKey;
1666
+ }).map((contribution) => ({
1667
+ branchId: contribution.branchId,
1668
+ props: contribution.forkProps
1669
+ }));
1670
+ const values = collectConflictingValues(property, changed);
1671
+ if (values.length === 0) {
1672
+ const deletedByFork = baseHas && contributions.some(
1673
+ (contribution) => !(property in contribution.forkProps)
1674
+ );
1675
+ if (baseHas && !deletedByFork) {
1676
+ merged[property] = baseProps[property];
1677
+ }
1678
+ continue;
1679
+ }
1680
+ const distinctValues = new Set(
1681
+ values.map((candidate) => canonicalValueKey(candidate.value))
1682
+ );
1683
+ if (distinctValues.size === 1) {
1684
+ merged[property] = values[0].value;
1685
+ continue;
1686
+ }
1687
+ const preferredValue = values.find(
1688
+ (value2) => value2.branchId === preferredBranchId
1689
+ )?.value;
1690
+ const canonicalValue = preferredValue ?? (baseHas ? baseProps[property] : values[0].value);
1691
+ const reportedValues = preferredBranchId === void 0 ? values : values.filter((value2) => value2.branchId !== preferredBranchId);
1692
+ const { value, conflict } = resolvePropertyUnion(
1693
+ { entityId, kind, property, values, reportedValues, canonicalValue },
1694
+ context,
1695
+ branchRank
1696
+ );
1697
+ merged[property] = value;
1698
+ if (conflict !== void 0) {
1699
+ conflicts.push(conflict);
1700
+ }
1701
+ }
1702
+ return { props: merged, conflicts };
1703
+ }
1704
+ function reconcileModifications(survivingModifications, policy, branchRank, weights, preferredBranchId) {
1705
+ return reconcileOver(
1706
+ survivingModifications,
1707
+ (item) => item.node,
1708
+ (representative, chosenBranch, mergedProps) => ({
1709
+ branchId: chosenBranch,
1710
+ node: { ...representative.node, forkProps: mergedProps }
1711
+ }),
1712
+ policy,
1713
+ branchRank,
1714
+ weights,
1715
+ preferredBranchId
1716
+ );
1717
+ }
1718
+ function resolveEdgeDeleteModify(staging, policy, branchRank, preferredBranchId) {
1719
+ const resolved = resolveDeleteModifyOver(
1720
+ staging.modifiedEdges,
1721
+ staging.deletedEdges,
1722
+ (item) => item.edge,
1723
+ (item) => item.edge,
1724
+ "edge",
1725
+ policy,
1726
+ branchRank,
1727
+ preferredBranchId
1728
+ );
1729
+ return {
1730
+ survivingModifications: resolved.survivingModifications,
1731
+ edgeDeletions: resolved.deletions,
1732
+ conflicts: resolved.conflicts,
1733
+ dropped: resolved.dropped
1734
+ };
1735
+ }
1736
+ function reconcileEdgeModifications(survivingModifications, policy, branchRank, weights, preferredBranchId) {
1737
+ return reconcileOver(
1738
+ survivingModifications,
1739
+ (item) => item.edge,
1740
+ (representative, chosenBranch, mergedProps) => ({
1741
+ branchId: chosenBranch,
1742
+ edge: { ...representative.edge, forkProps: mergedProps }
1743
+ }),
1744
+ policy,
1745
+ branchRank,
1746
+ weights,
1747
+ preferredBranchId
1748
+ );
1749
+ }
1750
+
1751
+ // src/graph-merge/edge-repoint.ts
1752
+ var ENDPOINT_DELETED_DROP_REASON = "edge:endpoint-deleted";
1753
+ function buildCanonicalMap(clusters, canonicalOf) {
1754
+ const map = /* @__PURE__ */ new Map();
1755
+ for (const cluster of clusters) {
1756
+ const canonical = canonicalOf(cluster);
1757
+ for (const member of cluster.members) {
1758
+ map.set(member, canonical);
1759
+ }
1760
+ }
1761
+ return map;
1762
+ }
1763
+ function repoint(key, canonicalOf) {
1764
+ return canonicalOf.get(key) ?? key;
1765
+ }
1766
+ function dedupeKey(fromKey, type, toKey, props) {
1767
+ return JSON.stringify([fromKey, type, toKey, canonicalizeProps(props)]);
1768
+ }
1769
+ function groupKey(fromKey, type, toKey) {
1770
+ return JSON.stringify([fromKey, type, toKey]);
1771
+ }
1772
+ function pickSurvivor(edges, preferredBranchId) {
1773
+ const preferred = preferredBranchId === void 0 ? [] : edges.filter((edge) => edge.staged.branchId === preferredBranchId);
1774
+ const candidates = preferred.length > 0 ? preferred : edges;
1775
+ let survivor = candidates[0];
1776
+ for (const candidate of candidates.slice(1)) {
1777
+ if (candidate.staged.id < survivor.staged.id) {
1778
+ survivor = candidate;
1779
+ }
1780
+ }
1781
+ return survivor;
1782
+ }
1783
+ function unionEdgeProps(survivorId, kind, survivor, edges, context, branchRank, preferredBranchId) {
1784
+ const propertyNames = /* @__PURE__ */ new Set();
1785
+ for (const edge of edges) {
1786
+ for (const name of Object.keys(edge.staged.props)) {
1787
+ propertyNames.add(name);
1788
+ }
1789
+ }
1790
+ const props = {};
1791
+ const conflicts = [];
1792
+ const contributions = edges.map((edge) => edge.staged);
1793
+ for (const property of [...propertyNames].sort(
1794
+ (left, right) => compareStrings(left, right)
1795
+ )) {
1796
+ const values = collectConflictingValues(property, contributions);
1797
+ if (values.length === 0) {
1798
+ continue;
1799
+ }
1800
+ const survivorValue = property in survivor.staged.props ? survivor.staged.props[property] : values[0].value;
1801
+ const canonicalValue = values.find((value2) => value2.branchId === preferredBranchId)?.value ?? survivorValue;
1802
+ const reportedValues = preferredBranchId === void 0 ? values : values.filter((value2) => value2.branchId !== preferredBranchId);
1803
+ const { value, conflict } = resolvePropertyUnion(
1804
+ {
1805
+ entityId: survivorId,
1806
+ kind,
1807
+ property,
1808
+ values,
1809
+ reportedValues,
1810
+ canonicalValue
1811
+ },
1812
+ context,
1813
+ branchRank
1814
+ );
1815
+ props[property] = value;
1816
+ if (conflict !== void 0) {
1817
+ conflicts.push(conflict);
1818
+ }
1819
+ }
1820
+ return { props, conflicts };
1821
+ }
1822
+ function repointEdges(stagedEdges, canonicalOf, deletedNodeIds, policy, branchRank, weights, preferredBranchId) {
1823
+ const dropped = [];
1824
+ const liveByDedupeKey = /* @__PURE__ */ new Map();
1825
+ const dedupeKeyByGroup = /* @__PURE__ */ new Map();
1826
+ for (const staged of stagedEdges) {
1827
+ const fromKey = repoint(
1828
+ mergeKey(staged.fromKind, staged.fromId),
1829
+ canonicalOf
1830
+ );
1831
+ const toKey = repoint(mergeKey(staged.toKind, staged.toId), canonicalOf);
1832
+ if (deletedNodeIds.has(fromKey) || deletedNodeIds.has(toKey)) {
1833
+ dropped.push({
1834
+ kind: "edge",
1835
+ id: staged.id,
1836
+ reason: ENDPOINT_DELETED_DROP_REASON
1837
+ });
1838
+ continue;
1839
+ }
1840
+ const repointed = { staged, fromKey, toKey };
1841
+ const key = dedupeKey(fromKey, staged.kind, toKey, staged.props);
1842
+ const bucket = liveByDedupeKey.get(key);
1843
+ if (bucket === void 0) {
1844
+ liveByDedupeKey.set(key, [repointed]);
1845
+ } else {
1846
+ bucket.push(repointed);
1847
+ }
1848
+ const group = groupKey(fromKey, staged.kind, toKey);
1849
+ const keysForGroup = dedupeKeyByGroup.get(group);
1850
+ if (keysForGroup === void 0) {
1851
+ dedupeKeyByGroup.set(group, /* @__PURE__ */ new Set([key]));
1852
+ } else {
1853
+ keysForGroup.add(key);
1854
+ }
1855
+ }
1856
+ const context = {
1857
+ policy,
1858
+ ...weights === void 0 ? {} : { weights }
1859
+ };
1860
+ const merged = [];
1861
+ const conflicts = [];
1862
+ const sortedGroups = [...dedupeKeyByGroup.keys()].sort(
1863
+ (left, right) => compareStrings(left, right)
1864
+ );
1865
+ for (const group of sortedGroups) {
1866
+ const dedupeKeys = [...dedupeKeyByGroup.get(group)];
1867
+ const groupEdges = [];
1868
+ for (const key of dedupeKeys) {
1869
+ for (const edge of liveByDedupeKey.get(key)) {
1870
+ groupEdges.push(edge);
1871
+ }
1872
+ }
1873
+ const survivor = pickSurvivor(groupEdges, preferredBranchId);
1874
+ const survivorId = survivor.staged.id;
1875
+ const mergedIds = [...groupEdges].map((edge) => edge.staged.id).sort((left, right) => compareStrings(left, right));
1876
+ const fromId = idOf(survivor.fromKey);
1877
+ const fromKind = kindOf(survivor.fromKey);
1878
+ const toId = idOf(survivor.toKey);
1879
+ const toKind = kindOf(survivor.toKey);
1880
+ if (dedupeKeys.length === 1) {
1881
+ merged.push({
1882
+ id: survivorId,
1883
+ kind: survivor.staged.kind,
1884
+ fromId,
1885
+ toId,
1886
+ fromKind,
1887
+ toKind,
1888
+ props: survivor.staged.props,
1889
+ mergedIds
1890
+ });
1891
+ continue;
1892
+ }
1893
+ const { props, conflicts: groupConflicts } = unionEdgeProps(
1894
+ survivorId,
1895
+ survivor.staged.kind,
1896
+ survivor,
1897
+ groupEdges,
1898
+ context,
1899
+ branchRank,
1900
+ preferredBranchId
1901
+ );
1902
+ for (const conflict of groupConflicts) {
1903
+ conflicts.push(conflict);
1904
+ }
1905
+ merged.push({
1906
+ id: survivorId,
1907
+ kind: survivor.staged.kind,
1908
+ fromId,
1909
+ toId,
1910
+ fromKind,
1911
+ toKind,
1912
+ props,
1913
+ mergedIds
1914
+ });
1915
+ }
1916
+ const sortedEdges = merged.map((edge) => ({
1917
+ edge,
1918
+ sortKey: dedupeKey(
1919
+ mergeKey(edge.fromKind, edge.fromId),
1920
+ edge.kind,
1921
+ mergeKey(edge.toKind, edge.toId),
1922
+ edge.props
1923
+ )
1924
+ })).sort((left, right) => compareStrings(left.sortKey, right.sortKey)).map(({ edge }) => edge);
1925
+ return {
1926
+ edges: sortedEdges,
1927
+ dropped: dropped.sort((left, right) => compareStrings(left.id, right.id)),
1928
+ conflicts: conflicts.sort(
1929
+ (left, right) => compareStrings(
1930
+ `${left.entityId}|${left.property}`,
1931
+ `${right.entityId}|${right.property}`
1932
+ )
1933
+ )
1934
+ };
1935
+ }
1936
+ var MERGE_OPTION_DEFAULTS = {
1937
+ reconcileTypes: "off",
1938
+ onPropertyConflict: "flag",
1939
+ onBasePropertyConflict: "flag",
1940
+ onDeleteModifyConflict: "flag",
1941
+ onComparisonCeiling: "error",
1942
+ provenance: true,
1943
+ persistProvenance: false
1944
+ };
1945
+ var propertyConflictPolicySchema = z.enum([
1946
+ "flag",
1947
+ "lastWriteWins",
1948
+ "provenanceWeighted"
1949
+ ]);
1950
+ var resolveConfigScalarSchema = z.object({
1951
+ threshold: z.number().min(0, { message: "threshold must be >= 0" }).max(1, { message: "threshold must be <= 1" })
1952
+ });
1953
+ var mergeOptionsScalarSchema = z.object({
1954
+ reconcileTypes: z.enum(["ontology", "off"]).default(MERGE_OPTION_DEFAULTS.reconcileTypes),
1955
+ onDeleteModifyConflict: z.enum(["deleteWins", "modifyWins", "flag"]).default(MERGE_OPTION_DEFAULTS.onDeleteModifyConflict),
1956
+ onComparisonCeiling: z.enum(["error", "mergeByIdOnly"]).default(MERGE_OPTION_DEFAULTS.onComparisonCeiling),
1957
+ provenance: z.boolean().default(MERGE_OPTION_DEFAULTS.provenance),
1958
+ persistProvenance: z.boolean().default(MERGE_OPTION_DEFAULTS.persistProvenance),
1959
+ maxComparisonsPerKind: z.number().int({ message: "maxComparisonsPerKind must be an integer" }).min(0, { message: "maxComparisonsPerKind must be >= 0" }).optional(),
1960
+ clusterMaxDiameter: z.number().positive({ message: "clusterMaxDiameter must be positive" }).optional()
1961
+ });
1962
+ function validatePropertyConflictPolicy(policy, label) {
1963
+ if (typeof policy === "string" && !propertyConflictPolicySchema.safeParse(policy).success) {
1964
+ throw new Error(
1965
+ `Invalid ${label} "${policy}": expected "flag", "lastWriteWins", "provenanceWeighted", or a function.`
1966
+ );
1967
+ }
1968
+ return policy;
1969
+ }
1970
+ function validateResolveMap(resolve) {
1971
+ if (resolve === void 0) {
1972
+ return {};
1973
+ }
1974
+ const configs = resolve;
1975
+ const validated = {};
1976
+ for (const [kind, config] of Object.entries(configs)) {
1977
+ const parsed = resolveConfigScalarSchema.safeParse({
1978
+ threshold: config.threshold
1979
+ });
1980
+ if (!parsed.success) {
1981
+ throw new Error(
1982
+ `Invalid resolve config for kind "${kind}": ${parsed.error.message}`
1983
+ );
1984
+ }
1985
+ if (config.keyless !== void 0 && (!Number.isInteger(config.keyless.window) || config.keyless.window < 1)) {
1986
+ throw new Error(
1987
+ `Invalid resolve config for kind "${kind}": keyless.window must be a positive integer, got ${config.keyless.window}.`
1988
+ );
1989
+ }
1990
+ const strategy = config.similarity;
1991
+ if (strategy.kind === "hybrid") {
1992
+ for (const component of ["vector", "fulltext"]) {
1993
+ const weight = strategy.weights?.[component];
1994
+ if (weight !== void 0 && (!Number.isFinite(weight) || weight < 0)) {
1995
+ throw new Error(
1996
+ `Invalid resolve config for kind "${kind}": similarity.weights.${component} must be a finite number >= 0, got ${String(weight)}.`
1997
+ );
1998
+ }
1999
+ }
2000
+ }
2001
+ validated[kind] = config;
2002
+ }
2003
+ return validated;
2004
+ }
2005
+ function validateProvenanceWeights(weights) {
2006
+ for (const [branchId, weight] of weights) {
2007
+ if (!Number.isFinite(weight) || weight < 0) {
2008
+ throw new Error(
2009
+ `Invalid provenanceWeights for branch "${branchId}": weight must be a finite number >= 0, got ${String(weight)}.`
2010
+ );
2011
+ }
2012
+ }
2013
+ return weights;
2014
+ }
2015
+ function normalizeMergeOptions(options = {}) {
2016
+ const scalar = mergeOptionsScalarSchema.parse({
2017
+ reconcileTypes: options.reconcileTypes,
2018
+ onDeleteModifyConflict: options.onDeleteModifyConflict,
2019
+ onComparisonCeiling: options.onComparisonCeiling,
2020
+ provenance: options.provenance,
2021
+ persistProvenance: options.persistProvenance,
2022
+ ...options.maxComparisonsPerKind === void 0 ? {} : { maxComparisonsPerKind: options.maxComparisonsPerKind },
2023
+ ...options.clusterMaxDiameter === void 0 ? {} : { clusterMaxDiameter: options.clusterMaxDiameter }
2024
+ });
2025
+ const onPropertyConflict = validatePropertyConflictPolicy(
2026
+ options.onPropertyConflict ?? MERGE_OPTION_DEFAULTS.onPropertyConflict,
2027
+ "onPropertyConflict"
2028
+ );
2029
+ const onBasePropertyConflict = validatePropertyConflictPolicy(
2030
+ options.onBasePropertyConflict ?? MERGE_OPTION_DEFAULTS.onBasePropertyConflict,
2031
+ "onBasePropertyConflict"
2032
+ );
2033
+ const provenanceWeights = options.provenanceWeights === void 0 ? void 0 : validateProvenanceWeights(options.provenanceWeights);
2034
+ const usesProvenanceWeighting = onPropertyConflict === "provenanceWeighted" || onBasePropertyConflict === "provenanceWeighted";
2035
+ if (usesProvenanceWeighting && (provenanceWeights === void 0 || provenanceWeights.size === 0)) {
2036
+ throw new Error(
2037
+ 'A "provenanceWeighted" property-conflict policy requires a non-empty provenanceWeights map.'
2038
+ );
2039
+ }
2040
+ return {
2041
+ resolve: validateResolveMap(options.resolve),
2042
+ reconcileTypes: scalar.reconcileTypes,
2043
+ onPropertyConflict,
2044
+ onBasePropertyConflict,
2045
+ onDeleteModifyConflict: scalar.onDeleteModifyConflict,
2046
+ onComparisonCeiling: scalar.onComparisonCeiling,
2047
+ provenance: scalar.provenance,
2048
+ persistProvenance: scalar.persistProvenance,
2049
+ ...options.canonical === void 0 ? {} : { canonical: options.canonical },
2050
+ ...options.embedder === void 0 ? {} : { embedder: options.embedder },
2051
+ ...options.target === void 0 ? {} : { target: options.target },
2052
+ ...scalar.maxComparisonsPerKind === void 0 ? {} : { maxComparisonsPerKind: scalar.maxComparisonsPerKind },
2053
+ ...scalar.clusterMaxDiameter === void 0 ? {} : { clusterMaxDiameter: scalar.clusterMaxDiameter },
2054
+ ...options.branchOrder === void 0 ? {} : { branchOrder: options.branchOrder },
2055
+ ...provenanceWeights === void 0 ? {} : { provenanceWeights }
2056
+ };
2057
+ }
2058
+ var Provenance = defineNode("Provenance", {
2059
+ schema: z.object({
2060
+ targetGraphId: z.string(),
2061
+ role: z.enum(["node", "edge"]),
2062
+ canonicalId: z.string(),
2063
+ canonicalKind: z.string(),
2064
+ branchId: z.string(),
2065
+ sourceId: z.string()
2066
+ })
2067
+ });
2068
+ function provenanceGraphId(targetGraphId) {
2069
+ return `${targetGraphId}::merge-provenance`;
2070
+ }
2071
+ function buildProvenanceGraph(targetGraphId) {
2072
+ return defineGraph({
2073
+ id: provenanceGraphId(targetGraphId),
2074
+ nodes: { Provenance: { type: Provenance } },
2075
+ edges: {}
2076
+ });
2077
+ }
2078
+ async function openProvenanceStore(backend, targetGraphId) {
2079
+ const [store] = await createStoreWithSchema(
2080
+ buildProvenanceGraph(targetGraphId),
2081
+ backend
2082
+ );
2083
+ return store;
2084
+ }
2085
+ var ID_SEPARATOR = "\0";
2086
+ var ID_DIGEST_BYTES = 16;
2087
+ async function provenanceNodeId(targetGraphId, record) {
2088
+ const tuple = [
2089
+ targetGraphId,
2090
+ record.role,
2091
+ // canonicalKind is part of node identity: two contributions to different-kind
2092
+ // canonicals that share a bare id (e.g. base Patient:x and Encounter:x) must NOT
2093
+ // hash to the same sidecar row and clobber each other (the (kind,id) identity model).
2094
+ record.canonicalKind,
2095
+ record.canonicalId,
2096
+ record.branchId,
2097
+ record.sourceId
2098
+ ].join(ID_SEPARATOR);
2099
+ const digest = await sha256Hex(tuple, ID_DIGEST_BYTES);
2100
+ return `prov_${digest}`;
2101
+ }
2102
+ async function persistProvenanceRecords(store, targetGraphId, records) {
2103
+ if (records.length === 0) {
2104
+ return 0;
2105
+ }
2106
+ const items = await Promise.all(
2107
+ records.map(async (record) => ({
2108
+ id: await provenanceNodeId(targetGraphId, record),
2109
+ props: {
2110
+ targetGraphId,
2111
+ role: record.role,
2112
+ canonicalId: record.canonicalId,
2113
+ canonicalKind: record.canonicalKind,
2114
+ branchId: record.branchId,
2115
+ sourceId: record.sourceId
2116
+ }
2117
+ }))
2118
+ );
2119
+ await store.nodes.Provenance.bulkUpsertById(items);
2120
+ return items.length;
2121
+ }
2122
+ async function readProvenance(store, query = {}) {
2123
+ const all = await store.nodes.Provenance.find();
2124
+ return all.filter(
2125
+ (node) => (query.branchId === void 0 || node.branchId === query.branchId) && (query.canonicalId === void 0 || node.canonicalId === query.canonicalId) && (query.role === void 0 || node.role === query.role)
2126
+ ).sort((left, right) => compareStrings(left.id, right.id));
2127
+ }
2128
+
2129
+ // src/graph-merge/sources.ts
2130
+ function orderEndpoints(left, right) {
2131
+ const leftKey = mergeKeyOf(left);
2132
+ const rightKey = mergeKeyOf(right);
2133
+ return compareMergeKeys(leftKey, rightKey) <= 0 ? { a: leftKey, b: rightKey, left, right } : { a: rightKey, b: leftKey, left: right, right: left };
2134
+ }
2135
+ function sortMembersById(members) {
2136
+ return [...members].sort((left, right) => compareStrings(left.id, right.id));
2137
+ }
2138
+ function sortedBucketKeys(blocks) {
2139
+ return [...blocks.keys()].sort((left, right) => compareStrings(left, right));
2140
+ }
2141
+ var NODE_SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "kind", "meta"]);
2142
+ function nodeProps(node) {
2143
+ const props = {};
2144
+ for (const [key, value] of Object.entries(
2145
+ node
2146
+ )) {
2147
+ if (!NODE_SYSTEM_FIELDS.has(key)) {
2148
+ props[key] = value;
2149
+ }
2150
+ }
2151
+ return props;
2152
+ }
2153
+ function keylessConfigFor(resolveConfig) {
2154
+ if (resolveConfig.keyless === void 0) {
2155
+ return void 0;
2156
+ }
2157
+ const strategy = resolveConfig.similarity;
2158
+ const sortFields = strategy.kind === "fulltext" || strategy.kind === "hybrid" ? strategy.fields : strategy.kind === "vector" ? [strategy.field] : [];
2159
+ return { window: resolveConfig.keyless.window, sortFields };
2160
+ }
2161
+ function windowedPairs(members, config) {
2162
+ const keyed = members.map((node) => ({
2163
+ node,
2164
+ key: fieldText(node, config.sortFields)
2165
+ }));
2166
+ keyed.sort(
2167
+ (left, right) => left.key === right.key ? compareStrings(left.node.id, right.node.id) : compareStrings(left.key, right.key)
2168
+ );
2169
+ const pairs = [];
2170
+ for (let index = 0; index < keyed.length; index += 1) {
2171
+ const last = Math.min(index + config.window, keyed.length - 1);
2172
+ for (let index_ = index + 1; index_ <= last; index_ += 1) {
2173
+ pairs.push(orderEndpoints(keyed[index].node, keyed[index_].node));
2174
+ }
2175
+ }
2176
+ return pairs;
2177
+ }
2178
+ function pairsFromBlocks(blocks, keyless) {
2179
+ const pairs = [];
2180
+ for (const bucketKey of sortedBucketKeys(blocks)) {
2181
+ if (isUniqueBucketKey(bucketKey)) {
2182
+ continue;
2183
+ }
2184
+ const members = sortMembersById(blocks.get(bucketKey) ?? []);
2185
+ if (keyless !== void 0 && bucketKey === UNBLOCKED_BUCKET_KEY) {
2186
+ pairs.push(...windowedPairs(members, keyless));
2187
+ continue;
2188
+ }
2189
+ for (let index = 0; index < members.length; index += 1) {
2190
+ for (let index_ = index + 1; index_ < members.length; index_ += 1) {
2191
+ pairs.push(orderEndpoints(members[index], members[index_]));
2192
+ }
2193
+ }
2194
+ }
2195
+ return pairs;
2196
+ }
2197
+ function forcedEdgesFromBlocks(blocks) {
2198
+ const edges = [];
2199
+ for (const bucketKey of sortedBucketKeys(blocks)) {
2200
+ if (!isUniqueBucketKey(bucketKey)) {
2201
+ continue;
2202
+ }
2203
+ const members = sortMembersById(blocks.get(bucketKey) ?? []);
2204
+ for (let index = 0; index < members.length; index += 1) {
2205
+ for (let index_ = index + 1; index_ < members.length; index_ += 1) {
2206
+ const { a, b } = orderEndpoints(members[index], members[index_]);
2207
+ edges.push({ a, b, score: FORCED_MATCH_SCORE });
2208
+ }
2209
+ }
2210
+ }
2211
+ return edges;
2212
+ }
2213
+ function ontologyRetypeEdges(stagedIdentities, isRetypeCompatible) {
2214
+ const identitiesById = /* @__PURE__ */ new Map();
2215
+ for (const identity of stagedIdentities) {
2216
+ const id = idOf(identity);
2217
+ const bucket = identitiesById.get(id);
2218
+ if (bucket === void 0) {
2219
+ identitiesById.set(id, [identity]);
2220
+ } else {
2221
+ bucket.push(identity);
2222
+ }
2223
+ }
2224
+ const edges = [];
2225
+ for (const id of [...identitiesById.keys()].sort(compareStrings)) {
2226
+ const identities = [...new Set(identitiesById.get(id))].sort(
2227
+ compareMergeKeys
2228
+ );
2229
+ if (identities.length < 2) {
2230
+ continue;
2231
+ }
2232
+ const group = unionByCompatibility(identities, isRetypeCompatible);
2233
+ for (const members of groupsOf(identities, group)) {
2234
+ if (members.length < 2 || !isRetypeCompatible(members.map((identity) => kindOf(identity)))) {
2235
+ continue;
2236
+ }
2237
+ for (let index = 1; index < members.length; index += 1) {
2238
+ edges.push({
2239
+ a: members[0],
2240
+ b: members[index],
2241
+ score: FORCED_MATCH_SCORE
2242
+ });
2243
+ }
2244
+ }
2245
+ }
2246
+ return edges;
2247
+ }
2248
+ function unionByCompatibility(identities, isRetypeCompatible) {
2249
+ const forest = new UnionFind(compareMergeKeys);
2250
+ for (const identity of identities) {
2251
+ forest.add(identity);
2252
+ }
2253
+ for (let index = 0; index < identities.length; index += 1) {
2254
+ for (let index_ = index + 1; index_ < identities.length; index_ += 1) {
2255
+ if (isRetypeCompatible([
2256
+ kindOf(identities[index]),
2257
+ kindOf(identities[index_])
2258
+ ])) {
2259
+ forest.union(identities[index], identities[index_]);
2260
+ }
2261
+ }
2262
+ }
2263
+ return new Map(
2264
+ identities.map((identity) => [identity, forest.find(identity)])
2265
+ );
2266
+ }
2267
+ function groupsOf(identities, representativeOf) {
2268
+ const byRoot = /* @__PURE__ */ new Map();
2269
+ for (const identity of identities) {
2270
+ const root = representativeOf.get(identity);
2271
+ const bucket = byRoot.get(root);
2272
+ if (bucket === void 0) {
2273
+ byRoot.set(root, [identity]);
2274
+ } else {
2275
+ bucket.push(identity);
2276
+ }
2277
+ }
2278
+ return [...byRoot.values()].map((group) => [...group].sort(compareMergeKeys));
2279
+ }
2280
+ var exactKeySource = {
2281
+ id: "exactKey",
2282
+ generate(scope) {
2283
+ return Promise.resolve({
2284
+ pairs: pairsFromBlocks(scope.blocks, scope.keyless),
2285
+ forcedEdges: [],
2286
+ baseMembers: []
2287
+ });
2288
+ }
2289
+ };
2290
+ var uniqueSource = {
2291
+ id: "unique",
2292
+ generate(scope) {
2293
+ return Promise.resolve({
2294
+ pairs: [],
2295
+ forcedEdges: forcedEdgesFromBlocks(scope.blocks),
2296
+ baseMembers: []
2297
+ });
2298
+ }
2299
+ };
2300
+ var baseUniqueSource = {
2301
+ id: "baseUnique",
2302
+ async generate(scope) {
2303
+ const { kind, nodes, uniqueConstraints, store } = scope;
2304
+ if (nodes === void 0 || uniqueConstraints === void 0 || store === void 0) {
2305
+ throw new MergeError(
2306
+ "baseUniqueSource requires nodes, uniqueConstraints, and store in the source scope.",
2307
+ { details: { kind, source: "baseUnique" } }
2308
+ );
2309
+ }
2310
+ const collection = store.nodes[kind];
2311
+ if (collection === void 0 || nodes.length === 0) {
2312
+ return { pairs: [], forcedEdges: [], baseMembers: [] };
2313
+ }
2314
+ const forcedEdges = [];
2315
+ const baseMembers = /* @__PURE__ */ new Map();
2316
+ const constraints = [...uniqueConstraints].sort(
2317
+ (left, right) => compareStrings(left.name, right.name)
2318
+ );
2319
+ const items = nodes.map((node) => ({ props: nodeProps(node) }));
2320
+ const matchesByConstraint = await Promise.all(
2321
+ constraints.map(async (constraint) => {
2322
+ try {
2323
+ return await collection.bulkFindByConstraint(constraint.name, items);
2324
+ } catch (error) {
2325
+ throw new MergeError(
2326
+ `baseUniqueSource: new-vs-base lookup for constraint "${constraint.name}" on kind "${kind}" failed \u2014 a staged node's props could not be validated against the kind's schema for constraint-key computation.`,
2327
+ { details: { kind, constraint: constraint.name }, cause: error }
2328
+ );
2329
+ }
2330
+ })
2331
+ );
2332
+ for (const matches of matchesByConstraint) {
2333
+ for (const [index, node] of nodes.entries()) {
2334
+ const base = matches[index];
2335
+ if (base === void 0) {
2336
+ continue;
2337
+ }
2338
+ const stagedNode = node;
2339
+ const stagedKey = mergeKey(stagedNode.kind, stagedNode.id);
2340
+ const baseKey = mergeKey(base.kind, base.id);
2341
+ if (!baseMembers.has(baseKey)) {
2342
+ baseMembers.set(baseKey, {
2343
+ origin: "base",
2344
+ id: base.id,
2345
+ kind: base.kind,
2346
+ props: nodeProps(base)
2347
+ });
2348
+ }
2349
+ if (stagedKey === baseKey) {
2350
+ continue;
2351
+ }
2352
+ const { a, b } = compareMergeKeys(stagedKey, baseKey) <= 0 ? { a: stagedKey, b: baseKey } : { a: baseKey, b: stagedKey };
2353
+ forcedEdges.push({ a, b, score: FORCED_MATCH_SCORE });
2354
+ }
2355
+ }
2356
+ return {
2357
+ pairs: [],
2358
+ forcedEdges,
2359
+ baseMembers: [...baseMembers.values()].sort(
2360
+ (left, right) => compareStrings(left.id, right.id)
2361
+ )
2362
+ };
2363
+ }
2364
+ };
2365
+ var baseKeySource = {
2366
+ id: "baseKey",
2367
+ async generate(scope) {
2368
+ const { kind, nodes, blockIndex, store } = scope;
2369
+ if (blockIndex === void 0) {
2370
+ return { pairs: [], forcedEdges: [], baseMembers: [] };
2371
+ }
2372
+ if (nodes === void 0 || store === void 0) {
2373
+ throw new MergeError(
2374
+ "baseKeySource requires nodes and store in the source scope.",
2375
+ { details: { kind, source: "baseKey" } }
2376
+ );
2377
+ }
2378
+ const collection = store.nodes[kind];
2379
+ if (collection === void 0 || nodes.length === 0) {
2380
+ return { pairs: [], forcedEdges: [], baseMembers: [] };
2381
+ }
2382
+ const items = nodes.map((node) => ({ props: nodeProps(node) }));
2383
+ let matchesByNode;
2384
+ try {
2385
+ matchesByNode = await collection.bulkFindByIndex(blockIndex, items);
2386
+ } catch (error) {
2387
+ throw new MergeError(
2388
+ `baseKeySource: new-vs-base lookup on index "${blockIndex}" for kind "${kind}" failed \u2014 the index is undeclared or a staged node's props are incompatible with its indexed fields.`,
2389
+ { details: { kind, index: blockIndex }, cause: error }
2390
+ );
2391
+ }
2392
+ const pairs = [];
2393
+ const baseMembers = /* @__PURE__ */ new Map();
2394
+ for (const [index, node] of nodes.entries()) {
2395
+ const stagedNode = node;
2396
+ const stagedKey = mergeKey(stagedNode.kind, stagedNode.id);
2397
+ for (const base of matchesByNode[index] ?? []) {
2398
+ const baseKey = mergeKey(base.kind, base.id);
2399
+ if (!baseMembers.has(baseKey)) {
2400
+ baseMembers.set(baseKey, {
2401
+ origin: "base",
2402
+ id: base.id,
2403
+ kind: base.kind,
2404
+ props: nodeProps(base)
2405
+ });
2406
+ }
2407
+ if (stagedKey === baseKey) {
2408
+ continue;
2409
+ }
2410
+ pairs.push(orderEndpoints(stagedNode, base));
2411
+ }
2412
+ }
2413
+ return {
2414
+ pairs,
2415
+ forcedEdges: [],
2416
+ baseMembers: [...baseMembers.values()].sort(
2417
+ (left, right) => compareStrings(left.id, right.id)
2418
+ )
2419
+ };
2420
+ }
2421
+ };
2422
+ var CANDIDATE_SOURCES = [
2423
+ exactKeySource,
2424
+ uniqueSource
2425
+ ];
2426
+
2427
+ // src/graph-merge/staging.ts
2428
+ function compareByIdThenBranch(left, right) {
2429
+ const byId2 = compareStrings(left.id, right.id);
2430
+ return byId2 === 0 ? compareStrings(left.branchId, right.branchId) : byId2;
2431
+ }
2432
+ function compareByKindIdBranch(left, right) {
2433
+ const byKind = compareStrings(left.kind, right.kind);
2434
+ if (byKind !== 0) {
2435
+ return byKind;
2436
+ }
2437
+ const byId2 = compareStrings(left.id, right.id);
2438
+ return byId2 === 0 ? compareStrings(left.branchId, right.branchId) : byId2;
2439
+ }
2440
+ function groupByKind(items) {
2441
+ const buckets = /* @__PURE__ */ new Map();
2442
+ for (const item of items) {
2443
+ const bucket = buckets.get(item.kind);
2444
+ if (bucket === void 0) {
2445
+ buckets.set(item.kind, [item]);
2446
+ } else {
2447
+ bucket.push(item);
2448
+ }
2449
+ }
2450
+ const ordered = /* @__PURE__ */ new Map();
2451
+ for (const kind of [...buckets.keys()].sort(
2452
+ (left, right) => compareStrings(left, right)
2453
+ )) {
2454
+ ordered.set(
2455
+ kind,
2456
+ [...buckets.get(kind)].sort(
2457
+ (left, right) => compareByIdThenBranch(left, right)
2458
+ )
2459
+ );
2460
+ }
2461
+ return ordered;
2462
+ }
2463
+ async function stageBranches(baseStore, branches) {
2464
+ const newNodes = [];
2465
+ const modifiedNodes = [];
2466
+ const deletedNodes = [];
2467
+ const newEdges = [];
2468
+ const modifiedEdges = [];
2469
+ const deletedEdges = [];
2470
+ for (const branch2 of branches) {
2471
+ const diff = await diffAgainstBase(baseStore, branch2.store);
2472
+ const branchId = branch2.id;
2473
+ for (const node of diff.nodes.new) {
2474
+ newNodes.push({ branchId, node, kind: node.kind, id: node.id });
2475
+ }
2476
+ for (const node of diff.nodes.modified) {
2477
+ modifiedNodes.push({ branchId, node, kind: node.kind, id: node.id });
2478
+ }
2479
+ for (const node of diff.nodes.deleted) {
2480
+ deletedNodes.push({ branchId, node, kind: node.kind, id: node.id });
2481
+ }
2482
+ for (const edge of diff.edges.new) {
2483
+ newEdges.push({ branchId, edge, kind: edge.kind, id: edge.id });
2484
+ }
2485
+ for (const edge of diff.edges.modified) {
2486
+ modifiedEdges.push({ branchId, edge, kind: edge.kind, id: edge.id });
2487
+ }
2488
+ for (const edge of diff.edges.deleted) {
2489
+ deletedEdges.push({ branchId, edge, kind: edge.kind, id: edge.id });
2490
+ }
2491
+ }
2492
+ return {
2493
+ newNodesByKind: groupByKind(newNodes),
2494
+ modifiedNodes: [...modifiedNodes].sort(
2495
+ (left, right) => compareByKindIdBranch(left, right)
2496
+ ),
2497
+ deletedNodes: [...deletedNodes].sort(
2498
+ (left, right) => compareByKindIdBranch(left, right)
2499
+ ),
2500
+ newEdgesByKind: groupByKind(newEdges),
2501
+ modifiedEdges: [...modifiedEdges].sort(
2502
+ (left, right) => compareByKindIdBranch(left, right)
2503
+ ),
2504
+ deletedEdges: [...deletedEdges].sort(
2505
+ (left, right) => compareByKindIdBranch(left, right)
2506
+ )
2507
+ };
2508
+ }
2509
+
2510
+ // src/graph-merge/tx-retry.ts
2511
+ var RETRYABLE_SQLSTATES = /* @__PURE__ */ new Set(["40001", "40P01"]);
2512
+ var RETRYABLE_MESSAGE_PATTERN = /could not serialize access|deadlock detected/i;
2513
+ var MAX_COMMIT_ATTEMPTS = 3;
2514
+ function isRetryableTxConflict(error) {
2515
+ const seen = /* @__PURE__ */ new Set();
2516
+ let current = error;
2517
+ while (current !== void 0 && current !== null && typeof current === "object" && !seen.has(current)) {
2518
+ seen.add(current);
2519
+ const candidate = current;
2520
+ if (typeof candidate.code === "string" && RETRYABLE_SQLSTATES.has(candidate.code)) {
2521
+ return true;
2522
+ }
2523
+ if (typeof candidate.message === "string" && RETRYABLE_MESSAGE_PATTERN.test(candidate.message)) {
2524
+ return true;
2525
+ }
2526
+ current = candidate.cause;
2527
+ }
2528
+ return false;
2529
+ }
2530
+ async function withTxConflictRetry(commit) {
2531
+ let lastConflict;
2532
+ for (let attempt = 1; attempt <= MAX_COMMIT_ATTEMPTS; attempt += 1) {
2533
+ try {
2534
+ return await commit();
2535
+ } catch (error) {
2536
+ if (!isRetryableTxConflict(error)) {
2537
+ throw error;
2538
+ }
2539
+ lastConflict = error;
2540
+ }
2541
+ }
2542
+ throw new MergeError(
2543
+ `Merge commit aborted by transaction conflicts (serialization failure or deadlock) on ${MAX_COMMIT_ATTEMPTS} consecutive attempts; giving up.`,
2544
+ {
2545
+ cause: lastConflict,
2546
+ details: { attempts: MAX_COMMIT_ATTEMPTS },
2547
+ suggestion: "Reduce concurrent writes to the merge target, or serialize merges against it."
2548
+ }
2549
+ );
2550
+ }
2551
+
2552
+ // src/graph-merge/type-reconcile.ts
2553
+ var INCOMPATIBLE_TYPES_FLAG_REASON = "type-reconcile:incompatible-kinds";
2554
+ function distinctKinds(kinds) {
2555
+ return [...new Set(kinds)].sort((left, right) => compareStrings(left, right));
2556
+ }
2557
+ function areEquivalent(closure, left, right) {
2558
+ return left !== right && isReachable2(closure, left, right) && isReachable2(closure, right, left);
2559
+ }
2560
+ function isAtOrBelow(closure, candidate, other) {
2561
+ if (candidate === other) {
2562
+ return true;
2563
+ }
2564
+ return isReachable2(closure, candidate, other) || areEquivalent(closure, candidate, other);
2565
+ }
2566
+ function mostSpecificCommonKind(closure, kinds) {
2567
+ const qualifiers = kinds.filter(
2568
+ (candidate) => kinds.every((other) => isAtOrBelow(closure, candidate, other))
2569
+ );
2570
+ if (qualifiers.length === 0) {
2571
+ return void 0;
2572
+ }
2573
+ return [...qualifiers].sort((left, right) => compareStrings(left, right))[0];
2574
+ }
2575
+ function reconcileTypes(clusters, closure, mode) {
2576
+ if (mode === "off") {
2577
+ return {
2578
+ reconciliations: [],
2579
+ retypeMap: /* @__PURE__ */ new Map(),
2580
+ dropped: []
2581
+ };
2582
+ }
2583
+ const reconciliations = [];
2584
+ const retypeMap = /* @__PURE__ */ new Map();
2585
+ const dropped = [];
2586
+ for (const cluster of clusters) {
2587
+ const kinds = distinctKinds(cluster.memberKinds);
2588
+ if (kinds.length <= 1) {
2589
+ continue;
2590
+ }
2591
+ const entityId = idOf(cluster.canonicalId);
2592
+ const toType = mostSpecificCommonKind(closure, kinds);
2593
+ if (toType === void 0) {
2594
+ dropped.push({
2595
+ kind: "node",
2596
+ id: entityId,
2597
+ reason: INCOMPATIBLE_TYPES_FLAG_REASON
2598
+ });
2599
+ continue;
2600
+ }
2601
+ reconciliations.push({
2602
+ entityId,
2603
+ fromTypes: kinds,
2604
+ toType
2605
+ });
2606
+ retypeMap.set(cluster.canonicalId, toType);
2607
+ }
2608
+ return {
2609
+ reconciliations: reconciliations.sort(
2610
+ (left, right) => compareStrings(left.entityId, right.entityId)
2611
+ ),
2612
+ retypeMap,
2613
+ dropped: dropped.sort((left, right) => compareStrings(left.id, right.id))
2614
+ };
2615
+ }
2616
+
2617
+ // src/graph-merge/merge.ts
2618
+ var COMMITTED_TARGET_BRANCH = asBranchId("__committed_target__");
2619
+ function asNode(staged) {
2620
+ return {
2621
+ kind: staged.node.kind,
2622
+ id: staged.node.id,
2623
+ ...staged.node.props
2624
+ };
2625
+ }
2626
+ function newNodesByKind(staging) {
2627
+ const ordered = /* @__PURE__ */ new Map();
2628
+ for (const [kind, items] of staging.newNodesByKind) {
2629
+ ordered.set(
2630
+ kind,
2631
+ [...items].sort((left, right) => {
2632
+ const byId2 = compareStrings(left.node.id, right.node.id);
2633
+ return byId2 === 0 ? compareStrings(left.branchId, right.branchId) : byId2;
2634
+ })
2635
+ );
2636
+ }
2637
+ return ordered;
2638
+ }
2639
+ function uniqueConstraintsFor(introspectionKinds, kind) {
2640
+ return introspectionKinds.get(kind) ?? [];
2641
+ }
2642
+ async function precomputeEmbeddings(byKind, resolve, embedder) {
2643
+ const texts = /* @__PURE__ */ new Set();
2644
+ for (const [kind, items] of byKind) {
2645
+ const config = resolve[kind];
2646
+ if (config === void 0) {
2647
+ continue;
2648
+ }
2649
+ const fields = embeddingFields(config.similarity);
2650
+ if (fields === void 0) {
2651
+ continue;
2652
+ }
2653
+ for (const staged of items) {
2654
+ const text = fieldText(asNode(staged), fields);
2655
+ if (text.length > 0) {
2656
+ texts.add(text);
2657
+ }
2658
+ }
2659
+ }
2660
+ const lookup = /* @__PURE__ */ new Map();
2661
+ if (texts.size === 0) {
2662
+ return lookup;
2663
+ }
2664
+ const orderedTexts = [...texts].sort(
2665
+ (left, right) => compareStrings(left, right)
2666
+ );
2667
+ const vectors = await embedder(orderedTexts);
2668
+ if (vectors.length !== orderedTexts.length) {
2669
+ throw new MergeError(
2670
+ `Embedder returned ${vectors.length} vectors for ${orderedTexts.length} texts; expected exactly one per text.`,
2671
+ {
2672
+ details: { texts: orderedTexts.length, vectors: vectors.length },
2673
+ suggestion: "Ensure MergeOptions.embedder returns one vector per input text, in order."
2674
+ }
2675
+ );
2676
+ }
2677
+ for (const [index, text] of orderedTexts.entries()) {
2678
+ lookup.set(text, vectors[index]);
2679
+ }
2680
+ return lookup;
2681
+ }
2682
+ async function embedMissingPairTexts(base, pairs, strategy, embedder) {
2683
+ const existing = base ?? /* @__PURE__ */ new Map();
2684
+ const fields = embeddingFields(strategy);
2685
+ if (fields === void 0) {
2686
+ return existing;
2687
+ }
2688
+ const missing = /* @__PURE__ */ new Set();
2689
+ for (const pair of pairs) {
2690
+ for (const node of [pair.left, pair.right]) {
2691
+ const text = fieldText(node, fields);
2692
+ if (text.length > 0 && !existing.has(text)) {
2693
+ missing.add(text);
2694
+ }
2695
+ }
2696
+ }
2697
+ if (missing.size === 0) {
2698
+ return existing;
2699
+ }
2700
+ const orderedTexts = [...missing].sort(
2701
+ (left, right) => compareStrings(left, right)
2702
+ );
2703
+ const vectors = await embedder(orderedTexts);
2704
+ if (vectors.length !== orderedTexts.length) {
2705
+ throw new MergeError(
2706
+ `Embedder returned ${vectors.length} vectors for ${orderedTexts.length} texts; expected exactly one per text.`,
2707
+ {
2708
+ details: { texts: orderedTexts.length, vectors: vectors.length },
2709
+ suggestion: "Ensure MergeOptions.embedder returns one vector per input text, in order."
2710
+ }
2711
+ );
2712
+ }
2713
+ const augmented = new Map(existing);
2714
+ for (const [index, text] of orderedTexts.entries()) {
2715
+ augmented.set(text, vectors[index]);
2716
+ }
2717
+ return augmented;
2718
+ }
2719
+ async function generateAllCandidates(target, staging, options, introspectionKinds, ctx, useBaseSources, embedder) {
2720
+ const allEdges = [];
2721
+ const warnings = [];
2722
+ const baseMembers = [];
2723
+ const byKind = newNodesByKind(staging);
2724
+ const sources = useBaseSources ? [...CANDIDATE_SOURCES, baseUniqueSource, baseKeySource] : CANDIDATE_SOURCES;
2725
+ const baseStore = target;
2726
+ const perKind = await Promise.all(
2727
+ [...byKind].map(
2728
+ async ([kind, items]) => {
2729
+ const resolveConfig = options.resolve[kind];
2730
+ if (resolveConfig === void 0) {
2731
+ return ok({ edges: [], warnings: [], baseMembers: [] });
2732
+ }
2733
+ const nodes = items.map((staged) => asNode(staged));
2734
+ const uniqueConstraints = uniqueConstraintsFor(
2735
+ introspectionKinds,
2736
+ kind
2737
+ );
2738
+ const blocks = blockNodes(nodes, resolveConfig, uniqueConstraints);
2739
+ const keylessConfig = keylessConfigFor(resolveConfig);
2740
+ const scope = {
2741
+ kind,
2742
+ blocks,
2743
+ nodes,
2744
+ uniqueConstraints,
2745
+ store: baseStore,
2746
+ ...resolveConfig.blockIndex === void 0 ? {} : { blockIndex: resolveConfig.blockIndex },
2747
+ ...keylessConfig === void 0 ? {} : { keyless: keylessConfig }
2748
+ };
2749
+ const pairs = [];
2750
+ const forcedEdges = [];
2751
+ const kindBaseMembers = [];
2752
+ for (const source of sources) {
2753
+ const produced = await source.generate(scope);
2754
+ pairs.push(...produced.pairs);
2755
+ forcedEdges.push(...produced.forcedEdges);
2756
+ kindBaseMembers.push(...produced.baseMembers);
2757
+ }
2758
+ const kindCtx = embedder === void 0 ? ctx : {
2759
+ ...ctx,
2760
+ embeddings: await embedMissingPairTexts(
2761
+ ctx.embeddings,
2762
+ pairs,
2763
+ resolveConfig.similarity,
2764
+ embedder
2765
+ )
2766
+ };
2767
+ const scored = scoreCandidates(
2768
+ { pairs, forcedEdges },
2769
+ resolveConfig,
2770
+ kindCtx,
2771
+ options.onComparisonCeiling,
2772
+ options.maxComparisonsPerKind
2773
+ );
2774
+ if (isErr(scored)) {
2775
+ return err(scored.error);
2776
+ }
2777
+ return ok({
2778
+ edges: scored.data.edges,
2779
+ warnings: scored.data.warnings.map(
2780
+ (warning) => `[${kind}] ${warning.message}`
2781
+ ),
2782
+ baseMembers: kindBaseMembers
2783
+ });
2784
+ }
2785
+ )
2786
+ );
2787
+ for (const result of perKind) {
2788
+ if (isErr(result)) {
2789
+ return err(result.error);
2790
+ }
2791
+ allEdges.push(...result.data.edges);
2792
+ warnings.push(...result.data.warnings);
2793
+ baseMembers.push(...result.data.baseMembers);
2794
+ }
2795
+ return ok({
2796
+ // The ONE shared `(a, b)` edge comparator (id-first `(kind, id)` order), so this
2797
+ // stage emits edges in exactly the order clustering consumes them.
2798
+ edges: allEdges.sort((left, right) => compareCandidateEdges(left, right)),
2799
+ warnings,
2800
+ // Total order over the composite `(kind, id)` identity (two same-id/different-kind
2801
+ // base members would tie under a bare-id compare).
2802
+ baseMembers: baseMembers.sort(
2803
+ (left, right) => compareMergeKeys(mergeKeyOf(left), mergeKeyOf(right))
2804
+ )
2805
+ });
2806
+ }
2807
+ function clusterMembersFor(cluster, newNodesById, baseMembersById) {
2808
+ const members = [];
2809
+ for (const key of cluster.members) {
2810
+ for (const staged of newNodesById.get(key) ?? []) {
2811
+ members.push({
2812
+ origin: "staged",
2813
+ id: staged.node.id,
2814
+ kind: staged.node.kind,
2815
+ branchId: staged.branchId,
2816
+ props: staged.node.props
2817
+ });
2818
+ }
2819
+ const base = baseMembersById.get(key);
2820
+ if (base !== void 0) {
2821
+ members.push({
2822
+ origin: "base",
2823
+ id: base.id,
2824
+ kind: base.kind,
2825
+ branchId: BASE_PROVENANCE_BRANCH,
2826
+ props: base.props
2827
+ });
2828
+ }
2829
+ }
2830
+ return members;
2831
+ }
2832
+ function indexNewNodesById(staging) {
2833
+ const index = /* @__PURE__ */ new Map();
2834
+ for (const items of staging.newNodesByKind.values()) {
2835
+ for (const staged of items) {
2836
+ const key = mergeKeyOf(staged.node);
2837
+ const bucket = index.get(key);
2838
+ if (bucket === void 0) {
2839
+ index.set(key, [staged]);
2840
+ } else {
2841
+ bucket.push(staged);
2842
+ }
2843
+ }
2844
+ }
2845
+ return index;
2846
+ }
2847
+ function buildStagedEdges(staging, modifiedEdges) {
2848
+ const staged = [];
2849
+ for (const items of staging.newEdgesByKind.values()) {
2850
+ for (const item of items) {
2851
+ staged.push(toStagedEdge(item.branchId, item));
2852
+ }
2853
+ }
2854
+ for (const item of modifiedEdges) {
2855
+ staged.push({
2856
+ id: item.edge.id,
2857
+ kind: item.edge.kind,
2858
+ fromId: item.edge.fromId,
2859
+ toId: item.edge.toId,
2860
+ fromKind: item.edge.fromKind,
2861
+ toKind: item.edge.toKind,
2862
+ props: item.edge.forkProps,
2863
+ branchId: item.branchId
2864
+ });
2865
+ }
2866
+ return staged;
2867
+ }
2868
+ function toStagedEdge(branchId, item) {
2869
+ return {
2870
+ id: item.edge.id,
2871
+ kind: item.edge.kind,
2872
+ fromId: item.edge.fromId,
2873
+ toId: item.edge.toId,
2874
+ fromKind: item.edge.fromKind,
2875
+ toKind: item.edge.toKind,
2876
+ props: item.edge.props,
2877
+ branchId
2878
+ };
2879
+ }
2880
+ function buildProvenanceIndex(records) {
2881
+ const byBranch = /* @__PURE__ */ new Map();
2882
+ for (const record of records) {
2883
+ let entry = byBranch.get(record.branchId);
2884
+ if (entry === void 0) {
2885
+ entry = { nodeIds: /* @__PURE__ */ new Set(), edgeIds: /* @__PURE__ */ new Set() };
2886
+ byBranch.set(record.branchId, entry);
2887
+ }
2888
+ if (record.role === "node") {
2889
+ entry.nodeIds.add(record.canonicalId);
2890
+ } else {
2891
+ entry.edgeIds.add(record.canonicalId);
2892
+ }
2893
+ }
2894
+ const frozen = /* @__PURE__ */ new Map();
2895
+ for (const [branchId, sets] of byBranch) {
2896
+ frozen.set(branchId, {
2897
+ nodeIds: [...sets.nodeIds].sort(
2898
+ (left, right) => compareStrings(left, right)
2899
+ ),
2900
+ edgeIds: [...sets.edgeIds].sort((left, right) => compareStrings(left, right)).map((id) => id)
2901
+ });
2902
+ }
2903
+ return {
2904
+ byBranch: (branchId) => frozen.get(branchId) ?? { nodeIds: [], edgeIds: [] }
2905
+ };
2906
+ }
2907
+ function pushProvenance(records, role, branchId, canonicalId, canonicalKind, sourceId) {
2908
+ records.push({ role, canonicalId, canonicalKind, branchId, sourceId });
2909
+ }
2910
+ var EMPTY_WEIGHTS = /* @__PURE__ */ new Map();
2911
+ function planMerge(staging, candidateEdges, candidateWarnings, baseMembers, options, branchRank, subClassClosure, preferredBranchId) {
2912
+ const provenanceRecords = [];
2913
+ const weights = options.provenanceWeights ?? EMPTY_WEIGHTS;
2914
+ const recordProvenance = (role, branchId, canonicalId, canonicalKind, sourceId) => {
2915
+ if (branchId === preferredBranchId) {
2916
+ return;
2917
+ }
2918
+ pushProvenance(
2919
+ provenanceRecords,
2920
+ role,
2921
+ branchId,
2922
+ canonicalId,
2923
+ canonicalKind,
2924
+ sourceId
2925
+ );
2926
+ };
2927
+ const newNodesById = indexNewNodesById(staging);
2928
+ const acceptedEndpointKeys = /* @__PURE__ */ new Set();
2929
+ for (const edge of candidateEdges) {
2930
+ acceptedEndpointKeys.add(edge.a);
2931
+ acceptedEndpointKeys.add(edge.b);
2932
+ }
2933
+ const baseMembersById = /* @__PURE__ */ new Map();
2934
+ for (const member of baseMembers) {
2935
+ const key = mergeKeyOf(member);
2936
+ if (acceptedEndpointKeys.has(key) || newNodesById.has(key)) {
2937
+ baseMembersById.set(key, member);
2938
+ }
2939
+ }
2940
+ const newNodeIds = [...newNodesById.keys(), ...baseMembersById.keys()];
2941
+ const isOntology = options.reconcileTypes === "ontology";
2942
+ const preferKind = isOntology ? (kinds) => mostSpecificCommonKind(subClassClosure, kinds) : void 0;
2943
+ const clusterEdges = isOntology ? [
2944
+ ...candidateEdges,
2945
+ ...ontologyRetypeEdges(
2946
+ newNodesById.keys(),
2947
+ (kinds) => mostSpecificCommonKind(subClassClosure, kinds) !== void 0
2948
+ )
2949
+ ] : candidateEdges;
2950
+ const baseIds = new Set(baseMembersById.keys());
2951
+ const guard = enforceBaseGuard(
2952
+ connectedComponents(clusterEdges, newNodeIds),
2953
+ clusterEdges,
2954
+ baseIds
2955
+ );
2956
+ const clusters = options.clusterMaxDiameter === void 0 ? guard.clusters : enforceDiameter(guard.clusters, clusterEdges, options.clusterMaxDiameter);
2957
+ const baseAmbiguities = guard.events.map((event) => ({
2958
+ baseIds: event.baseIds.map((key) => ({ kind: kindOf(key), id: idOf(key) })),
2959
+ memberIds: event.memberIds.map((key) => ({
2960
+ kind: kindOf(key),
2961
+ id: idOf(key)
2962
+ }))
2963
+ }));
2964
+ const canonicalEntities = [];
2965
+ const resolutions = [];
2966
+ const propertyConflicts = [];
2967
+ const reconcileInputs = [];
2968
+ for (const cluster of clusters) {
2969
+ const members = clusterMembersFor(cluster, newNodesById, baseMembersById);
2970
+ if (members.length === 0) {
2971
+ continue;
2972
+ }
2973
+ const entity = canonicalizeCluster(
2974
+ cluster,
2975
+ members,
2976
+ options.onPropertyConflict,
2977
+ branchRank,
2978
+ weights,
2979
+ options.canonical,
2980
+ options.onBasePropertyConflict,
2981
+ preferKind,
2982
+ preferredBranchId
2983
+ );
2984
+ canonicalEntities.push(entity);
2985
+ reconcileInputs.push({
2986
+ canonicalId: mergeKey(entity.kind, entity.canonicalId),
2987
+ memberKinds: members.map((member) => member.kind)
2988
+ });
2989
+ const distinctMergedIds = new Set(
2990
+ cluster.members.map((member) => idOf(member))
2991
+ );
2992
+ if (distinctMergedIds.size > 1) {
2993
+ resolutions.push(entity.resolution);
2994
+ }
2995
+ for (const conflict of entity.conflicts) {
2996
+ propertyConflicts.push(conflict);
2997
+ }
2998
+ for (const member of members) {
2999
+ recordProvenance(
3000
+ "node",
3001
+ member.branchId,
3002
+ entity.canonicalId,
3003
+ entity.kind,
3004
+ member.id
3005
+ );
3006
+ }
3007
+ }
3008
+ const deleteModify = resolveDeleteModify(
3009
+ staging,
3010
+ options.onDeleteModifyConflict,
3011
+ branchRank,
3012
+ preferredBranchId
3013
+ );
3014
+ const nodeDeletions = /* @__PURE__ */ new Map();
3015
+ for (const deletion of deleteModify.nodeDeletions) {
3016
+ nodeDeletions.set(mergeKey(deletion.kind, deletion.id), deletion.kind);
3017
+ }
3018
+ for (const modification of deleteModify.survivingModifications) {
3019
+ recordProvenance(
3020
+ "node",
3021
+ modification.branchId,
3022
+ modification.node.id,
3023
+ modification.node.kind,
3024
+ modification.node.id
3025
+ );
3026
+ }
3027
+ const reconciledModifications = reconcileModifications(
3028
+ deleteModify.survivingModifications,
3029
+ options.onPropertyConflict,
3030
+ branchRank,
3031
+ weights,
3032
+ preferredBranchId
3033
+ );
3034
+ for (const conflict of reconciledModifications.conflicts) {
3035
+ propertyConflicts.push(conflict);
3036
+ }
3037
+ const edgeDeleteModify = resolveEdgeDeleteModify(
3038
+ staging,
3039
+ options.onDeleteModifyConflict,
3040
+ branchRank,
3041
+ preferredBranchId
3042
+ );
3043
+ const edgeDeletions = /* @__PURE__ */ new Map();
3044
+ for (const deletion of edgeDeleteModify.edgeDeletions) {
3045
+ edgeDeletions.set(mergeKey(deletion.kind, deletion.id), deletion.kind);
3046
+ }
3047
+ for (const modification of edgeDeleteModify.survivingModifications) {
3048
+ recordProvenance(
3049
+ "edge",
3050
+ modification.branchId,
3051
+ modification.edge.id,
3052
+ modification.edge.kind,
3053
+ modification.edge.id
3054
+ );
3055
+ }
3056
+ const reconciledEdgeModifications = reconcileEdgeModifications(
3057
+ edgeDeleteModify.survivingModifications,
3058
+ options.onPropertyConflict,
3059
+ branchRank,
3060
+ weights,
3061
+ preferredBranchId
3062
+ );
3063
+ for (const conflict of reconciledEdgeModifications.conflicts) {
3064
+ propertyConflicts.push(conflict);
3065
+ }
3066
+ const reconciliation = reconcileTypes(
3067
+ reconcileInputs,
3068
+ subClassClosure,
3069
+ options.reconcileTypes
3070
+ );
3071
+ const entityByIdentity = /* @__PURE__ */ new Map();
3072
+ for (const entity of canonicalEntities) {
3073
+ entityByIdentity.set(mergeKey(entity.kind, entity.canonicalId), entity);
3074
+ }
3075
+ const canonicalOf = buildCanonicalMap(
3076
+ clusters,
3077
+ (cluster) => pickClusterCanonical(cluster, entityByIdentity)
3078
+ );
3079
+ const stagedEdges = buildStagedEdges(
3080
+ staging,
3081
+ reconciledEdgeModifications.survivingModifications
3082
+ );
3083
+ const stagedEdgeBranches = /* @__PURE__ */ new Map();
3084
+ for (const staged of stagedEdges) {
3085
+ const branches = stagedEdgeBranches.get(staged.id);
3086
+ if (branches === void 0) {
3087
+ stagedEdgeBranches.set(staged.id, /* @__PURE__ */ new Set([staged.branchId]));
3088
+ } else {
3089
+ branches.add(staged.branchId);
3090
+ }
3091
+ }
3092
+ const deletedNodeIdSet = new Set(nodeDeletions.keys());
3093
+ const repoint2 = repointEdges(
3094
+ stagedEdges,
3095
+ canonicalOf,
3096
+ deletedNodeIdSet,
3097
+ options.onPropertyConflict,
3098
+ branchRank,
3099
+ weights,
3100
+ preferredBranchId
3101
+ );
3102
+ for (const merged of repoint2.edges) {
3103
+ for (const sourceId of merged.mergedIds) {
3104
+ for (const branchId of stagedEdgeBranches.get(sourceId) ?? []) {
3105
+ recordProvenance("edge", branchId, merged.id, merged.kind, sourceId);
3106
+ }
3107
+ }
3108
+ }
3109
+ const dropped = [
3110
+ ...deleteModify.dropped,
3111
+ ...edgeDeleteModify.dropped,
3112
+ ...repoint2.dropped,
3113
+ ...reconciliation.dropped
3114
+ ].sort(
3115
+ (left, right) => compareStrings(`${left.kind}|${left.id}`, `${right.kind}|${right.id}`)
3116
+ );
3117
+ return {
3118
+ canonicalEntities,
3119
+ survivingModifications: reconciledModifications.survivingModifications,
3120
+ nodeDeletions,
3121
+ edgeDeletions,
3122
+ mergedEdges: repoint2.edges,
3123
+ inheritedEdgeBaseProps: new Map(
3124
+ reconciledEdgeModifications.survivingModifications.map((modification) => [
3125
+ modification.edge.id,
3126
+ modification.edge.baseProps
3127
+ ])
3128
+ ),
3129
+ retypeMap: reconciliation.retypeMap,
3130
+ // Order report arrays by the composite (kind, id) identity — never a bare id
3131
+ // or a `|`-joined string. A bare id ties two different-kind entities that
3132
+ // share an id, and a `|` separator collides on caller-supplied ids/property
3133
+ // names that contain `|`; either makes the comparator non-total, so the
3134
+ // returned order would depend on stable-sort + insertion order and break the
3135
+ // order-independence the whole subsystem guarantees.
3136
+ resolutions: resolutions.sort(
3137
+ (left, right) => compareMergeKeys(
3138
+ mergeKey(left.kind, left.canonicalId),
3139
+ mergeKey(right.kind, right.canonicalId)
3140
+ )
3141
+ ),
3142
+ propertyConflicts: [...propertyConflicts, ...repoint2.conflicts].sort(
3143
+ (left, right) => {
3144
+ const byEntity = compareMergeKeys(
3145
+ mergeKey(left.kind, left.entityId),
3146
+ mergeKey(right.kind, right.entityId)
3147
+ );
3148
+ return byEntity === 0 ? compareStrings(left.property, right.property) : byEntity;
3149
+ }
3150
+ ),
3151
+ deleteModifyConflicts: [
3152
+ ...deleteModify.conflicts,
3153
+ ...edgeDeleteModify.conflicts
3154
+ ].sort(
3155
+ (left, right) => compareMergeKeys(
3156
+ mergeKey(left.kind, left.entityId),
3157
+ mergeKey(right.kind, right.entityId)
3158
+ )
3159
+ ),
3160
+ typeReconciliations: reconciliation.reconciliations,
3161
+ dropped,
3162
+ baseAmbiguities: baseAmbiguities.sort(
3163
+ (left, right) => compareMergeKeys(
3164
+ mergeKey(left.baseIds[0].kind, left.baseIds[0].id),
3165
+ mergeKey(right.baseIds[0].kind, right.baseIds[0].id)
3166
+ )
3167
+ ),
3168
+ provenanceRecords,
3169
+ warnings: candidateWarnings
3170
+ };
3171
+ }
3172
+ function pickClusterCanonical(cluster, entityByIdentity) {
3173
+ for (const member of cluster.members) {
3174
+ if (entityByIdentity.has(member)) {
3175
+ return member;
3176
+ }
3177
+ }
3178
+ throw new MergeError(
3179
+ "Internal invariant: cluster has no canonical entity in pickClusterCanonical.",
3180
+ { details: { members: cluster.members.map((member) => idOf(member)) } }
3181
+ );
3182
+ }
3183
+ function commitModificationProps(baseProps, forkProps) {
3184
+ const props = { ...forkProps };
3185
+ for (const key of Object.keys(baseProps)) {
3186
+ if (!(key in forkProps)) {
3187
+ props[key] = void 0;
3188
+ }
3189
+ }
3190
+ return props;
3191
+ }
3192
+ async function applyMergePlan(plan, nodesApi, edgesApi) {
3193
+ const committedNodeIds = /* @__PURE__ */ new Set();
3194
+ const canonicalIdentities = /* @__PURE__ */ new Set();
3195
+ for (const entity of plan.canonicalEntities) {
3196
+ canonicalIdentities.add(mergeKey(entity.kind, entity.canonicalId));
3197
+ }
3198
+ const survivingModProps = /* @__PURE__ */ new Map();
3199
+ for (const modification of plan.survivingModifications) {
3200
+ survivingModProps.set(mergeKeyOf(modification.node), {
3201
+ baseProps: modification.node.baseProps,
3202
+ forkProps: modification.node.forkProps
3203
+ });
3204
+ }
3205
+ for (const modification of plan.survivingModifications) {
3206
+ const identity = mergeKeyOf(modification.node);
3207
+ if (plan.nodeDeletions.has(identity) || canonicalIdentities.has(identity)) {
3208
+ continue;
3209
+ }
3210
+ const collection = nodeCollection(nodesApi, modification.node.kind);
3211
+ await collection.upsertByIdFromRecord(
3212
+ modification.node.id,
3213
+ commitModificationProps(
3214
+ modification.node.baseProps,
3215
+ modification.node.forkProps
3216
+ )
3217
+ );
3218
+ committedNodeIds.add(identity);
3219
+ }
3220
+ for (const entity of plan.canonicalEntities) {
3221
+ const identity = mergeKey(entity.kind, entity.canonicalId);
3222
+ if (plan.nodeDeletions.has(identity)) {
3223
+ continue;
3224
+ }
3225
+ const kind = plan.retypeMap.get(identity) ?? entity.kind;
3226
+ const modification = survivingModProps.get(identity);
3227
+ const props = modification === void 0 ? entity.props : {
3228
+ ...entity.props,
3229
+ ...commitModificationProps(
3230
+ modification.baseProps,
3231
+ modification.forkProps
3232
+ )
3233
+ };
3234
+ const collection = nodeCollection(nodesApi, kind);
3235
+ await collection.upsertByIdFromRecord(entity.canonicalId, props);
3236
+ committedNodeIds.add(mergeKey(kind, entity.canonicalId));
3237
+ }
3238
+ for (const [identity, kind] of plan.nodeDeletions) {
3239
+ const collection = nodeCollection(nodesApi, kind);
3240
+ await collection.delete(idOf(identity));
3241
+ }
3242
+ for (const [identity, kind] of plan.edgeDeletions) {
3243
+ await edgeCollection(edgesApi, kind).delete(idOf(identity));
3244
+ }
3245
+ const edgesByKind = /* @__PURE__ */ new Map();
3246
+ for (const edge of plan.mergedEdges) {
3247
+ const fromKind = plan.retypeMap.get(mergeKey(edge.fromKind, edge.fromId));
3248
+ const toKind = plan.retypeMap.get(mergeKey(edge.toKind, edge.toId));
3249
+ const edgeBaseProps = plan.inheritedEdgeBaseProps.get(edge.id);
3250
+ const props = edgeBaseProps === void 0 ? edge.props : commitModificationProps(edgeBaseProps, edge.props);
3251
+ const item = {
3252
+ id: edge.id,
3253
+ from: { kind: fromKind ?? edge.fromKind, id: edge.fromId },
3254
+ to: { kind: toKind ?? edge.toKind, id: edge.toId },
3255
+ props
3256
+ };
3257
+ const existing = edgesByKind.get(edge.kind);
3258
+ if (existing === void 0) {
3259
+ edgesByKind.set(edge.kind, [item]);
3260
+ } else {
3261
+ existing.push(item);
3262
+ }
3263
+ }
3264
+ let committedEdges = 0;
3265
+ for (const [kind, items] of edgesByKind) {
3266
+ await edgeCollection(edgesApi, kind).bulkUpsertById(items);
3267
+ committedEdges += items.length;
3268
+ }
3269
+ return { nodes: committedNodeIds.size, edges: committedEdges };
3270
+ }
3271
+ async function commitPlan(target, plan, expectedBaseVersion) {
3272
+ if (!target.backend.capabilities.transactions) {
3273
+ throw new MergeError(
3274
+ "merge() requires a transaction-capable target backend. The merged plan (canonical upserts, soft-deletes, edge upserts) must commit atomically; a non-transactional fallback would leave a partially-merged graph on a mid-commit failure. (mergeIncremental() enforces the same requirement.)",
3275
+ { details: { capability: "transactions" } }
3276
+ );
3277
+ }
3278
+ return withTxConflictRetry(
3279
+ () => target.transaction(async (tx) => {
3280
+ if (expectedBaseVersion !== void 0) {
3281
+ await assertTargetUnchanged(tx.backend, target, expectedBaseVersion);
3282
+ }
3283
+ return applyMergePlan(
3284
+ plan,
3285
+ tx.nodes,
3286
+ tx.edges
3287
+ );
3288
+ }, MERGE_COMMIT_TX_OPTIONS)
3289
+ );
3290
+ }
3291
+ var MERGE_COMMIT_TX_OPTIONS = {
3292
+ isolationLevel: "serializable"
3293
+ };
3294
+ async function assertTargetUnchanged(txBackend, target, expectedBaseVersion) {
3295
+ const liveContent = await computeContentComponent(
3296
+ txBackend,
3297
+ target.graphId,
3298
+ target.graph
3299
+ );
3300
+ const expectedContent = contentComponentOf(expectedBaseVersion);
3301
+ if (liveContent !== expectedContent) {
3302
+ throw new BaseVersionMismatchError(
3303
+ "The merge target was modified between the base@V check and the commit transaction; the resolved plan no longer describes the live target and was not applied.",
3304
+ {
3305
+ details: {
3306
+ expectedContentFingerprint: expectedContent,
3307
+ liveContentFingerprint: liveContent
3308
+ },
3309
+ suggestion: "Re-run the merge (and re-branch if the divergence is real), or serialize writers against merges on this target."
3310
+ }
3311
+ );
3312
+ }
3313
+ }
3314
+ function nodeCollection(nodes, kind) {
3315
+ const collection = nodes[kind];
3316
+ if (collection === void 0) {
3317
+ throw new MergeError(`No node collection for kind "${kind}".`, {
3318
+ details: { kind }
3319
+ });
3320
+ }
3321
+ return collection;
3322
+ }
3323
+ function edgeCollection(edges, kind) {
3324
+ const collection = edges[kind];
3325
+ if (collection === void 0) {
3326
+ throw new MergeError(`No edge collection for kind "${kind}".`, {
3327
+ details: { kind }
3328
+ });
3329
+ }
3330
+ return collection;
3331
+ }
3332
+ async function validateBaseVersions(target, branches) {
3333
+ const targetVersion = await computeBaseVersion(target);
3334
+ for (const branch2 of branches) {
3335
+ if (branch2.base !== targetVersion) {
3336
+ return err(
3337
+ new BaseVersionMismatchError(
3338
+ `Branch "${branch2.id}" forked from base@V "${branch2.base}", which does not match the merge target's current base@V "${targetVersion}".`,
3339
+ {
3340
+ details: {
3341
+ branchId: branch2.id,
3342
+ branchBase: branch2.base,
3343
+ targetBase: targetVersion
3344
+ }
3345
+ }
3346
+ )
3347
+ );
3348
+ }
3349
+ }
3350
+ return ok(targetVersion);
3351
+ }
3352
+ function tryNormalize(optionsInput) {
3353
+ try {
3354
+ return ok(normalizeMergeOptions(optionsInput));
3355
+ } catch (error) {
3356
+ return err(new MergeError("Invalid merge options.", { cause: error }));
3357
+ }
3358
+ }
3359
+ async function resolveMerge(store, target, branches, options, useBaseSources, incremental, expectedBaseVersion) {
3360
+ let targetBranchSeen = false;
3361
+ for (const branch2 of branches) {
3362
+ if (branch2.id === BASE_PROVENANCE_BRANCH) {
3363
+ return err(
3364
+ new MergeError(
3365
+ `Branch id "${BASE_PROVENANCE_BRANCH}" is reserved for committed-base provenance and cannot be used as a branch id.`,
3366
+ { details: { branchId: branch2.id } }
3367
+ )
3368
+ );
3369
+ }
3370
+ if (branch2.id === COMMITTED_TARGET_BRANCH) {
3371
+ if (incremental?.targetBranchId !== COMMITTED_TARGET_BRANCH || targetBranchSeen) {
3372
+ return err(
3373
+ new MergeError(
3374
+ `Branch id "${COMMITTED_TARGET_BRANCH}" is reserved for the committed incremental target and cannot be used as a branch id.`,
3375
+ { details: { branchId: branch2.id } }
3376
+ )
3377
+ );
3378
+ }
3379
+ targetBranchSeen = true;
3380
+ }
3381
+ }
3382
+ try {
3383
+ const staging = await stageBranches(store, branches);
3384
+ const preferredBranchId = incremental?.targetBranchId;
3385
+ const branchIds = branches.map((branch2) => branch2.id);
3386
+ const branchOrder = preferredBranchId === void 0 ? options.branchOrder ?? [] : [
3387
+ preferredBranchId,
3388
+ ...(options.branchOrder ?? []).filter(
3389
+ (branchId) => branchId !== preferredBranchId
3390
+ )
3391
+ ];
3392
+ const branchRank = buildBranchRank(branchOrder, branchIds);
3393
+ const introspection = store.introspect();
3394
+ const introspectionKinds = /* @__PURE__ */ new Map();
3395
+ for (const kind of introspection.kinds) {
3396
+ introspectionKinds.set(kind.name, kind.unique);
3397
+ }
3398
+ const subClassClosure = buildSubClassClosure(introspection.ontology);
3399
+ const embeddings = options.embedder === void 0 ? void 0 : await precomputeEmbeddings(
3400
+ newNodesByKind(staging),
3401
+ options.resolve,
3402
+ options.embedder
3403
+ );
3404
+ const ctx = {
3405
+ backend: store.backend,
3406
+ ...embeddings === void 0 ? {} : { embeddings }
3407
+ };
3408
+ const candidates = await generateAllCandidates(
3409
+ target,
3410
+ staging,
3411
+ options,
3412
+ introspectionKinds,
3413
+ ctx,
3414
+ useBaseSources,
3415
+ options.embedder
3416
+ );
3417
+ if (isErr(candidates)) {
3418
+ return err(candidates.error);
3419
+ }
3420
+ const plan = planMerge(
3421
+ staging,
3422
+ candidates.data.edges,
3423
+ candidates.data.warnings,
3424
+ candidates.data.baseMembers,
3425
+ options,
3426
+ branchRank,
3427
+ subClassClosure,
3428
+ preferredBranchId
3429
+ );
3430
+ const merged = incremental === void 0 ? await commitPlan(target, plan, expectedBaseVersion) : await commitIncrementalPlan(target, plan, {
3431
+ stagedNewByKind: newNodesByKind(staging),
3432
+ options,
3433
+ introspectionKinds,
3434
+ // The committed (kind, id) keys the base sources matched at PLAN time;
3435
+ // anything NEW the in-tx re-probe surfaces is a window write.
3436
+ plannedBaseMatchKeys: new Set(
3437
+ candidates.data.baseMembers.map((member) => mergeKeyOf(member))
3438
+ )
3439
+ });
3440
+ const provenance = options.provenance ? buildProvenanceIndex(plan.provenanceRecords) : { byBranch: () => ({ nodeIds: [], edgeIds: [] }) };
3441
+ const warnings = [...plan.warnings];
3442
+ let provenancePersisted;
3443
+ if (options.persistProvenance) {
3444
+ try {
3445
+ const provenanceStore = await openProvenanceStore(
3446
+ target.backend,
3447
+ target.graphId
3448
+ );
3449
+ const count = await persistProvenanceRecords(
3450
+ provenanceStore,
3451
+ target.graphId,
3452
+ plan.provenanceRecords
3453
+ );
3454
+ provenancePersisted = {
3455
+ graphId: provenanceGraphId(target.graphId),
3456
+ count
3457
+ };
3458
+ } catch (error) {
3459
+ warnings.push(
3460
+ "provenance persistence failed (graph committed; provenance not persisted): " + (error instanceof Error ? error.message : String(error))
3461
+ );
3462
+ }
3463
+ }
3464
+ const report = {
3465
+ merged,
3466
+ resolutions: plan.resolutions,
3467
+ conflicts: plan.propertyConflicts,
3468
+ deleteModifyConflicts: plan.deleteModifyConflicts,
3469
+ typeReconciliations: plan.typeReconciliations,
3470
+ dropped: plan.dropped,
3471
+ baseAmbiguities: plan.baseAmbiguities,
3472
+ provenance,
3473
+ warnings,
3474
+ ...provenancePersisted === void 0 ? {} : { provenancePersisted }
3475
+ };
3476
+ return ok(report);
3477
+ } catch (error) {
3478
+ if (error instanceof MergeError) {
3479
+ return err(error);
3480
+ }
3481
+ return err(
3482
+ new MergeError(
3483
+ `Merge failed while staging or committing: ${describeCause(error)}`,
3484
+ { cause: error }
3485
+ )
3486
+ );
3487
+ }
3488
+ }
3489
+ async function merge(store, branches, optionsInput = {}) {
3490
+ const normalized = tryNormalize(optionsInput);
3491
+ if (isErr(normalized)) {
3492
+ return err(normalized.error);
3493
+ }
3494
+ const options = normalized.data;
3495
+ const target = options.target ?? store;
3496
+ const precondition = await validateBaseVersions(target, branches);
3497
+ if (isErr(precondition)) {
3498
+ return err(precondition.error);
3499
+ }
3500
+ return resolveMerge(
3501
+ store,
3502
+ target,
3503
+ branches,
3504
+ options,
3505
+ false,
3506
+ void 0,
3507
+ precondition.data
3508
+ );
3509
+ }
3510
+ async function validateForkPointVersions(forkPoint, branches) {
3511
+ const forkVersion = await computeBaseVersion(forkPoint);
3512
+ for (const branch2 of branches) {
3513
+ if (branch2.base !== forkVersion) {
3514
+ return err(
3515
+ new BaseVersionMismatchError(
3516
+ `Branch "${branch2.id}" forked from base@V "${branch2.base}", which does not match the fork-point's base@V "${forkVersion}". mergeIncremental() requires every branch to have forked from the supplied forkPoint.`,
3517
+ {
3518
+ details: {
3519
+ branchId: branch2.id,
3520
+ branchBase: branch2.base,
3521
+ forkPointBase: forkVersion
3522
+ }
3523
+ }
3524
+ )
3525
+ );
3526
+ }
3527
+ }
3528
+ return ok(forkVersion);
3529
+ }
3530
+ function nodeSchemaFor(target, kind) {
3531
+ const registry = target.graph.nodes;
3532
+ return registry[kind]?.type?.schema;
3533
+ }
3534
+ function edgeSchemaFor(target, kind) {
3535
+ const registry = target.graph.edges;
3536
+ return registry[kind]?.type?.schema;
3537
+ }
3538
+ function analyzeRevalidatingWrite(schema, current, planned) {
3539
+ const merged = { ...current, ...planned };
3540
+ if (schema === void 0) {
3541
+ const storageWouldChange = canonicalizeProps(merged) !== canonicalizeProps(current);
3542
+ return {
3543
+ status: "valid",
3544
+ storageWouldChange,
3545
+ schemaWouldChange: storageWouldChange,
3546
+ stripsCurrentProps: false
3547
+ };
3548
+ }
3549
+ const currentParsed = schema.safeParse(current);
3550
+ const mergedParsed = schema.safeParse(merged);
3551
+ if (!currentParsed.success || !mergedParsed.success) {
3552
+ return { status: "invalid" };
3553
+ }
3554
+ const normalizedCurrent = currentParsed.data;
3555
+ const normalizedMerged = mergedParsed.data;
3556
+ return {
3557
+ status: "valid",
3558
+ storageWouldChange: canonicalizeProps(normalizedMerged) !== canonicalizeProps(current),
3559
+ schemaWouldChange: canonicalizeProps(normalizedMerged) !== canonicalizeProps(normalizedCurrent),
3560
+ stripsCurrentProps: canonicalizeProps(normalizedCurrent) !== canonicalizeProps(current)
3561
+ };
3562
+ }
3563
+ function nodeProps2(node) {
3564
+ const props = {};
3565
+ for (const [key, value] of Object.entries(node)) {
3566
+ if (key === "id" || key === "kind" || key === "meta") continue;
3567
+ props[key] = value;
3568
+ }
3569
+ return props;
3570
+ }
3571
+ function edgeProps(edge) {
3572
+ const props = {};
3573
+ for (const [key, value] of Object.entries(edge)) {
3574
+ if (key === "id" || key === "kind" || key === "fromKind" || key === "fromId" || key === "toKind" || key === "toId" || key === "meta") {
3575
+ continue;
3576
+ }
3577
+ props[key] = value;
3578
+ }
3579
+ return props;
3580
+ }
3581
+ var INCLUDE_TOMBSTONES = { temporalMode: "includeTombstones" };
3582
+ function finalEdgeFrom(edge, plan) {
3583
+ return {
3584
+ kind: plan.retypeMap.get(mergeKey(edge.fromKind, edge.fromId)) ?? edge.fromKind,
3585
+ id: edge.fromId
3586
+ };
3587
+ }
3588
+ function finalEdgeTo(edge, plan) {
3589
+ return {
3590
+ kind: plan.retypeMap.get(mergeKey(edge.toKind, edge.toId)) ?? edge.toKind,
3591
+ id: edge.toId
3592
+ };
3593
+ }
3594
+ function assertValidRevalidatingWrite(analysis, message, details) {
3595
+ if (analysis.status === "invalid") {
3596
+ throw new MergeError(message, { details });
3597
+ }
3598
+ }
3599
+ function describeCause(cause) {
3600
+ return cause instanceof Error ? cause.message : String(cause);
3601
+ }
3602
+ function plannedNodeWrites(plan) {
3603
+ const canonicalIdentities = /* @__PURE__ */ new Set();
3604
+ for (const entity of plan.canonicalEntities) {
3605
+ canonicalIdentities.add(mergeKey(entity.kind, entity.canonicalId));
3606
+ }
3607
+ const survivingModProps = /* @__PURE__ */ new Map();
3608
+ for (const modification of plan.survivingModifications) {
3609
+ survivingModProps.set(
3610
+ mergeKeyOf(modification.node),
3611
+ modification.node.forkProps
3612
+ );
3613
+ }
3614
+ const writes = [];
3615
+ for (const modification of plan.survivingModifications) {
3616
+ const identity = mergeKeyOf(modification.node);
3617
+ if (plan.nodeDeletions.has(identity) || canonicalIdentities.has(identity)) {
3618
+ continue;
3619
+ }
3620
+ writes.push({
3621
+ identity,
3622
+ kind: modification.node.kind,
3623
+ id: modification.node.id,
3624
+ props: modification.node.forkProps
3625
+ });
3626
+ }
3627
+ for (const entity of plan.canonicalEntities) {
3628
+ const sourceIdentity = mergeKey(entity.kind, entity.canonicalId);
3629
+ if (plan.nodeDeletions.has(sourceIdentity)) {
3630
+ continue;
3631
+ }
3632
+ const kind = plan.retypeMap.get(sourceIdentity) ?? entity.kind;
3633
+ const forkProps = survivingModProps.get(sourceIdentity);
3634
+ writes.push({
3635
+ identity: mergeKey(kind, entity.canonicalId),
3636
+ kind,
3637
+ id: entity.canonicalId,
3638
+ props: forkProps === void 0 ? entity.props : { ...entity.props, ...forkProps }
3639
+ });
3640
+ }
3641
+ return writes;
3642
+ }
3643
+ function edgeWriteSignature(edge, plan) {
3644
+ const from = finalEdgeFrom(edge, plan);
3645
+ const to = finalEdgeTo(edge, plan);
3646
+ return JSON.stringify([
3647
+ edge.kind,
3648
+ from.kind,
3649
+ from.id,
3650
+ to.kind,
3651
+ to.id,
3652
+ canonicalizeProps(edge.props)
3653
+ ]);
3654
+ }
3655
+ async function validateIncrementalNodeWrites(target, nodesApi, plan) {
3656
+ const writes = plannedNodeWrites(plan);
3657
+ const nodeIdsByKind = /* @__PURE__ */ new Map();
3658
+ for (const write of writes) {
3659
+ const bucket = nodeIdsByKind.get(write.kind) ?? [];
3660
+ bucket.push(write.id);
3661
+ nodeIdsByKind.set(write.kind, bucket);
3662
+ }
3663
+ const currentByIdentity = /* @__PURE__ */ new Map();
3664
+ for (const [kind, ids] of nodeIdsByKind) {
3665
+ const rows = await nodeCollection(nodesApi, kind).getByIds(
3666
+ ids,
3667
+ INCLUDE_TOMBSTONES
3668
+ );
3669
+ for (const [index, id] of ids.entries()) {
3670
+ currentByIdentity.set(mergeKey(kind, id), rows[index]);
3671
+ }
3672
+ }
3673
+ for (const write of writes) {
3674
+ const current = currentByIdentity.get(write.identity);
3675
+ if (current === void 0) {
3676
+ continue;
3677
+ }
3678
+ if (current.meta.deletedAt !== void 0) {
3679
+ throw new MergeError(
3680
+ `mergeIncremental() would resurrect soft-deleted committed node "${write.id}" (kind "${write.kind}").`,
3681
+ { details: { id: write.id, kind: write.kind } }
3682
+ );
3683
+ }
3684
+ const analysis = analyzeRevalidatingWrite(
3685
+ nodeSchemaFor(target, write.kind),
3686
+ nodeProps2(current),
3687
+ write.props
3688
+ );
3689
+ assertValidRevalidatingWrite(
3690
+ analysis,
3691
+ `mergeIncremental() found an existing committed node "${write.id}" (kind "${write.kind}") whose current props do not validate against the active schema.`,
3692
+ { id: write.id, kind: write.kind }
3693
+ );
3694
+ if (analysis.stripsCurrentProps && analysis.schemaWouldChange) {
3695
+ throw new MergeError(
3696
+ `mergeIncremental() would update committed node "${write.id}" (kind "${write.kind}") but the write would strip existing props outside the active schema (lossy base update).`,
3697
+ { details: { id: write.id, kind: write.kind } }
3698
+ );
3699
+ }
3700
+ }
3701
+ }
3702
+ async function validateIncrementalEdgeWrites(target, edgesApi, plan) {
3703
+ const signatureById = /* @__PURE__ */ new Map();
3704
+ for (const edge of plan.mergedEdges) {
3705
+ const signature = edgeWriteSignature(edge, plan);
3706
+ const existing = signatureById.get(edge.id);
3707
+ if (existing !== void 0 && existing !== signature) {
3708
+ throw new MergeError(
3709
+ `mergeIncremental() would overwrite committed edge "${edge.id}" with multiple planned endpoint/prop shapes (edge.id collision).`,
3710
+ { details: { id: edge.id } }
3711
+ );
3712
+ }
3713
+ signatureById.set(edge.id, signature);
3714
+ }
3715
+ const plannedKindById = /* @__PURE__ */ new Map();
3716
+ const idsByKind = /* @__PURE__ */ new Map();
3717
+ for (const edge of plan.mergedEdges) {
3718
+ if (plannedKindById.has(edge.id)) {
3719
+ continue;
3720
+ }
3721
+ plannedKindById.set(edge.id, edge.kind);
3722
+ const bucket = idsByKind.get(edge.kind) ?? [];
3723
+ bucket.push(edge.id);
3724
+ idsByKind.set(edge.kind, bucket);
3725
+ }
3726
+ const existingById = /* @__PURE__ */ new Map();
3727
+ for (const [kind, ids] of idsByKind) {
3728
+ const rows = await edgeCollection(edgesApi, kind).getByIds(
3729
+ ids,
3730
+ INCLUDE_TOMBSTONES
3731
+ );
3732
+ for (const [index, id] of ids.entries()) {
3733
+ const row = rows[index];
3734
+ if (row !== void 0) {
3735
+ existingById.set(id, row);
3736
+ }
3737
+ }
3738
+ }
3739
+ const notFound = [...plannedKindById.keys()].filter((id) => !existingById.has(id)).sort((left, right) => compareStrings(left, right));
3740
+ if (notFound.length > 0) {
3741
+ const crossKindById = /* @__PURE__ */ new Map();
3742
+ for (const kind of Object.keys(target.graph.edges)) {
3743
+ const rows = await edgeCollection(edgesApi, kind).getByIds(
3744
+ notFound,
3745
+ INCLUDE_TOMBSTONES
3746
+ );
3747
+ for (const [index, id] of notFound.entries()) {
3748
+ const row = rows[index];
3749
+ if (row !== void 0 && !crossKindById.has(id)) {
3750
+ crossKindById.set(id, row);
3751
+ }
3752
+ }
3753
+ }
3754
+ for (const id of notFound) {
3755
+ const row = crossKindById.get(id);
3756
+ if (row === void 0) {
3757
+ continue;
3758
+ }
3759
+ if (row.meta.deletedAt !== void 0) {
3760
+ throw new MergeError(
3761
+ `mergeIncremental() would resurrect soft-deleted committed edge "${id}" (kind "${row.kind}").`,
3762
+ { details: { id, committedKind: row.kind } }
3763
+ );
3764
+ }
3765
+ throw new MergeError(
3766
+ `mergeIncremental() would overwrite committed edge "${id}" (kind "${row.kind}") with a different-kind edge "${plannedKindById.get(id)}" of the same id.`,
3767
+ {
3768
+ details: {
3769
+ id,
3770
+ committedKind: row.kind,
3771
+ plannedKind: plannedKindById.get(id)
3772
+ }
3773
+ }
3774
+ );
3775
+ }
3776
+ }
3777
+ for (const edge of plan.mergedEdges) {
3778
+ const current = existingById.get(edge.id);
3779
+ if (current === void 0) {
3780
+ continue;
3781
+ }
3782
+ if (current.meta.deletedAt !== void 0) {
3783
+ throw new MergeError(
3784
+ `mergeIncremental() would resurrect soft-deleted committed edge "${edge.id}" (kind "${current.kind}").`,
3785
+ { details: { id: edge.id, committedKind: current.kind } }
3786
+ );
3787
+ }
3788
+ const from = finalEdgeFrom(edge, plan);
3789
+ const to = finalEdgeTo(edge, plan);
3790
+ if (current.fromId !== from.id || current.fromKind !== from.kind || current.toId !== to.id || current.toKind !== to.kind) {
3791
+ throw new MergeError(
3792
+ `mergeIncremental() would overwrite committed edge "${edge.id}" (kind "${edge.kind}") with different endpoints.`,
3793
+ { details: { id: edge.id, kind: edge.kind } }
3794
+ );
3795
+ }
3796
+ const analysis = analyzeRevalidatingWrite(
3797
+ edgeSchemaFor(target, edge.kind),
3798
+ edgeProps(current),
3799
+ edge.props
3800
+ );
3801
+ assertValidRevalidatingWrite(
3802
+ analysis,
3803
+ `mergeIncremental() found an existing committed edge "${edge.id}" (kind "${edge.kind}") whose current props do not validate against the active schema.`,
3804
+ { id: edge.id, kind: edge.kind }
3805
+ );
3806
+ if (analysis.stripsCurrentProps && analysis.schemaWouldChange) {
3807
+ throw new MergeError(
3808
+ `mergeIncremental() would update committed edge "${edge.id}" (kind "${edge.kind}") but the write would strip existing props outside the active schema.`,
3809
+ { details: { id: edge.id, kind: edge.kind } }
3810
+ );
3811
+ }
3812
+ }
3813
+ }
3814
+ async function collectBaseMatchKeys(lookupStore, guard) {
3815
+ const matched = /* @__PURE__ */ new Set();
3816
+ for (const [kind, stagedNodes] of guard.stagedNewByKind) {
3817
+ const resolveConfig = guard.options.resolve[kind];
3818
+ if (resolveConfig === void 0) {
3819
+ continue;
3820
+ }
3821
+ const collection = lookupStore.nodes[kind];
3822
+ if (collection === void 0 || stagedNodes.length === 0) {
3823
+ continue;
3824
+ }
3825
+ const items = stagedNodes.map((staged) => ({ props: staged.node.props }));
3826
+ for (const constraint of uniqueConstraintsFor(
3827
+ guard.introspectionKinds,
3828
+ kind
3829
+ )) {
3830
+ const matches = await collection.bulkFindByConstraint(
3831
+ constraint.name,
3832
+ items
3833
+ );
3834
+ for (const base of matches) {
3835
+ if (base !== void 0) {
3836
+ matched.add(mergeKeyOf(base));
3837
+ }
3838
+ }
3839
+ }
3840
+ if (resolveConfig.blockIndex !== void 0) {
3841
+ const matchesByItem = await collection.bulkFindByIndex(
3842
+ resolveConfig.blockIndex,
3843
+ items
3844
+ );
3845
+ for (const perItem of matchesByItem) {
3846
+ for (const base of perItem) {
3847
+ matched.add(mergeKeyOf(base));
3848
+ }
3849
+ }
3850
+ }
3851
+ }
3852
+ return matched;
3853
+ }
3854
+ async function assertBaseResolutionStable(lookupStore, guard) {
3855
+ const liveKeys = await collectBaseMatchKeys(lookupStore, guard);
3856
+ const appeared = [...liveKeys].filter((key) => !guard.plannedBaseMatchKeys.has(key)).sort((left, right) => compareMergeKeys(left, right));
3857
+ if (appeared.length === 0) {
3858
+ return;
3859
+ }
3860
+ throw new BaseVersionMismatchError(
3861
+ `mergeIncremental() resolved its branch additions against the target as of planning, but ${appeared.length} committed row(s) matching a branch addition's identity key (a unique constraint or block index) were inserted before the commit transaction. Committing the plan would create duplicate entities the base-source resolution would otherwise have collapsed.`,
3862
+ {
3863
+ details: { appeared },
3864
+ suggestion: "Re-run mergeIncremental(); the re-planned merge resolves the branch additions against the now-committed rows."
3865
+ }
3866
+ );
3867
+ }
3868
+ async function commitIncrementalPlan(target, plan, guard) {
3869
+ if (!target.backend.capabilities.transactions) {
3870
+ throw new MergeError(
3871
+ "mergeIncremental() requires a transaction-capable target backend. Incremental writes must preflight existing rows and commit atomically; non-transactional fallback would allow partial graph writes.",
3872
+ { details: { capability: "transactions" } }
3873
+ );
3874
+ }
3875
+ return withTxConflictRetry(
3876
+ () => target.transaction(async (tx) => {
3877
+ const nodesApi = tx.nodes;
3878
+ const edgesApi = tx.edges;
3879
+ await assertBaseResolutionStable(tx, guard);
3880
+ await validateIncrementalNodeWrites(target, nodesApi, plan);
3881
+ await validateIncrementalEdgeWrites(target, edgesApi, plan);
3882
+ return applyMergePlan(plan, nodesApi, edgesApi);
3883
+ }, MERGE_COMMIT_TX_OPTIONS)
3884
+ );
3885
+ }
3886
+ async function mergeIncremental(args) {
3887
+ const { forkPoint, target, branches } = args;
3888
+ const normalized = tryNormalize(args.options ?? {});
3889
+ if (isErr(normalized)) {
3890
+ return err(normalized.error);
3891
+ }
3892
+ const options = normalized.data;
3893
+ if (options.onBasePropertyConflict !== "flag") {
3894
+ return err(
3895
+ new MergeError(
3896
+ 'mergeIncremental() requires onBasePropertyConflict: "flag" (keep-base); a non-keep-base policy could overwrite a newer committed base value with a stale branch value.',
3897
+ { details: {} }
3898
+ )
3899
+ );
3900
+ }
3901
+ const forkPrecondition = await validateForkPointVersions(forkPoint, branches);
3902
+ if (isErr(forkPrecondition)) {
3903
+ return err(forkPrecondition.error);
3904
+ }
3905
+ const forkVersion = forkPrecondition.data;
3906
+ const [forkSchema, targetSchema] = await Promise.all([
3907
+ computeSchemaComponent(forkPoint),
3908
+ computeSchemaComponent(target)
3909
+ ]);
3910
+ if (forkSchema !== targetSchema) {
3911
+ return err(
3912
+ new MergeError(
3913
+ "mergeIncremental() requires target and forkPoint to share a schema; schema drift is not supported (the target content may differ \u2014 only the schema must match).",
3914
+ { details: {} }
3915
+ )
3916
+ );
3917
+ }
3918
+ const targetBranch = {
3919
+ id: COMMITTED_TARGET_BRANCH,
3920
+ base: forkVersion,
3921
+ store: target
3922
+ };
3923
+ return resolveMerge(
3924
+ forkPoint,
3925
+ target,
3926
+ [targetBranch, ...branches],
3927
+ options,
3928
+ true,
3929
+ { targetBranchId: COMMITTED_TARGET_BRANCH }
3930
+ );
3931
+ }
3932
+
3933
+ export { BaseVersionMismatchError, BranchError, MERGE_ERROR_CODES, MERGE_OPTION_DEFAULTS, MergeConflictError, MergeError, SimilarityUnavailableError, asBaseVersion, asBranchId, branch, cloneWorkingCopyStrategy, computeBaseVersion, merge, mergeIncremental, normalizeMergeOptions, openProvenanceStore, persistProvenanceRecords, provenanceGraphId, readProvenance };
3934
+ //# sourceMappingURL=index.js.map
3935
+ //# sourceMappingURL=index.js.map