@nicia-ai/typegraph 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-3JLRHQ67.js → chunk-2BVHPOIG.js} +7 -6
- package/dist/chunk-2BVHPOIG.js.map +1 -0
- package/dist/chunk-2LAT5BIK.cjs +16277 -0
- package/dist/chunk-2LAT5BIK.cjs.map +1 -0
- package/dist/chunk-2YMGNYXY.js +40 -0
- package/dist/chunk-2YMGNYXY.js.map +1 -0
- package/dist/chunk-3EUKJEQ3.js +77 -0
- package/dist/chunk-3EUKJEQ3.js.map +1 -0
- package/dist/{chunk-WFCECD6Q.cjs → chunk-3HQDMOBK.cjs} +73 -68
- package/dist/chunk-3HQDMOBK.cjs.map +1 -0
- package/dist/chunk-3QRBXYT5.cjs +94 -0
- package/dist/chunk-3QRBXYT5.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-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-IUIJJYPI.js → chunk-7WW3WL6E.js} +6 -5
- package/dist/chunk-7WW3WL6E.js.map +1 -0
- package/dist/chunk-A3W7QG7K.js +45 -0
- package/dist/chunk-A3W7QG7K.js.map +1 -0
- package/dist/chunk-AW4A3P5A.cjs +51 -0
- package/dist/chunk-AW4A3P5A.cjs.map +1 -0
- package/dist/{chunk-AI3RNREM.js → chunk-C4C4Q7A6.js} +94 -496
- package/dist/chunk-C4C4Q7A6.js.map +1 -0
- package/dist/{chunk-4LBO6KIF.cjs → chunk-CAMMC3E4.cjs} +60 -59
- package/dist/chunk-CAMMC3E4.cjs.map +1 -0
- package/dist/{chunk-3IRMLJEK.js → chunk-CEF2QOTO.js} +3 -3
- package/dist/{chunk-3IRMLJEK.js.map → chunk-CEF2QOTO.js.map} +1 -1
- package/dist/{chunk-UESTHN47.cjs → chunk-CFNT3ITG.cjs} +95 -507
- package/dist/chunk-CFNT3ITG.cjs.map +1 -0
- package/dist/chunk-CWFPJ3RC.js +282 -0
- package/dist/chunk-CWFPJ3RC.js.map +1 -0
- package/dist/{chunk-7JGLPAFV.cjs → chunk-EEYV6UOE.cjs} +16 -16
- package/dist/chunk-EEYV6UOE.cjs.map +1 -0
- package/dist/{chunk-FWBVPATK.cjs → chunk-FDN43KXV.cjs} +19 -19
- package/dist/{chunk-FWBVPATK.cjs.map → chunk-FDN43KXV.cjs.map} +1 -1
- package/dist/chunk-HFBRAOF7.js +610 -0
- package/dist/chunk-HFBRAOF7.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-WEMZM7J7.cjs → chunk-JKRKIEVA.cjs} +5 -41
- package/dist/chunk-JKRKIEVA.cjs.map +1 -0
- package/dist/{chunk-SGPXKJ4A.cjs → chunk-KTIEB7VH.cjs} +26 -16
- package/dist/chunk-KTIEB7VH.cjs.map +1 -0
- package/dist/{chunk-K4N35E7B.cjs → chunk-LAZGHOZW.cjs} +72 -71
- package/dist/chunk-LAZGHOZW.cjs.map +1 -0
- package/dist/{chunk-VFX3LC76.cjs → chunk-LM2IC4ZJ.cjs} +14 -14
- package/dist/{chunk-VFX3LC76.cjs.map → chunk-LM2IC4ZJ.cjs.map} +1 -1
- package/dist/{chunk-K5VREXMZ.js → chunk-NDM5PXEH.js} +7 -6
- package/dist/chunk-NDM5PXEH.js.map +1 -0
- package/dist/chunk-PIHXFF73.js +90 -0
- package/dist/chunk-PIHXFF73.js.map +1 -0
- package/dist/{chunk-FVIGJTS7.js → chunk-PRMKPFN5.js} +4 -74
- package/dist/chunk-PRMKPFN5.js.map +1 -0
- package/dist/chunk-SAJSMR2B.js +35 -0
- package/dist/chunk-SAJSMR2B.js.map +1 -0
- package/dist/chunk-SB65GJNY.cjs +624 -0
- package/dist/chunk-SB65GJNY.cjs.map +1 -0
- package/dist/{chunk-N2OEOGBB.js → chunk-TUA3MEJJ.js} +13 -8
- package/dist/chunk-TUA3MEJJ.js.map +1 -0
- package/dist/{chunk-MAQJYNJR.js → chunk-W4ZX2GCP.js} +14 -16
- package/dist/chunk-W4ZX2GCP.js.map +1 -0
- package/dist/chunk-WNORITK6.js +16231 -0
- package/dist/chunk-WNORITK6.js.map +1 -0
- package/dist/chunk-WWVV44DY.cjs +287 -0
- package/dist/chunk-WWVV44DY.cjs.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-2QA5EHEN.js → chunk-XKNAK27R.js} +3 -3
- package/dist/{chunk-2QA5EHEN.js.map → chunk-XKNAK27R.js.map} +1 -1
- package/dist/{chunk-633N7DSE.js → chunk-XQFAJVYW.js} +4 -36
- package/dist/chunk-XQFAJVYW.js.map +1 -0
- package/dist/{chunk-ENDUY2CD.js → chunk-YCV72E6L.js} +23 -17
- package/dist/chunk-YCV72E6L.js.map +1 -0
- package/dist/{chunk-26HWIIVS.cjs → chunk-YL4F3CMD.cjs} +63 -62
- package/dist/chunk-YL4F3CMD.cjs.map +1 -0
- package/dist/{chunk-IE6GN7L3.cjs → chunk-YW7GHMF4.cjs} +17 -98
- package/dist/chunk-YW7GHMF4.cjs.map +1 -0
- package/dist/{ddl-EdkIiFVU.d.ts → ddl-C8e8zDAE.d.ts} +3 -3
- package/dist/{ddl-BSFlbGQF.d.cts → ddl-CwyG4HoQ.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 +3966 -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 +3935 -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 +291 -15919
- 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 +24 -15791
- 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 +9 -9
- package/dist/interchange/index.d.ts +9 -9
- package/dist/interchange/index.js +2 -588
- package/dist/interchange/index.js.map +1 -1
- package/dist/{manager-BbdyNhkv.d.ts → manager-8htmuGr-.d.ts} +3 -3
- package/dist/{manager-wTDgjoXG.d.cts → manager-aVgxbGt-.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-Dhom7iiX.d.ts} +1 -1
- package/dist/{postgres-ymXYC6Rr.d.ts → postgres-jgXmD45C.d.cts} +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 +30 -29
- 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 +7 -6
- package/dist/schema/index.js.map +1 -1
- package/dist/{sqlite-R8YGOqVQ.d.ts → sqlite-BfLsCEqO.d.ts} +1 -1
- package/dist/{sqlite-DxH1lrwD.d.cts → sqlite-DcQ_BCUW.d.cts} +1 -1
- package/dist/{store-CNKM334w.d.ts → store-view-lTcb_cnp.d.cts} +694 -366
- package/dist/{store--z1Tmk7U.d.cts → store-view-pBOUrQte.d.ts} +694 -366
- package/dist/{types-D5B6Be5c.d.cts → types-5RQwzWXx.d.cts} +2 -2
- package/dist/{types-CMvxx2kd.d.ts → types-DGDiXAYc.d.ts} +2 -2
- package/dist/{types-BBiDqJCU.d.ts → types-Dk8r09KY.d.ts} +16 -2
- package/dist/{types-dL-clxO3.d.cts → types-Dvmn-eal.d.cts} +16 -2
- package/dist/{where-CAo6wsBZ.d.ts → where-CHZFELX0.d.ts} +2 -2
- package/dist/{where-bJjuOCeV.d.cts → where-Cx_glEEB.d.cts} +2 -2
- package/package.json +7 -2
- 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-7JGLPAFV.cjs.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-MAQJYNJR.js.map +0 -1
- 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,1191 @@
|
|
|
1
|
+
import { G as GraphDef, t as NodeKinds, u as GetNodeType } from '../manager-aVgxbGt-.cjs';
|
|
2
|
+
import { S as Store, N as Node, U as UniqueIntrospection } from '../store-view-lTcb_cnp.cjs';
|
|
3
|
+
import { d as NodeId, N as NodeType, e as EdgeId, J as JsonValue, f as KindAnnotations, g as EdgeType, D as DeleteBehavior, T as TemporalMode, h as GraphExtensionVersion, i as ExtensionNodeDef, j as ExtensionEdgeDef, k as ExtensionOntologyRelation, l as ExtensionIndex } from '../extension-types-D1k0inWU.cjs';
|
|
4
|
+
import { T as TypeGraphError } from '../index-C8sSTErx.cjs';
|
|
5
|
+
import { R as Result } from '../result-DWo6mUVz.cjs';
|
|
6
|
+
export { i as isErr, a as isOk, u as unwrap } from '../result-DWo6mUVz.cjs';
|
|
7
|
+
import { G as GraphBackend, M as MetaEdge, I as IndexDeclaration } from '../types-Dvmn-eal.cjs';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import 'drizzle-orm';
|
|
10
|
+
import '../materialize-indexes-DEYkU-wr.cjs';
|
|
11
|
+
import 'drizzle-orm/pg-core';
|
|
12
|
+
import 'drizzle-orm/sqlite-core';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Core type model for the graph-merge primitive.
|
|
16
|
+
*
|
|
17
|
+
* Finalized against TypeGraph's real generic machinery: the design's illustrative
|
|
18
|
+
* `Node<G, K>` / `NodeId<NodeKinds<G>>` collapse onto TypeGraph's public
|
|
19
|
+
* `Node` / `Edge` / `NodeId<NodeType>` / `EdgeId` (which are themselves branded
|
|
20
|
+
* over `NodeType` / `EdgeType`). The merge surface is parameterized over
|
|
21
|
+
* `G extends GraphDef` so `Store<G>` and `GraphBranch<G>` thread the caller's
|
|
22
|
+
* concrete graph definition end-to-end.
|
|
23
|
+
*
|
|
24
|
+
* This module is pure type declarations plus the two branding helpers — no
|
|
25
|
+
* runtime merge logic.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Opaque identifier for a branch (working copy) of a base store. Branded so a
|
|
30
|
+
* raw string cannot be passed where a deliberately-minted branch id is required.
|
|
31
|
+
*/
|
|
32
|
+
type BranchId = string & Readonly<{
|
|
33
|
+
readonly __brand: "BranchId";
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Opaque token identifying the immutable `base@V` a branch was forked from.
|
|
37
|
+
* Combines a schema hash with a content fingerprint (computed in T3). Branded so
|
|
38
|
+
* it cannot be confused with an arbitrary string.
|
|
39
|
+
*/
|
|
40
|
+
type BaseVersion = string & Readonly<{
|
|
41
|
+
readonly __brand: "BaseVersion";
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Mints a {@link BranchId} from a raw string. Centralizes the brand cast so the
|
|
45
|
+
* unsafe assertion lives in exactly one place.
|
|
46
|
+
*/
|
|
47
|
+
declare function asBranchId(value: string): BranchId;
|
|
48
|
+
/**
|
|
49
|
+
* Mints a {@link BaseVersion} from a raw string. Centralizes the brand cast so
|
|
50
|
+
* the unsafe assertion lives in exactly one place.
|
|
51
|
+
*/
|
|
52
|
+
declare function asBaseVersion(value: string): BaseVersion;
|
|
53
|
+
/**
|
|
54
|
+
* A working-copy handle for one branch: its id, the `base@V` it forked from, and
|
|
55
|
+
* a {@link Store} over the branch's own backend.
|
|
56
|
+
*/
|
|
57
|
+
type GraphBranch<G extends GraphDef> = Readonly<{
|
|
58
|
+
id: BranchId;
|
|
59
|
+
base: BaseVersion;
|
|
60
|
+
store: Store<G>;
|
|
61
|
+
}>;
|
|
62
|
+
/**
|
|
63
|
+
* Options for {@link GraphBranch} creation. `id` is optional — when omitted a
|
|
64
|
+
* fresh id is generated.
|
|
65
|
+
*/
|
|
66
|
+
type BranchOptions = Readonly<{
|
|
67
|
+
id?: BranchId;
|
|
68
|
+
}>;
|
|
69
|
+
/**
|
|
70
|
+
* Turns text into an embedding vector. Injected via {@link MergeOptions.embedder}
|
|
71
|
+
* and used by the `vector` / `hybrid` similarity strategies to score candidate
|
|
72
|
+
* pairs by cosine IN MEMORY — the staged candidate nodes are unindexed in the
|
|
73
|
+
* working copy, so a backend ANN index cannot score them pairwise (see
|
|
74
|
+
* `scorePair`). Exact in-memory cosine over real model vectors is both the right
|
|
75
|
+
* scale for bounded candidate dedup and deterministic, which the merge contract
|
|
76
|
+
* requires.
|
|
77
|
+
*
|
|
78
|
+
* Batched and async: given N texts it returns N vectors in the SAME order, each a
|
|
79
|
+
* fixed-dimension `Float32Array` (every vector a given embedder returns shares one
|
|
80
|
+
* length — the model's embedding dimension). The function MUST be deterministic —
|
|
81
|
+
* the same text always yields the same vector — because the whole merge is
|
|
82
|
+
* order-independent and reproducible. Vectors need NOT be pre-normalized; cosine
|
|
83
|
+
* scoring normalizes internally.
|
|
84
|
+
*
|
|
85
|
+
* The concrete local model lives in the CONSUMER (the harness ships an
|
|
86
|
+
* all-MiniLM-L6-v2 embedder); this package depends only on the function shape, so
|
|
87
|
+
* it stays model-agnostic and lean inside the TypeGraph core package.
|
|
88
|
+
*/
|
|
89
|
+
type Embedder = (texts: readonly string[]) => Promise<readonly Float32Array[]>;
|
|
90
|
+
/**
|
|
91
|
+
* Pluggable per-kind similarity strategy (design §8).
|
|
92
|
+
*
|
|
93
|
+
* - `vector` / `hybrid` score candidate pairs by cosine over an injected
|
|
94
|
+
* {@link Embedder} ({@link MergeOptions.embedder}), computed in memory; they
|
|
95
|
+
* fail with `SimilarityUnavailableError` when no embedder is configured.
|
|
96
|
+
* - `fulltext` and `custom` run with ZERO embeddings — the cross-DB-safe
|
|
97
|
+
* default. `fulltext` uses an in-memory Sørensen–Dice trigram scorer (T6).
|
|
98
|
+
*
|
|
99
|
+
* The generic mirrors the design's `SimilarityStrategy<G, K>`: `K` constrains the
|
|
100
|
+
* `custom` score function's node arguments to the resolved kind.
|
|
101
|
+
*/
|
|
102
|
+
type SimilarityStrategy<G extends GraphDef = GraphDef, K extends NodeType = NodeType> = Readonly<{
|
|
103
|
+
kind: "hybrid";
|
|
104
|
+
fields: readonly string[];
|
|
105
|
+
weights?: Readonly<{
|
|
106
|
+
vector?: number;
|
|
107
|
+
fulltext?: number;
|
|
108
|
+
}>;
|
|
109
|
+
readonly __graph?: G;
|
|
110
|
+
}> | Readonly<{
|
|
111
|
+
kind: "vector";
|
|
112
|
+
field: string;
|
|
113
|
+
readonly __graph?: G;
|
|
114
|
+
}> | Readonly<{
|
|
115
|
+
kind: "fulltext";
|
|
116
|
+
fields: readonly string[];
|
|
117
|
+
readonly __graph?: G;
|
|
118
|
+
}> | Readonly<{
|
|
119
|
+
kind: "custom";
|
|
120
|
+
score: (a: Node<K>, b: Node<K>) => number;
|
|
121
|
+
readonly __graph?: G;
|
|
122
|
+
}>;
|
|
123
|
+
/**
|
|
124
|
+
* Per-kind entity-resolution configuration.
|
|
125
|
+
*/
|
|
126
|
+
type ResolveConfig<G extends GraphDef = GraphDef, K extends NodeType = NodeType> = Readonly<{
|
|
127
|
+
/**
|
|
128
|
+
* Cheap exact-equality blocking key, evaluated before similarity to bound the
|
|
129
|
+
* O(n²) candidate comparisons. Returning `undefined` places the node in the
|
|
130
|
+
* shared `"unblocked"` bucket (compared all-vs-all within its kind).
|
|
131
|
+
*
|
|
132
|
+
* STAGED-vs-staged only: it is an arbitrary JS function, so it cannot be queried
|
|
133
|
+
* against the committed base. To recall committed entities by a block key
|
|
134
|
+
* (new-vs-base, the `baseKey` source §6.2), declare the key as a TypeGraph node
|
|
135
|
+
* index and name it via {@link ResolveConfig.blockIndex} instead.
|
|
136
|
+
*/
|
|
137
|
+
block?: (node: Node<K>) => string | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* Name of a declared TypeGraph node index (`defineNodeIndex`) whose key is this
|
|
140
|
+
* kind's NEW-vs-BASE block key (design §6.2). When set and the new-vs-base scope is
|
|
141
|
+
* driven, the `baseKey` source issues an indexed `bulkFindByIndex` lookup of
|
|
142
|
+
* committed nodes sharing each staged node's index key and proposes them as scored
|
|
143
|
+
* candidate pairs (a shared block key is a candidate, not a definitional match).
|
|
144
|
+
*
|
|
145
|
+
* The index keys on real fields (a field-set + scope + optional partial-`where`), so
|
|
146
|
+
* only a FIELD-SET block key migrates here; a transform key (`slice`/`soundex`/…)
|
|
147
|
+
* stays on the staged-only {@link block}. Unused on the public snapshot `merge()`
|
|
148
|
+
* path. An undeclared name surfaces a typed error at lookup time.
|
|
149
|
+
*/
|
|
150
|
+
blockIndex?: string;
|
|
151
|
+
/**
|
|
152
|
+
* Bounded coarse candidate generation for the NO-KEY case (design §6.2, the
|
|
153
|
+
* `keyless` source). A node whose {@link block} returns `undefined` (and has no
|
|
154
|
+
* unique signature) lands in the shared `"unblocked"` bucket, which is otherwise
|
|
155
|
+
* compared ALL-vs-all — an O(n²) cliff that `maxComparisonsPerKind` then truncates
|
|
156
|
+
* to id-only. Set `keyless` to bound that bucket by single-pass SORTED-NEIGHBOURHOOD
|
|
157
|
+
* instead: the unblocked nodes are sorted by their similarity-field text (tie-broken
|
|
158
|
+
* by id) and each is proposed only against its next `window` neighbours — O(n·window),
|
|
159
|
+
* deterministic. Unset preserves today's all-vs-all behaviour. Only the `"unblocked"`
|
|
160
|
+
* bucket is affected; keyed `block()` buckets are unchanged.
|
|
161
|
+
*/
|
|
162
|
+
keyless?: Readonly<{
|
|
163
|
+
/** Forward-neighbour window: each unblocked node is paired with its next `window`
|
|
164
|
+
* neighbours in the sort. Must be a positive integer. Larger → more recall, more
|
|
165
|
+
* comparisons (→ all-vs-all as `window` ≥ bucket size). */
|
|
166
|
+
window: number;
|
|
167
|
+
}>;
|
|
168
|
+
/** Similarity strategy (design §8). */
|
|
169
|
+
similarity: SimilarityStrategy<G, K>;
|
|
170
|
+
/** Candidate-merge threshold in `[0, 1]`. */
|
|
171
|
+
threshold: number;
|
|
172
|
+
}>;
|
|
173
|
+
/**
|
|
174
|
+
* A resolved cluster of node ids that the merge collapses into one canonical
|
|
175
|
+
* survivor.
|
|
176
|
+
*/
|
|
177
|
+
type ResolvedCluster = Readonly<{
|
|
178
|
+
members: readonly NodeId<NodeType>[];
|
|
179
|
+
}>;
|
|
180
|
+
/**
|
|
181
|
+
* Policy for resolving conflicting property values across cluster members.
|
|
182
|
+
*
|
|
183
|
+
* - `"flag"` keeps the canonical's value and records a {@link PropertyConflict}
|
|
184
|
+
* without auto-resolving.
|
|
185
|
+
* - `"lastWriteWins"` picks by the stable branch/logical total order — NEVER
|
|
186
|
+
* wall-clock arrival.
|
|
187
|
+
* - `"provenanceWeighted"` picks by per-branch trust weight.
|
|
188
|
+
* - A function delegates the decision, returning the surviving {@link JsonValue}.
|
|
189
|
+
*/
|
|
190
|
+
type PropertyConflictPolicy<G extends GraphDef = GraphDef> = "flag" | "lastWriteWins" | "provenanceWeighted" | ((conflict: PropertyConflict<G>) => JsonValue);
|
|
191
|
+
/**
|
|
192
|
+
* Policy for resolving an inherited node that is deleted by one branch and
|
|
193
|
+
* modified by another (design §6.2).
|
|
194
|
+
*
|
|
195
|
+
* - `"deleteWins"` — the node is finally DELETED; the modification is discarded.
|
|
196
|
+
* - `"modifyWins"` — the node is RESURRECTED; the modification survives.
|
|
197
|
+
* - `"flag"` (default) — the **modification SURVIVES in the merged output** (as
|
|
198
|
+
* with `"modifyWins"`) AND an **unresolved {@link DeleteModifyConflict} is
|
|
199
|
+
* recorded** in `report.deleteModifyConflicts` for human review. `"flag"` is
|
|
200
|
+
* therefore NOT neutral — it keeps data and surfaces the disagreement, on the
|
|
201
|
+
* posture that a merge must never silently destroy the only branch still
|
|
202
|
+
* carrying data. Choose `"deleteWins"` to honor the delete by default instead.
|
|
203
|
+
*/
|
|
204
|
+
type DeleteModifyPolicy = "deleteWins" | "modifyWins" | "flag";
|
|
205
|
+
/**
|
|
206
|
+
* Behavior when `maxComparisonsPerKind` is exceeded for a kind.
|
|
207
|
+
*
|
|
208
|
+
* - `"error"` fails the merge with a typed error (default).
|
|
209
|
+
* - `"mergeByIdOnly"` skips similarity for that kind, emits no candidate edges,
|
|
210
|
+
* and records a report warning.
|
|
211
|
+
*/
|
|
212
|
+
type ComparisonCeilingPolicy = "error" | "mergeByIdOnly";
|
|
213
|
+
/**
|
|
214
|
+
* Ontology type-reconciliation mode. `"off"` is a no-op (default); `"ontology"`
|
|
215
|
+
* collapses compatible types to the most-specific via the public subClassOf
|
|
216
|
+
* closure (T2a / T10).
|
|
217
|
+
*/
|
|
218
|
+
type ReconcileTypesMode = "ontology" | "off";
|
|
219
|
+
/**
|
|
220
|
+
* Map of node kind name → its {@link ResolveConfig}. Keys are constrained to the
|
|
221
|
+
* graph's node kinds (`keyof G["nodes"]`), so a typo or a kind that does not
|
|
222
|
+
* belong to the graph is a COMPILE error rather than a silently-ignored config
|
|
223
|
+
* (which would let those nodes merge by id only without warning). Each kind's
|
|
224
|
+
* config is bound to that kind's concrete `NodeType`, so `block(node)` and the
|
|
225
|
+
* `custom` scorer see the right node shape. All keys are optional: kinds omitted
|
|
226
|
+
* from this map merge by ID only (new fork nodes added as-is, no fuzzy
|
|
227
|
+
* resolution). For the default unparameterized `GraphDef` the keys widen back to
|
|
228
|
+
* `string`.
|
|
229
|
+
*/
|
|
230
|
+
type ResolveMap<G extends GraphDef = GraphDef> = Readonly<Partial<{
|
|
231
|
+
[K in NodeKinds<G>]: ResolveConfig<G, GetNodeType<G, K>>;
|
|
232
|
+
}>>;
|
|
233
|
+
/**
|
|
234
|
+
* Caller-facing options for {@link merge}. All fields are optional with frozen
|
|
235
|
+
* defaults applied by `normalizeMergeOptions` (see `options.ts`).
|
|
236
|
+
*/
|
|
237
|
+
type MergeOptions<G extends GraphDef = GraphDef> = Readonly<{
|
|
238
|
+
/** Per-kind entity resolution. Omitted kinds merge by ID only. */
|
|
239
|
+
resolve?: ResolveMap<G>;
|
|
240
|
+
/** Reconcile differing types across forks. Default `"off"`. */
|
|
241
|
+
reconcileTypes?: ReconcileTypesMode;
|
|
242
|
+
/** Property-conflict policy for staged-vs-staged disagreements. Default `"flag"`. */
|
|
243
|
+
onPropertyConflict?: PropertyConflictPolicy<G>;
|
|
244
|
+
/**
|
|
245
|
+
* Property-conflict policy for BASE↔branch disagreements in a new-vs-base merge
|
|
246
|
+
* (§6.4-C). DISTINCT from {@link onPropertyConflict} and DOES NOT inherit it:
|
|
247
|
+
* `onPropertyConflict` can be `"lastWriteWins"` / `"provenanceWeighted"` / a
|
|
248
|
+
* function, any of which would let a fuzzy branch match silently OVERWRITE
|
|
249
|
+
* committed data. Default `"flag"` keeps the committed base value (and records the
|
|
250
|
+
* conflict). Mirrors how `onDeleteModifyConflict` is kept separate. Only consulted
|
|
251
|
+
* when a cluster contains a base member; the staged path never uses it.
|
|
252
|
+
*/
|
|
253
|
+
onBasePropertyConflict?: PropertyConflictPolicy<G>;
|
|
254
|
+
/** Delete/modify-conflict policy. Default `"flag"`. */
|
|
255
|
+
onDeleteModifyConflict?: DeleteModifyPolicy;
|
|
256
|
+
/** Comparison-ceiling behavior. Default `"error"`. */
|
|
257
|
+
onComparisonCeiling?: ComparisonCeilingPolicy;
|
|
258
|
+
/**
|
|
259
|
+
* Deterministic survivor selection within a cluster. Default: the member with
|
|
260
|
+
* the lexicographically-minimal node id.
|
|
261
|
+
*/
|
|
262
|
+
canonical?: (cluster: ResolvedCluster) => NodeId<NodeType>;
|
|
263
|
+
/** Populate the report-only provenance index. Default `true`. */
|
|
264
|
+
provenance?: boolean;
|
|
265
|
+
/**
|
|
266
|
+
* Persist provenance ON-GRAPH: after the commit, upsert one `{branch, sourceId}`
|
|
267
|
+
* row per contribution into a sidecar provenance graph on the target's backend
|
|
268
|
+
* (queryable via `openProvenanceStore` / `readProvenance`). Default `false`
|
|
269
|
+
* (report-only). Best-effort and post-commit — a persistence failure surfaces as
|
|
270
|
+
* a {@link MergeReport.warnings} entry, never a failed merge.
|
|
271
|
+
*/
|
|
272
|
+
persistProvenance?: boolean;
|
|
273
|
+
/**
|
|
274
|
+
* Local embedder for `vector` / `hybrid` similarity (in-memory cosine over the
|
|
275
|
+
* staged candidate pairs). Required ONLY when a kind's resolve strategy is
|
|
276
|
+
* `vector` or `hybrid`; `fulltext` / `custom` ignore it. A vector/hybrid
|
|
277
|
+
* strategy with no embedder configured fails with a typed
|
|
278
|
+
* {@link import("./errors").SimilarityUnavailableError}.
|
|
279
|
+
*/
|
|
280
|
+
embedder?: Embedder;
|
|
281
|
+
/** Merge receiver. Default: the base `store`, written transactionally. */
|
|
282
|
+
target?: Store<G>;
|
|
283
|
+
/** Safety ceiling on candidate comparisons per kind. Default: unbounded. */
|
|
284
|
+
maxComparisonsPerKind?: number;
|
|
285
|
+
/**
|
|
286
|
+
* Optional single-link diameter guard. When set, clusters whose pairwise
|
|
287
|
+
* distance exceeds it are split by the deterministic drop-weakest rule (T8).
|
|
288
|
+
*/
|
|
289
|
+
clusterMaxDiameter?: number;
|
|
290
|
+
/**
|
|
291
|
+
* Explicit stable branch order used by `lastWriteWins` / tie-breaking. When
|
|
292
|
+
* omitted, branch ids sorted lexicographically are used. NEVER wall-clock.
|
|
293
|
+
*/
|
|
294
|
+
branchOrder?: readonly BranchId[];
|
|
295
|
+
/**
|
|
296
|
+
* Per-branch trust weights consulted ONLY by the `"provenanceWeighted"`
|
|
297
|
+
* property-conflict policy ({@link onPropertyConflict} /
|
|
298
|
+
* {@link onBasePropertyConflict}): when branches disagree on a property value,
|
|
299
|
+
* the value contributed by the highest-weight branch wins. Ties fall back to
|
|
300
|
+
* {@link branchOrder} (then canonical value order); branches absent from the map
|
|
301
|
+
* default to weight `0`. Ignored by every other policy. Keyed by
|
|
302
|
+
* {@link BranchId}, like {@link branchOrder}. Without this map,
|
|
303
|
+
* `"provenanceWeighted"` has no weights to consult and degrades to
|
|
304
|
+
* `"lastWriteWins"` semantics.
|
|
305
|
+
*/
|
|
306
|
+
provenanceWeights?: ReadonlyMap<BranchId, number>;
|
|
307
|
+
}>;
|
|
308
|
+
/**
|
|
309
|
+
* Object-form arguments for {@link mergeIncremental} (§6.6). The two same-typed
|
|
310
|
+
* stores are NAMED so `forkPoint` (the frozen ancestor the branches forked from, the
|
|
311
|
+
* diff reference) and `target` (the live committed graph that base lookups and the
|
|
312
|
+
* commit land on) cannot be swapped. `options.target` is ignored — `target` is the
|
|
313
|
+
* explicit arg.
|
|
314
|
+
*/
|
|
315
|
+
type MergeIncrementalArgs<G extends GraphDef = GraphDef> = Readonly<{
|
|
316
|
+
forkPoint: Store<G>;
|
|
317
|
+
target: Store<G>;
|
|
318
|
+
branches: readonly GraphBranch<G>[];
|
|
319
|
+
options?: MergeOptions<G>;
|
|
320
|
+
}>;
|
|
321
|
+
/**
|
|
322
|
+
* Records that a set of fork node ids resolved to a single canonical survivor.
|
|
323
|
+
*/
|
|
324
|
+
type EntityResolution = Readonly<{
|
|
325
|
+
canonicalId: NodeId<NodeType>;
|
|
326
|
+
memberIds: readonly NodeId<NodeType>[];
|
|
327
|
+
kind: string;
|
|
328
|
+
branchOrigins: readonly BranchId[];
|
|
329
|
+
}>;
|
|
330
|
+
/**
|
|
331
|
+
* One candidate value contributing to a property conflict, tagged by its origin
|
|
332
|
+
* branch.
|
|
333
|
+
*/
|
|
334
|
+
type ConflictingValue = Readonly<{
|
|
335
|
+
branchId: BranchId;
|
|
336
|
+
value: JsonValue;
|
|
337
|
+
}>;
|
|
338
|
+
/**
|
|
339
|
+
* A property whose value differed across cluster members (or across the two
|
|
340
|
+
* collapsed edges for an edge conflict). `resolution` records the value the
|
|
341
|
+
* policy selected.
|
|
342
|
+
*/
|
|
343
|
+
type PropertyConflict<G extends GraphDef = GraphDef> = Readonly<{
|
|
344
|
+
entityId: NodeId<NodeType> | EdgeId;
|
|
345
|
+
kind: string;
|
|
346
|
+
property: string;
|
|
347
|
+
values: readonly ConflictingValue[];
|
|
348
|
+
resolution: JsonValue;
|
|
349
|
+
readonly __graph?: G;
|
|
350
|
+
}>;
|
|
351
|
+
/**
|
|
352
|
+
* An inherited node OR edge deleted by one branch and modified by another, with
|
|
353
|
+
* the resolution the {@link DeleteModifyPolicy} produced. `entityId` is a
|
|
354
|
+
* {@link NodeId} for a node conflict and an {@link EdgeId} for an edge conflict.
|
|
355
|
+
*/
|
|
356
|
+
type DeleteModifyConflict = Readonly<{
|
|
357
|
+
entityId: NodeId<NodeType> | EdgeId;
|
|
358
|
+
kind: string;
|
|
359
|
+
deletedBy: BranchId;
|
|
360
|
+
modifiedBy: BranchId;
|
|
361
|
+
resolution: DeleteModifyPolicy;
|
|
362
|
+
}>;
|
|
363
|
+
/**
|
|
364
|
+
* Records that a cluster's mixed member kinds were collapsed to a single
|
|
365
|
+
* canonical (most-specific) type via ontology reconciliation.
|
|
366
|
+
*/
|
|
367
|
+
type TypeReconciliation = Readonly<{
|
|
368
|
+
entityId: NodeId<NodeType>;
|
|
369
|
+
fromTypes: readonly string[];
|
|
370
|
+
toType: string;
|
|
371
|
+
}>;
|
|
372
|
+
/**
|
|
373
|
+
* An item omitted from the merged result (e.g. an edge whose endpoint was
|
|
374
|
+
* deleted, or an incompatible-typed cluster member).
|
|
375
|
+
*/
|
|
376
|
+
type DroppedItem = Readonly<{
|
|
377
|
+
kind: "edge" | "node";
|
|
378
|
+
id: NodeId<NodeType> | EdgeId;
|
|
379
|
+
reason: string;
|
|
380
|
+
}>;
|
|
381
|
+
/**
|
|
382
|
+
* A `(kind, id)` node identity as surfaced in the merge report. Node identity is the
|
|
383
|
+
* PAIR, never the bare id (a `Doctor` and a `SpecialistDoctor` can share an id string),
|
|
384
|
+
* so report shapes that name nodes carry both halves.
|
|
385
|
+
*/
|
|
386
|
+
type ReportNodeIdentity = Readonly<{
|
|
387
|
+
kind: string;
|
|
388
|
+
id: NodeId<NodeType>;
|
|
389
|
+
}>;
|
|
390
|
+
/**
|
|
391
|
+
* An AMBIGUOUS new-vs-base match (design §6.4-A): a connected component that
|
|
392
|
+
* bridged ≥2 distinct committed base entities (directly, `baseA ~ new ~ baseB`, or
|
|
393
|
+
* through staged hops, `baseA ~ new1 ~ new2 ~ baseB`). `baseIds` are the committed
|
|
394
|
+
* entities the component spanned; `memberIds` are all of its members. Both are full
|
|
395
|
+
* `(kind, id)` identities — the guard keys on the composite identity, so a component
|
|
396
|
+
* spanning two SAME-id/different-kind bases stays distinguishable in the report. The
|
|
397
|
+
* base↔base collapse is ALWAYS REFUSED — the component is split so the committed
|
|
398
|
+
* entities stay separate. Reported regardless of how the component split. (A
|
|
399
|
+
* deliberate-collapse trust path is deferred until committed-entity re-keying + edge
|
|
400
|
+
* repoint exist; §6.4-C.)
|
|
401
|
+
*/
|
|
402
|
+
type BaseAmbiguity = Readonly<{
|
|
403
|
+
baseIds: readonly ReportNodeIdentity[];
|
|
404
|
+
memberIds: readonly ReportNodeIdentity[];
|
|
405
|
+
}>;
|
|
406
|
+
/**
|
|
407
|
+
* The contribution one branch made to the merged result. Returned by
|
|
408
|
+
* {@link ProvenanceIndex.byBranch}. EXPLICITLY in-memory / report-only for P0 —
|
|
409
|
+
* no on-graph prop tagging (deferred to the AgentFS phase).
|
|
410
|
+
*/
|
|
411
|
+
type BranchProvenance = Readonly<{
|
|
412
|
+
nodeIds: readonly NodeId<NodeType>[];
|
|
413
|
+
edgeIds: readonly EdgeId[];
|
|
414
|
+
}>;
|
|
415
|
+
/**
|
|
416
|
+
* Report-only, in-memory provenance index. `byBranch` answers "which
|
|
417
|
+
* nodes/edges did this branch contribute to the merged result?".
|
|
418
|
+
*/
|
|
419
|
+
type ProvenanceIndex = Readonly<{
|
|
420
|
+
byBranch: (branchId: BranchId) => BranchProvenance;
|
|
421
|
+
}>;
|
|
422
|
+
/**
|
|
423
|
+
* One `{branch, sourceId}` → canonical contribution — the unit of provenance.
|
|
424
|
+
*
|
|
425
|
+
* The in-memory {@link ProvenanceIndex} collapses these to `branch → {canonical
|
|
426
|
+
* ids}`; the full record (which keeps the contributing `sourceId` and kind) is what
|
|
427
|
+
* `persistProvenance` writes to the sidecar provenance graph. `sourceId` is the
|
|
428
|
+
* fork-local id the contribution had in its branch BEFORE the merge collapsed it to
|
|
429
|
+
* `canonicalId` (equal to `canonicalId` for an in-place modification).
|
|
430
|
+
*/
|
|
431
|
+
type ProvenanceRecord = Readonly<{
|
|
432
|
+
role: "node" | "edge";
|
|
433
|
+
canonicalId: string;
|
|
434
|
+
canonicalKind: string;
|
|
435
|
+
branchId: BranchId;
|
|
436
|
+
sourceId: string;
|
|
437
|
+
}>;
|
|
438
|
+
/**
|
|
439
|
+
* The full result of a {@link merge}: counts, every resolution/conflict/
|
|
440
|
+
* reconciliation/drop, and the report-only provenance index.
|
|
441
|
+
*/
|
|
442
|
+
type MergeReport<G extends GraphDef = GraphDef> = Readonly<{
|
|
443
|
+
merged: Readonly<{
|
|
444
|
+
nodes: number;
|
|
445
|
+
edges: number;
|
|
446
|
+
}>;
|
|
447
|
+
resolutions: readonly EntityResolution[];
|
|
448
|
+
conflicts: readonly PropertyConflict<G>[];
|
|
449
|
+
deleteModifyConflicts: readonly DeleteModifyConflict[];
|
|
450
|
+
typeReconciliations: readonly TypeReconciliation[];
|
|
451
|
+
dropped: readonly DroppedItem[];
|
|
452
|
+
/**
|
|
453
|
+
* Ambiguous new-vs-base matches (§6.4-A): components that bridged ≥2 committed
|
|
454
|
+
* base entities. Empty on the staged-vs-staged snapshot path.
|
|
455
|
+
*/
|
|
456
|
+
baseAmbiguities: readonly BaseAmbiguity[];
|
|
457
|
+
provenance: ProvenanceIndex;
|
|
458
|
+
/**
|
|
459
|
+
* Non-fatal advisories from the merge — comparison-ceiling skips and, when
|
|
460
|
+
* `persistProvenance` is set, a best-effort provenance-persistence failure (the
|
|
461
|
+
* graph still committed). Empty on a clean merge.
|
|
462
|
+
*/
|
|
463
|
+
warnings: readonly string[];
|
|
464
|
+
/**
|
|
465
|
+
* Present only when `persistProvenance` ran and SUCCEEDED: the sidecar provenance
|
|
466
|
+
* graph id and how many `{branch, sourceId}` rows were upserted. Absent when
|
|
467
|
+
* persistence was off or failed (a failure adds a {@link MergeReport.warnings}).
|
|
468
|
+
*/
|
|
469
|
+
provenancePersisted?: Readonly<{
|
|
470
|
+
graphId: string;
|
|
471
|
+
count: number;
|
|
472
|
+
}>;
|
|
473
|
+
}>;
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* `base@V` stamping.
|
|
477
|
+
*
|
|
478
|
+
* A {@link BaseVersion} is the immutable token a branch is forked from. It must
|
|
479
|
+
* change whenever either the schema OR the live content of the base store
|
|
480
|
+
* changes, so that `merge()`'s precondition check (T11) can reject a branch that
|
|
481
|
+
* forked from a divergent base.
|
|
482
|
+
*
|
|
483
|
+
* The token is two stable components joined by a separator:
|
|
484
|
+
*
|
|
485
|
+
* 1. A **schema hash** — `computeSchemaHash(serializeSchema(graph, version))`.
|
|
486
|
+
* This is content-addressed (the public `computeSchemaHash` deliberately
|
|
487
|
+
* excludes the version number and `generatedAt`, so it is stable across
|
|
488
|
+
* re-saves of the same schema).
|
|
489
|
+
* 2. A **content fingerprint** — a SHA-256 digest (truncated to a fixed-width
|
|
490
|
+
* hex string) of every LIVE node and edge over the base store (`id`,
|
|
491
|
+
* `updated_at`, the bitemporal `valid_from`/`valid_to`, and canonicalized
|
|
492
|
+
* `props`; edges also carry their endpoint ids), sorted by id, so two stores
|
|
493
|
+
* with identical live content fingerprint identically regardless of row
|
|
494
|
+
* enumeration order or insertion order. Props AND validity are folded in so
|
|
495
|
+
* the token changes on a content or validity edit even when `updated_at` ties
|
|
496
|
+
* at millisecond granularity. Hashing keeps the token fixed-width instead of
|
|
497
|
+
* growing linearly with store size.
|
|
498
|
+
*
|
|
499
|
+
* The token MUST be computed off the ORIGINAL base store, never off a clone:
|
|
500
|
+
* `exportGraph`/`importGraph` regenerate `created_at`/`updated_at`, so a clone's
|
|
501
|
+
* fingerprint would not match its source. (See the working-copy fidelity note in
|
|
502
|
+
* T4.)
|
|
503
|
+
*
|
|
504
|
+
* `computeBaseVersion` is async because schema hashing and live-content
|
|
505
|
+
* enumeration go through async TypeGraph internals. The design's synchronous
|
|
506
|
+
* illustrative signature does not survive contact with the real store surface.
|
|
507
|
+
*/
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Computes the immutable `base@V` token for a store. Combines the schema hash
|
|
511
|
+
* with a live-content fingerprint into a single branded {@link BaseVersion}.
|
|
512
|
+
*
|
|
513
|
+
* MUST be called on the ORIGINAL base store, not a clone (clones regenerate
|
|
514
|
+
* timestamps and would fingerprint differently).
|
|
515
|
+
*/
|
|
516
|
+
declare function computeBaseVersion<G extends GraphDef>(store: Store<G>): Promise<BaseVersion>;
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Error hierarchy for the graph-merge primitive.
|
|
520
|
+
*
|
|
521
|
+
* Every error extends the publicly-exported {@link TypeGraphError}, so consumers
|
|
522
|
+
* can use the same `isTypeGraphError`/category machinery they already use for
|
|
523
|
+
* TypeGraph itself. Each subclass carries a stable machine-readable `code`, a
|
|
524
|
+
* fixed `ErrorCategory`, and a cause chain for debugging.
|
|
525
|
+
*/
|
|
526
|
+
/**
|
|
527
|
+
* Machine-readable error codes for the merge primitive. Stable identifiers so
|
|
528
|
+
* callers can branch on `error.code` without string-matching messages.
|
|
529
|
+
*/
|
|
530
|
+
declare const MERGE_ERROR_CODES: {
|
|
531
|
+
readonly merge: "GRAPH_MERGE_ERROR";
|
|
532
|
+
readonly branch: "GRAPH_MERGE_BRANCH_ERROR";
|
|
533
|
+
readonly similarityUnavailable: "GRAPH_MERGE_SIMILARITY_UNAVAILABLE";
|
|
534
|
+
readonly conflict: "GRAPH_MERGE_CONFLICT";
|
|
535
|
+
readonly baseVersionMismatch: "GRAPH_MERGE_BASE_VERSION_MISMATCH";
|
|
536
|
+
};
|
|
537
|
+
/**
|
|
538
|
+
* Options shared by every merge error. Mirrors the relevant subset of
|
|
539
|
+
* TypeGraphError's options while making `cause`/`details`/`suggestion`
|
|
540
|
+
* uniformly optional at the merge-error boundary.
|
|
541
|
+
*/
|
|
542
|
+
type MergeErrorOptions = Readonly<{
|
|
543
|
+
details?: Record<string, unknown>;
|
|
544
|
+
suggestion?: string;
|
|
545
|
+
cause?: unknown;
|
|
546
|
+
}>;
|
|
547
|
+
/**
|
|
548
|
+
* Generic failure raised while computing or committing a merge. The catch-all
|
|
549
|
+
* for the orchestrator (comparison-ceiling overruns, commit failures, etc.).
|
|
550
|
+
*/
|
|
551
|
+
declare class MergeError extends TypeGraphError {
|
|
552
|
+
constructor(message: string, options?: MergeErrorOptions);
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Failure raised while creating a working-copy branch of a base store
|
|
556
|
+
* (clone/export/import failures, backend construction failures).
|
|
557
|
+
*/
|
|
558
|
+
declare class BranchError extends TypeGraphError {
|
|
559
|
+
constructor(message: string, options?: MergeErrorOptions);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Raised when a `vector`/`hybrid` similarity strategy is requested but no
|
|
563
|
+
* {@link import("./types").Embedder} was configured (`MergeOptions.embedder` is
|
|
564
|
+
* absent). The `vector`/`hybrid` scorers compute cosine over real embeddings in
|
|
565
|
+
* memory, so an embedder is mandatory for them; `fulltext`/`custom` need none.
|
|
566
|
+
*/
|
|
567
|
+
declare class SimilarityUnavailableError extends MergeError {
|
|
568
|
+
readonly code: "GRAPH_MERGE_SIMILARITY_UNAVAILABLE";
|
|
569
|
+
constructor(message: string, options?: MergeErrorOptions);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Raised when a conflict cannot be resolved by the configured policy and the
|
|
573
|
+
* caller has opted into hard-failing rather than flagging.
|
|
574
|
+
*/
|
|
575
|
+
declare class MergeConflictError extends MergeError {
|
|
576
|
+
readonly code: "GRAPH_MERGE_CONFLICT";
|
|
577
|
+
constructor(message: string, options?: MergeErrorOptions);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Raised by the `merge()` precondition check when a branch's `base@V` token
|
|
581
|
+
* does not match the merge target's current base version (the branch forked
|
|
582
|
+
* from a divergent schema or content fingerprint).
|
|
583
|
+
*/
|
|
584
|
+
declare class BaseVersionMismatchError extends MergeError {
|
|
585
|
+
readonly code: "GRAPH_MERGE_BASE_VERSION_MISMATCH";
|
|
586
|
+
constructor(message: string, options?: MergeErrorOptions);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Pluggable working-copy strategy: how `branch()` produces an isolated,
|
|
591
|
+
* independently-mutable copy of a base store.
|
|
592
|
+
*
|
|
593
|
+
* The P0 default is a faithful CLONE via the public interchange API
|
|
594
|
+
* ({@link cloneWorkingCopyStrategy}): `exportGraph` the base, then `importGraph`
|
|
595
|
+
* into a fresh store on a caller-provided backend. IDs are preserved by
|
|
596
|
+
* interchange, so the diff engine (T3) can key on stable ids across base and
|
|
597
|
+
* fork. This leverages public entrypoints only, needs zero schema changes, and
|
|
598
|
+
* behaves identically across SQLite and Postgres.
|
|
599
|
+
*
|
|
600
|
+
* INTERCHANGE FIDELITY LIMITATION (verified, design §13.x): the interchange
|
|
601
|
+
* `meta` schema has no `deletedAt` field, so a base row that is already
|
|
602
|
+
* soft-deleted would round-trip into the clone as LIVE (its tombstone lost). A
|
|
603
|
+
* resurrected row would then read as a spurious `new` node in the fork's diff —
|
|
604
|
+
* the base row is non-live, so `diffNodeKind` takes its `!isLive(base)` branch and
|
|
605
|
+
* reports the (live, clone-resurrected) row as an addition — silently re-creating a
|
|
606
|
+
* deleted node on commit. We therefore export with `includeDeleted: false`: the
|
|
607
|
+
* clone carries only the base's LIVE state, exactly what `branch()` needs. The
|
|
608
|
+
* merge state-diff is still computed against the ORIGINAL base store (live rows
|
|
609
|
+
* only) as the immutable reference, never against the clone (clones regenerate
|
|
610
|
+
* `created_at`/`updated_at`, which would otherwise destabilize `base@V`).
|
|
611
|
+
*
|
|
612
|
+
* Logical-namespace (copy-on-write within one backend, no full data copy) is a
|
|
613
|
+
* future strategy slot — see the `WorkingCopyStrategy` interface — deferred past
|
|
614
|
+
* P0.
|
|
615
|
+
*/
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* How `branch()` materializes a working copy of a base store.
|
|
619
|
+
*
|
|
620
|
+
* `create` receives the live base store and returns a fresh, independently
|
|
621
|
+
* mutable {@link Store} over the SAME graph definition, seeded with the base's
|
|
622
|
+
* current state. Mutating the returned store MUST NOT affect the base.
|
|
623
|
+
*
|
|
624
|
+
* The single method is the only extension point: alternative strategies
|
|
625
|
+
* (e.g. a future logical-namespace copy-on-write within one backend) implement
|
|
626
|
+
* the same contract.
|
|
627
|
+
*/
|
|
628
|
+
type WorkingCopyStrategy<G extends GraphDef> = Readonly<{
|
|
629
|
+
create: (baseStore: Store<G>) => Promise<Store<G>>;
|
|
630
|
+
}>;
|
|
631
|
+
/**
|
|
632
|
+
* Factory for a caller-provided backend. `branch()` stays backend-agnostic by
|
|
633
|
+
* delegating backend construction to the caller: the clone strategy calls this
|
|
634
|
+
* once per `create()` to obtain the fresh backend that backs the working copy.
|
|
635
|
+
*
|
|
636
|
+
* Returning a promise lets async backends (e.g. PGlite, which boots an
|
|
637
|
+
* in-process Postgres engine) be constructed lazily at branch time.
|
|
638
|
+
*
|
|
639
|
+
* The returned backend MUST be EMPTY (no rows for the base graph): the clone
|
|
640
|
+
* seeds it from the base via `importGraph` with `onConflict: "error"`, so a
|
|
641
|
+
* pre-existing row is surfaced as a {@link BranchError} rather than silently
|
|
642
|
+
* skipped (which would leave the working copy diverging from the base).
|
|
643
|
+
*/
|
|
644
|
+
type MakeBackend = () => Promise<GraphBackend>;
|
|
645
|
+
/**
|
|
646
|
+
* The P0 default working-copy strategy: faithful clone via export/import.
|
|
647
|
+
*
|
|
648
|
+
* On each `create(baseStore)`:
|
|
649
|
+
* 1. `exportGraph(baseStore, { includeMeta: true, includeDeleted: false })` —
|
|
650
|
+
* `includeMeta: true` carries `created_at`/`updated_at`; `includeDeleted:
|
|
651
|
+
* false` keeps the clone to LIVE rows only. Shipping soft-deleted rows is
|
|
652
|
+
* unsafe: the meta schema has no `deletedAt`, so they would import as live and
|
|
653
|
+
* resurrect on the fork's diff (see the fidelity note above). `branch()` only
|
|
654
|
+
* needs the base's live state.
|
|
655
|
+
* 2. Create a fresh store over the caller-provided backend with the SAME graph
|
|
656
|
+
* definition via `createStoreWithSchema`.
|
|
657
|
+
* 3. `importGraph(freshStore, data, { onConflict: "error", ... })` — ids are
|
|
658
|
+
* preserved so the diff engine can key on them. `onConflict: "error"`
|
|
659
|
+
* requires the backend to be EMPTY: a pre-existing row is a contract
|
|
660
|
+
* violation that must surface loudly, never be silently skipped (a skipped
|
|
661
|
+
* row would leave the clone diverging from the base, so the fork's diff would
|
|
662
|
+
* report phantom modifications/deletions). `importGraph` RETURNS
|
|
663
|
+
* `{ success, errors }` rather than throwing on a per-row rejection, so its
|
|
664
|
+
* result is checked and any failure fails the branch.
|
|
665
|
+
*
|
|
666
|
+
* The backend `makeBackend()` returns is opened here, so any failure AFTER it is
|
|
667
|
+
* created closes it before rethrowing — only the success path hands the backend
|
|
668
|
+
* (via the returned store) to the caller, who then owns its lifecycle.
|
|
669
|
+
*
|
|
670
|
+
* @param makeBackend - Constructs the fresh, EMPTY backend the working copy is
|
|
671
|
+
* built on.
|
|
672
|
+
*/
|
|
673
|
+
declare function cloneWorkingCopyStrategy<G extends GraphDef>(makeBackend: MakeBackend): WorkingCopyStrategy<G>;
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* `branch()` — fork an isolated, independently-mutable working copy of a base
|
|
677
|
+
* store (design §7.1).
|
|
678
|
+
*
|
|
679
|
+
* A branch is a {@link GraphBranch}: a fresh {@link BranchId}, the immutable
|
|
680
|
+
* `base@V` token the copy forked from (computed off the ORIGINAL base store via
|
|
681
|
+
* {@link computeBaseVersion}, never off the clone), and a {@link Store} over the
|
|
682
|
+
* branch's own backend seeded with the base's live state.
|
|
683
|
+
*
|
|
684
|
+
* The copy mechanism is pluggable behind {@link WorkingCopyStrategy}. The P0
|
|
685
|
+
* default is the faithful export/import clone ({@link cloneWorkingCopyStrategy}),
|
|
686
|
+
* which keeps this primitive backend-agnostic: the caller supplies a
|
|
687
|
+
* `makeBackend` factory, and `branch()` never names a concrete backend.
|
|
688
|
+
*/
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Creates an isolated working-copy branch of `baseStore`.
|
|
692
|
+
*
|
|
693
|
+
* Stamps the `base@V` token off the original base store, mints (or accepts) a
|
|
694
|
+
* {@link BranchId}, and materializes the working copy via the resolved strategy.
|
|
695
|
+
* The default strategy is a faithful clone over a fresh backend produced by
|
|
696
|
+
* `makeBackend`; pass an explicit `strategy` to override (e.g. a future
|
|
697
|
+
* logical-namespace copy-on-write).
|
|
698
|
+
*
|
|
699
|
+
* Returns a {@link Result}: success yields the {@link GraphBranch}; any failure
|
|
700
|
+
* (base-version stamping, backend construction, export/import) is wrapped in a
|
|
701
|
+
* {@link BranchError} with the underlying cause attached. Errors are returned,
|
|
702
|
+
* never thrown — this is internal-logic surface (the caller converts to a thrown
|
|
703
|
+
* error at the framework boundary).
|
|
704
|
+
*
|
|
705
|
+
* @param baseStore - The store to fork. Remains untouched.
|
|
706
|
+
* @param makeBackend - Factory for the working copy's backend (keeps the
|
|
707
|
+
* primitive backend-agnostic). Used only by the default clone strategy; ignored
|
|
708
|
+
* when an explicit `strategy` is supplied.
|
|
709
|
+
* @param options - Optional `{ id }` to set an explicit branch id.
|
|
710
|
+
* @param strategy - Optional working-copy strategy override.
|
|
711
|
+
*/
|
|
712
|
+
declare function branch<G extends GraphDef>(baseStore: GraphBranch<G>["store"], makeBackend: MakeBackend, options?: BranchOptions, strategy?: WorkingCopyStrategy<G>): Promise<Result<GraphBranch<G>, BranchError>>;
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* The composite MERGE IDENTITY key for a node: the pair `(kind, id)`.
|
|
716
|
+
*
|
|
717
|
+
* TypeGraph node identity is the PAIR `(kind, id)` — a bare id string is NOT unique
|
|
718
|
+
* on its own (a `Patient` and an `Encounter` may both carry the id "x" as two
|
|
719
|
+
* DISTINCT committed nodes; ids are caller-supplied). Every place the merge pipeline
|
|
720
|
+
* groups, clusters, de-dupes, repoints, retypes, or deletes BY NODE IDENTITY must
|
|
721
|
+
* key on this pair — never the bare id — or two different-kind nodes that happen to
|
|
722
|
+
* share an id string silently fuse into one cluster (wrong merge, dropped node,
|
|
723
|
+
* incoherent commit) and the §6.4-A base guard is bypassed.
|
|
724
|
+
*
|
|
725
|
+
* Represented as a NUL-joined string (a branded {@link MergeKey}) so it doubles as a
|
|
726
|
+
* `Map`/`Set` key and a deterministic ordering key. `kind` is a schema identifier
|
|
727
|
+
* (NUL-free), so the FIRST NUL unambiguously delimits kind from id even when a
|
|
728
|
+
* caller-supplied id itself contains a NUL byte. This matches the `(kind, id)`
|
|
729
|
+
* separator the commit-time write guard already uses.
|
|
730
|
+
*
|
|
731
|
+
* {@link compareMergeKeys} orders by the bare id FIRST (kind only breaks a same-id
|
|
732
|
+
* tie), so the merge's "minimum-id survivor" / id-sorted-members semantics are
|
|
733
|
+
* preserved unchanged for the common single-kind cluster — the composite key changes
|
|
734
|
+
* WHICH nodes share an identity, never the ordering among genuinely distinct ids.
|
|
735
|
+
*/
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* A composite `(kind, id)` node-identity key. Branded so it cannot be confused with
|
|
739
|
+
* a bare {@link NodeId} at the type level — the whole point is that the two are NOT
|
|
740
|
+
* interchangeable as identities.
|
|
741
|
+
*/
|
|
742
|
+
type MergeKey = string & {
|
|
743
|
+
readonly __mergeKey: unique symbol;
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* The shared SCORING stage (design §4, the single match-decision point).
|
|
748
|
+
*
|
|
749
|
+
* Candidate generation is three layers — sources → scoring → reconciler (§4).
|
|
750
|
+
* This module is the MIDDLE layer: it turns the candidate PROPOSALS every source
|
|
751
|
+
* emits into the {@link CandidateEdge} set the reconciler consumes, applying the
|
|
752
|
+
* EXACT same scorer + threshold regardless of which source proposed a pair. A
|
|
753
|
+
* source proposes (recall); scoring disposes (the match decision).
|
|
754
|
+
*
|
|
755
|
+
* Two proposal kinds enter:
|
|
756
|
+
*
|
|
757
|
+
* - {@link CandidatePair}s — unscored `(a, b)` node pairs from any source. They
|
|
758
|
+
* are deduped by canonical `(a, b)` and each scored EXACTLY ONCE by
|
|
759
|
+
* {@link scorePair}; only pairs clearing the kind's threshold become edges.
|
|
760
|
+
* - FORCED {@link CandidateEdge}s — DEFINITIONAL matches (a shared unique value)
|
|
761
|
+
* that BYPASS scoring entirely, emitted at {@link FORCED_MATCH_SCORE}. A forced
|
|
762
|
+
* pair is never also fuzzy-scored: forced `(a, b)` keys are reserved first, so
|
|
763
|
+
* a fuzzy pair proposing the same endpoints is dropped before scoring.
|
|
764
|
+
*
|
|
765
|
+
* This was previously fused into `candidate-gen.ts`'s `generateCandidates` (which
|
|
766
|
+
* scored AND thresholded inline); naming scoring as its own stage is what makes
|
|
767
|
+
* "the scorer — not a source — decides matches" concrete (§4). `generateCandidates`
|
|
768
|
+
* now composes the bucket sources over this stage.
|
|
769
|
+
*
|
|
770
|
+
* Determinism: the emitted edge set is a pure function of the proposal SETS — pairs
|
|
771
|
+
* are deduped by canonical `(a, b)`, scored by the symmetric {@link scorePair}, and
|
|
772
|
+
* the final list is sorted by `(a, b)`, so neither the order proposals arrive in
|
|
773
|
+
* nor the directionality of a pair affects the result.
|
|
774
|
+
*
|
|
775
|
+
* The {@link ComparisonCeilingPolicy} bounds only the FUZZY scoring work (the
|
|
776
|
+
* embedder step for vector/hybrid); FORCED edges are definitional, not
|
|
777
|
+
* similarity-based, so they are emitted regardless of the ceiling:
|
|
778
|
+
*
|
|
779
|
+
* - `"error"` — exceeding the ceiling fails with a typed {@link MergeError}.
|
|
780
|
+
* - `"mergeByIdOnly"` — fuzzy similarity is SKIPPED for the kind (no
|
|
781
|
+
* threshold-scored edges), FORCED edges are still emitted,
|
|
782
|
+
* and a {@link CandidateWarning} is recorded.
|
|
783
|
+
*/
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* An undirected candidate-merge edge between two nodes that should merge.
|
|
787
|
+
* Endpoints are stored in ascending id order (`a < b`) so the edge has a single
|
|
788
|
+
* canonical representation. Fuzzy edges carry their similarity score; FORCED
|
|
789
|
+
* (definitional) edges carry {@link FORCED_MATCH_SCORE}.
|
|
790
|
+
*/
|
|
791
|
+
type CandidateEdge = Readonly<{
|
|
792
|
+
a: MergeKey;
|
|
793
|
+
b: MergeKey;
|
|
794
|
+
score: number;
|
|
795
|
+
}>;
|
|
796
|
+
/**
|
|
797
|
+
* An unscored candidate pair a source proposes for scoring: the canonical
|
|
798
|
+
* endpoint ids `(a, b)` (`a < b`) plus the two node objects the scorer reads
|
|
799
|
+
* fields off of. `left`/`right` carry the nodes whose ids are `a`/`b`
|
|
800
|
+
* respectively, but {@link scorePair} is symmetric so their roles are
|
|
801
|
+
* interchangeable for scoring.
|
|
802
|
+
*/
|
|
803
|
+
type CandidatePair<K extends NodeType = NodeType> = Readonly<{
|
|
804
|
+
a: MergeKey;
|
|
805
|
+
b: MergeKey;
|
|
806
|
+
left: Node<K>;
|
|
807
|
+
right: Node<K>;
|
|
808
|
+
}>;
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* `merge()` orchestrator (design §7.2, T11).
|
|
812
|
+
*
|
|
813
|
+
* Composes every phase built in T3–T10 into one DB-agnostic primitive:
|
|
814
|
+
*
|
|
815
|
+
* 1. PRECONDITION — compute the target's `base@V` and reject any branch whose
|
|
816
|
+
* `base` token does not match it (`BaseVersionMismatchError`). A branch
|
|
817
|
+
* forked from a divergent schema or content fingerprint cannot be merged
|
|
818
|
+
* safely (design §5.2 / §13.6).
|
|
819
|
+
* 2. STAGE — `stageBranches` (T7): the provenance-tagged UNION of every
|
|
820
|
+
* branch's state-diff against the immutable base.
|
|
821
|
+
* 3. CANDIDATE-GEN — per resolved kind: build node-shaped objects from the
|
|
822
|
+
* staged NEW nodes, `blockNodes` (T5, folding in unique constraints from
|
|
823
|
+
* `introspect()`), `generateCandidates` (T6, fulltext Dice + custom; vector/
|
|
824
|
+
* hybrid guarded). Kinds NOT in `options.resolve` merge by id only.
|
|
825
|
+
* 4. CLUSTER — `connectedComponents` (T8) over the accumulated candidate edges
|
|
826
|
+
* and every staged new-node id, with the optional diameter guard.
|
|
827
|
+
* 5. CANONICALIZE — `canonicalizeCluster` (T8): min-id survivor + commutative
|
|
828
|
+
* property union under the stable, non-wall-clock conflict policy.
|
|
829
|
+
* 6. DELETE/MODIFY — `resolveDeleteModify` (T8a): the authoritative final
|
|
830
|
+
* liveness of every inherited node + the delete/modify conflicts.
|
|
831
|
+
* 7. TYPE-RECONCILE — `reconcileTypes` (T10) over the public-closure glue when
|
|
832
|
+
* `reconcileTypes: "ontology"`; otherwise a no-op.
|
|
833
|
+
* 8. EDGE REPOINT — `repointEdges` (T9): repoint every staged edge onto its
|
|
834
|
+
* cluster canonical, drop edges to finally-deleted endpoints, dedupe + union
|
|
835
|
+
* edge props under the same conflict policy.
|
|
836
|
+
* 9. COMMIT — apply everything to `target` (default = base store) in a single
|
|
837
|
+
* `store.transaction` when the backend is transactional, else non-atomically
|
|
838
|
+
* with a report warning (out of the P0 acceptance path — SQLite + Postgres
|
|
839
|
+
* are both transactional).
|
|
840
|
+
* 10. REPORT — assemble the {@link MergeReport} with merged counts, every
|
|
841
|
+
* resolution / conflict / reconciliation / drop, and the in-memory
|
|
842
|
+
* {@link ProvenanceIndex}. With `persistProvenance`, ALSO upsert the
|
|
843
|
+
* `{branch, sourceId}` records to a sidecar provenance graph (post-commit,
|
|
844
|
+
* best-effort — a failure surfaces as a report warning, never a failed merge).
|
|
845
|
+
*
|
|
846
|
+
* DETERMINISM: every phase is order-independent (pure functions over the
|
|
847
|
+
* unordered branch / staged sets, with a branch order captured ONCE for
|
|
848
|
+
* conflict resolution), so shuffling `branches` yields a deep-equal report and an
|
|
849
|
+
* identical committed graph. T12 proves this with a fast-check shuffle property.
|
|
850
|
+
*/
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Merges a set of branches back into a target store (design §7.2).
|
|
854
|
+
*
|
|
855
|
+
* Validates that every branch forked from the target's current `base@V`, stages
|
|
856
|
+
* the union of their diffs, resolves entities / conflicts / types through the
|
|
857
|
+
* T3–T10 phases, and commits the merged result to `target` (default: the base
|
|
858
|
+
* `store`) in a single transaction. Returns a {@link MergeReport} on success.
|
|
859
|
+
*
|
|
860
|
+
* This is the SNAPSHOT entry point: candidate generation runs the staged sources
|
|
861
|
+
* only (`exactKey`, `unique`) — it never resolves a staged node against the
|
|
862
|
+
* committed base. New-vs-base resolution is the separate {@link mergeAgainstBase}
|
|
863
|
+
* scope, which has a weaker `base@V` contract.
|
|
864
|
+
*
|
|
865
|
+
* Errors are RETURNED (never thrown) as a typed {@link MergeError} subclass:
|
|
866
|
+
* `BaseVersionMismatchError` for the precondition, `MergeError` for a
|
|
867
|
+
* comparison-ceiling overrun / commit failure, `SimilarityUnavailableError` for a
|
|
868
|
+
* `vector`/`hybrid` strategy with no configured vector strategy.
|
|
869
|
+
*
|
|
870
|
+
* @param store The base store the branches forked from. Used as the default merge
|
|
871
|
+
* target and as the immutable diff reference.
|
|
872
|
+
* @param branches The branches to merge. ORDER DOES NOT AFFECT THE RESULT — the
|
|
873
|
+
* report and committed graph are identical across any permutation.
|
|
874
|
+
* @param optionsInput Caller-facing {@link MergeOptions}; normalized internally.
|
|
875
|
+
*/
|
|
876
|
+
declare function merge<G extends GraphDef>(store: Store<G>, branches: readonly GraphBranch<G>[], optionsInput?: MergeOptions<G>): Promise<Result<MergeReport<G>, MergeError>>;
|
|
877
|
+
/**
|
|
878
|
+
* Incremental merge — the public fork-point-vs-live-target entry point (design
|
|
879
|
+
* §6.4-B / §6.6). It treats `forkPoint` as the immutable ancestor, folds the live
|
|
880
|
+
* `target` in as a preferred committed branch, resolves branch additions against
|
|
881
|
+
* committed rows, and propagates inherited node/edge modifications and deletions
|
|
882
|
+
* through the same three-way merge planner.
|
|
883
|
+
*
|
|
884
|
+
* Object-form args so the two same-typed stores (`forkPoint`, `target`) cannot be
|
|
885
|
+
* swapped. `options.target` is ignored — `target` is the explicit arg.
|
|
886
|
+
*
|
|
887
|
+
* Preconditions (typed errors): every branch forked from `forkPoint`
|
|
888
|
+
* (`branch.base === computeBaseVersion(forkPoint)`); `forkPoint` and `target` share a
|
|
889
|
+
* schema hash (schema drift is fatal; target CONTENT may have advanced); and
|
|
890
|
+
* `onBasePropertyConflict` is `"flag"` (keep-base for committed-row conflicts).
|
|
891
|
+
*/
|
|
892
|
+
declare function mergeIncremental<G extends GraphDef>(args: MergeIncrementalArgs<G>): Promise<Result<MergeReport<G>, MergeError>>;
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Validation + default-application for {@link MergeOptions}.
|
|
896
|
+
*
|
|
897
|
+
* The zod schema validates the scalar / enum surface (thresholds, ceilings,
|
|
898
|
+
* enums) and applies the frozen P0 defaults. Function- and store-valued fields
|
|
899
|
+
* (`canonical`, a function `onPropertyConflict`, `target`, per-kind `block` /
|
|
900
|
+
* `custom.score`, `branchOrder`) are not meaningfully validatable by zod, so they
|
|
901
|
+
* are threaded through unchanged after the scalar surface validates.
|
|
902
|
+
*
|
|
903
|
+
* `normalizeMergeOptions` is the single entry point: it returns a
|
|
904
|
+
* {@link NormalizedMergeOptions} with every default resolved, so downstream
|
|
905
|
+
* phases never branch on `undefined`.
|
|
906
|
+
*/
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Frozen P0 defaults. Exported so tests and downstream phases assert against the
|
|
910
|
+
* same constants rather than re-typing literals.
|
|
911
|
+
*/
|
|
912
|
+
declare const MERGE_OPTION_DEFAULTS: {
|
|
913
|
+
readonly reconcileTypes: "off";
|
|
914
|
+
readonly onPropertyConflict: "flag";
|
|
915
|
+
readonly onBasePropertyConflict: "flag";
|
|
916
|
+
readonly onDeleteModifyConflict: "flag";
|
|
917
|
+
readonly onComparisonCeiling: "error";
|
|
918
|
+
readonly provenance: true;
|
|
919
|
+
readonly persistProvenance: false;
|
|
920
|
+
};
|
|
921
|
+
/**
|
|
922
|
+
* Fully-normalized merge options: every default resolved, the (validated)
|
|
923
|
+
* pass-through fields attached. Downstream phases consume this, never the raw
|
|
924
|
+
* {@link MergeOptions}.
|
|
925
|
+
*/
|
|
926
|
+
type NormalizedMergeOptions<G extends GraphDef = GraphDef> = Readonly<{
|
|
927
|
+
resolve: Readonly<Record<string, ResolveConfig<G>>>;
|
|
928
|
+
reconcileTypes: ReconcileTypesMode;
|
|
929
|
+
onPropertyConflict: PropertyConflictPolicy<G>;
|
|
930
|
+
onBasePropertyConflict: PropertyConflictPolicy<G>;
|
|
931
|
+
onDeleteModifyConflict: DeleteModifyPolicy;
|
|
932
|
+
onComparisonCeiling: ComparisonCeilingPolicy;
|
|
933
|
+
provenance: boolean;
|
|
934
|
+
persistProvenance: boolean;
|
|
935
|
+
canonical?: (cluster: ResolvedCluster) => ReturnType<NonNullable<MergeOptions<G>["canonical"]>>;
|
|
936
|
+
embedder?: Embedder;
|
|
937
|
+
target?: MergeOptions<G>["target"];
|
|
938
|
+
maxComparisonsPerKind?: number;
|
|
939
|
+
clusterMaxDiameter?: number;
|
|
940
|
+
branchOrder?: readonly BranchId[];
|
|
941
|
+
provenanceWeights?: ReadonlyMap<BranchId, number>;
|
|
942
|
+
}>;
|
|
943
|
+
/**
|
|
944
|
+
* Validates and normalizes {@link MergeOptions}, applying every P0 default.
|
|
945
|
+
*
|
|
946
|
+
* Throws (not a `Result`) on invalid scalar input — option validation is a
|
|
947
|
+
* caller-boundary concern, surfaced as a thrown error per project conventions;
|
|
948
|
+
* `merge()` converts it back to a typed `MergeError` at its own boundary.
|
|
949
|
+
*
|
|
950
|
+
* @throws if a threshold is outside `[0, 1]`, `maxComparisonsPerKind` is
|
|
951
|
+
* negative/non-integer, or `clusterMaxDiameter` is non-positive.
|
|
952
|
+
*/
|
|
953
|
+
declare function normalizeMergeOptions<G extends GraphDef>(options?: MergeOptions<G>): NormalizedMergeOptions<G>;
|
|
954
|
+
|
|
955
|
+
/** The Provenance node: one row per `{branch, sourceId}` → canonical contribution. */
|
|
956
|
+
declare const Provenance: Readonly<{
|
|
957
|
+
kind: "Provenance";
|
|
958
|
+
schema: z.ZodObject<{
|
|
959
|
+
targetGraphId: z.ZodString;
|
|
960
|
+
role: z.ZodEnum<{
|
|
961
|
+
node: "node";
|
|
962
|
+
edge: "edge";
|
|
963
|
+
}>;
|
|
964
|
+
canonicalId: z.ZodString;
|
|
965
|
+
canonicalKind: z.ZodString;
|
|
966
|
+
branchId: z.ZodString;
|
|
967
|
+
sourceId: z.ZodString;
|
|
968
|
+
}, z.core.$strip>;
|
|
969
|
+
description: string | undefined;
|
|
970
|
+
annotations: KindAnnotations | undefined;
|
|
971
|
+
__nodeType: true;
|
|
972
|
+
}>;
|
|
973
|
+
/**
|
|
974
|
+
* Derives the sidecar graph id for a target graph. Suffixing the target's own id
|
|
975
|
+
* keeps each target graph's provenance in its own `graphId`-namespaced tables on a
|
|
976
|
+
* shared backend, while a single `Provenance` schema serves all of them.
|
|
977
|
+
*/
|
|
978
|
+
declare function provenanceGraphId(targetGraphId: string): string;
|
|
979
|
+
/** Builds the sidecar provenance graph definition for a target graph. */
|
|
980
|
+
declare function buildProvenanceGraph(targetGraphId: string): Readonly<{
|
|
981
|
+
id: string;
|
|
982
|
+
nodes: {
|
|
983
|
+
readonly Provenance: {
|
|
984
|
+
readonly type: Readonly<{
|
|
985
|
+
kind: "Provenance";
|
|
986
|
+
schema: z.ZodObject<{
|
|
987
|
+
targetGraphId: z.ZodString;
|
|
988
|
+
role: z.ZodEnum<{
|
|
989
|
+
node: "node";
|
|
990
|
+
edge: "edge";
|
|
991
|
+
}>;
|
|
992
|
+
canonicalId: z.ZodString;
|
|
993
|
+
canonicalKind: z.ZodString;
|
|
994
|
+
branchId: z.ZodString;
|
|
995
|
+
sourceId: z.ZodString;
|
|
996
|
+
}, z.core.$strip>;
|
|
997
|
+
description: string | undefined;
|
|
998
|
+
annotations: KindAnnotations | undefined;
|
|
999
|
+
__nodeType: true;
|
|
1000
|
+
}>;
|
|
1001
|
+
};
|
|
1002
|
+
};
|
|
1003
|
+
edges: {};
|
|
1004
|
+
ontology: readonly Readonly<{
|
|
1005
|
+
metaEdge: MetaEdge;
|
|
1006
|
+
from: NodeType | EdgeType | string;
|
|
1007
|
+
to: NodeType | EdgeType | string;
|
|
1008
|
+
}>[];
|
|
1009
|
+
defaults: Readonly<{
|
|
1010
|
+
onNodeDelete: DeleteBehavior;
|
|
1011
|
+
temporalMode: TemporalMode;
|
|
1012
|
+
}>;
|
|
1013
|
+
indexes: readonly IndexDeclaration[] | undefined;
|
|
1014
|
+
extension: Readonly<{
|
|
1015
|
+
version?: GraphExtensionVersion;
|
|
1016
|
+
nodes?: Readonly<Record<string, ExtensionNodeDef>>;
|
|
1017
|
+
edges?: Readonly<Record<string, ExtensionEdgeDef>>;
|
|
1018
|
+
ontology?: readonly ExtensionOntologyRelation[];
|
|
1019
|
+
indexes?: readonly ExtensionIndex[];
|
|
1020
|
+
}> | undefined;
|
|
1021
|
+
deprecatedKinds: ReadonlySet<string>;
|
|
1022
|
+
__graphDef: true;
|
|
1023
|
+
}>;
|
|
1024
|
+
/** The concrete sidecar provenance graph type. */
|
|
1025
|
+
type ProvenanceGraph = ReturnType<typeof buildProvenanceGraph>;
|
|
1026
|
+
/** A persisted provenance node (the queryable record). */
|
|
1027
|
+
type ProvenanceNode = Node<typeof Provenance>;
|
|
1028
|
+
/**
|
|
1029
|
+
* Opens — materializing the schema if needed — the provenance store for a target
|
|
1030
|
+
* graph on the given backend. Idempotent: safe to call before every persist/query,
|
|
1031
|
+
* and shares the backend with the target (so the caller must NOT close it
|
|
1032
|
+
* separately — closing the shared backend is the target owner's job).
|
|
1033
|
+
*/
|
|
1034
|
+
declare function openProvenanceStore(backend: GraphBackend, targetGraphId: string): Promise<Store<ProvenanceGraph>>;
|
|
1035
|
+
/**
|
|
1036
|
+
* Upserts one `Provenance` node per record into the sidecar store, keyed by the
|
|
1037
|
+
* deterministic id (re-running the same merge is a no-op upsert, never a
|
|
1038
|
+
* duplicate). Returns the row count written. The caller wraps this for best-effort
|
|
1039
|
+
* behavior — a failure here must not fail an already-committed merge.
|
|
1040
|
+
*/
|
|
1041
|
+
declare function persistProvenanceRecords(store: Store<ProvenanceGraph>, targetGraphId: string, records: readonly ProvenanceRecord[]): Promise<number>;
|
|
1042
|
+
/** Filter for {@link readProvenance}. Each field, when set, narrows the result. */
|
|
1043
|
+
type ProvenanceQuery = Readonly<{
|
|
1044
|
+
branchId?: BranchId | string;
|
|
1045
|
+
canonicalId?: string;
|
|
1046
|
+
role?: "node" | "edge";
|
|
1047
|
+
}>;
|
|
1048
|
+
/**
|
|
1049
|
+
* Reads persisted provenance back, filtered and stably ordered. The sidecar is a
|
|
1050
|
+
* normal typed graph, so this is a thin ergonomic wrapper over
|
|
1051
|
+
* `store.nodes.Provenance.find()` (filtered in memory — provenance volumes are
|
|
1052
|
+
* modest; a query-builder `where` is the scale path). Answers "which canonical
|
|
1053
|
+
* entities did branch X contribute to?" and "who contributed canonical Y?".
|
|
1054
|
+
*/
|
|
1055
|
+
declare function readProvenance(store: Store<ProvenanceGraph>, query?: ProvenanceQuery): Promise<readonly ProvenanceNode[]>;
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Candidate SOURCES (design §4 / §6.1) — the RECALL layer of candidate generation.
|
|
1059
|
+
*
|
|
1060
|
+
* Candidate generation is three layers — sources → scoring → reconciler (§4). This
|
|
1061
|
+
* module is the FIRST layer: each source PROPOSES candidates and decides nothing
|
|
1062
|
+
* about a fuzzy match. A source emits three things:
|
|
1063
|
+
*
|
|
1064
|
+
* - `pairs` — unscored `(a, b)` node pairs handed to the shared scoring
|
|
1065
|
+
* stage (`scoring.ts`), which makes every fuzzy match decision.
|
|
1066
|
+
* - `forcedEdges` — DEFINITIONAL matches (a shared unique value) that bypass
|
|
1067
|
+
* scoring at {@link FORCED_MATCH_SCORE}.
|
|
1068
|
+
* - `baseMembers` — committed base nodes the source pulled into scope, so the
|
|
1069
|
+
* reconciler can seed + canonicalize a base endpoint without
|
|
1070
|
+
* re-querying the backend. EMPTY for staged-only sources.
|
|
1071
|
+
*
|
|
1072
|
+
* Two sources ship here — today's two blocking strategies (`blocking.ts`),
|
|
1073
|
+
* refactored behind the source interface:
|
|
1074
|
+
*
|
|
1075
|
+
* - {@link exactKeySource} (`exactKey`, §6.1) — the `block(node) => string`
|
|
1076
|
+
* bucket: same-bucket node pairs become fuzzy `pairs`. Staged-vs-staged.
|
|
1077
|
+
* - {@link uniqueSource} (`unique`, §6.1) — the unique-constraint signature
|
|
1078
|
+
* buckets: every same-signature pair is a FORCED edge (a shared unique value
|
|
1079
|
+
* is definitionally the same entity). Staged-vs-staged.
|
|
1080
|
+
*
|
|
1081
|
+
* Both read the SAME pre-computed `blockNodes` map off the scope (the UNION of the
|
|
1082
|
+
* `block()` key and the per-constraint signatures, `blocking.ts`), filtering it by
|
|
1083
|
+
* {@link isUniqueBucketKey}: `exactKey` owns the non-unique buckets, `unique` owns
|
|
1084
|
+
* the unique buckets. Sharing one blocked map (vs. re-blocking per source) keeps the
|
|
1085
|
+
* source split byte-identical to the old fused `generateCandidates` — a node with a
|
|
1086
|
+
* unique signature but no `block()` key lands ONLY in its unique bucket, never the
|
|
1087
|
+
* all-vs-all `unblocked` bucket.
|
|
1088
|
+
*
|
|
1089
|
+
* Determinism: the extraction visits buckets in sorted-key order over id-sorted
|
|
1090
|
+
* members and enumerates `i < j`, so proposals are emitted in a canonical order;
|
|
1091
|
+
* the scoring stage additionally dedups + sorts, so neither source's emission order
|
|
1092
|
+
* leaks into the merge result.
|
|
1093
|
+
*/
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* A committed BASE node a source pulled into the cluster universe (design §4
|
|
1097
|
+
* member contribution / §6.4-D). It carries the same `(id, kind, props)` a staged
|
|
1098
|
+
* member does, plus a reserved `"base"` origin so the reconciler can enforce
|
|
1099
|
+
* base-id-wins (§6.4-C) and tag provenance with the base sentinel (§6.4-D). EMPTY
|
|
1100
|
+
* for staged-only sources (`exactKey`, `unique`).
|
|
1101
|
+
*/
|
|
1102
|
+
type BaseMember = Readonly<{
|
|
1103
|
+
id: NodeId<NodeType>;
|
|
1104
|
+
kind: string;
|
|
1105
|
+
props: Readonly<Record<string, JsonValue>>;
|
|
1106
|
+
origin: "base";
|
|
1107
|
+
}>;
|
|
1108
|
+
/**
|
|
1109
|
+
* What a candidate source emits for one kind: fuzzy `pairs` to score, FORCED
|
|
1110
|
+
* (definitional) edges to pass through unscored, and the committed `baseMembers`
|
|
1111
|
+
* it pulled into scope.
|
|
1112
|
+
*/
|
|
1113
|
+
type SourceResult = Readonly<{
|
|
1114
|
+
pairs: readonly CandidatePair[];
|
|
1115
|
+
forcedEdges: readonly CandidateEdge[];
|
|
1116
|
+
baseMembers: readonly BaseMember[];
|
|
1117
|
+
}>;
|
|
1118
|
+
/**
|
|
1119
|
+
* The minimal node-collection surface a base-querying source needs: the public
|
|
1120
|
+
* `bulkFindByConstraint` (typegraph 0.29.0). It computes each item's constraint key
|
|
1121
|
+
* from its props, returns the live committed match per item in INPUT ORDER
|
|
1122
|
+
* (`undefined` for misses, soft-deleted excluded), and is indexed by the `uniques`
|
|
1123
|
+
* table PK — so graph-merge never reconstructs constraint keys itself. A runtime,
|
|
1124
|
+
* kind-string-keyed view (like the commit's `TxNodes`) so a source can dispatch on
|
|
1125
|
+
* a kind string without threading the caller's concrete `Store<G>` generic.
|
|
1126
|
+
*/
|
|
1127
|
+
type BaseNodeLookup = Readonly<{
|
|
1128
|
+
bulkFindByConstraint: (constraintName: string, items: readonly Readonly<{
|
|
1129
|
+
props: Record<string, unknown>;
|
|
1130
|
+
}>[]) => Promise<readonly (Node<NodeType> | undefined)[]>;
|
|
1131
|
+
/**
|
|
1132
|
+
* The non-unique sibling (typegraph 0.30): for each item, the live committed nodes
|
|
1133
|
+
* sharing its declared-INDEX key (computed from props), returned per item in input
|
|
1134
|
+
* order, each inner array id-sorted, soft-deleted excluded. `limitPerInput` bounds
|
|
1135
|
+
* per-item fan-out (id-ordered, so the cap is deterministic). Candidate retrieval —
|
|
1136
|
+
* NOT a uniqueness/identity guarantee — so `baseKey` emits scored pairs, not forced
|
|
1137
|
+
* edges.
|
|
1138
|
+
*/
|
|
1139
|
+
bulkFindByIndex: (indexName: string, items: readonly Readonly<{
|
|
1140
|
+
props: Record<string, unknown>;
|
|
1141
|
+
}>[], options?: Readonly<{
|
|
1142
|
+
limitPerInput?: number;
|
|
1143
|
+
}>) => Promise<readonly (readonly Node<NodeType>[])[]>;
|
|
1144
|
+
}>;
|
|
1145
|
+
/** Runtime, kind-string-keyed view of a store's node collections for base lookups. */
|
|
1146
|
+
type BaseLookupStore = Readonly<{
|
|
1147
|
+
nodes: Readonly<Record<string, BaseNodeLookup>>;
|
|
1148
|
+
}>;
|
|
1149
|
+
/**
|
|
1150
|
+
* The per-kind input a source generates over. `blocks` is the UNION `blockNodes`
|
|
1151
|
+
* result (`block()` key + per-constraint signatures) the staged sources read.
|
|
1152
|
+
*
|
|
1153
|
+
* The remaining fields are present only when a BASE-querying source (`baseUnique` /
|
|
1154
|
+
* `baseKey`, §6.2) is driven: the kind's materialized staged new `nodes` (the lookup
|
|
1155
|
+
* items), its `uniqueConstraints` (for `baseUnique`), the declared `blockIndex` name
|
|
1156
|
+
* (for `baseKey`, when the kind configured one), and the committed `store` to query
|
|
1157
|
+
* against. Staged-only sources (`exactKey`, `unique`) ignore them, so the public
|
|
1158
|
+
* `merge()` candidate path leaves them unset.
|
|
1159
|
+
*/
|
|
1160
|
+
type SourceScope = Readonly<{
|
|
1161
|
+
kind: string;
|
|
1162
|
+
blocks: ReadonlyMap<string, readonly Node<NodeType>[]>;
|
|
1163
|
+
nodes?: readonly Node<NodeType>[];
|
|
1164
|
+
uniqueConstraints?: readonly UniqueIntrospection[];
|
|
1165
|
+
blockIndex?: string;
|
|
1166
|
+
/** When set, `exactKey` bounds the no-key (`"unblocked"`) bucket by sorted-
|
|
1167
|
+
* neighbourhood instead of all-vs-all (the `keyless` source, §6.2). */
|
|
1168
|
+
keyless?: KeylessConfig;
|
|
1169
|
+
store?: BaseLookupStore;
|
|
1170
|
+
}>;
|
|
1171
|
+
/**
|
|
1172
|
+
* A candidate source (design §4): proposes pairs (recall) + definitional forced
|
|
1173
|
+
* edges, and supplies the base members it pulled into scope. The `id` attributes
|
|
1174
|
+
* a source in reports and pins its determinism level (§7).
|
|
1175
|
+
*/
|
|
1176
|
+
type CandidateSource = Readonly<{
|
|
1177
|
+
readonly id: string;
|
|
1178
|
+
generate(scope: SourceScope): Promise<SourceResult>;
|
|
1179
|
+
}>;
|
|
1180
|
+
/**
|
|
1181
|
+
* The bounded coarse source for the NO-KEY case (design §6.2, `keyless`): which
|
|
1182
|
+
* unblocked nodes to compare, and how far. `window` is the forward-neighbour count;
|
|
1183
|
+
* `sortFields` is the kind's similarity text fields, so the SORTED-NEIGHBOURHOOD sort
|
|
1184
|
+
* groups lexically-similar values adjacently (the same text the scorer reads).
|
|
1185
|
+
*/
|
|
1186
|
+
type KeylessConfig = Readonly<{
|
|
1187
|
+
window: number;
|
|
1188
|
+
sortFields: readonly string[];
|
|
1189
|
+
}>;
|
|
1190
|
+
|
|
1191
|
+
export { type BaseAmbiguity, type BaseNodeLookup, type BaseVersion, BaseVersionMismatchError, BranchError, type BranchId, type BranchOptions, type BranchProvenance, type CandidateSource, type ComparisonCeilingPolicy, type ConflictingValue, type DeleteModifyConflict, type DeleteModifyPolicy, type DroppedItem, type Embedder, type EntityResolution, type GraphBranch, type KeylessConfig, MERGE_ERROR_CODES, MERGE_OPTION_DEFAULTS, type MakeBackend, MergeConflictError, MergeError, type MergeIncrementalArgs, type MergeOptions, type MergeReport, type NormalizedMergeOptions, type PropertyConflict, type PropertyConflictPolicy, type ProvenanceGraph, type ProvenanceIndex, type ProvenanceNode, type ProvenanceQuery, type ProvenanceRecord, type ReconcileTypesMode, type ResolveConfig, type ResolveMap, type ResolvedCluster, Result, type SimilarityStrategy, SimilarityUnavailableError, type SourceScope, type TypeReconciliation, type WorkingCopyStrategy, asBaseVersion, asBranchId, branch, cloneWorkingCopyStrategy, computeBaseVersion, merge, mergeIncremental, normalizeMergeOptions, openProvenanceStore, persistProvenanceRecords, provenanceGraphId, readProvenance };
|