eventmodeler 0.2.3 → 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.
Files changed (31) hide show
  1. package/dist/index.js +63 -0
  2. package/dist/lib/element-lookup.d.ts +47 -0
  3. package/dist/lib/element-lookup.js +86 -0
  4. package/dist/lib/slice-utils.d.ts +83 -0
  5. package/dist/lib/slice-utils.js +135 -0
  6. package/dist/slices/add-field/index.js +4 -33
  7. package/dist/slices/add-scenario/index.js +15 -77
  8. package/dist/slices/create-automation-slice/index.d.ts +2 -0
  9. package/dist/slices/create-automation-slice/index.js +217 -0
  10. package/dist/slices/create-flow/index.d.ts +2 -0
  11. package/dist/slices/create-flow/index.js +177 -0
  12. package/dist/slices/create-state-change-slice/index.d.ts +2 -0
  13. package/dist/slices/create-state-change-slice/index.js +239 -0
  14. package/dist/slices/create-state-view-slice/index.d.ts +2 -0
  15. package/dist/slices/create-state-view-slice/index.js +120 -0
  16. package/dist/slices/list-chapters/index.js +2 -2
  17. package/dist/slices/list-commands/index.js +2 -2
  18. package/dist/slices/list-events/index.js +3 -2
  19. package/dist/slices/list-slices/index.js +2 -2
  20. package/dist/slices/mark-slice-status/index.js +2 -11
  21. package/dist/slices/remove-field/index.js +4 -33
  22. package/dist/slices/remove-scenario/index.js +45 -11
  23. package/dist/slices/show-actor/index.js +2 -11
  24. package/dist/slices/show-aggregate-completeness/index.js +2 -11
  25. package/dist/slices/show-chapter/index.js +6 -14
  26. package/dist/slices/show-command/index.js +4 -12
  27. package/dist/slices/show-completeness/index.js +378 -32
  28. package/dist/slices/show-event/index.js +4 -12
  29. package/dist/slices/show-slice/index.js +14 -17
  30. package/dist/slices/update-field/index.js +4 -33
  31. package/package.json +1 -1
@@ -7,38 +7,92 @@ const INCOMING_FLOW_TYPES = {
7
7
  processor: ['ReadModelToProcessor'],
8
8
  };
