@unrdf/kgc-runtime 26.4.2
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/IMPLEMENTATION_SUMMARY.json +150 -0
- package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
- package/README.md +98 -0
- package/TRANSACTION_IMPLEMENTATION.json +119 -0
- package/capability-map.md +93 -0
- package/docs/api-stability.md +269 -0
- package/docs/extensions/plugin-development.md +382 -0
- package/package.json +40 -0
- package/plugins/registry.json +35 -0
- package/src/admission-gate.mjs +414 -0
- package/src/api-version.mjs +373 -0
- package/src/atomic-admission.mjs +310 -0
- package/src/bounds.mjs +289 -0
- package/src/bulkhead-manager.mjs +280 -0
- package/src/capsule.mjs +524 -0
- package/src/crdt.mjs +361 -0
- package/src/enhanced-bounds.mjs +614 -0
- package/src/executor.mjs +73 -0
- package/src/freeze-restore.mjs +521 -0
- package/src/index.mjs +62 -0
- package/src/materialized-views.mjs +371 -0
- package/src/merge.mjs +472 -0
- package/src/plugin-isolation.mjs +392 -0
- package/src/plugin-manager.mjs +441 -0
- package/src/projections-api.mjs +336 -0
- package/src/projections-cli.mjs +238 -0
- package/src/projections-docs.mjs +300 -0
- package/src/projections-ide.mjs +278 -0
- package/src/receipt.mjs +340 -0
- package/src/rollback.mjs +258 -0
- package/src/saga-orchestrator.mjs +355 -0
- package/src/schemas.mjs +1330 -0
- package/src/storage-optimization.mjs +359 -0
- package/src/tool-registry.mjs +272 -0
- package/src/transaction.mjs +466 -0
- package/src/validators.mjs +485 -0
- package/src/work-item.mjs +449 -0
- package/templates/plugin-template/README.md +58 -0
- package/templates/plugin-template/index.mjs +162 -0
- package/templates/plugin-template/plugin.json +19 -0
- package/test/admission-gate.test.mjs +583 -0
- package/test/api-version.test.mjs +74 -0
- package/test/atomic-admission.test.mjs +155 -0
- package/test/bounds.test.mjs +341 -0
- package/test/bulkhead-manager.test.mjs +236 -0
- package/test/capsule.test.mjs +625 -0
- package/test/crdt.test.mjs +215 -0
- package/test/enhanced-bounds.test.mjs +487 -0
- package/test/freeze-restore.test.mjs +472 -0
- package/test/materialized-views.test.mjs +243 -0
- package/test/merge.test.mjs +665 -0
- package/test/plugin-isolation.test.mjs +109 -0
- package/test/plugin-manager.test.mjs +208 -0
- package/test/projections-api.test.mjs +293 -0
- package/test/projections-cli.test.mjs +204 -0
- package/test/projections-docs.test.mjs +173 -0
- package/test/projections-ide.test.mjs +230 -0
- package/test/receipt.test.mjs +295 -0
- package/test/rollback.test.mjs +132 -0
- package/test/saga-orchestrator.test.mjs +279 -0
- package/test/schemas.test.mjs +716 -0
- package/test/storage-optimization.test.mjs +503 -0
- package/test/tool-registry.test.mjs +341 -0
- package/test/transaction.test.mjs +189 -0
- package/test/validators.test.mjs +463 -0
- package/test/work-item.test.mjs +548 -0
- package/test/work-item.test.mjs.bak +548 -0
- package/var/kgc/test-atomic-log.json +519 -0
- package/var/kgc/test-cascading-log.json +145 -0
- package/vitest.config.mjs +18 -0
package/src/crdt.mjs
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CRDT (Conflict-free Replicated Data Types) for KGC Runtime
|
|
3
|
+
* Implements LWW-Register and OR-Set for semantic content merging
|
|
4
|
+
*
|
|
5
|
+
* Pattern: Pure functions + vector clocks + deterministic merge
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Schemas
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Timestamp schema - Lamport timestamp for causality
|
|
16
|
+
*/
|
|
17
|
+
const TimestampSchema = z.object({
|
|
18
|
+
counter: z.number().int().nonnegative(),
|
|
19
|
+
actor: z.string(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* LWW-Register schema - Last Write Wins register
|
|
24
|
+
*/
|
|
25
|
+
const LWWRegisterSchema = z.object({
|
|
26
|
+
value: z.any(),
|
|
27
|
+
timestamp: TimestampSchema,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* OR-Set element schema
|
|
32
|
+
*/
|
|
33
|
+
const ORSetElementSchema = z.object({
|
|
34
|
+
value: z.any(),
|
|
35
|
+
addTimestamp: TimestampSchema,
|
|
36
|
+
removeTimestamp: TimestampSchema.optional().nullable(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* OR-Set schema - Observed-Remove set
|
|
41
|
+
*/
|
|
42
|
+
const ORSetSchema = z.object({
|
|
43
|
+
elements: z.array(ORSetElementSchema),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {z.infer<typeof LWWRegisterSchema>} LWWRegister
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {z.infer<typeof ORSetSchema>} ORSet
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {z.infer<typeof TimestampSchema>} Timestamp
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// CRDT Operations
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a Lamport timestamp
|
|
64
|
+
*
|
|
65
|
+
* @param {number} counter - Logical clock counter
|
|
66
|
+
* @param {string} actor - Actor ID
|
|
67
|
+
* @returns {Timestamp} Lamport timestamp
|
|
68
|
+
*/
|
|
69
|
+
export function createTimestamp(counter, actor) {
|
|
70
|
+
return TimestampSchema.parse({ counter, actor });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Compare two timestamps (returns -1, 0, 1)
|
|
75
|
+
*
|
|
76
|
+
* @param {Timestamp} t1 - First timestamp
|
|
77
|
+
* @param {Timestamp} t2 - Second timestamp
|
|
78
|
+
* @returns {number} Comparison result
|
|
79
|
+
*/
|
|
80
|
+
export function compareTimestamps(t1, t2) {
|
|
81
|
+
if (t1.counter !== t2.counter) {
|
|
82
|
+
return t1.counter - t2.counter;
|
|
83
|
+
}
|
|
84
|
+
return t1.actor.localeCompare(t2.actor);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// LWW-Register (Last Write Wins)
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create LWW-Register
|
|
93
|
+
*
|
|
94
|
+
* @param {any} value - Initial value
|
|
95
|
+
* @param {Timestamp} timestamp - Initial timestamp
|
|
96
|
+
* @returns {LWWRegister} LWW-Register
|
|
97
|
+
*/
|
|
98
|
+
export function createLWWRegister(value, timestamp) {
|
|
99
|
+
return LWWRegisterSchema.parse({ value, timestamp });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Update LWW-Register value
|
|
104
|
+
*
|
|
105
|
+
* @param {LWWRegister} register - Register to update
|
|
106
|
+
* @param {any} newValue - New value
|
|
107
|
+
* @param {Timestamp} timestamp - Update timestamp
|
|
108
|
+
* @returns {LWWRegister} Updated register
|
|
109
|
+
*/
|
|
110
|
+
export function updateLWWRegister(register, newValue, timestamp) {
|
|
111
|
+
const validated = LWWRegisterSchema.parse(register);
|
|
112
|
+
|
|
113
|
+
// Only update if new timestamp is later
|
|
114
|
+
if (compareTimestamps(timestamp, validated.timestamp) > 0) {
|
|
115
|
+
return createLWWRegister(newValue, timestamp);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return validated;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Merge two LWW-Registers (last write wins)
|
|
123
|
+
*
|
|
124
|
+
* @param {LWWRegister} r1 - First register
|
|
125
|
+
* @param {LWWRegister} r2 - Second register
|
|
126
|
+
* @returns {LWWRegister} Merged register
|
|
127
|
+
*/
|
|
128
|
+
export function mergeLWWRegisters(r1, r2) {
|
|
129
|
+
const reg1 = LWWRegisterSchema.parse(r1);
|
|
130
|
+
const reg2 = LWWRegisterSchema.parse(r2);
|
|
131
|
+
|
|
132
|
+
// Compare timestamps - later wins
|
|
133
|
+
const cmp = compareTimestamps(reg1.timestamp, reg2.timestamp);
|
|
134
|
+
|
|
135
|
+
if (cmp > 0) {
|
|
136
|
+
return reg1;
|
|
137
|
+
} else if (cmp < 0) {
|
|
138
|
+
return reg2;
|
|
139
|
+
} else {
|
|
140
|
+
// Same timestamp - use deterministic tiebreaker (lexicographic on value)
|
|
141
|
+
const v1Str = JSON.stringify(reg1.value);
|
|
142
|
+
const v2Str = JSON.stringify(reg2.value);
|
|
143
|
+
return v1Str.localeCompare(v2Str) > 0 ? reg1 : reg2;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// OR-Set (Observed-Remove Set)
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create empty OR-Set
|
|
153
|
+
*
|
|
154
|
+
* @returns {ORSet} Empty OR-Set
|
|
155
|
+
*/
|
|
156
|
+
export function createORSet() {
|
|
157
|
+
return ORSetSchema.parse({ elements: [] });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Add element to OR-Set
|
|
162
|
+
*
|
|
163
|
+
* @param {ORSet} set - OR-Set to modify
|
|
164
|
+
* @param {any} value - Value to add
|
|
165
|
+
* @param {Timestamp} timestamp - Add timestamp
|
|
166
|
+
* @returns {ORSet} Updated OR-Set
|
|
167
|
+
*/
|
|
168
|
+
export function addToORSet(set, value, timestamp) {
|
|
169
|
+
const validated = ORSetSchema.parse(set);
|
|
170
|
+
|
|
171
|
+
// Add new element with timestamp
|
|
172
|
+
const newElement = ORSetElementSchema.parse({
|
|
173
|
+
value,
|
|
174
|
+
addTimestamp: timestamp,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return ORSetSchema.parse({
|
|
178
|
+
elements: [...validated.elements, newElement],
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Remove element from OR-Set
|
|
184
|
+
*
|
|
185
|
+
* @param {ORSet} set - OR-Set to modify
|
|
186
|
+
* @param {any} value - Value to remove
|
|
187
|
+
* @param {Timestamp} timestamp - Remove timestamp
|
|
188
|
+
* @returns {ORSet} Updated OR-Set
|
|
189
|
+
*/
|
|
190
|
+
export function removeFromORSet(set, value, timestamp) {
|
|
191
|
+
const validated = ORSetSchema.parse(set);
|
|
192
|
+
|
|
193
|
+
// Mark elements as removed if they match value
|
|
194
|
+
const updatedElements = validated.elements.map(elem => {
|
|
195
|
+
const valueMatch = JSON.stringify(elem.value) === JSON.stringify(value);
|
|
196
|
+
if (valueMatch && !elem.removeTimestamp) {
|
|
197
|
+
return { ...elem, removeTimestamp: timestamp };
|
|
198
|
+
}
|
|
199
|
+
return elem;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return ORSetSchema.parse({ elements: updatedElements });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get current values in OR-Set (not removed)
|
|
207
|
+
*
|
|
208
|
+
* @param {ORSet} set - OR-Set
|
|
209
|
+
* @returns {any[]} Active values
|
|
210
|
+
*/
|
|
211
|
+
export function getORSetValues(set) {
|
|
212
|
+
const validated = ORSetSchema.parse(set);
|
|
213
|
+
|
|
214
|
+
// Return values that are not removed
|
|
215
|
+
return validated.elements
|
|
216
|
+
.filter(elem => !elem.removeTimestamp)
|
|
217
|
+
.map(elem => elem.value);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Merge two OR-Sets
|
|
222
|
+
*
|
|
223
|
+
* @param {ORSet} s1 - First OR-Set
|
|
224
|
+
* @param {ORSet} s2 - Second OR-Set
|
|
225
|
+
* @returns {ORSet} Merged OR-Set
|
|
226
|
+
*/
|
|
227
|
+
export function mergeORSets(s1, s2) {
|
|
228
|
+
const set1 = ORSetSchema.parse(s1);
|
|
229
|
+
const set2 = ORSetSchema.parse(s2);
|
|
230
|
+
|
|
231
|
+
// Combine all elements
|
|
232
|
+
const allElements = [...set1.elements, ...set2.elements];
|
|
233
|
+
|
|
234
|
+
// Group by value
|
|
235
|
+
const valueMap = new Map();
|
|
236
|
+
|
|
237
|
+
for (const elem of allElements) {
|
|
238
|
+
const key = JSON.stringify(elem.value);
|
|
239
|
+
|
|
240
|
+
if (!valueMap.has(key)) {
|
|
241
|
+
valueMap.set(key, []);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
valueMap.get(key).push(elem);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Merge each group
|
|
248
|
+
const mergedElements = [];
|
|
249
|
+
|
|
250
|
+
for (const [, elems] of valueMap) {
|
|
251
|
+
// Keep all unique add timestamps
|
|
252
|
+
const addTimestamps = new Map();
|
|
253
|
+
for (const elem of elems) {
|
|
254
|
+
const key = `${elem.addTimestamp.counter}_${elem.addTimestamp.actor}`;
|
|
255
|
+
if (!addTimestamps.has(key)) {
|
|
256
|
+
addTimestamps.set(key, elem);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Find latest remove timestamp
|
|
261
|
+
let latestRemove = null;
|
|
262
|
+
for (const elem of elems) {
|
|
263
|
+
if (elem.removeTimestamp) {
|
|
264
|
+
if (!latestRemove || compareTimestamps(elem.removeTimestamp, latestRemove) > 0) {
|
|
265
|
+
latestRemove = elem.removeTimestamp;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Keep elements where add timestamp > remove timestamp
|
|
271
|
+
for (const [, elem] of addTimestamps) {
|
|
272
|
+
if (!latestRemove || compareTimestamps(elem.addTimestamp, latestRemove) > 0) {
|
|
273
|
+
mergedElements.push({
|
|
274
|
+
value: elem.value,
|
|
275
|
+
addTimestamp: elem.addTimestamp,
|
|
276
|
+
removeTimestamp: latestRemove,
|
|
277
|
+
});
|
|
278
|
+
} else {
|
|
279
|
+
// Element was removed after being added
|
|
280
|
+
mergedElements.push({
|
|
281
|
+
value: elem.value,
|
|
282
|
+
addTimestamp: elem.addTimestamp,
|
|
283
|
+
removeTimestamp: latestRemove,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return ORSetSchema.parse({ elements: mergedElements });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// Three-way Merge with Common Ancestor
|
|
294
|
+
// ============================================================================
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Three-way merge for LWW-Registers
|
|
298
|
+
*
|
|
299
|
+
* @param {LWWRegister} ancestor - Common ancestor state
|
|
300
|
+
* @param {LWWRegister} left - Left branch
|
|
301
|
+
* @param {LWWRegister} right - Right branch
|
|
302
|
+
* @returns {LWWRegister} Merged state
|
|
303
|
+
*/
|
|
304
|
+
export function threeWayMergeLWW(ancestor, left, right) {
|
|
305
|
+
// If both branches modified, use LWW merge
|
|
306
|
+
const ancestorValidated = LWWRegisterSchema.parse(ancestor);
|
|
307
|
+
const leftValidated = LWWRegisterSchema.parse(left);
|
|
308
|
+
const rightValidated = LWWRegisterSchema.parse(right);
|
|
309
|
+
|
|
310
|
+
// Check if either branch is unchanged from ancestor
|
|
311
|
+
const leftUnchanged = compareTimestamps(leftValidated.timestamp, ancestorValidated.timestamp) === 0;
|
|
312
|
+
const rightUnchanged = compareTimestamps(rightValidated.timestamp, ancestorValidated.timestamp) === 0;
|
|
313
|
+
|
|
314
|
+
if (leftUnchanged && rightUnchanged) {
|
|
315
|
+
// No changes - return ancestor
|
|
316
|
+
return ancestorValidated;
|
|
317
|
+
} else if (leftUnchanged) {
|
|
318
|
+
// Only right changed
|
|
319
|
+
return rightValidated;
|
|
320
|
+
} else if (rightUnchanged) {
|
|
321
|
+
// Only left changed
|
|
322
|
+
return leftValidated;
|
|
323
|
+
} else {
|
|
324
|
+
// Both changed - use LWW merge
|
|
325
|
+
return mergeLWWRegisters(leftValidated, rightValidated);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Three-way merge for OR-Sets
|
|
331
|
+
*
|
|
332
|
+
* @param {ORSet} ancestor - Common ancestor state
|
|
333
|
+
* @param {ORSet} left - Left branch
|
|
334
|
+
* @param {ORSet} right - Right branch
|
|
335
|
+
* @returns {ORSet} Merged state
|
|
336
|
+
*/
|
|
337
|
+
export function threeWayMergeORSet(ancestor, left, right) {
|
|
338
|
+
// Merge left and right branches
|
|
339
|
+
const merged = mergeORSets(left, right);
|
|
340
|
+
|
|
341
|
+
return merged;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// Exports
|
|
346
|
+
// ============================================================================
|
|
347
|
+
|
|
348
|
+
export default {
|
|
349
|
+
createTimestamp,
|
|
350
|
+
compareTimestamps,
|
|
351
|
+
createLWWRegister,
|
|
352
|
+
updateLWWRegister,
|
|
353
|
+
mergeLWWRegisters,
|
|
354
|
+
threeWayMergeLWW,
|
|
355
|
+
createORSet,
|
|
356
|
+
addToORSet,
|
|
357
|
+
removeFromORSet,
|
|
358
|
+
getORSetValues,
|
|
359
|
+
mergeORSets,
|
|
360
|
+
threeWayMergeORSet,
|
|
361
|
+
};
|