eventmodeler 0.2.4 → 0.2.5
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/projection.js
CHANGED
|
@@ -824,138 +824,6 @@ function applyEvent(model, event) {
|
|
|
824
824
|
case 'ScreenDesignUpdated':
|
|
825
825
|
// Screen designs are visual-only, not relevant for CLI
|
|
826
826
|
break;
|
|
827
|
-
// Compound slice placement events - place entire slices atomically
|
|
828
|
-
case 'StateChangeSlicePlaced': {
|
|
829
|
-
// Create slice
|
|
830
|
-
model.slices.set(event.sliceId, {
|
|
831
|
-
id: event.sliceId,
|
|
832
|
-
name: event.sliceName,
|
|
833
|
-
status: 'created',
|
|
834
|
-
position: event.slicePosition,
|
|
835
|
-
size: event.sliceSize,
|
|
836
|
-
nodeIds: [event.screenId, event.commandId, event.eventId],
|
|
837
|
-
});
|
|
838
|
-
// Create screen
|
|
839
|
-
model.screens.set(event.screenId, {
|
|
840
|
-
id: event.screenId,
|
|
841
|
-
name: event.screenName,
|
|
842
|
-
fields: event.screenFields,
|
|
843
|
-
position: event.screenPosition,
|
|
844
|
-
width: event.screenSize.width,
|
|
845
|
-
height: event.screenSize.height,
|
|
846
|
-
});
|
|
847
|
-
// Create command
|
|
848
|
-
model.commands.set(event.commandId, {
|
|
849
|
-
id: event.commandId,
|
|
850
|
-
name: event.commandName,
|
|
851
|
-
fields: event.commandFields,
|
|
852
|
-
position: event.commandPosition,
|
|
853
|
-
width: event.commandSize.width,
|
|
854
|
-
height: event.commandSize.height,
|
|
855
|
-
});
|
|
856
|
-
// Create event
|
|
857
|
-
model.events.set(event.eventId, {
|
|
858
|
-
id: event.eventId,
|
|
859
|
-
name: event.eventName,
|
|
860
|
-
fields: event.eventFields,
|
|
861
|
-
position: event.eventPosition,
|
|
862
|
-
width: event.eventSize.width,
|
|
863
|
-
height: event.eventSize.height,
|
|
864
|
-
});
|
|
865
|
-
// Create screen->command flow
|
|
866
|
-
model.flows.set(event.screenToCommandFlowId, {
|
|
867
|
-
id: event.screenToCommandFlowId,
|
|
868
|
-
flowType: 'ScreenToCommand',
|
|
869
|
-
sourceId: event.screenId,
|
|
870
|
-
targetId: event.commandId,
|
|
871
|
-
fieldMappings: event.screenToCommandMappings,
|
|
872
|
-
});
|
|
873
|
-
// Create command->event flow
|
|
874
|
-
model.flows.set(event.commandToEventFlowId, {
|
|
875
|
-
id: event.commandToEventFlowId,
|
|
876
|
-
flowType: 'CommandToEvent',
|
|
877
|
-
sourceId: event.commandId,
|
|
878
|
-
targetId: event.eventId,
|
|
879
|
-
fieldMappings: event.commandToEventMappings,
|
|
880
|
-
});
|
|
881
|
-
break;
|
|
882
|
-
}
|
|
883
|
-
case 'AutomationSlicePlaced': {
|
|
884
|
-
// Create slice
|
|
885
|
-
model.slices.set(event.sliceId, {
|
|
886
|
-
id: event.sliceId,
|
|
887
|
-
name: event.sliceName,
|
|
888
|
-
status: 'created',
|
|
889
|
-
position: event.slicePosition,
|
|
890
|
-
size: event.sliceSize,
|
|
891
|
-
nodeIds: [event.processorId, event.commandId, event.eventId],
|
|
892
|
-
});
|
|
893
|
-
// Create processor
|
|
894
|
-
model.processors.set(event.processorId, {
|
|
895
|
-
id: event.processorId,
|
|
896
|
-
name: event.processorName,
|
|
897
|
-
fields: [],
|
|
898
|
-
position: event.processorPosition,
|
|
899
|
-
width: event.processorSize.width,
|
|
900
|
-
height: event.processorSize.height,
|
|
901
|
-
});
|
|
902
|
-
// Create command
|
|
903
|
-
model.commands.set(event.commandId, {
|
|
904
|
-
id: event.commandId,
|
|
905
|
-
name: event.commandName,
|
|
906
|
-
fields: event.commandFields,
|
|
907
|
-
position: event.commandPosition,
|
|
908
|
-
width: event.commandSize.width,
|
|
909
|
-
height: event.commandSize.height,
|
|
910
|
-
});
|
|
911
|
-
// Create event
|
|
912
|
-
model.events.set(event.eventId, {
|
|
913
|
-
id: event.eventId,
|
|
914
|
-
name: event.eventName,
|
|
915
|
-
fields: event.eventFields,
|
|
916
|
-
position: event.eventPosition,
|
|
917
|
-
width: event.eventSize.width,
|
|
918
|
-
height: event.eventSize.height,
|
|
919
|
-
});
|
|
920
|
-
// Create processor->command flow
|
|
921
|
-
model.flows.set(event.processorToCommandFlowId, {
|
|
922
|
-
id: event.processorToCommandFlowId,
|
|
923
|
-
flowType: 'ProcessorToCommand',
|
|
924
|
-
sourceId: event.processorId,
|
|
925
|
-
targetId: event.commandId,
|
|
926
|
-
fieldMappings: [],
|
|
927
|
-
});
|
|
928
|
-
// Create command->event flow
|
|
929
|
-
model.flows.set(event.commandToEventFlowId, {
|
|
930
|
-
id: event.commandToEventFlowId,
|
|
931
|
-
flowType: 'CommandToEvent',
|
|
932
|
-
sourceId: event.commandId,
|
|
933
|
-
targetId: event.eventId,
|
|
934
|
-
fieldMappings: event.commandToEventMappings,
|
|
935
|
-
});
|
|
936
|
-
break;
|
|
937
|
-
}
|
|
938
|
-
case 'StateViewSlicePlaced': {
|
|
939
|
-
// Create slice
|
|
940
|
-
model.slices.set(event.sliceId, {
|
|
941
|
-
id: event.sliceId,
|
|
942
|
-
name: event.sliceName,
|
|
943
|
-
status: 'created',
|
|
944
|
-
position: event.slicePosition,
|
|
945
|
-
size: event.sliceSize,
|
|
946
|
-
nodeIds: [event.readModelId],
|
|
947
|
-
});
|
|
948
|
-
// Create read model
|
|
949
|
-
model.readModels.set(event.readModelId, {
|
|
950
|
-
id: event.readModelId,
|
|
951
|
-
name: event.readModelName,
|
|
952
|
-
fields: event.readModelFields,
|
|
953
|
-
position: event.readModelPosition,
|
|
954
|
-
width: event.readModelSize.width,
|
|
955
|
-
height: event.readModelSize.height,
|
|
956
|
-
});
|
|
957
|
-
break;
|
|
958
|
-
}
|
|
959
827
|
}
|
|
960
828
|
}
|
|
961
829
|
// Merge propagated fields with existing fields:
|
|
@@ -207,15 +207,20 @@ export function addScenario(model, filePath, sliceName, input) {
|
|
|
207
207
|
};
|
|
208
208
|
}
|
|
209
209
|
// Calculate position below slice
|
|
210
|
+
// Scenarios are tall (GWT rows with 100px stickies + labels + padding = ~400px+)
|
|
211
|
+
// so we need adequate spacing between them
|
|
212
|
+
const SCENARIO_GAP = 20; // Match slice gap
|
|
213
|
+
const SCENARIO_ESTIMATED_HEIGHT = 450; // Approximate rendered height for positioning
|
|
210
214
|
const existingScenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
|
|
211
215
|
const sliceBottom = slice.position.y + slice.size.height;
|
|
212
216
|
let positionY;
|
|
213
217
|
if (existingScenarios.length === 0) {
|
|
214
|
-
positionY = sliceBottom +
|
|
218
|
+
positionY = sliceBottom + SCENARIO_GAP;
|
|
215
219
|
}
|
|
216
220
|
else {
|
|
217
|
-
|
|
218
|
-
|
|
221
|
+
// Use estimated height since stored height (80px) doesn't reflect actual rendered size
|
|
222
|
+
const lowestY = Math.max(...existingScenarios.map(s => s.position.y + SCENARIO_ESTIMATED_HEIGHT));
|
|
223
|
+
positionY = lowestY + SCENARIO_GAP;
|
|
219
224
|
}
|
|
220
225
|
const position = {
|
|
221
226
|
x: slice.position.x + 10,
|
|
@@ -23,7 +23,7 @@ function findElementByName(model, name) {
|
|
|
23
23
|
}
|
|
24
24
|
for (const rm of model.readModels.values()) {
|
|
25
25
|
if (rm.id.toLowerCase().startsWith(idSearch)) {
|
|
26
|
-
return { element: { id: rm.id, name: rm.name, fields: rm.fields, type: 'readModel' }, ambiguous: [] };
|
|
26
|
+
return { element: { id: rm.id, name: rm.name, fields: rm.fields, type: 'readModel', canonicalId: rm.canonicalId }, ambiguous: [] };
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
for (const scr of model.screens.values()) {
|
|
@@ -49,7 +49,7 @@ function findElementByName(model, name) {
|
|
|
49
49
|
return { element: { id: evt.id, name: evt.name, fields: evt.fields, type: 'event' }, ambiguous: [] };
|
|
50
50
|
const rm = model.readModels.get(name);
|
|
51
51
|
if (rm)
|
|
52
|
-
return { element: { id: rm.id, name: rm.name, fields: rm.fields, type: 'readModel' }, ambiguous: [] };
|
|
52
|
+
return { element: { id: rm.id, name: rm.name, fields: rm.fields, type: 'readModel', canonicalId: rm.canonicalId }, ambiguous: [] };
|
|
53
53
|
const scr = model.screens.get(name);
|
|
54
54
|
if (scr)
|
|
55
55
|
return { element: { id: scr.id, name: scr.name, fields: scr.fields, type: 'screen' }, ambiguous: [] };
|
|
@@ -73,7 +73,7 @@ function findElementByName(model, name) {
|
|
|
73
73
|
}
|
|
74
74
|
for (const rm of model.readModels.values()) {
|
|
75
75
|
if (rm.name.toLowerCase() === nameLower) {
|
|
76
|
-
matches.push({ element: { id: rm.id, name: rm.name, fields: rm.fields, type: 'readModel' }, type: 'readModel' });
|
|
76
|
+
matches.push({ element: { id: rm.id, name: rm.name, fields: rm.fields, type: 'readModel', canonicalId: rm.canonicalId }, type: 'readModel' });
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
for (const scr of model.screens.values()) {
|
|
@@ -123,6 +123,95 @@ function flattenFields(fields, prefix = '') {
|
|
|
123
123
|
}
|
|
124
124
|
return result;
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Calculates completeness for a read model using the union approach.
|
|
128
|
+
* A field is satisfied if ANY incoming event provides it.
|
|
129
|
+
* For linked copies, considers flows into ALL nodes in the canonical group.
|
|
130
|
+
*/
|
|
131
|
+
function calculateReadModelUnionCompleteness(model, readModelId, readModelFields, canonicalId) {
|
|
132
|
+
// Find all node IDs in the canonical group (original + all copies)
|
|
133
|
+
let targetNodeIds;
|
|
134
|
+
if (canonicalId) {
|
|
135
|
+
targetNodeIds = [...model.readModels.values()]
|
|
136
|
+
.filter(rm => rm.canonicalId === canonicalId)
|
|
137
|
+
.map(rm => rm.id);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
targetNodeIds = [readModelId];
|
|
141
|
+
}
|
|
142
|
+
// Find all incoming EventToReadModel flows to ANY node in the group
|
|
143
|
+
const incomingFlows = [...model.flows.values()].filter(f => targetNodeIds.includes(f.targetId) && f.flowType === 'EventToReadModel');
|
|
144
|
+
// Collect all source fields from all incoming events (union)
|
|
145
|
+
const allSourceFields = [];
|
|
146
|
+
const allManualMappings = [];
|
|
147
|
+
const sourceEventNames = [];
|
|
148
|
+
for (const flow of incomingFlows) {
|
|
149
|
+
const sourceFields = getSourceFields(model, flow.sourceId);
|
|
150
|
+
allSourceFields.push(...flattenFields(sourceFields));
|
|
151
|
+
if (flow.fieldMappings) {
|
|
152
|
+
allManualMappings.push(...flow.fieldMappings);
|
|
153
|
+
}
|
|
154
|
+
// Get event name for reporting
|
|
155
|
+
const event = model.events.get(flow.sourceId);
|
|
156
|
+
if (event && !sourceEventNames.includes(event.name)) {
|
|
157
|
+
sourceEventNames.push(event.name);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Check each target field against the union of all sources
|
|
161
|
+
const flatTarget = flattenFields(readModelFields);
|
|
162
|
+
const untraceableFields = [];
|
|
163
|
+
const optionalMissingFields = [];
|
|
164
|
+
const fieldStatuses = [];
|
|
165
|
+
for (const { id, path, field } of flatTarget) {
|
|
166
|
+
// 1. Generated fields don't need a source
|
|
167
|
+
if (field.isGenerated) {
|
|
168
|
+
fieldStatuses.push({ fieldId: id, fieldName: path, status: 'generated' });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// 2. Check if there's a manual mapping from any flow
|
|
172
|
+
const manualMapping = allManualMappings.find(m => m.targetFieldId === id);
|
|
173
|
+
if (manualMapping) {
|
|
174
|
+
const sourceField = allSourceFields.find(sf => sf.id === manualMapping.sourceFieldId);
|
|
175
|
+
fieldStatuses.push({
|
|
176
|
+
fieldId: id,
|
|
177
|
+
fieldName: path,
|
|
178
|
+
status: 'satisfied',
|
|
179
|
+
sourceFieldId: manualMapping.sourceFieldId,
|
|
180
|
+
sourceFieldName: sourceField?.path,
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// 3. Check auto-mapping by path match from any source
|
|
185
|
+
const autoMatch = allSourceFields.find(sf => sf.path === path);
|
|
186
|
+
if (autoMatch) {
|
|
187
|
+
fieldStatuses.push({
|
|
188
|
+
fieldId: id,
|
|
189
|
+
fieldName: path,
|
|
190
|
+
status: 'satisfied',
|
|
191
|
+
sourceFieldId: autoMatch.id,
|
|
192
|
+
sourceFieldName: autoMatch.path,
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// 4. Optional fields missing is a warning, not an error
|
|
197
|
+
if (field.isOptional) {
|
|
198
|
+
optionalMissingFields.push(path);
|
|
199
|
+
fieldStatuses.push({ fieldId: id, fieldName: path, status: 'optional-missing' });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
// Field is untraceable (required but no source)
|
|
203
|
+
untraceableFields.push(path);
|
|
204
|
+
fieldStatuses.push({ fieldId: id, fieldName: path, status: 'unsatisfied' });
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
isComplete: untraceableFields.length === 0,
|
|
208
|
+
hasWarnings: optionalMissingFields.length > 0,
|
|
209
|
+
untraceableFields,
|
|
210
|
+
optionalMissingFields,
|
|
211
|
+
sourceEvents: sourceEventNames,
|
|
212
|
+
fieldStatuses,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
126
215
|
function calculateFlowCompleteness(model, flow, targetFields, sourceFields) {
|
|
127
216
|
const flatSource = flattenFields(sourceFields);
|
|
128
217
|
const flatTarget = flattenFields(targetFields);
|
|
@@ -254,10 +343,112 @@ export function showCompleteness(model, elementName, format) {
|
|
|
254
343
|
}
|
|
255
344
|
process.exit(1);
|
|
256
345
|
}
|
|
257
|
-
|
|
346
|
+
const elementTypeLabel = element.type === 'readModel' ? 'read-model' : element.type;
|
|
347
|
+
// For read models, use union completeness (all incoming events combined)
|
|
348
|
+
if (element.type === 'readModel') {
|
|
349
|
+
const unionResult = calculateReadModelUnionCompleteness(model, element.id, element.fields, element.canonicalId);
|
|
350
|
+
// Check if there are any incoming flows at all
|
|
351
|
+
let targetNodeIds;
|
|
352
|
+
if (element.canonicalId) {
|
|
353
|
+
targetNodeIds = [...model.readModels.values()]
|
|
354
|
+
.filter(rm => rm.canonicalId === element.canonicalId)
|
|
355
|
+
.map(rm => rm.id);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
targetNodeIds = [element.id];
|
|
359
|
+
}
|
|
360
|
+
const hasIncomingFlows = [...model.flows.values()].some(f => targetNodeIds.includes(f.targetId) && f.flowType === 'EventToReadModel');
|
|
361
|
+
if (!hasIncomingFlows) {
|
|
362
|
+
if (format === 'json') {
|
|
363
|
+
outputJson({
|
|
364
|
+
elementType: elementTypeLabel,
|
|
365
|
+
name: element.name,
|
|
366
|
+
message: `No flows into this ${elementTypeLabel}`
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
console.log(`<completeness ${elementTypeLabel}="${escapeXml(element.name)}">`);
|
|
371
|
+
console.log(` <no-flows>No flows into this ${elementTypeLabel}</no-flows>`);
|
|
372
|
+
console.log('</completeness>');
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const overallStatus = unionResult.isComplete ? 'complete' : 'incomplete';
|
|
377
|
+
const flatTargetFields = flattenFields(element.fields);
|
|
378
|
+
if (format === 'json') {
|
|
379
|
+
outputJson({
|
|
380
|
+
elementType: elementTypeLabel,
|
|
381
|
+
name: element.name,
|
|
382
|
+
status: overallStatus,
|
|
383
|
+
...(unionResult.hasWarnings ? { hasWarnings: true } : {}),
|
|
384
|
+
sourceEvents: unionResult.sourceEvents,
|
|
385
|
+
...(unionResult.untraceableFields.length > 0 ? { untraceableFields: unionResult.untraceableFields } : {}),
|
|
386
|
+
...(unionResult.optionalMissingFields.length > 0 ? { optionalMissingFields: unionResult.optionalMissingFields } : {}),
|
|
387
|
+
fieldStatuses: unionResult.fieldStatuses.map(f => ({
|
|
388
|
+
name: f.fieldName,
|
|
389
|
+
status: f.status,
|
|
390
|
+
...(f.sourceFieldName ? { source: f.sourceFieldName } : {})
|
|
391
|
+
})),
|
|
392
|
+
elementFields: flatTargetFields.map(({ id, path, field }) => ({
|
|
393
|
+
id,
|
|
394
|
+
name: path,
|
|
395
|
+
type: field.fieldType,
|
|
396
|
+
...(field.isList ? { isList: true } : {}),
|
|
397
|
+
...(field.isOptional ? { isOptional: true } : {}),
|
|
398
|
+
...(field.isGenerated ? { isGenerated: true } : {})
|
|
399
|
+
}))
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
// XML output for read models
|
|
404
|
+
console.log(`<completeness ${elementTypeLabel}="${escapeXml(element.name)}" status="${overallStatus}"${unionResult.hasWarnings ? ' hasWarnings="true"' : ''}>`);
|
|
405
|
+
console.log(' <source-events>');
|
|
406
|
+
for (const eventName of unionResult.sourceEvents) {
|
|
407
|
+
console.log(` <event name="${escapeXml(eventName)}"/>`);
|
|
408
|
+
}
|
|
409
|
+
console.log(' </source-events>');
|
|
410
|
+
if (unionResult.untraceableFields.length > 0) {
|
|
411
|
+
console.log(' <untraceable-fields>');
|
|
412
|
+
for (const fieldName of unionResult.untraceableFields) {
|
|
413
|
+
console.log(` <field name="${escapeXml(fieldName)}"/>`);
|
|
414
|
+
}
|
|
415
|
+
console.log(' </untraceable-fields>');
|
|
416
|
+
}
|
|
417
|
+
if (unionResult.optionalMissingFields.length > 0) {
|
|
418
|
+
console.log(' <optional-missing-fields>');
|
|
419
|
+
for (const fieldName of unionResult.optionalMissingFields) {
|
|
420
|
+
console.log(` <field name="${escapeXml(fieldName)}"/>`);
|
|
421
|
+
}
|
|
422
|
+
console.log(' </optional-missing-fields>');
|
|
423
|
+
}
|
|
424
|
+
console.log(' <field-statuses>');
|
|
425
|
+
for (const field of unionResult.fieldStatuses) {
|
|
426
|
+
if (field.sourceFieldName) {
|
|
427
|
+
console.log(` <field name="${escapeXml(field.fieldName)}" status="${field.status}" source="${escapeXml(field.sourceFieldName)}"/>`);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
console.log(` <field name="${escapeXml(field.fieldName)}" status="${field.status}"/>`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
console.log(' </field-statuses>');
|
|
434
|
+
console.log(` <${elementTypeLabel}-fields>`);
|
|
435
|
+
for (const { id, path, field } of flatTargetFields) {
|
|
436
|
+
const attrs = [`id="${id}"`, `name="${escapeXml(path)}"`, `type="${field.fieldType}"`];
|
|
437
|
+
if (field.isList)
|
|
438
|
+
attrs.push('isList="true"');
|
|
439
|
+
if (field.isOptional)
|
|
440
|
+
attrs.push('isOptional="true"');
|
|
441
|
+
if (field.isGenerated)
|
|
442
|
+
attrs.push('isGenerated="true"');
|
|
443
|
+
console.log(` <field ${attrs.join(' ')}/>`);
|
|
444
|
+
}
|
|
445
|
+
console.log(` </${elementTypeLabel}-fields>`);
|
|
446
|
+
console.log('</completeness>');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
// For non-read-model elements, use per-flow completeness
|
|
258
450
|
const validFlowTypes = INCOMING_FLOW_TYPES[element.type];
|
|
259
451
|
const incomingFlows = [...model.flows.values()].filter(f => f.targetId === element.id && validFlowTypes.includes(f.flowType));
|
|
260
|
-
const elementTypeLabel = element.type === 'readModel' ? 'read-model' : element.type;
|
|
261
452
|
if (incomingFlows.length === 0) {
|
|
262
453
|
if (format === 'json') {
|
|
263
454
|
outputJson({
|
|
@@ -367,14 +558,22 @@ export function showModelCompleteness(model, format) {
|
|
|
367
558
|
const incompleteFlows = [];
|
|
368
559
|
let completeCount = 0;
|
|
369
560
|
let totalCount = 0;
|
|
561
|
+
// For read models, we use union completeness - track which canonical groups we've processed
|
|
562
|
+
const processedReadModelGroups = new Set();
|
|
563
|
+
const incompleteReadModels = [];
|
|
564
|
+
let readModelCompleteCount = 0;
|
|
565
|
+
let readModelTotalCount = 0;
|
|
370
566
|
for (const flow of allFlows) {
|
|
567
|
+
// Skip EventToReadModel flows - we handle read models separately with union logic
|
|
568
|
+
if (flow.flowType === 'EventToReadModel') {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
371
571
|
// Get target element info
|
|
372
572
|
let targetName = 'Unknown';
|
|
373
573
|
let targetType = 'unknown';
|
|
374
574
|
let targetFields = [];
|
|
375
575
|
const targetCmd = model.commands.get(flow.targetId);
|
|
376
576
|
const targetEvt = model.events.get(flow.targetId);
|
|
377
|
-
const targetRm = model.readModels.get(flow.targetId);
|
|
378
577
|
const targetScr = model.screens.get(flow.targetId);
|
|
379
578
|
const targetProc = model.processors.get(flow.targetId);
|
|
380
579
|
if (targetCmd) {
|
|
@@ -387,11 +586,6 @@ export function showModelCompleteness(model, format) {
|
|
|
387
586
|
targetType = 'event';
|
|
388
587
|
targetFields = targetEvt.fields;
|
|
389
588
|
}
|
|
390
|
-
else if (targetRm) {
|
|
391
|
-
targetName = targetRm.name;
|
|
392
|
-
targetType = 'read-model';
|
|
393
|
-
targetFields = targetRm.fields;
|
|
394
|
-
}
|
|
395
589
|
else if (targetScr) {
|
|
396
590
|
targetName = targetScr.name;
|
|
397
591
|
targetType = 'screen';
|
|
@@ -423,13 +617,51 @@ export function showModelCompleteness(model, format) {
|
|
|
423
617
|
});
|
|
424
618
|
}
|
|
425
619
|
}
|
|
620
|
+
// Process read models with union completeness
|
|
621
|
+
// Group read models by canonicalId (or use id if no canonicalId)
|
|
622
|
+
for (const rm of model.readModels.values()) {
|
|
623
|
+
const groupKey = rm.canonicalId ?? rm.id;
|
|
624
|
+
// Skip if we've already processed this canonical group
|
|
625
|
+
if (processedReadModelGroups.has(groupKey)) {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
processedReadModelGroups.add(groupKey);
|
|
629
|
+
// Check if this read model has any incoming EventToReadModel flows
|
|
630
|
+
let targetNodeIds;
|
|
631
|
+
if (rm.canonicalId) {
|
|
632
|
+
targetNodeIds = [...model.readModels.values()]
|
|
633
|
+
.filter(r => r.canonicalId === rm.canonicalId)
|
|
634
|
+
.map(r => r.id);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
targetNodeIds = [rm.id];
|
|
638
|
+
}
|
|
639
|
+
const hasIncomingFlows = allFlows.some(f => targetNodeIds.includes(f.targetId) && f.flowType === 'EventToReadModel');
|
|
640
|
+
if (!hasIncomingFlows) {
|
|
641
|
+
// No incoming flows - skip this read model
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
readModelTotalCount++;
|
|
645
|
+
const unionResult = calculateReadModelUnionCompleteness(model, rm.id, rm.fields, rm.canonicalId);
|
|
646
|
+
if (unionResult.isComplete) {
|
|
647
|
+
readModelCompleteCount++;
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
incompleteReadModels.push({
|
|
651
|
+
name: rm.name,
|
|
652
|
+
untraceableFields: unionResult.untraceableFields,
|
|
653
|
+
sourceEvents: unionResult.sourceEvents,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
426
657
|
const incompleteCount = totalCount - completeCount;
|
|
658
|
+
const readModelIncompleteCount = readModelTotalCount - readModelCompleteCount;
|
|
427
659
|
if (format === 'json') {
|
|
428
660
|
outputJson({
|
|
429
661
|
summary: {
|
|
430
|
-
complete: completeCount,
|
|
431
|
-
incomplete: incompleteCount,
|
|
432
|
-
total: totalCount
|
|
662
|
+
complete: completeCount + readModelCompleteCount,
|
|
663
|
+
incomplete: incompleteCount + readModelIncompleteCount,
|
|
664
|
+
total: totalCount + readModelTotalCount
|
|
433
665
|
},
|
|
434
666
|
incompleteFlows: incompleteFlows.map(item => ({
|
|
435
667
|
from: item.sourceName,
|
|
@@ -437,12 +669,19 @@ export function showModelCompleteness(model, format) {
|
|
|
437
669
|
targetType: item.targetType,
|
|
438
670
|
flowType: item.flow.flowType,
|
|
439
671
|
unsatisfiedFields: item.unsatisfiedFields
|
|
440
|
-
}))
|
|
672
|
+
})),
|
|
673
|
+
...(incompleteReadModels.length > 0 ? {
|
|
674
|
+
incompleteReadModels: incompleteReadModels.map(item => ({
|
|
675
|
+
name: item.name,
|
|
676
|
+
untraceableFields: item.untraceableFields,
|
|
677
|
+
sourceEvents: item.sourceEvents
|
|
678
|
+
}))
|
|
679
|
+
} : {})
|
|
441
680
|
});
|
|
442
681
|
return;
|
|
443
682
|
}
|
|
444
683
|
console.log('<model-completeness>');
|
|
445
|
-
console.log(` <summary complete="${completeCount}" incomplete="${incompleteCount}" total="${totalCount}"/>`);
|
|
684
|
+
console.log(` <summary complete="${completeCount + readModelCompleteCount}" incomplete="${incompleteCount + readModelIncompleteCount}" total="${totalCount + readModelTotalCount}"/>`);
|
|
446
685
|
if (incompleteFlows.length > 0) {
|
|
447
686
|
console.log(' <incomplete-flows>');
|
|
448
687
|
for (const item of incompleteFlows) {
|
|
@@ -458,5 +697,23 @@ export function showModelCompleteness(model, format) {
|
|
|
458
697
|
}
|
|
459
698
|
console.log(' </incomplete-flows>');
|
|
460
699
|
}
|
|
700
|
+
if (incompleteReadModels.length > 0) {
|
|
701
|
+
console.log(' <incomplete-read-models>');
|
|
702
|
+
for (const item of incompleteReadModels) {
|
|
703
|
+
console.log(` <read-model name="${escapeXml(item.name)}">`);
|
|
704
|
+
console.log(' <untraceable-fields>');
|
|
705
|
+
for (const fieldName of item.untraceableFields) {
|
|
706
|
+
console.log(` <field name="${escapeXml(fieldName)}"/>`);
|
|
707
|
+
}
|
|
708
|
+
console.log(' </untraceable-fields>');
|
|
709
|
+
console.log(' <source-events>');
|
|
710
|
+
for (const eventName of item.sourceEvents) {
|
|
711
|
+
console.log(` <event name="${escapeXml(eventName)}"/>`);
|
|
712
|
+
}
|
|
713
|
+
console.log(' </source-events>');
|
|
714
|
+
console.log(' </read-model>');
|
|
715
|
+
}
|
|
716
|
+
console.log(' </incomplete-read-models>');
|
|
717
|
+
}
|
|
461
718
|
console.log('</model-completeness>');
|
|
462
719
|
}
|