eventmodeler 0.3.2 → 0.3.3

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.
@@ -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;
@@ -1,3 +1,16 @@
1
+ // Resolve a linked copy to its canonical original, or return the ID unchanged if not a copy
2
+ export function resolveToCanonical(model, id) {
3
+ const screen = model.screens.get(id);
4
+ if (screen?.originalNodeId)
5
+ return screen.originalNodeId;
6
+ const event = model.events.get(id);
7
+ if (event?.originalNodeId)
8
+ return event.originalNodeId;
9
+ const readModel = model.readModels.get(id);
10
+ if (readModel?.originalNodeId)
11
+ return readModel.originalNodeId;
12
+ return id;
13
+ }
1
14
  // Get element info (name and type) by ID
2
15
  export function getElementInfo(model, id) {
3
16
  const screen = model.screens.get(id);
@@ -61,16 +74,13 @@ function isElementInSlice(slice, position, width, height) {
61
74
  centerY >= slice.position.y &&
62
75
  centerY <= slice.position.y + slice.size.height);
63
76
  }
64
- // Get all component IDs in a slice (including canonical group members for linked copies)
77
+ // Get all component IDs in a slice (excludes linked copies - they are UI-only)
65
78
  export function getSliceComponentIds(model, slice) {
66
79
  const ids = new Set();
67
- const canonicalIds = new Set();
68
- // First pass: collect IDs and canonical IDs of elements in the slice
80
+ // Only include canonical elements (not linked copies) that are spatially in the slice
69
81
  for (const screen of model.screens.values()) {
70
- if (isElementInSlice(slice, screen.position, screen.width, screen.height)) {
82
+ if (!screen.originalNodeId && isElementInSlice(slice, screen.position, screen.width, screen.height)) {
71
83
  ids.add(screen.id);
72
- if (screen.canonicalId)
73
- canonicalIds.add(screen.canonicalId);
74
84
  }
75
85
  }
76
86
  for (const command of model.commands.values()) {
@@ -79,17 +89,13 @@ export function getSliceComponentIds(model, slice) {
79
89
  }
80
90
  }
81
91
  for (const event of model.events.values()) {
82
- if (isElementInSlice(slice, event.position, event.width, event.height)) {
92
+ if (!event.originalNodeId && isElementInSlice(slice, event.position, event.width, event.height)) {
83
93
  ids.add(event.id);
84
- if (event.canonicalId)
85
- canonicalIds.add(event.canonicalId);
86
94
  }
87
95
  }
88
96
  for (const readModel of model.readModels.values()) {
89
- if (isElementInSlice(slice, readModel.position, readModel.width, readModel.height)) {
97
+ if (!readModel.originalNodeId && isElementInSlice(slice, readModel.position, readModel.width, readModel.height)) {
90
98
  ids.add(readModel.id);
91
- if (readModel.canonicalId)
92
- canonicalIds.add(readModel.canonicalId);
93
99
  }
94
100
  }
95
101
  for (const processor of model.processors.values()) {
@@ -97,25 +103,6 @@ export function getSliceComponentIds(model, slice) {
97
103
  ids.add(processor.id);
98
104
  }
99
105
  }
100
- // Second pass: add all elements that share a canonical ID with elements in the slice
101
- // This ensures flows targeting any element in a canonical group are detected
102
- if (canonicalIds.size > 0) {
103
- for (const screen of model.screens.values()) {
104
- if (screen.canonicalId && canonicalIds.has(screen.canonicalId)) {
105
- ids.add(screen.id);
106
- }
107
- }
108
- for (const event of model.events.values()) {
109
- if (event.canonicalId && canonicalIds.has(event.canonicalId)) {
110
- ids.add(event.id);
111
- }
112
- }
113
- for (const readModel of model.readModels.values()) {
114
- if (readModel.canonicalId && canonicalIds.has(readModel.canonicalId)) {
115
- ids.add(readModel.id);
116
- }
117
- }
118
- }
119
106
  return ids;
120
107
  }
121
108
  // Find which slice contains a component
@@ -163,10 +150,13 @@ export function findSliceForComponent(model, componentId) {
163
150
  }
164
151
  return null;
165
152
  }
166
- // Build FlowInfo from a Flow
153
+ // Build FlowInfo from a Flow (resolves linked copies to their canonical originals)
167
154
  function buildFlowInfo(model, flow) {
168
- const source = getElementInfo(model, flow.sourceId);
169
- const target = getElementInfo(model, flow.targetId);
155
+ // Resolve linked copies to canonical originals
156
+ const canonicalSourceId = resolveToCanonical(model, flow.sourceId);
157
+ const canonicalTargetId = resolveToCanonical(model, flow.targetId);
158
+ const source = getElementInfo(model, canonicalSourceId);
159
+ const target = getElementInfo(model, canonicalTargetId);
170
160
  if (!source || !target)
171
161
  return null;
172
162
  return {
@@ -177,13 +167,16 @@ function buildFlowInfo(model, flow) {
177
167
  fieldMappings: enrichFieldMappings(model, flow),
178
168
  };
179
169
  }
180
- // Build SliceFlowInfo from a Flow (includes slice info for source/target)
170
+ // Build SliceFlowInfo from a Flow (includes slice info for source/target, resolves linked copies)
181
171
  function buildSliceFlowInfo(model, flow) {
182
172
  const base = buildFlowInfo(model, flow);
183
173
  if (!base)
184
174
  return null;
185
- const sourceSlice = findSliceForComponent(model, flow.sourceId);
186
- const targetSlice = findSliceForComponent(model, flow.targetId);
175
+ // Resolve linked copies to canonical originals for slice lookup
176
+ const canonicalSourceId = resolveToCanonical(model, flow.sourceId);
177
+ const canonicalTargetId = resolveToCanonical(model, flow.targetId);
178
+ const sourceSlice = findSliceForComponent(model, canonicalSourceId);
179
+ const targetSlice = findSliceForComponent(model, canonicalTargetId);
187
180
  return {
188
181
  ...base,
189
182
  sourceSlice: sourceSlice ? { id: sourceSlice.id, name: sourceSlice.name } : undefined,
@@ -195,7 +188,10 @@ export function getInboundFlows(model, slice) {
195
188
  const componentIds = getSliceComponentIds(model, slice);
196
189
  const flows = [];
197
190
  for (const flow of model.flows.values()) {
198
- if (componentIds.has(flow.targetId) && !componentIds.has(flow.sourceId)) {
191
+ // Resolve linked copies to canonical originals for containment check
192
+ const canonicalSourceId = resolveToCanonical(model, flow.sourceId);
193
+ const canonicalTargetId = resolveToCanonical(model, flow.targetId);
194
+ if (componentIds.has(canonicalTargetId) && !componentIds.has(canonicalSourceId)) {
199
195
  const flowInfo = buildSliceFlowInfo(model, flow);
200
196
  if (flowInfo)
201
197
  flows.push(flowInfo);
@@ -208,7 +204,10 @@ export function getOutboundFlows(model, slice) {
208
204
  const componentIds = getSliceComponentIds(model, slice);
209
205
  const flows = [];
210
206
  for (const flow of model.flows.values()) {
211
- if (componentIds.has(flow.sourceId) && !componentIds.has(flow.targetId)) {
207
+ // Resolve linked copies to canonical originals for containment check
208
+ const canonicalSourceId = resolveToCanonical(model, flow.sourceId);
209
+ const canonicalTargetId = resolveToCanonical(model, flow.targetId);
210
+ if (componentIds.has(canonicalSourceId) && !componentIds.has(canonicalTargetId)) {
212
211
  const flowInfo = buildSliceFlowInfo(model, flow);
213
212
  if (flowInfo)
214
213
  flows.push(flowInfo);
@@ -221,7 +220,10 @@ export function getInternalFlows(model, slice) {
221
220
  const componentIds = getSliceComponentIds(model, slice);
222
221
  const flows = [];
223
222
  for (const flow of model.flows.values()) {
224
- if (componentIds.has(flow.sourceId) && componentIds.has(flow.targetId)) {
223
+ // Resolve linked copies to canonical originals for containment check
224
+ const canonicalSourceId = resolveToCanonical(model, flow.sourceId);
225
+ const canonicalTargetId = resolveToCanonical(model, flow.targetId);
226
+ if (componentIds.has(canonicalSourceId) && componentIds.has(canonicalTargetId)) {
225
227
  const flowInfo = buildFlowInfo(model, flow);
226
228
  if (flowInfo)
227
229
  flows.push(flowInfo);
@@ -229,17 +231,22 @@ export function getInternalFlows(model, slice) {
229
231
  }
230
232
  return flows;
231
233
  }
232
- // Get all flows for a specific element (both directions)
234
+ // Get all flows for a specific element (both directions, resolves linked copies)
233
235
  export function getFlowsForElement(model, elementId) {
234
236
  const incoming = [];
235
237
  const outgoing = [];
238
+ // Resolve element ID to canonical if it's a linked copy
239
+ const canonicalElementId = resolveToCanonical(model, elementId);
236
240
  for (const flow of model.flows.values()) {
237
- if (flow.targetId === elementId) {
241
+ // Resolve flow endpoints to canonical originals
242
+ const canonicalTargetId = resolveToCanonical(model, flow.targetId);
243
+ const canonicalSourceId = resolveToCanonical(model, flow.sourceId);
244
+ if (canonicalTargetId === canonicalElementId) {
238
245
  const flowInfo = buildFlowInfo(model, flow);
239
246
  if (flowInfo)
240
247
  incoming.push(flowInfo);
241
248
  }
242
- if (flow.sourceId === elementId) {
249
+ if (canonicalSourceId === canonicalElementId) {
243
250
  const flowInfo = buildFlowInfo(model, flow);
244
251
  if (flowInfo)
245
252
  outgoing.push(flowInfo);
@@ -257,13 +264,16 @@ export function findSliceToSliceFlows(model, slices) {
257
264
  // Group flows by slice pair
258
265
  const flowsBySlicePair = new Map();
259
266
  for (const flow of model.flows.values()) {
267
+ // Resolve linked copies to canonical originals
268
+ const canonicalSourceId = resolveToCanonical(model, flow.sourceId);
269
+ const canonicalTargetId = resolveToCanonical(model, flow.targetId);
260
270
  let sourceSliceId = null;
261
271
  let targetSliceId = null;
262
272
  // Find which slice contains source and target
263
273
  for (const [sliceId, componentIds] of sliceComponentMap.entries()) {
264
- if (componentIds.has(flow.sourceId))
274
+ if (componentIds.has(canonicalSourceId))
265
275
  sourceSliceId = sliceId;
266
- if (componentIds.has(flow.targetId))
276
+ if (componentIds.has(canonicalTargetId))
267
277
  targetSliceId = sliceId;
268
278
  }
269
279
  // Only include flows between different slices in our set
@@ -304,7 +314,10 @@ export function findChapterInboundFlows(model, slices) {
304
314
  }
305
315
  const flows = [];
306
316
  for (const flow of model.flows.values()) {
307
- if (chapterComponentIds.has(flow.targetId) && !chapterComponentIds.has(flow.sourceId)) {
317
+ // Resolve linked copies to canonical originals
318
+ const canonicalSourceId = resolveToCanonical(model, flow.sourceId);
319
+ const canonicalTargetId = resolveToCanonical(model, flow.targetId);
320
+ if (chapterComponentIds.has(canonicalTargetId) && !chapterComponentIds.has(canonicalSourceId)) {
308
321
  const flowInfo = buildSliceFlowInfo(model, flow);
309
322
  if (flowInfo)
310
323
  flows.push(flowInfo);
@@ -322,7 +335,10 @@ export function findChapterOutboundFlows(model, slices) {
322
335
  }
323
336
  const flows = [];
324
337
  for (const flow of model.flows.values()) {
325
- if (chapterComponentIds.has(flow.sourceId) && !chapterComponentIds.has(flow.targetId)) {
338
+ // Resolve linked copies to canonical originals
339
+ const canonicalSourceId = resolveToCanonical(model, flow.sourceId);
340
+ const canonicalTargetId = resolveToCanonical(model, flow.targetId);
341
+ if (chapterComponentIds.has(canonicalSourceId) && !chapterComponentIds.has(canonicalTargetId)) {
326
342
  const flowInfo = buildSliceFlowInfo(model, flow);
327
343
  if (flowInfo)
328
344
  flows.push(flowInfo);
@@ -1,7 +1,7 @@
1
1
  import { escapeXml, escapeXmlText, outputJson } from '../../lib/format.js';
2
2
  import { findElementOrExit } from '../../lib/element-lookup.js';
3
3
  import { findChapterForSlice, getChapterHierarchy } from '../../lib/chapter-utils.js';
4
- import { getInboundFlows, getOutboundFlows, getFlowsForElement, getSliceComponentIds, } from '../../lib/flow-utils.js';
4
+ import { getInboundFlows, getOutboundFlows, getFlowsForElement, getSliceComponentIds, resolveToCanonical, } from '../../lib/flow-utils.js';
5
5
  function formatFieldValues(values) {
6
6
  if (!values || Object.keys(values).length === 0)
7
7
  return '';
@@ -46,36 +46,16 @@ function getSliceComponents(model, slice) {
46
46
  const centerY = pos.y + height / 2;
47
47
  return centerX >= bounds.left && centerX <= bounds.right && centerY >= bounds.top && centerY <= bounds.bottom;
48
48
  }
49
- // Include all elements in the slice (including linked copies)
50
- // Linked copies will be marked as such in the output
49
+ // Only include canonical elements - linked copies are UI-only conveniences
50
+ // and should not appear as slice contents in CLI output
51
51
  return {
52
52
  commands: [...model.commands.values()].filter(c => isInSlice(c.position, c.width, c.height)),
53
- events: [...model.events.values()].filter(e => isInSlice(e.position, e.width, e.height)),
54
- readModels: [...model.readModels.values()].filter(rm => isInSlice(rm.position, rm.width, rm.height)),
55
- screens: [...model.screens.values()].filter(s => isInSlice(s.position, s.width, s.height)),
53
+ events: [...model.events.values()].filter(e => !e.originalNodeId && isInSlice(e.position, e.width, e.height)),
54
+ readModels: [...model.readModels.values()].filter(rm => !rm.originalNodeId && isInSlice(rm.position, rm.width, rm.height)),
55
+ screens: [...model.screens.values()].filter(s => !s.originalNodeId && isInSlice(s.position, s.width, s.height)),
56
56
  processors: [...model.processors.values()].filter(p => isInSlice(p.position, p.width, p.height)),
57
57
  };
58
58
  }
59
- // Find which slice contains the original of a linked copy
60
- function findSliceForNode(model, nodeId) {
61
- const event = model.events.get(nodeId);
62
- const readModel = model.readModels.get(nodeId);
63
- const screen = model.screens.get(nodeId);
64
- const node = event ?? readModel ?? screen;
65
- if (!node)
66
- return null;
67
- for (const slice of model.slices.values()) {
68
- const centerX = node.position.x + node.width / 2;
69
- const centerY = node.position.y + node.height / 2;
70
- if (centerX >= slice.position.x &&
71
- centerX <= slice.position.x + slice.size.width &&
72
- centerY >= slice.position.y &&
73
- centerY <= slice.position.y + slice.size.height) {
74
- return slice;
75
- }
76
- }
77
- return null;
78
- }
79
59
  // Find which aggregate an event belongs to (center point inside aggregate bounds)
80
60
  function findAggregateForEvent(model, event) {
81
61
  const centerX = event.position.x + event.width / 2;
@@ -191,8 +171,17 @@ function formatSliceXml(model, slice) {
191
171
  const chapter = findChapterForSlice(model, slice);
192
172
  // Use shared function that handles canonical groups for linked copies
193
173
  const componentIds = getSliceComponentIds(model, slice);
194
- const flows = [...model.flows.values()].filter(f => componentIds.has(f.sourceId) || componentIds.has(f.targetId));
195
- const internalFlows = flows.filter(f => componentIds.has(f.sourceId) && componentIds.has(f.targetId));
174
+ // Resolve linked copies to canonical originals when checking flow containment
175
+ const flows = [...model.flows.values()].filter(f => {
176
+ const canonicalSourceId = resolveToCanonical(model, f.sourceId);
177
+ const canonicalTargetId = resolveToCanonical(model, f.targetId);
178
+ return componentIds.has(canonicalSourceId) || componentIds.has(canonicalTargetId);
179
+ });
180
+ const internalFlows = flows.filter(f => {
181
+ const canonicalSourceId = resolveToCanonical(model, f.sourceId);
182
+ const canonicalTargetId = resolveToCanonical(model, f.targetId);
183
+ return componentIds.has(canonicalSourceId) && componentIds.has(canonicalTargetId);
184
+ });
196
185
  // Get inbound and outbound flows for the slice
197
186
  const inboundFlows = getInboundFlows(model, slice);
198
187
  const outboundFlows = getOutboundFlows(model, slice);
@@ -210,19 +199,10 @@ function formatSliceXml(model, slice) {
210
199
  }
211
200
  xml += ' <components>\n';
212
201
  for (const screen of components.screens) {
213
- // Check if this is a linked copy
214
- const copyAttr = screen.originalNodeId ? ' linked-copy="true"' : '';
215
- let originAttr = '';
216
- if (screen.originalNodeId) {
217
- const originSlice = findSliceForNode(model, screen.originalNodeId);
218
- if (originSlice) {
219
- originAttr = ` origin-slice="${escapeXml(originSlice.name)}"`;
220
- }
221
- }
222
202
  // Check which actor this screen belongs to
223
203
  const actor = findActorForScreen(model, screen);
224
204
  const actorAttr = actor ? ` actor="${escapeXml(actor.name)}"` : '';
225
- xml += ` <screen id="${screen.id}" name="${escapeXml(screen.name)}"${copyAttr}${originAttr}${actorAttr}>\n`;
205
+ xml += ` <screen id="${screen.id}" name="${escapeXml(screen.name)}"${actorAttr}>\n`;
226
206
  // Add flow annotations
227
207
  const screenFlows = getFlowsForElement(model, screen.id);
228
208
  xml += formatFlowAnnotationsXml(screen.id, screenFlows.incoming, screenFlows.outgoing, componentIds, ' ');
@@ -258,19 +238,10 @@ function formatSliceXml(model, slice) {
258
238
  xml += ' </command>\n';
259
239
  }
260
240
  for (const event of components.events) {
261
- // Check if this is a linked copy
262
- const copyAttr = event.originalNodeId ? ' linked-copy="true"' : '';
263
- let originAttr = '';
264
- if (event.originalNodeId) {
265
- const originSlice = findSliceForNode(model, event.originalNodeId);
266
- if (originSlice) {
267
- originAttr = ` origin-slice="${escapeXml(originSlice.name)}"`;
268
- }
269
- }
270
241
  // Check which aggregate this event belongs to
271
242
  const aggregate = findAggregateForEvent(model, event);
272
243
  const aggregateAttr = aggregate ? ` aggregate="${escapeXml(aggregate.name)}"` : '';
273
- xml += ` <event id="${event.id}" name="${escapeXml(event.name)}"${copyAttr}${originAttr}${aggregateAttr}>\n`;
244
+ xml += ` <event id="${event.id}" name="${escapeXml(event.name)}"${aggregateAttr}>\n`;
274
245
  // Add flow annotations
275
246
  const eventFlows = getFlowsForElement(model, event.id);
276
247
  xml += formatFlowAnnotationsXml(event.id, eventFlows.incoming, eventFlows.outgoing, componentIds, ' ');
@@ -295,16 +266,7 @@ function formatSliceXml(model, slice) {
295
266
  xml += ' </event>\n';
296
267
  }
297
268
  for (const readModel of components.readModels) {
298
- // Check if this is a linked copy
299
- const copyAttr = readModel.originalNodeId ? ' linked-copy="true"' : '';
300
- let originAttr = '';
301
- if (readModel.originalNodeId) {
302
- const originSlice = findSliceForNode(model, readModel.originalNodeId);
303
- if (originSlice) {
304
- originAttr = ` origin-slice="${escapeXml(originSlice.name)}"`;
305
- }
306
- }
307
- xml += ` <read-model id="${readModel.id}" name="${escapeXml(readModel.name)}"${copyAttr}${originAttr}>\n`;
269
+ xml += ` <read-model id="${readModel.id}" name="${escapeXml(readModel.name)}">\n`;
308
270
  // Add flow annotations
309
271
  const rmFlows = getFlowsForElement(model, readModel.id);
310
272
  xml += formatFlowAnnotationsXml(readModel.id, rmFlows.incoming, rmFlows.outgoing, componentIds, ' ');
@@ -428,8 +390,17 @@ function formatSliceJson(model, slice) {
428
390
  const chapter = findChapterForSlice(model, slice);
429
391
  // Use shared function that handles canonical groups for linked copies
430
392
  const componentIds = getSliceComponentIds(model, slice);
431
- const flows = [...model.flows.values()].filter(f => componentIds.has(f.sourceId) || componentIds.has(f.targetId));
432
- const internalFlows = flows.filter(f => componentIds.has(f.sourceId) && componentIds.has(f.targetId));
393
+ // Resolve linked copies to canonical originals when checking flow containment
394
+ const flows = [...model.flows.values()].filter(f => {
395
+ const canonicalSourceId = resolveToCanonical(model, f.sourceId);
396
+ const canonicalTargetId = resolveToCanonical(model, f.targetId);
397
+ return componentIds.has(canonicalSourceId) || componentIds.has(canonicalTargetId);
398
+ });
399
+ const internalFlows = flows.filter(f => {
400
+ const canonicalSourceId = resolveToCanonical(model, f.sourceId);
401
+ const canonicalTargetId = resolveToCanonical(model, f.targetId);
402
+ return componentIds.has(canonicalSourceId) && componentIds.has(canonicalTargetId);
403
+ });
433
404
  // Get inbound and outbound flows for the slice
434
405
  const inboundFlows = getInboundFlows(model, slice);
435
406
  const outboundFlows = getOutboundFlows(model, slice);
@@ -453,12 +424,6 @@ function formatSliceJson(model, slice) {
453
424
  name: screen.name,
454
425
  fields: screen.fields.map(fieldToJson)
455
426
  };
456
- if (screen.originalNodeId) {
457
- screenObj.linkedCopy = true;
458
- const originSlice = findSliceForNode(model, screen.originalNodeId);
459
- if (originSlice)
460
- screenObj.originSlice = originSlice.name;
461
- }
462
427
  const actor = findActorForScreen(model, screen);
463
428
  if (actor)
464
429
  screenObj.actor = actor.name;
@@ -509,12 +474,6 @@ function formatSliceJson(model, slice) {
509
474
  name: event.name,
510
475
  fields: event.fields.map(fieldToJson)
511
476
  };
512
- if (event.originalNodeId) {
513
- eventObj.linkedCopy = true;
514
- const originSlice = findSliceForNode(model, event.originalNodeId);
515
- if (originSlice)
516
- eventObj.originSlice = originSlice.name;
517
- }
518
477
  const aggregate = findAggregateForEvent(model, event);
519
478
  if (aggregate)
520
479
  eventObj.aggregate = aggregate.name;
@@ -542,12 +501,6 @@ function formatSliceJson(model, slice) {
542
501
  name: rm.name,
543
502
  fields: rm.fields.map(fieldToJson)
544
503
  };
545
- if (rm.originalNodeId) {
546
- rmObj.linkedCopy = true;
547
- const originSlice = findSliceForNode(model, rm.originalNodeId);
548
- if (originSlice)
549
- rmObj.originSlice = originSlice.name;
550
- }
551
504
  // Add flow annotations
552
505
  const rmFlows = getFlowsForElement(model, rm.id);
553
506
  if (rmFlows.incoming.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eventmodeler",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "CLI tool for interacting with Event Model files - query, update, and export event models from the terminal",
5
5
  "type": "module",
6
6
  "bin": {