eventmodeler 0.3.2 → 0.3.4
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/dist/index.js +96 -0
- package/dist/lib/diff/merge-rules.d.ts +45 -0
- package/dist/lib/diff/merge-rules.js +210 -0
- package/dist/lib/diff/model-differ.d.ts +8 -0
- package/dist/lib/diff/model-differ.js +568 -0
- package/dist/lib/diff/three-way-merge.d.ts +7 -0
- package/dist/lib/diff/three-way-merge.js +390 -0
- package/dist/lib/diff/types.d.ts +75 -0
- package/dist/lib/diff/types.js +1 -0
- package/dist/lib/element-lookup.d.ts +9 -0
- package/dist/lib/element-lookup.js +9 -0
- package/dist/lib/file-loader.d.ts +3 -0
- package/dist/lib/file-loader.js +24 -0
- package/dist/lib/flow-utils.d.ts +1 -0
- package/dist/lib/flow-utils.js +63 -47
- package/dist/slices/add-field/index.js +7 -4
- package/dist/slices/add-scenario/index.js +6 -6
- package/dist/slices/create-flow/index.js +9 -9
- package/dist/slices/diff/index.d.ts +11 -0
- package/dist/slices/diff/index.js +293 -0
- package/dist/slices/git/index.d.ts +2 -0
- package/dist/slices/git/index.js +125 -0
- package/dist/slices/merge/index.d.ts +19 -0
- package/dist/slices/merge/index.js +147 -0
- package/dist/slices/remove-field/index.js +7 -4
- package/dist/slices/show-completeness/index.js +4 -3
- package/dist/slices/show-slice/index.js +31 -78
- package/dist/slices/update-field/index.js +7 -4
- package/package.json +1 -1
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { diffModels } from './model-differ.js';
|
|
2
|
+
import { resolvePropertyConflict, isLayoutOnlyChange, categorizeFieldChanges, } from './merge-rules.js';
|
|
3
|
+
function groupChangesByEntity(diff) {
|
|
4
|
+
const entityChanges = new Map();
|
|
5
|
+
const flowChanges = new Map();
|
|
6
|
+
for (const change of diff.entityChanges) {
|
|
7
|
+
entityChanges.set(change.entityId, change);
|
|
8
|
+
}
|
|
9
|
+
for (const change of diff.flowChanges) {
|
|
10
|
+
flowChanges.set(change.flowId, change);
|
|
11
|
+
}
|
|
12
|
+
return { entityChanges, flowChanges };
|
|
13
|
+
}
|
|
14
|
+
// Check if two entity changes are semantically identical
|
|
15
|
+
function changesAreIdentical(a, b) {
|
|
16
|
+
if (a.changeType !== b.changeType)
|
|
17
|
+
return false;
|
|
18
|
+
if (a.entityType !== b.entityType)
|
|
19
|
+
return false;
|
|
20
|
+
if (a.changeType === 'added' || a.changeType === 'removed') {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
const aProps = JSON.stringify(a.propertyChanges ?? []);
|
|
24
|
+
const bProps = JSON.stringify(b.propertyChanges ?? []);
|
|
25
|
+
const aFields = JSON.stringify(a.fieldChanges ?? []);
|
|
26
|
+
const bFields = JSON.stringify(b.fieldChanges ?? []);
|
|
27
|
+
return aProps === bProps && aFields === bFields;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Analyze two modifications and determine conflicts.
|
|
31
|
+
*/
|
|
32
|
+
function analyzeModificationConflicts(oursChange, theirsChange, strategy) {
|
|
33
|
+
const conflicts = [];
|
|
34
|
+
const autoResolved = [];
|
|
35
|
+
let hasHardConflict = false;
|
|
36
|
+
const entityType = oursChange.entityType;
|
|
37
|
+
const entityId = oursChange.entityId;
|
|
38
|
+
const entityName = oursChange.entityName;
|
|
39
|
+
// Analyze property conflicts
|
|
40
|
+
const oursProps = new Map((oursChange.propertyChanges ?? []).map(p => [p.property, p]));
|
|
41
|
+
const theirsProps = new Map((theirsChange.propertyChanges ?? []).map(p => [p.property, p]));
|
|
42
|
+
// Properties only in ours or theirs: auto-merge
|
|
43
|
+
for (const [prop, change] of oursProps) {
|
|
44
|
+
if (!theirsProps.has(prop)) {
|
|
45
|
+
autoResolved.push({
|
|
46
|
+
entityType,
|
|
47
|
+
entityId,
|
|
48
|
+
entityName,
|
|
49
|
+
action: `property-${prop}-from-ours`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const [prop, change] of theirsProps) {
|
|
54
|
+
if (!oursProps.has(prop)) {
|
|
55
|
+
autoResolved.push({
|
|
56
|
+
entityType,
|
|
57
|
+
entityId,
|
|
58
|
+
entityName,
|
|
59
|
+
action: `property-${prop}-from-theirs`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Properties in both: check for conflicts
|
|
64
|
+
for (const [prop, oursProp] of oursProps) {
|
|
65
|
+
const theirsProp = theirsProps.get(prop);
|
|
66
|
+
if (!theirsProp)
|
|
67
|
+
continue;
|
|
68
|
+
const resolution = resolvePropertyConflict(prop, oursProp.newValue, theirsProp.newValue, entityType);
|
|
69
|
+
if (resolution.type === 'auto') {
|
|
70
|
+
autoResolved.push({
|
|
71
|
+
entityType,
|
|
72
|
+
entityId,
|
|
73
|
+
entityName,
|
|
74
|
+
action: resolution.action,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
else if (resolution.type === 'hard') {
|
|
78
|
+
if (strategy) {
|
|
79
|
+
autoResolved.push({
|
|
80
|
+
entityType,
|
|
81
|
+
entityId,
|
|
82
|
+
entityName,
|
|
83
|
+
action: `${prop}-resolved-${strategy}`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
hasHardConflict = true;
|
|
88
|
+
conflicts.push({
|
|
89
|
+
entityType,
|
|
90
|
+
entityId,
|
|
91
|
+
entityName,
|
|
92
|
+
reason: resolution.reason,
|
|
93
|
+
property: prop,
|
|
94
|
+
oursValue: oursProp.newValue,
|
|
95
|
+
theirsValue: theirsProp.newValue,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Analyze field conflicts
|
|
101
|
+
const oursFields = oursChange.fieldChanges ?? [];
|
|
102
|
+
const theirsFields = theirsChange.fieldChanges ?? [];
|
|
103
|
+
if (oursFields.length > 0 || theirsFields.length > 0) {
|
|
104
|
+
const { oursOnly, theirsOnly, conflicts: fieldConflicts } = categorizeFieldChanges(oursFields, theirsFields);
|
|
105
|
+
// Fields only in one branch: auto-merge
|
|
106
|
+
for (const field of oursOnly) {
|
|
107
|
+
autoResolved.push({
|
|
108
|
+
entityType,
|
|
109
|
+
entityId,
|
|
110
|
+
entityName,
|
|
111
|
+
action: `field-${field.fieldPath}-from-ours`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
for (const field of theirsOnly) {
|
|
115
|
+
autoResolved.push({
|
|
116
|
+
entityType,
|
|
117
|
+
entityId,
|
|
118
|
+
entityName,
|
|
119
|
+
action: `field-${field.fieldPath}-from-theirs`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Field conflicts
|
|
123
|
+
for (const { ours, theirs, resolution } of fieldConflicts) {
|
|
124
|
+
if (resolution.type === 'auto') {
|
|
125
|
+
autoResolved.push({
|
|
126
|
+
entityType,
|
|
127
|
+
entityId,
|
|
128
|
+
entityName,
|
|
129
|
+
action: `field-${ours.fieldPath}-${resolution.action}`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else if (resolution.type === 'hard') {
|
|
133
|
+
if (strategy) {
|
|
134
|
+
autoResolved.push({
|
|
135
|
+
entityType,
|
|
136
|
+
entityId,
|
|
137
|
+
entityName,
|
|
138
|
+
action: `field-${ours.fieldPath}-resolved-${strategy}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
hasHardConflict = true;
|
|
143
|
+
conflicts.push({
|
|
144
|
+
entityType,
|
|
145
|
+
entityId,
|
|
146
|
+
entityName,
|
|
147
|
+
reason: resolution.reason,
|
|
148
|
+
property: `field:${ours.fieldPath}`,
|
|
149
|
+
oursValue: ours.newValue ?? ours.oldValue,
|
|
150
|
+
theirsValue: theirs.newValue ?? theirs.oldValue,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { conflicts, autoResolved, hasHardConflict };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Perform a three-way merge of event models.
|
|
160
|
+
*/
|
|
161
|
+
export function threeWayMerge(baseModel, oursModel, theirsModel, baseEvents, oursEvents, theirsEvents, strategy) {
|
|
162
|
+
const conflicts = [];
|
|
163
|
+
const autoResolved = [];
|
|
164
|
+
// Compute diffs from base
|
|
165
|
+
const oursDiff = diffModels(baseModel, oursModel);
|
|
166
|
+
const theirsDiff = diffModels(baseModel, theirsModel);
|
|
167
|
+
const oursChanges = groupChangesByEntity(oursDiff);
|
|
168
|
+
const theirsChanges = groupChangesByEntity(theirsDiff);
|
|
169
|
+
const baseTimestamp = baseEvents.length > 0
|
|
170
|
+
? Math.max(...baseEvents.map((e) => e.timestamp))
|
|
171
|
+
: 0;
|
|
172
|
+
const oursNewEvents = oursEvents.filter((e) => e.timestamp > baseTimestamp);
|
|
173
|
+
const theirsNewEvents = theirsEvents.filter((e) => e.timestamp > baseTimestamp);
|
|
174
|
+
const conflictingEntityIds = new Set();
|
|
175
|
+
const conflictingFlowIds = new Set();
|
|
176
|
+
// Analyze entity changes
|
|
177
|
+
const allEntityIds = new Set([
|
|
178
|
+
...oursChanges.entityChanges.keys(),
|
|
179
|
+
...theirsChanges.entityChanges.keys(),
|
|
180
|
+
]);
|
|
181
|
+
for (const entityId of allEntityIds) {
|
|
182
|
+
const oursChange = oursChanges.entityChanges.get(entityId);
|
|
183
|
+
const theirsChange = theirsChanges.entityChanges.get(entityId);
|
|
184
|
+
// Only one side changed - no conflict
|
|
185
|
+
if (!oursChange || !theirsChange) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// Both sides made the same change
|
|
189
|
+
if (changesAreIdentical(oursChange, theirsChange)) {
|
|
190
|
+
autoResolved.push({
|
|
191
|
+
entityType: oursChange.entityType,
|
|
192
|
+
entityId,
|
|
193
|
+
entityName: oursChange.entityName,
|
|
194
|
+
action: `both-${oursChange.changeType}-identically`,
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// Layout-only changes: auto-resolve with theirs
|
|
199
|
+
if (isLayoutOnlyChange(oursChange) && isLayoutOnlyChange(theirsChange)) {
|
|
200
|
+
autoResolved.push({
|
|
201
|
+
entityType: oursChange.entityType,
|
|
202
|
+
entityId,
|
|
203
|
+
entityName: oursChange.entityName,
|
|
204
|
+
action: 'layout-only-use-theirs',
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// One removed, other modified
|
|
209
|
+
if ((oursChange.changeType === 'removed' && theirsChange.changeType === 'modified') ||
|
|
210
|
+
(oursChange.changeType === 'modified' && theirsChange.changeType === 'removed')) {
|
|
211
|
+
if (strategy) {
|
|
212
|
+
autoResolved.push({
|
|
213
|
+
entityType: oursChange.entityType,
|
|
214
|
+
entityId,
|
|
215
|
+
entityName: oursChange.entityName,
|
|
216
|
+
action: `delete-vs-modify-resolved-${strategy}`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
conflictingEntityIds.add(entityId);
|
|
221
|
+
conflicts.push({
|
|
222
|
+
entityType: oursChange.entityType,
|
|
223
|
+
entityId,
|
|
224
|
+
entityName: oursChange.entityName,
|
|
225
|
+
reason: oursChange.changeType === 'removed'
|
|
226
|
+
? 'Ours deleted, theirs modified'
|
|
227
|
+
: 'Ours modified, theirs deleted',
|
|
228
|
+
oursValue: oursChange.changeType,
|
|
229
|
+
theirsValue: theirsChange.changeType,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
// Both modified - detailed analysis
|
|
235
|
+
if (oursChange.changeType === 'modified' && theirsChange.changeType === 'modified') {
|
|
236
|
+
const analysis = analyzeModificationConflicts(oursChange, theirsChange, strategy);
|
|
237
|
+
autoResolved.push(...analysis.autoResolved);
|
|
238
|
+
conflicts.push(...analysis.conflicts);
|
|
239
|
+
if (analysis.hasHardConflict) {
|
|
240
|
+
conflictingEntityIds.add(entityId);
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
// Both added with same ID
|
|
245
|
+
if (oursChange.changeType === 'added' && theirsChange.changeType === 'added') {
|
|
246
|
+
if (strategy) {
|
|
247
|
+
autoResolved.push({
|
|
248
|
+
entityType: oursChange.entityType,
|
|
249
|
+
entityId,
|
|
250
|
+
entityName: oursChange.entityName,
|
|
251
|
+
action: `both-added-resolved-${strategy}`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
conflictingEntityIds.add(entityId);
|
|
256
|
+
conflicts.push({
|
|
257
|
+
entityType: oursChange.entityType,
|
|
258
|
+
entityId,
|
|
259
|
+
entityName: oursChange.entityName,
|
|
260
|
+
reason: 'Both added entity with same ID',
|
|
261
|
+
oursValue: oursChange.entityName,
|
|
262
|
+
theirsValue: theirsChange.entityName,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Analyze flow changes
|
|
268
|
+
const allFlowIds = new Set([
|
|
269
|
+
...oursChanges.flowChanges.keys(),
|
|
270
|
+
...theirsChanges.flowChanges.keys(),
|
|
271
|
+
]);
|
|
272
|
+
for (const flowId of allFlowIds) {
|
|
273
|
+
const oursChange = oursChanges.flowChanges.get(flowId);
|
|
274
|
+
const theirsChange = theirsChanges.flowChanges.get(flowId);
|
|
275
|
+
if (!oursChange || !theirsChange)
|
|
276
|
+
continue;
|
|
277
|
+
// Both added same flow
|
|
278
|
+
if (oursChange.changeType === 'added' && theirsChange.changeType === 'added') {
|
|
279
|
+
autoResolved.push({
|
|
280
|
+
entityType: 'flow',
|
|
281
|
+
entityId: flowId,
|
|
282
|
+
entityName: `${oursChange.sourceName} → ${oursChange.targetName}`,
|
|
283
|
+
action: 'both-added-flow-keep-one',
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// Both removed
|
|
288
|
+
if (oursChange.changeType === 'removed' && theirsChange.changeType === 'removed') {
|
|
289
|
+
autoResolved.push({
|
|
290
|
+
entityType: 'flow',
|
|
291
|
+
entityId: flowId,
|
|
292
|
+
entityName: `${oursChange.sourceName} → ${oursChange.targetName}`,
|
|
293
|
+
action: 'both-removed-identically',
|
|
294
|
+
});
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
// One removed, other modified
|
|
298
|
+
if ((oursChange.changeType === 'removed' && theirsChange.changeType === 'modified') ||
|
|
299
|
+
(oursChange.changeType === 'modified' && theirsChange.changeType === 'removed')) {
|
|
300
|
+
if (strategy) {
|
|
301
|
+
autoResolved.push({
|
|
302
|
+
entityType: 'flow',
|
|
303
|
+
entityId: flowId,
|
|
304
|
+
entityName: `${oursChange.sourceName} → ${oursChange.targetName}`,
|
|
305
|
+
action: `flow-delete-vs-modify-resolved-${strategy}`,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
conflictingFlowIds.add(flowId);
|
|
310
|
+
conflicts.push({
|
|
311
|
+
entityType: 'flow',
|
|
312
|
+
entityId: flowId,
|
|
313
|
+
entityName: `${oursChange.sourceName} → ${oursChange.targetName}`,
|
|
314
|
+
reason: oursChange.changeType === 'removed'
|
|
315
|
+
? 'Ours removed flow, theirs modified mappings'
|
|
316
|
+
: 'Ours modified mappings, theirs removed flow',
|
|
317
|
+
oursValue: oursChange.changeType,
|
|
318
|
+
theirsValue: theirsChange.changeType,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
// Both modified mappings
|
|
324
|
+
if (oursChange.changeType === 'modified' && theirsChange.changeType === 'modified') {
|
|
325
|
+
if (strategy) {
|
|
326
|
+
autoResolved.push({
|
|
327
|
+
entityType: 'flow',
|
|
328
|
+
entityId: flowId,
|
|
329
|
+
entityName: `${oursChange.sourceName} → ${oursChange.targetName}`,
|
|
330
|
+
action: `flow-mappings-resolved-${strategy}`,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
conflictingFlowIds.add(flowId);
|
|
335
|
+
conflicts.push({
|
|
336
|
+
entityType: 'flow',
|
|
337
|
+
entityId: flowId,
|
|
338
|
+
entityName: `${oursChange.sourceName} → ${oursChange.targetName}`,
|
|
339
|
+
reason: 'Both modified flow field mappings',
|
|
340
|
+
oursValue: oursChange.mappingChanges,
|
|
341
|
+
theirsValue: theirsChange.mappingChanges,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Build merged events
|
|
347
|
+
const mergedEvents = [...baseEvents];
|
|
348
|
+
const affectsConflictingEntity = (event) => {
|
|
349
|
+
const idFields = [
|
|
350
|
+
'commandStickyId', 'eventStickyId', 'readModelStickyId',
|
|
351
|
+
'screenId', 'processorId', 'sliceId', 'chapterId',
|
|
352
|
+
'aggregateId', 'actorId', 'scenarioId', 'flowId',
|
|
353
|
+
];
|
|
354
|
+
for (const field of idFields) {
|
|
355
|
+
const id = event[field];
|
|
356
|
+
if (id && (conflictingEntityIds.has(id) || conflictingFlowIds.has(id))) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
};
|
|
362
|
+
// Add non-conflicting events from ours
|
|
363
|
+
for (const event of oursNewEvents) {
|
|
364
|
+
if (!affectsConflictingEntity(event)) {
|
|
365
|
+
mergedEvents.push(event);
|
|
366
|
+
}
|
|
367
|
+
else if (strategy === 'ours') {
|
|
368
|
+
mergedEvents.push(event);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// Add non-conflicting events from theirs
|
|
372
|
+
const oursEventKeys = new Set(oursNewEvents.map(e => `${e.type}:${JSON.stringify(e)}`));
|
|
373
|
+
for (const event of theirsNewEvents) {
|
|
374
|
+
const eventKey = `${event.type}:${JSON.stringify(event)}`;
|
|
375
|
+
if (oursEventKeys.has(eventKey))
|
|
376
|
+
continue;
|
|
377
|
+
if (!affectsConflictingEntity(event)) {
|
|
378
|
+
mergedEvents.push(event);
|
|
379
|
+
}
|
|
380
|
+
else if (strategy === 'theirs') {
|
|
381
|
+
mergedEvents.push(event);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
success: conflicts.length === 0,
|
|
386
|
+
conflicts,
|
|
387
|
+
autoResolved,
|
|
388
|
+
mergedEvents,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { RawEvent, Field } from '../../types.js';
|
|
2
|
+
export type EntityType = 'command' | 'event' | 'readModel' | 'screen' | 'processor' | 'slice' | 'chapter' | 'aggregate' | 'actor' | 'scenario' | 'flow';
|
|
3
|
+
export type ChangeType = 'added' | 'removed' | 'modified';
|
|
4
|
+
export interface FieldChange {
|
|
5
|
+
fieldPath: string;
|
|
6
|
+
changeType: ChangeType;
|
|
7
|
+
oldValue?: Field;
|
|
8
|
+
newValue?: Field;
|
|
9
|
+
}
|
|
10
|
+
export interface EntityChange {
|
|
11
|
+
entityType: EntityType;
|
|
12
|
+
entityId: string;
|
|
13
|
+
entityName: string;
|
|
14
|
+
canonicalId?: string;
|
|
15
|
+
changeType: ChangeType;
|
|
16
|
+
propertyChanges?: PropertyChange[];
|
|
17
|
+
fieldChanges?: FieldChange[];
|
|
18
|
+
}
|
|
19
|
+
export interface PropertyChange {
|
|
20
|
+
property: string;
|
|
21
|
+
oldValue?: unknown;
|
|
22
|
+
newValue?: unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface FlowChange {
|
|
25
|
+
flowId: string;
|
|
26
|
+
flowType: string;
|
|
27
|
+
changeType: ChangeType;
|
|
28
|
+
sourceName?: string;
|
|
29
|
+
targetName?: string;
|
|
30
|
+
mappingChanges?: FieldMappingChange[];
|
|
31
|
+
}
|
|
32
|
+
export interface FieldMappingChange {
|
|
33
|
+
changeType: ChangeType;
|
|
34
|
+
sourceFieldName?: string;
|
|
35
|
+
targetFieldName?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface DiffResult {
|
|
38
|
+
summary: {
|
|
39
|
+
added: number;
|
|
40
|
+
removed: number;
|
|
41
|
+
modified: number;
|
|
42
|
+
};
|
|
43
|
+
entityChanges: EntityChange[];
|
|
44
|
+
flowChanges: FlowChange[];
|
|
45
|
+
}
|
|
46
|
+
export interface MergeConflict {
|
|
47
|
+
entityType: EntityType;
|
|
48
|
+
entityId: string;
|
|
49
|
+
entityName: string;
|
|
50
|
+
reason: string;
|
|
51
|
+
property?: string;
|
|
52
|
+
baseValue?: unknown;
|
|
53
|
+
oursValue?: unknown;
|
|
54
|
+
theirsValue?: unknown;
|
|
55
|
+
}
|
|
56
|
+
export interface MergeResult {
|
|
57
|
+
success: boolean;
|
|
58
|
+
conflicts: MergeConflict[];
|
|
59
|
+
autoResolved: AutoResolution[];
|
|
60
|
+
mergedEvents: RawEvent[];
|
|
61
|
+
}
|
|
62
|
+
export interface AutoResolution {
|
|
63
|
+
entityType: EntityType;
|
|
64
|
+
entityId: string;
|
|
65
|
+
entityName: string;
|
|
66
|
+
action: string;
|
|
67
|
+
}
|
|
68
|
+
export interface MergeOptions {
|
|
69
|
+
basePath: string;
|
|
70
|
+
oursPath: string;
|
|
71
|
+
theirsPath: string;
|
|
72
|
+
outputPath: string;
|
|
73
|
+
strategy?: 'ours' | 'theirs';
|
|
74
|
+
dryRun?: boolean;
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -10,6 +10,15 @@ export type LookupResult<T> = {
|
|
|
10
10
|
error: 'not_found' | 'ambiguous';
|
|
11
11
|
matches: T[];
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Filter out linked copies from a collection of elements.
|
|
15
|
+
* Linked copies have `originalNodeId` set and should not be considered
|
|
16
|
+
* as separate elements for lookup - they're UI conveniences only.
|
|
17
|
+
*/
|
|
18
|
+
export declare function excludeLinkedCopies<T extends {
|
|
19
|
+
id: string;
|
|
20
|
+
originalNodeId?: string;
|
|
21
|
+
}>(elements: Map<string, T> | T[]): T[];
|
|
13
22
|
/**
|
|
14
23
|
* Find an element by name (fuzzy) or UUID.
|
|
15
24
|
* - If search starts with "id:", treats rest as UUID/UUID prefix
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
* Element lookup utilities for CLI commands.
|
|
3
3
|
* Provides fuzzy lookup with UUID disambiguation for ambiguous matches.
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Filter out linked copies from a collection of elements.
|
|
7
|
+
* Linked copies have `originalNodeId` set and should not be considered
|
|
8
|
+
* as separate elements for lookup - they're UI conveniences only.
|
|
9
|
+
*/
|
|
10
|
+
export function excludeLinkedCopies(elements) {
|
|
11
|
+
const elementArray = Array.isArray(elements) ? elements : [...elements.values()];
|
|
12
|
+
return elementArray.filter(e => !e.originalNodeId);
|
|
13
|
+
}
|
|
5
14
|
/**
|
|
6
15
|
* Normalize a string for fuzzy matching:
|
|
7
16
|
* - lowercase
|
|
@@ -3,3 +3,6 @@ export declare function promptForFile(files: string[]): Promise<string>;
|
|
|
3
3
|
export declare function findEventModelFile(): Promise<string | null>;
|
|
4
4
|
export declare function loadModel(filePath: string): EventModel;
|
|
5
5
|
export declare function appendEvent(filePath: string, event: RawEvent): void;
|
|
6
|
+
export declare function loadRawEvents(filePath: string): RawEvent[];
|
|
7
|
+
export declare function writeEvents(filePath: string, events: RawEvent[]): void;
|
|
8
|
+
export declare function loadModelFromContent(content: string): EventModel;
|
package/dist/lib/file-loader.js
CHANGED
|
@@ -51,3 +51,27 @@ export function loadModel(filePath) {
|
|
|
51
51
|
export function appendEvent(filePath, event) {
|
|
52
52
|
fs.appendFileSync(filePath, JSON.stringify(event) + '\n');
|
|
53
53
|
}
|
|
54
|
+
export function loadRawEvents(filePath) {
|
|
55
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
56
|
+
if (!content.trim()) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return content
|
|
60
|
+
.trim()
|
|
61
|
+
.split('\n')
|
|
62
|
+
.map(line => JSON.parse(line));
|
|
63
|
+
}
|
|
64
|
+
export function writeEvents(filePath, events) {
|
|
65
|
+
const content = events.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
66
|
+
fs.writeFileSync(filePath, content);
|
|
67
|
+
}
|
|
68
|
+
export function loadModelFromContent(content) {
|
|
69
|
+
if (!content.trim()) {
|
|
70
|
+
return projectEvents([]);
|
|
71
|
+
}
|
|
72
|
+
const events = content
|
|
73
|
+
.trim()
|
|
74
|
+
.split('\n')
|
|
75
|
+
.map(line => JSON.parse(line));
|
|
76
|
+
return projectEvents(events);
|
|
77
|
+
}
|
package/dist/lib/flow-utils.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface SliceFlowInfo extends FlowInfo {
|
|
|
26
26
|
name: string;
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
export declare function resolveToCanonical(model: EventModel, id: string): string;
|
|
29
30
|
export declare function getElementInfo(model: EventModel, id: string): ElementInfo | null;
|
|
30
31
|
export declare function getSliceComponentIds(model: EventModel, slice: Slice): Set<string>;
|
|
31
32
|
export declare function findSliceForComponent(model: EventModel, componentId: string): Slice | null;
|