eventmodeler 0.2.3 → 0.2.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.
Files changed (32) 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/projection.js +132 -0
  7. package/dist/slices/add-field/index.js +4 -33
  8. package/dist/slices/add-scenario/index.js +7 -74
  9. package/dist/slices/create-automation-slice/index.d.ts +2 -0
  10. package/dist/slices/create-automation-slice/index.js +217 -0
  11. package/dist/slices/create-flow/index.d.ts +2 -0
  12. package/dist/slices/create-flow/index.js +177 -0
  13. package/dist/slices/create-state-change-slice/index.d.ts +2 -0
  14. package/dist/slices/create-state-change-slice/index.js +239 -0
  15. package/dist/slices/create-state-view-slice/index.d.ts +2 -0
  16. package/dist/slices/create-state-view-slice/index.js +120 -0
  17. package/dist/slices/list-chapters/index.js +2 -2
  18. package/dist/slices/list-commands/index.js +2 -2
  19. package/dist/slices/list-events/index.js +3 -2
  20. package/dist/slices/list-slices/index.js +2 -2
  21. package/dist/slices/mark-slice-status/index.js +2 -11
  22. package/dist/slices/remove-field/index.js +4 -33
  23. package/dist/slices/remove-scenario/index.js +45 -11
  24. package/dist/slices/show-actor/index.js +2 -11
  25. package/dist/slices/show-aggregate-completeness/index.js +2 -11
  26. package/dist/slices/show-chapter/index.js +6 -14
  27. package/dist/slices/show-command/index.js +4 -12
  28. package/dist/slices/show-completeness/index.js +108 -19
  29. package/dist/slices/show-event/index.js +4 -12
  30. package/dist/slices/show-slice/index.js +14 -17
  31. package/dist/slices/update-field/index.js +4 -33
  32. package/package.json +1 -1