9
9
  function findElementByName(model, name) {
10
+ // Check for UUID lookup (id:prefix or full UUID format)
11
+ if (name.startsWith('id:')) {
12
+ const idSearch = name.slice(3).toLowerCase();
13
+ // Search all element types by ID
14
+ for (const cmd of model.commands.values()) {
15
+ if (cmd.id.toLowerCase().startsWith(idSearch)) {
16
+ return { element: { id: cmd.id, name: cmd.name, fields: cmd.fields, type: 'command' }, ambiguous: [] };
17
+ }
18
+ }
19
+ for (const evt of model.events.values()) {
20
+ if (evt.id.toLowerCase().startsWith(idSearch)) {
21
+ return { element: { id: evt.id, name: evt.name, fields: evt.fields, type: 'event' }, ambiguous: [] };
22
+ }
23
+ }
24
+ for (const rm of model.readModels.values()) {
25
+ if (rm.id.toLowerCase().startsWith(idSearch)) {
26
+ return { element: { id: rm.id, name: rm.name, fields: rm.fields, type: 'readModel', canonicalId: rm.canonicalId }, ambiguous: [] };
27
+ }
28
+ }
29
+ for (const scr of model.screens.values()) {
30
+ if (scr.id.toLowerCase().startsWith(idSearch)) {
31
+ return { element: { id: scr.id, name: scr.name, fields: scr.fields, type: 'screen' }, ambiguous: [] };
32
+ }
33
+ }
34
+ for (const proc of model.processors.values()) {
35
+ if (proc.id.toLowerCase().startsWith(idSearch)) {
36
+ return { element: { id: proc.id, name: proc.name, fields: proc.fields, type: 'processor' }, ambiguous: [] };
37
+ }
38
+ }
39
+ return { element: null, ambiguous: [] };
40
+ }
41
+ // Check if it's a full UUID
42
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
43
+ if (uuidPattern.test(name)) {
44
+ const cmd = model.commands.get(name);
45
+ if (cmd)
46
+ return { element: { id: cmd.id, name: cmd.name, fields: cmd.fields, type: 'command' }, ambiguous: [] };
47
+ const evt = model.events.get(name);
48
+ if (evt)
49
+ return { element: { id: evt.id, name: evt.name, fields: evt.fields, type: 'event' }, ambiguous: [] };
50
+ const rm = model.readModels.get(name);
51
+ if (rm)
52
+ return { element: { id: rm.id, name: rm.name, fields: rm.fields, type: 'readModel', canonicalId: rm.canonicalId }, ambiguous: [] };
53
+ const scr = model.screens.get(name);
54
+ if (scr)
55
+ return { element: { id: scr.id, name: scr.name, fields: scr.fields, type: 'screen' }, ambiguous: [] };
56
+ const proc = model.processors.get(name);
57
+ if (proc)
58
+ return { element: { id: proc.id, name: proc.name, fields: proc.fields, type: 'processor' }, ambiguous: [] };
59
+ return { element: null, ambiguous: [] };
60
+ }
61
+ // Case-insensitive exact name match across all types
10
62
  const nameLower = name.toLowerCase();
11
- // Search commands
63
+ const matches = [];
12
64
  for (const cmd of model.commands.values()) {
13
- if (cmd.name.toLowerCase() === nameLower || cmd.name.toLowerCase().includes(nameLower)) {
14
- return { id: cmd.id, name: cmd.name, fields: cmd.fields, type: 'command' };
65
+ if (cmd.name.toLowerCase() === nameLower) {
66
+ matches.push({ element: { id: cmd.id, name: cmd.name, fields: cmd.fields, type: 'command' }, type: 'command' });
15
67
  }
16
68
  }
17
- // Search events
18
69
  for (const evt of model.events.values()) {
19
- if (evt.name.toLowerCase() === nameLower || evt.name.toLowerCase().includes(nameLower)) {
20
- return { id: evt.id, name: evt.name, fields: evt.fields, type: 'event' };
70
+ if (evt.name.toLowerCase() === nameLower) {
71
+ matches.push({ element: { id: evt.id, name: evt.name, fields: evt.fields, type: 'event' }, type: 'event' });
21
72
  }
22
73
  }
23
- // Search read models
24
74
  for (const rm of model.readModels.values()) {
25
- if (rm.name.toLowerCase() === nameLower || rm.name.toLowerCase().includes(nameLower)) {
26
- return { id: rm.id, name: rm.name, fields: rm.fields, type: 'readModel' };
75
+ if (rm.name.toLowerCase() === nameLower) {
76
+ matches.push({ element: { id: rm.id, name: rm.name, fields: rm.fields, type: 'readModel', canonicalId: rm.canonicalId }, type: 'readModel' });
27
77
  }
28
78
  }
29
- // Search screens
30
79
  for (const scr of model.screens.values()) {
31
- if (scr.name.toLowerCase() === nameLower || scr.name.toLowerCase().includes(nameLower)) {
32
- return { id: scr.id, name: scr.name, fields: scr.fields, type: 'screen' };
80
+ if (scr.name.toLowerCase() === nameLower) {
81
+ matches.push({ element: { id: scr.id, name: scr.name, fields: scr.fields, type: 'screen' }, type: 'screen' });
33
82
  }
34
83
  }
35
- // Search processors
36
84
  for (const proc of model.processors.values()) {
37
- if (proc.name.toLowerCase() === nameLower || proc.name.toLowerCase().includes(nameLower)) {
38
- return { id: proc.id, name: proc.name, fields: proc.fields, type: 'processor' };
85
+ if (proc.name.toLowerCase() === nameLower) {
86
+ matches.push({ element: { id: proc.id, name: proc.name, fields: proc.fields, type: 'processor' }, type: 'processor' });
39
87
  }
40
88
  }
41
- return null;
89
+ if (matches.length === 1) {
90
+ return { element: matches[0].element, ambiguous: [] };
91
+ }
92
+ if (matches.length > 1) {
93
+ return { element: null, ambiguous: matches };
94
+ }
95
+ return { element: null, ambiguous: [] };
42
96
  }
43
97
  function getSourceFields(model, sourceId) {
44
98
  const cmd = model.commands.get(sourceId);
@@ -69,6 +123,95 @@ function flattenFields(fields, prefix = '') {
69
123
  }
70
124
  return result;
71
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
+ }
72
215
  function calculateFlowCompleteness(model, flow, targetFields, sourceFields) {
73
216
  const flatSource = flattenFields(sourceFields);
74
217
  const flatTarget = flattenFields(targetFields);
@@ -159,16 +302,153 @@ function calculateFlowCompleteness(model, flow, targetFields, sourceFields) {
159
302
  }
160
303
  export function showCompleteness(model, elementName, format) {
161
304
  // Find the element by name (searches all element types)
162
- const element = findElementByName(model, elementName);
305
+ const { element, ambiguous } = findElementByName(model, elementName);
306
+ if (ambiguous.length > 0) {
307
+ console.error(`Error: Multiple elements found with name "${elementName}"`);
308
+ console.error('Please specify using the element ID:');
309
+ for (const match of ambiguous) {
310
+ console.error(` - "${match.element.name}" (${match.type}) (id: ${match.element.id})`);
311
+ }
312
+ console.error('');
313
+ console.error(`Usage: eventmodeler show completeness "id:${ambiguous[0].element.id.slice(0, 8)}"`);
314
+ process.exit(1);
315
+ }
163
316
  if (!element) {
164
- console.error(`Error: Element not found: ${elementName}`);
165
- console.error('Search looked in: commands, events, read models, screens, processors');
317
+ console.error(`Error: Element not found: "${elementName}"`);
318
+ // List all available elements with their IDs
319
+ const allElements = [];
320
+ for (const cmd of model.commands.values()) {
321
+ allElements.push({ name: cmd.name, type: 'command', id: cmd.id });
322
+ }
323
+ for (const evt of model.events.values()) {
324
+ allElements.push({ name: evt.name, type: 'event', id: evt.id });
325
+ }
326
+ for (const rm of model.readModels.values()) {
327
+ allElements.push({ name: rm.name, type: 'read model', id: rm.id });
328
+ }
329
+ for (const scr of model.screens.values()) {
330
+ allElements.push({ name: scr.name, type: 'screen', id: scr.id });
331
+ }
332
+ for (const proc of model.processors.values()) {
333
+ allElements.push({ name: proc.name, type: 'processor', id: proc.id });
334
+ }
335
+ if (allElements.length > 0) {
336
+ console.error('Available elements:');
337
+ for (const el of allElements) {
338
+ console.error(` - "${el.name}" (${el.type}) (id: ${el.id.slice(0, 8)})`);
339
+ }
340
+ }
341
+ else {
342
+ console.error('No elements exist in the model.');
343
+ }
166
344
  process.exit(1);
167
345
  }
168
- // Find all incoming flows for this element type
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
169
450
  const validFlowTypes = INCOMING_FLOW_TYPES[element.type];
170
451
  const incomingFlows = [...model.flows.values()].filter(f => f.targetId === element.id && validFlowTypes.includes(f.flowType));
171
- const elementTypeLabel = element.type === 'readModel' ? 'read-model' : element.type;
172
452
  if (incomingFlows.length === 0) {
173
453
  if (format === 'json') {
174
454
  outputJson({
@@ -278,14 +558,22 @@ export function showModelCompleteness(model, format) {
278
558
  const incompleteFlows = [];
279
559
  let completeCount = 0;
280
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;
281
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
+ }
282
571
  // Get target element info
283
572
  let targetName = 'Unknown';
284
573
  let targetType = 'unknown';
285
574
  let targetFields = [];
286
575
  const targetCmd = model.commands.get(flow.targetId);
287
576
  const targetEvt = model.events.get(flow.targetId);
288
- const targetRm = model.readModels.get(flow.targetId);
289
577
  const targetScr = model.screens.get(flow.targetId);
290
578
  const targetProc = model.processors.get(flow.targetId);
291
579
  if (targetCmd) {
@@ -298,11 +586,6 @@ export function showModelCompleteness(model, format) {
298
586
  targetType = 'event';
299
587
  targetFields = targetEvt.fields;
300
588
  }
301
- else if (targetRm) {
302
- targetName = targetRm.name;
303
- targetType = 'read-model';
304
- targetFields = targetRm.fields;
305
- }
306
589
  else if (targetScr) {
307
590
  targetName = targetScr.name;
308
591
  targetType = 'screen';
@@ -334,13 +617,51 @@ export function showModelCompleteness(model, format) {
334
617
  });
335
618
  }
336
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
+ }
337
657
  const incompleteCount = totalCount - completeCount;
658
+ const readModelIncompleteCount = readModelTotalCount - readModelCompleteCount;
338
659
  if (format === 'json') {
339
660
  outputJson({
340
661
  summary: {
341
- complete: completeCount,
342
- incomplete: incompleteCount,
343
- total: totalCount
662
+ complete: completeCount + readModelCompleteCount,
663
+ incomplete: incompleteCount + readModelIncompleteCount,
664
+ total: totalCount + readModelTotalCount
344
665
  },
345
666
  incompleteFlows: incompleteFlows.map(item => ({
346
667
  from: item.sourceName,
@@ -348,12 +669,19 @@ export function showModelCompleteness(model, format) {
348
669
  targetType: item.targetType,
349
670
  flowType: item.flow.flowType,
350
671
  unsatisfiedFields: item.unsatisfiedFields
351
- }))
672
+ })),
673
+ ...(incompleteReadModels.length > 0 ? {
674
+ incompleteReadModels: incompleteReadModels.map(item => ({
675
+ name: item.name,
676
+ untraceableFields: item.untraceableFields,
677
+ sourceEvents: item.sourceEvents
678
+ }))
679
+ } : {})
352
680
  });
353
681
  return;
354
682
  }
355
683
  console.log('<model-completeness>');
356
- console.log(` <summary complete="${completeCount}" incomplete="${incompleteCount}" total="${totalCount}"/>`);
684
+ console.log(` <summary complete="${completeCount + readModelCompleteCount}" incomplete="${incompleteCount + readModelIncompleteCount}" total="${totalCount + readModelTotalCount}"/>`);
357
685
  if (incompleteFlows.length > 0) {
358
686
  console.log(' <incomplete-flows>');
359
687
  for (const item of incompleteFlows) {
@@ -369,5 +697,23 @@ export function showModelCompleteness(model, format) {
369
697
  }
370
698
  console.log(' </incomplete-flows>');
371
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
+ }
372
718
  console.log('</model-completeness>');
373
719
  }
@@ -1,4 +1,5 @@
1
1
  import { escapeXml, outputJson } from '../../lib/format.js';
2
+ import { findElementOrExit } from '../../lib/element-lookup.js';
2
3
  function formatFieldXml(field, indent) {
3
4
  const attrs = [
4
5
  `name="${escapeXml(field.name)}"`,
@@ -56,7 +57,7 @@ function findAggregateForEvent(model, event) {
56
57
  function formatEventXml(model, event) {
57
58
  const aggregate = findAggregateForEvent(model, event);
58
59
  const aggregateAttr = aggregate ? ` aggregate="${escapeXml(aggregate.name)}"` : '';
59
- let xml = `<event name="${escapeXml(event.name)}"${aggregateAttr}>\n`;
60
+ let xml = `<event id="${event.id}" name="${escapeXml(event.name)}"${aggregateAttr}>\n`;
60
61
  if (event.fields.length > 0) {
61
62
  xml += ' <fields>\n';
62
63
  for (const field of event.fields) {
@@ -88,22 +89,13 @@ function formatEventXml(model, event) {
88
89
  return xml;
89
90
  }
90
91
  export function showEvent(model, name, format) {
91
- const events = [...model.events.values()];
92
- const nameLower = name.toLowerCase();
93
- const event = events.find(e => e.name.toLowerCase() === nameLower || e.name.toLowerCase().includes(nameLower));
94
- if (!event) {
95
- console.error(`Error: Event not found: ${name}`);
96
- console.error('Available events:');
97
- for (const e of events) {
98
- console.error(` - ${e.name}`);
99
- }
100
- process.exit(1);
101
- }
92
+ const event = findElementOrExit(model.events, name, 'event');
102
93
  if (format === 'json') {
103
94
  const aggregate = findAggregateForEvent(model, event);
104
95
  const incomingFlows = [...model.flows.values()].filter(f => f.targetId === event.id);
105
96
  const outgoingFlows = [...model.flows.values()].filter(f => f.sourceId === event.id);
106
97
  const result = {
98
+ id: event.id,
107
99
  name: event.name,
108
100
  fields: event.fields.map(fieldToJson)
109
101
  };
@@ -1,4 +1,5 @@
1
1
  import { escapeXml, outputJson } from '../../lib/format.js';
2
+ import { findElementOrExit } from '../../lib/element-lookup.js';
2
3
  function formatFieldValues(values) {
3
4
  if (!values || Object.keys(values).length === 0)
4
5
  return '';
@@ -120,7 +121,7 @@ function formatSliceXml(model, slice) {
120
121
  model.processors.get(id)?.name ??
121
122
  id);
122
123
  }
123
- let xml = `<slice name="${escapeXml(slice.name)}" status="${slice.status}">\n`;
124
+ let xml = `<slice id="${slice.id}" name="${escapeXml(slice.name)}" status="${slice.status}">\n`;
124
125
  xml += ' <components>\n';
125
126
  for (const screen of components.screens) {
126
127
  // Check if this is a linked copy
@@ -135,7 +136,7 @@ function formatSliceXml(model, slice) {
135
136
  // Check which actor this screen belongs to
136
137
  const actor = findActorForScreen(model, screen);
137
138
  const actorAttr = actor ? ` actor="${escapeXml(actor.name)}"` : '';
138
- xml += ` <screen name="${escapeXml(screen.name)}"${copyAttr}${originAttr}${actorAttr}>\n`;
139
+ xml += ` <screen id="${screen.id}" name="${escapeXml(screen.name)}"${copyAttr}${originAttr}${actorAttr}>\n`;
139
140
  if (screen.fields.length > 0) {
140
141
  xml += ' <fields>\n';
141
142
  for (const field of screen.fields) {
@@ -146,7 +147,7 @@ function formatSliceXml(model, slice) {
146
147
  xml += ' </screen>\n';
147
148
  }
148
149
  for (const command of components.commands) {
149
- xml += ` <command name="${escapeXml(command.name)}">\n`;
150
+ xml += ` <command id="${command.id}" name="${escapeXml(command.name)}">\n`;
150
151
  if (command.fields.length > 0) {
151
152
  xml += ' <fields>\n';
152
153
  for (const field of command.fields) {
@@ -177,7 +178,7 @@ function formatSliceXml(model, slice) {
177
178
  // Check which aggregate this event belongs to
178
179
  const aggregate = findAggregateForEvent(model, event);
179
180
  const aggregateAttr = aggregate ? ` aggregate="${escapeXml(aggregate.name)}"` : '';
180
- xml += ` <event name="${escapeXml(event.name)}"${copyAttr}${originAttr}${aggregateAttr}>\n`;
181
+ xml += ` <event id="${event.id}" name="${escapeXml(event.name)}"${copyAttr}${originAttr}${aggregateAttr}>\n`;
181
182
  if (event.fields.length > 0) {
182
183
  xml += ' <fields>\n';
183
184
  for (const field of event.fields) {
@@ -208,7 +209,7 @@ function formatSliceXml(model, slice) {
208
209
  originAttr = ` origin-slice="${escapeXml(originSlice.name)}"`;
209
210
  }
210
211
  }
211
- xml += ` <read-model name="${escapeXml(readModel.name)}"${copyAttr}${originAttr}>\n`;
212
+ xml += ` <read-model id="${readModel.id}" name="${escapeXml(readModel.name)}"${copyAttr}${originAttr}>\n`;
212
213
  if (readModel.fields.length > 0) {
213
214
  xml += ' <fields>\n';
214
215
  for (const field of readModel.fields) {
@@ -219,7 +220,7 @@ function formatSliceXml(model, slice) {
219
220
  xml += ' </read-model>\n';
220
221
  }
221
222
  for (const processor of components.processors) {
222
- xml += ` <processor name="${escapeXml(processor.name)}">\n`;
223
+ xml += ` <processor id="${processor.id}" name="${escapeXml(processor.name)}">\n`;
223
224
  if (processor.fields.length > 0) {
224
225
  xml += ' <fields>\n';
225
226
  for (const field of processor.fields) {
@@ -337,11 +338,13 @@ function formatSliceJson(model, slice) {
337
338
  id);
338
339
  }
339
340
  const result = {
341
+ id: slice.id,
340
342
  name: slice.name,
341
343
  status: slice.status,
342
344
  components: {
343
345
  screens: components.screens.map(screen => {
344
346
  const screenObj = {
347
+ id: screen.id,
345
348
  name: screen.name,
346
349
  fields: screen.fields.map(fieldToJson)
347
350
  };
@@ -357,11 +360,13 @@ function formatSliceJson(model, slice) {
357
360
  return screenObj;
358
361
  }),
359
362
  commands: components.commands.map(cmd => ({
363
+ id: cmd.id,
360
364
  name: cmd.name,
361
365
  fields: cmd.fields.map(fieldToJson)
362
366
  })),
363
367
  events: components.events.map(event => {
364
368
  const eventObj = {
369
+ id: event.id,
365
370
  name: event.name,
366
371
  fields: event.fields.map(fieldToJson)
367
372
  };
@@ -378,6 +383,7 @@ function formatSliceJson(model, slice) {
378
383
  }),
379
384
  readModels: components.readModels.map(rm => {
380
385
  const rmObj = {
386
+ id: rm.id,
381
387
  name: rm.name,
382
388
  fields: rm.fields.map(fieldToJson)
383
389
  };
@@ -390,6 +396,7 @@ function formatSliceJson(model, slice) {
390
396
  return rmObj;
391
397
  }),
392
398
  processors: components.processors.map(proc => ({
399
+ id: proc.id,
393
400
  name: proc.name,
394
401
  fields: proc.fields.map(fieldToJson)
395
402
  }))
@@ -456,17 +463,7 @@ function formatSliceJson(model, slice) {
456
463
  return result;
457
464
  }
458
465
  export function showSlice(model, name, format) {
459
- const slices = [...model.slices.values()];
460
- const nameLower = name.toLowerCase();
461
- const slice = slices.find(s => s.name.toLowerCase() === nameLower || s.name.toLowerCase().includes(nameLower));
462
- if (!slice) {
463
- console.error(`Error: Slice not found: ${name}`);
464
- console.error('Available slices:');
465
- for (const s of slices) {
466
- console.error(` - ${s.name}`);
467
- }
468
- process.exit(1);
469
- }
466
+ const slice = findElementOrExit(model.slices, name, 'slice');
470
467
  if (format === 'json') {
471
468
  outputJson(formatSliceJson(model, slice));
472
469
  return;