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