@@ -0,0 +1,239 @@
1
+ import { appendEvent } from '../../lib/file-loader.js';
2
+ import { STATE_CHANGE_SLICE, SLICE_GAP, calculateSlicePosition, validateSliceNameUnique, inferFieldMappings, parseFieldsFromXml, getSlicesToShift, fieldInputToField, } from '../../lib/slice-utils.js';
3
+ function getAttr(attrs, name) {
4
+ const match = attrs.match(new RegExp(`${name}="([^"]*)"`));
5
+ return match ? match[1] : undefined;
6
+ }
7
+ function parseXmlInput(xml) {
8
+ // Parse <state-change-slice name="..." after="..." before="...">
9
+ const sliceMatch = xml.match(/<state-change-slice([^>]*)>/);
10
+ if (!sliceMatch) {
11
+ throw new Error('Invalid XML: missing <state-change-slice> tag');
12
+ }
13
+ const sliceName = getAttr(sliceMatch[1], 'name');
14
+ if (!sliceName) {
15
+ throw new Error('Invalid XML: state-change-slice must have a name attribute');
16
+ }
17
+ const after = getAttr(sliceMatch[1], 'after');
18
+ const before = getAttr(sliceMatch[1], 'before');
19
+ // Parse screen
20
+ const screenMatch = xml.match(/<screen([^>]*)>([\s\S]*?)<\/screen>/);
21
+ if (!screenMatch) {
22
+ throw new Error('Invalid XML: missing <screen> element');
23
+ }
24
+ const screenName = getAttr(screenMatch[1], 'name');
25
+ if (!screenName) {
26
+ throw new Error('Invalid XML: screen must have a name attribute');
27
+ }
28
+ const screenFields = parseFieldsFromXml(screenMatch[2]);
29
+ // Parse command
30
+ const commandMatch = xml.match(/<command([^>]*)>([\s\S]*?)<\/command>/);
31
+ if (!commandMatch) {
32
+ throw new Error('Invalid XML: missing <command> element');
33
+ }
34
+ const commandName = getAttr(commandMatch[1], 'name');
35
+ if (!commandName) {
36
+ throw new Error('Invalid XML: command must have a name attribute');
37
+ }
38
+ const commandFields = parseFieldsFromXml(commandMatch[2]);
39
+ // Parse event
40
+ const eventMatch = xml.match(/<event([^>]*)>([\s\S]*?)<\/event>/);
41
+ if (!eventMatch) {
42
+ throw new Error('Invalid XML: missing <event> element');
43
+ }
44
+ const eventName = getAttr(eventMatch[1], 'name');
45
+ if (!eventName) {
46
+ throw new Error('Invalid XML: event must have a name attribute');
47
+ }
48
+ const eventFields = parseFieldsFromXml(eventMatch[2]);
49
+ return {
50
+ sliceName,
51
+ after,
52
+ before,
53
+ screen: { name: screenName, fields: screenFields },
54
+ command: { name: commandName, fields: commandFields },
55
+ event: { name: eventName, fields: eventFields },
56
+ };
57
+ }
58
+ export function createStateChangeSlice(model, filePath, xmlInput) {
59
+ // Parse input
60
+ let input;
61
+ try {
62
+ input = parseXmlInput(xmlInput);
63
+ }
64
+ catch (err) {
65
+ console.error(`Error: ${err.message}`);
66
+ process.exit(1);
67
+ }
68
+ // Validate slice name is unique
69
+ try {
70
+ validateSliceNameUnique(model, input.sliceName);
71
+ }
72
+ catch (err) {
73
+ console.error(`Error: ${err.message}`);
74
+ process.exit(1);
75
+ }
76
+ // Calculate position
77
+ let slicePosition;
78
+ try {
79
+ slicePosition = calculateSlicePosition(model, STATE_CHANGE_SLICE.width, {
80
+ after: input.after,
81
+ before: input.before,
82
+ });
83
+ }
84
+ catch (err) {
85
+ console.error(`Error: ${err.message}`);
86
+ process.exit(1);
87
+ }
88
+ // Handle "before" positioning - shift existing slices
89
+ if (input.before) {
90
+ const shiftAmount = STATE_CHANGE_SLICE.width + SLICE_GAP;
91
+ const toShift = getSlicesToShift(model, slicePosition.x, shiftAmount);
92
+ for (const { sliceId, newX, currentY } of toShift) {
93
+ appendEvent(filePath, {
94
+ type: 'SliceMoved',
95
+ sliceId,
96
+ position: { x: newX, y: currentY },
97
+ timestamp: Date.now(),
98
+ });
99
+ }
100
+ }
101
+ // Generate IDs
102
+ const sliceId = crypto.randomUUID();
103
+ const screenId = crypto.randomUUID();
104
+ const commandId = crypto.randomUUID();
105
+ const eventId = crypto.randomUUID();
106
+ const screenToCommandFlowId = crypto.randomUUID();
107
+ const commandToEventFlowId = crypto.randomUUID();
108
+ // Convert field inputs to fields with IDs
109
+ const screenFields = input.screen.fields.map(fieldInputToField);
110
+ const commandFields = input.command.fields.map(fieldInputToField);
111
+ const eventFields = input.event.fields.map(fieldInputToField);
112
+ // Infer field mappings
113
+ const screenToCommandMappings = inferFieldMappings(screenFields, commandFields);
114
+ const commandToEventMappings = inferFieldMappings(commandFields, eventFields);
115
+ // Calculate absolute positions for components within the slice
116
+ const screenPosition = {
117
+ x: slicePosition.x + STATE_CHANGE_SLICE.screen.offsetX,
118
+ y: slicePosition.y + STATE_CHANGE_SLICE.screen.offsetY,
119
+ };
120
+ const commandPosition = {
121
+ x: slicePosition.x + STATE_CHANGE_SLICE.command.offsetX,
122
+ y: slicePosition.y + STATE_CHANGE_SLICE.command.offsetY,
123
+ };
124
+ const eventPosition = {
125
+ x: slicePosition.x + STATE_CHANGE_SLICE.event.offsetX,
126
+ y: slicePosition.y + STATE_CHANGE_SLICE.event.offsetY,
127
+ };
128
+ // Emit atomic events in sequence
129
+ // 1. Create the slice
130
+ appendEvent(filePath, {
131
+ type: 'SlicePlaced',
132
+ sliceId,
133
+ name: input.sliceName,
134
+ position: slicePosition,
135
+ size: { width: STATE_CHANGE_SLICE.width, height: STATE_CHANGE_SLICE.height },
136
+ timestamp: Date.now(),
137
+ });
138
+ // 2. Create the screen
139
+ appendEvent(filePath, {
140
+ type: 'ScreenPlaced',
141
+ screenId,
142
+ name: input.screen.name,
143
+ position: screenPosition,
144
+ width: STATE_CHANGE_SLICE.screen.width,
145
+ height: STATE_CHANGE_SLICE.screen.height,
146
+ timestamp: Date.now(),
147
+ });
148
+ // 3. Add screen fields
149
+ for (const field of screenFields) {
150
+ appendEvent(filePath, {
151
+ type: 'ScreenFieldAdded',
152
+ screenId,
153
+ field,
154
+ timestamp: Date.now(),
155
+ });
156
+ }
157
+ // 4. Create the command
158
+ appendEvent(filePath, {
159
+ type: 'CommandStickyPlaced',
160
+ commandStickyId: commandId,
161
+ name: input.command.name,
162
+ position: commandPosition,
163
+ width: STATE_CHANGE_SLICE.command.width,
164
+ height: STATE_CHANGE_SLICE.command.height,
165
+ timestamp: Date.now(),
166
+ });
167
+ // 5. Add command fields
168
+ for (const field of commandFields) {
169
+ appendEvent(filePath, {
170
+ type: 'CommandFieldAdded',
171
+ commandStickyId: commandId,
172
+ field,
173
+ timestamp: Date.now(),
174
+ });
175
+ }
176
+ // 6. Create the event
177
+ appendEvent(filePath, {
178
+ type: 'EventStickyPlaced',
179
+ eventStickyId: eventId,
180
+ name: input.event.name,
181
+ position: eventPosition,
182
+ width: STATE_CHANGE_SLICE.event.width,
183
+ height: STATE_CHANGE_SLICE.event.height,
184
+ timestamp: Date.now(),
185
+ });
186
+ // 7. Add event fields
187
+ for (const field of eventFields) {
188
+ appendEvent(filePath, {
189
+ type: 'EventFieldAdded',
190
+ eventStickyId: eventId,
191
+ field,
192
+ timestamp: Date.now(),
193
+ });
194
+ }
195
+ // 8. Create screen -> command flow
196
+ appendEvent(filePath, {
197
+ type: 'ScreenToCommandFlowSpecified',
198
+ flowId: screenToCommandFlowId,
199
+ screenId,
200
+ commandStickyId: commandId,
201
+ sourceHandle: 'bottom-source',
202
+ targetHandle: 'top-target',
203
+ timestamp: Date.now(),
204
+ });
205
+ // 9. Add screen -> command field mappings
206
+ if (screenToCommandMappings.length > 0) {
207
+ appendEvent(filePath, {
208
+ type: 'FieldMappingSpecified',
209
+ flowId: screenToCommandFlowId,
210
+ mappings: screenToCommandMappings,
211
+ timestamp: Date.now(),
212
+ });
213
+ }
214
+ // 10. Create command -> event flow
215
+ appendEvent(filePath, {
216
+ type: 'CommandToEventFlowSpecified',
217
+ flowId: commandToEventFlowId,
218
+ commandStickyId: commandId,
219
+ eventStickyId: eventId,
220
+ sourceHandle: 'bottom-source',
221
+ targetHandle: 'top-target',
222
+ timestamp: Date.now(),
223
+ });
224
+ // 11. Add command -> event field mappings
225
+ if (commandToEventMappings.length > 0) {
226
+ appendEvent(filePath, {
227
+ type: 'FieldMappingSpecified',
228
+ flowId: commandToEventFlowId,
229
+ mappings: commandToEventMappings,
230
+ timestamp: Date.now(),
231
+ });
232
+ }
233
+ console.log(`Created state-change slice "${input.sliceName}"`);
234
+ console.log(` Screen: ${input.screen.name} (${screenFields.length} fields)`);
235
+ console.log(` Command: ${input.command.name} (${commandFields.length} fields)`);
236
+ console.log(` Event: ${input.event.name} (${eventFields.length} fields)`);
237
+ console.log(` Screen -> Command mappings: ${screenToCommandMappings.length}`);
238
+ console.log(` Command -> Event mappings: ${commandToEventMappings.length}`);
239
+ }
@@ -0,0 +1,2 @@
1
+ import type { EventModel } from '../../types.js';
2
+ export declare function createStateViewSlice(model: EventModel, filePath: string, xmlInput: string): void;
@@ -0,0 +1,120 @@
1
+ import { appendEvent } from '../../lib/file-loader.js';
2
+ import { STATE_VIEW_SLICE, SLICE_GAP, calculateSlicePosition, validateSliceNameUnique, parseFieldsFromXml, getSlicesToShift, fieldInputToField, } from '../../lib/slice-utils.js';
3
+ function getAttr(attrs, name) {
4
+ const match = attrs.match(new RegExp(`${name}="([^"]*)"`));
5
+ return match ? match[1] : undefined;
6
+ }
7
+ function parseXmlInput(xml) {
8
+ // Parse <state-view-slice name="..." after="..." before="...">
9
+ const sliceMatch = xml.match(/<state-view-slice([^>]*)>/);
10
+ if (!sliceMatch) {
11
+ throw new Error('Invalid XML: missing <state-view-slice> tag');
12
+ }
13
+ const sliceName = getAttr(sliceMatch[1], 'name');
14
+ if (!sliceName) {
15
+ throw new Error('Invalid XML: state-view-slice must have a name attribute');
16
+ }
17
+ const after = getAttr(sliceMatch[1], 'after');
18
+ const before = getAttr(sliceMatch[1], 'before');
19
+ // Parse read-model
20
+ const readModelMatch = xml.match(/<read-model([^>]*)>([\s\S]*?)<\/read-model>/);
21
+ if (!readModelMatch) {
22
+ throw new Error('Invalid XML: missing <read-model> element');
23
+ }
24
+ const readModelName = getAttr(readModelMatch[1], 'name');
25
+ if (!readModelName) {
26
+ throw new Error('Invalid XML: read-model must have a name attribute');
27
+ }
28
+ const readModelFields = parseFieldsFromXml(readModelMatch[2]);
29
+ return {
30
+ sliceName,
31
+ after,
32
+ before,
33
+ readModel: { name: readModelName, fields: readModelFields },
34
+ };
35
+ }
36
+ export function createStateViewSlice(model, filePath, xmlInput) {
37
+ // Parse input
38
+ let input;
39
+ try {
40
+ input = parseXmlInput(xmlInput);
41
+ }
42
+ catch (err) {
43
+ console.error(`Error: ${err.message}`);
44
+ process.exit(1);
45
+ }
46
+ // Validate slice name is unique
47
+ try {
48
+ validateSliceNameUnique(model, input.sliceName);
49
+ }
50
+ catch (err) {
51
+ console.error(`Error: ${err.message}`);
52
+ process.exit(1);
53
+ }
54
+ // Calculate position
55
+ let slicePosition;
56
+ try {
57
+ slicePosition = calculateSlicePosition(model, STATE_VIEW_SLICE.width, {
58
+ after: input.after,
59
+ before: input.before,
60
+ });
61
+ }
62
+ catch (err) {
63
+ console.error(`Error: ${err.message}`);
64
+ process.exit(1);
65
+ }
66
+ // Handle "before" positioning - shift existing slices
67
+ if (input.before) {
68
+ const shiftAmount = STATE_VIEW_SLICE.width + SLICE_GAP;
69
+ const toShift = getSlicesToShift(model, slicePosition.x, shiftAmount);
70
+ for (const { sliceId, newX, currentY } of toShift) {
71
+ appendEvent(filePath, {
72
+ type: 'SliceMoved',
73
+ sliceId,
74
+ position: { x: newX, y: currentY },
75
+ timestamp: Date.now(),
76
+ });
77
+ }
78
+ }
79
+ // Generate IDs
80
+ const sliceId = crypto.randomUUID();
81
+ const readModelId = crypto.randomUUID();
82
+ // Convert field inputs to fields with IDs
83
+ const readModelFields = input.readModel.fields.map(fieldInputToField);
84
+ // Calculate absolute position for read model within the slice
85
+ const readModelPosition = {
86
+ x: slicePosition.x + STATE_VIEW_SLICE.readModel.offsetX,
87
+ y: slicePosition.y + STATE_VIEW_SLICE.readModel.offsetY,
88
+ };
89
+ // Emit atomic events in sequence
90
+ // 1. Create the slice
91
+ appendEvent(filePath, {
92
+ type: 'SlicePlaced',
93
+ sliceId,
94
+ name: input.sliceName,
95
+ position: slicePosition,
96
+ size: { width: STATE_VIEW_SLICE.width, height: STATE_VIEW_SLICE.height },
97
+ timestamp: Date.now(),
98
+ });
99
+ // 2. Create the read model
100
+ appendEvent(filePath, {
101
+ type: 'ReadModelStickyPlaced',
102
+ readModelStickyId: readModelId,
103
+ name: input.readModel.name,
104
+ position: readModelPosition,
105
+ width: STATE_VIEW_SLICE.readModel.width,
106
+ height: STATE_VIEW_SLICE.readModel.height,
107
+ timestamp: Date.now(),
108
+ });
109
+ // 3. Add read model fields
110
+ for (const field of readModelFields) {
111
+ appendEvent(filePath, {
112
+ type: 'ReadModelFieldAdded',
113
+ readModelStickyId: readModelId,
114
+ field,
115
+ timestamp: Date.now(),
116
+ });
117
+ }
118
+ console.log(`Created state-view slice "${input.sliceName}"`);
119
+ console.log(` ReadModel: ${input.readModel.name} (${readModelFields.length} fields)`);
120
+ }
@@ -5,7 +5,7 @@ export function listChapters(model, format) {
5
5
  const sorted = [...chapters].sort((a, b) => a.position.x - b.position.x);
6
6
  if (format === 'json') {
7
7
  outputJson({
8
- chapters: sorted.map(ch => ({ name: ch.name }))
8
+ chapters: sorted.map(ch => ({ id: ch.id, name: ch.name }))
9
9
  });
10
10
  return;
11
11
  }
@@ -15,7 +15,7 @@ export function listChapters(model, format) {
15
15
  }
16
16
  console.log('<chapters>');
17
17
  for (const chapter of sorted) {
18
- console.log(` <chapter name="${escapeXml(chapter.name)}"/>`);
18
+ console.log(` <chapter id="${chapter.id}" name="${escapeXml(chapter.name)}"/>`);
19
19
  }
20
20
  console.log('</chapters>');
21
21
  }
@@ -4,7 +4,7 @@ export function listCommands(model, format) {
4
4
  const sorted = [...commands].sort((a, b) => a.name.localeCompare(b.name));
5
5
  if (format === 'json') {
6
6
  outputJson({
7
- commands: sorted.map(cmd => ({ name: cmd.name, fields: cmd.fields.length }))
7
+ commands: sorted.map(cmd => ({ id: cmd.id, name: cmd.name, fields: cmd.fields.length }))
8
8
  });
9
9
  return;
10
10
  }
@@ -14,7 +14,7 @@ export function listCommands(model, format) {
14
14
  }
15
15
  console.log('<commands>');
16
16
  for (const cmd of sorted) {
17
- console.log(` <command name="${escapeXml(cmd.name)}" fields="${cmd.fields.length}"/>`);
17
+ console.log(` <command id="${cmd.id}" name="${escapeXml(cmd.name)}" fields="${cmd.fields.length}"/>`);
18
18
  }
19
19
  console.log('</commands>');
20
20
  }
@@ -47,6 +47,7 @@ export function listEvents(model, format) {
47
47
  events: sorted.map(evt => {
48
48
  const aggregate = findAggregateForEvent(model, evt);
49
49
  const result = {
50
+ id: evt.id,
50
51
  name: evt.name,
51
52
  fields: evt.fields.length,
52
53
  };
@@ -82,7 +83,7 @@ export function listEvents(model, format) {
82
83
  const original = model.events.get(evt.originalNodeId);
83
84
  const originSlice = original ? findSliceForEvent(model, original) : null;
84
85
  const originAttr = originSlice ? ` origin-slice="${escapeXml(originSlice)}"` : '';
85
- console.log(` <event name="${escapeXml(evt.name)}" fields="${evt.fields.length}"${aggregateAttr} linked-copy="true"${originAttr}/>`);
86
+ console.log(` <event id="${evt.id}" name="${escapeXml(evt.name)}" fields="${evt.fields.length}"${aggregateAttr} linked-copy="true"${originAttr}/>`);
86
87
  }
87
88
  else {
88
89
  // This is an original - show copy count
@@ -90,7 +91,7 @@ export function listEvents(model, format) {
90
91
  ? [...model.events.values()].filter(e => e.canonicalId === evt.canonicalId && e.originalNodeId).length
91
92
  : 0;
92
93
  const copiesAttr = copyCount > 0 ? ` copies="${copyCount}"` : '';
93
- console.log(` <event name="${escapeXml(evt.name)}" fields="${evt.fields.length}"${aggregateAttr}${copiesAttr}/>`);
94
+ console.log(` <event id="${evt.id}" name="${escapeXml(evt.name)}" fields="${evt.fields.length}"${aggregateAttr}${copiesAttr}/>`);
94
95
  }
95
96
  }
96
97
  console.log('</events>');
@@ -4,7 +4,7 @@ export function listSlices(model, format) {
4
4
  const sorted = [...slices].sort((a, b) => a.position.x - b.position.x);
5
5
  if (format === 'json') {
6
6
  outputJson({
7
- slices: sorted.map(s => ({ name: s.name, status: s.status }))
7
+ slices: sorted.map(s => ({ id: s.id, name: s.name, status: s.status }))
8
8
  });
9
9
  return;
10
10
  }
@@ -14,7 +14,7 @@ export function listSlices(model, format) {
14
14
  }
15
15
  console.log('<slices>');
16
16
  for (const slice of sorted) {
17
- console.log(` <slice name="${escapeXml(slice.name)}" status="${slice.status}"/>`);
17
+ console.log(` <slice id="${slice.id}" name="${escapeXml(slice.name)}" status="${slice.status}"/>`);
18
18
  }
19
19
  console.log('</slices>');
20
20
  }
@@ -1,4 +1,5 @@
1
1
  import { appendEvent } from '../../lib/file-loader.js';
2
+ import { findElementOrExit } from '../../lib/element-lookup.js';
2
3
  const validStatuses = ['created', 'in-progress', 'blocked', 'done'];
3
4
  const eventTypeMap = {
4
5
  'created': 'SliceMarkedAsCreated',
@@ -13,17 +14,7 @@ export function markSliceStatus(model, filePath, sliceName, status) {
13
14
  process.exit(1);
14
15
  }
15
16
  const newStatus = status;
16
- const slices = [...model.slices.values()];
17
- const sliceNameLower = sliceName.toLowerCase();
18
- const slice = slices.find(s => s.name.toLowerCase() === sliceNameLower || s.name.toLowerCase().includes(sliceNameLower));
19
- if (!slice) {
20
- console.error(`Error: Slice not found: ${sliceName}`);
21
- console.error('Available slices:');
22
- for (const s of slices) {
23
- console.error(` - ${s.name}`);
24
- }
25
- process.exit(1);
26
- }
17
+ const slice = findElementOrExit(model.slices, sliceName, 'slice');
27
18
  if (slice.status === newStatus) {
28
19
  console.log(`Slice "${slice.name}" is already marked as ${newStatus}`);
29
20
  return;
@@ -1,4 +1,5 @@
1
1
  import { appendEvent } from '../../lib/file-loader.js';
2
+ import { findElementOrExit } from '../../lib/element-lookup.js';
2
3
  export function removeField(model, filePath, options, fieldName) {
3
4
  // Determine which entity type
4
5
  const entityCount = [options.command, options.event, options.readModel].filter(Boolean).length;
@@ -21,17 +22,7 @@ export function removeField(model, filePath, options, fieldName) {
21
22
  }
22
23
  }
23
24
  function removeFieldFromCommand(model, filePath, commandName, fieldName) {
24
- const nameLower = commandName.toLowerCase();
25
- const commands = [...model.commands.values()];
26
- const command = commands.find(c => c.name.toLowerCase() === nameLower || c.name.toLowerCase().includes(nameLower));
27
- if (!command) {
28
- console.error(`Error: Command not found: ${commandName}`);
29
- console.error('Available commands:');
30
- for (const c of commands) {
31
- console.error(` - ${c.name}`);
32
- }
33
- process.exit(1);
34
- }
25
+ const command = findElementOrExit(model.commands, commandName, 'command');
35
26
  const fieldNameLower = fieldName.toLowerCase();
36
27
  const field = command.fields.find(f => f.name.toLowerCase() === fieldNameLower);
37
28
  if (!field) {
@@ -56,17 +47,7 @@ function removeFieldFromCommand(model, filePath, commandName, fieldName) {
56
47
  console.log(`Removed field "${field.name}" from command "${command.name}"`);
57
48
  }
58
49
  function removeFieldFromEvent(model, filePath, eventName, fieldName) {
59
- const nameLower = eventName.toLowerCase();
60
- const events = [...model.events.values()];
61
- const event = events.find(e => e.name.toLowerCase() === nameLower || e.name.toLowerCase().includes(nameLower));
62
- if (!event) {
63
- console.error(`Error: Event not found: ${eventName}`);
64
- console.error('Available events:');
65
- for (const e of events) {
66
- console.error(` - ${e.name}`);
67
- }
68
- process.exit(1);
69
- }
50
+ const event = findElementOrExit(model.events, eventName, 'event');
70
51
  const fieldNameLower = fieldName.toLowerCase();
71
52
  const field = event.fields.find(f => f.name.toLowerCase() === fieldNameLower);
72
53
  if (!field) {
@@ -91,17 +72,7 @@ function removeFieldFromEvent(model, filePath, eventName, fieldName) {
91
72
  console.log(`Removed field "${field.name}" from event "${event.name}"`);
92
73
  }
93
74
  function removeFieldFromReadModel(model, filePath, readModelName, fieldName) {
94
- const nameLower = readModelName.toLowerCase();
95
- const readModels = [...model.readModels.values()];
96
- const readModel = readModels.find(rm => rm.name.toLowerCase() === nameLower || rm.name.toLowerCase().includes(nameLower));
97
- if (!readModel) {
98
- console.error(`Error: Read model not found: ${readModelName}`);
99
- console.error('Available read models:');
100
- for (const rm of readModels) {
101
- console.error(` - ${rm.name}`);
102
- }
103
- process.exit(1);
104
- }
75
+ const readModel = findElementOrExit(model.readModels, readModelName, 'read model');
105
76
  const fieldNameLower = fieldName.toLowerCase();
106
77
  const field = readModel.fields.find(f => f.name.toLowerCase() === fieldNameLower);
107
78
  if (!field) {
@@ -1,31 +1,65 @@
1
1
  import { appendEvent } from '../../lib/file-loader.js';
2
+ import { findElement, formatElementWithId } from '../../lib/element-lookup.js';
2
3
  export function removeScenario(model, filePath, scenarioName, sliceName) {
4
+ // Check for UUID lookup
5
+ if (scenarioName.startsWith('id:')) {
6
+ const idSearch = scenarioName.slice(3).toLowerCase();
7
+ const scenario = [...model.scenarios.values()].find(s => s.id.toLowerCase().startsWith(idSearch));
8
+ if (!scenario) {
9
+ console.error(`Error: Scenario not found with ID starting with: "${idSearch}"`);
10
+ console.error('Available scenarios:');
11
+ for (const s of model.scenarios.values()) {
12
+ const slice = model.slices.get(s.sliceId);
13
+ console.error(` - "${s.name}" (id: ${s.id.slice(0, 8)}) (in slice "${slice?.name ?? 'unknown'}")`);
14
+ }
15
+ process.exit(1);
16
+ }
17
+ const slice = model.slices.get(scenario.sliceId);
18
+ appendEvent(filePath, {
19
+ type: 'ScenarioRemoved',
20
+ scenarioId: scenario.id,
21
+ timestamp: Date.now(),
22
+ });
23
+ console.log(`Removed scenario "${scenario.name}" from slice "${slice?.name ?? 'unknown'}"`);
24
+ return;
25
+ }
26
+ // Case-insensitive exact name match
3
27
  const scenarioNameLower = scenarioName.toLowerCase();
4
- // Find scenarios matching the name
5
- let matchingScenarios = [...model.scenarios.values()].filter(s => s.name.toLowerCase() === scenarioNameLower || s.name.toLowerCase().includes(scenarioNameLower));
6
- // If slice name provided, filter to that slice
28
+ let matchingScenarios = [...model.scenarios.values()].filter(s => s.name.toLowerCase() === scenarioNameLower);
29
+ // If slice name provided, filter to that slice using exact match
7
30
  if (sliceName && matchingScenarios.length > 0) {
8
- const sliceNameLower = sliceName.toLowerCase();
9
- const slice = [...model.slices.values()].find(s => s.name.toLowerCase() === sliceNameLower || s.name.toLowerCase().includes(sliceNameLower));
10
- if (slice) {
11
- matchingScenarios = matchingScenarios.filter(s => s.sliceId === slice.id);
31
+ const sliceResult = findElement(model.slices, sliceName);
32
+ if (sliceResult.success) {
33
+ matchingScenarios = matchingScenarios.filter(s => s.sliceId === sliceResult.element.id);
34
+ }
35
+ else if (sliceResult.error === 'ambiguous') {
36
+ console.error(`Error: Multiple slices found with name "${sliceName}"`);
37
+ console.error('Please specify using the slice ID:');
38
+ for (const s of sliceResult.matches) {
39
+ console.error(` - ${formatElementWithId(s, false)}`);
40
+ }
41
+ process.exit(1);
12
42
  }
43
+ // If slice not found, just proceed without filtering
13
44
  }
14
45
  if (matchingScenarios.length === 0) {
15
- console.error(`Error: Scenario not found: ${scenarioName}`);
46
+ console.error(`Error: Scenario not found: "${scenarioName}"`);
16
47
  console.error('Available scenarios:');
17
48
  for (const s of model.scenarios.values()) {
18
49
  const slice = model.slices.get(s.sliceId);
19
- console.error(` - "${s.name}" (in slice "${slice?.name ?? 'unknown'}")`);
50
+ console.error(` - "${s.name}" (id: ${s.id.slice(0, 8)}) (in slice "${slice?.name ?? 'unknown'}")`);
20
51
  }
21
52
  process.exit(1);
22
53
  }
23
54
  if (matchingScenarios.length > 1) {
24
- console.error(`Error: Multiple scenarios match "${scenarioName}". Use --slice to disambiguate:`);
55
+ console.error(`Error: Multiple scenarios found with name "${scenarioName}"`);
56
+ console.error('Please specify using the scenario ID or --slice to disambiguate:');
25
57
  for (const s of matchingScenarios) {
26
58
  const slice = model.slices.get(s.sliceId);
27
- console.error(` - "${s.name}" in slice "${slice?.name ?? 'unknown'}"`);
59
+ console.error(` - "${s.name}" (id: ${s.id.slice(0, 8)}) (in slice "${slice?.name ?? 'unknown'}")`);
28
60
  }
61
+ console.error('');
62
+ console.error(`Usage: eventmodeler remove scenario "id:${matchingScenarios[0].id.slice(0, 8)}"`);
29
63
  process.exit(1);
30
64
  }
31
65
  const scenario = matchingScenarios[0];
@@ -1,4 +1,5 @@
1
1
  import { escapeXml, outputJson } from '../../lib/format.js';
2
+ import { findElementOrExit } from '../../lib/element-lookup.js';
2
3
  // Get screens whose center point is inside the actor bounds
3
4
  function getScreensInActor(model, actor) {
4
5
  const bounds = {
@@ -30,17 +31,7 @@ function findSliceForScreen(model, screen) {
30
31
  }
31
32
  export function showActor(model, actorName, format) {
32
33
  // Find the actor
33
- const nameLower = actorName.toLowerCase();
34
- const actors = [...model.actors.values()];
35
- const actor = actors.find(a => a.name.toLowerCase() === nameLower || a.name.toLowerCase().includes(nameLower));
36
- if (!actor) {
37
- console.error(`Error: Actor not found: ${actorName}`);
38
- console.error('Available actors:');
39
- for (const a of actors) {
40
- console.error(` - ${a.name}`);
41
- }
42
- process.exit(1);
43
- }
34
+ const actor = findElementOrExit(model.actors, actorName, 'actor');
44
35
  // Get screens dynamically based on position (center point inside actor bounds)
45
36
  const screensInActor = getScreensInActor(model, actor);
46
37
  if (format === 'json') {
@@ -1,4 +1,5 @@
1
1
  import { escapeXml, outputJson } from '../../lib/format.js';
2
+ import { findElementOrExit } from '../../lib/element-lookup.js';
2
3
  function findMatchingField(fields, targetName, targetType) {
3
4
  for (const field of fields) {
4
5
  // Check if name and type match
@@ -34,17 +35,7 @@ function getEventsInAggregate(model, aggregate) {
34
35
  }
35
36
  export function showAggregateCompleteness(model, aggregateName, format) {
36
37
  // Find the aggregate
37
- const nameLower = aggregateName.toLowerCase();
38
- const aggregates = [...model.aggregates.values()];
39
- const aggregate = aggregates.find(a => a.name.toLowerCase() === nameLower || a.name.toLowerCase().includes(nameLower));
40
- if (!aggregate) {
41
- console.error(`Error: Aggregate not found: ${aggregateName}`);
42
- console.error('Available aggregates:');
43
- for (const a of aggregates) {
44
- console.error(` - ${a.name}`);
45
- }
46
- process.exit(1);
47
- }
38
+ const aggregate = findElementOrExit(model.aggregates, aggregateName, 'aggregate');
48
39
  // Get events dynamically based on position (center point inside aggregate bounds)
49
40
  const eventsInAggregate = getEventsInAggregate(model, aggregate);
50
41
  // Check if aggregate has ID field configured