eventmodeler 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -54,17 +54,19 @@ USAGE:
54
54
  eventmodeler <command> [options] Run a CLI command
55
55
 
56
56
  COMMANDS:
57
- list slices List all slices with their status
57
+ list slices [--chapter <name>] List all slices (optionally filtered by chapter)
58
58
  list events List all events
59
59
  list commands List all commands
60
60
  list chapters List all chapters
61
61
  list aggregates List all aggregates
62
62
  list actors List all actors
63
63
 
64
- show slice <name> Show detailed XML view of a slice
64
+ show slice <name> Show slice with components, flow annotations, and
65
+ inbound/outbound dependencies
65
66
  show event <name> Show detailed XML view of an event
66
67
  show command <name> Show detailed XML view of a command
67
- show chapter <name> Show chapter with its slices
68
+ show chapter <name> Show chapter with slices, flow graph, and
69
+ external dependencies
68
70
  show completeness <name> Show field mapping completeness for any element
69
71
  show model-completeness Show completeness of all flows in the model
70
72
  show aggregate-completeness <name>
@@ -134,6 +136,7 @@ EXAMPLES:
134
136
  async function main() {
135
137
  let fileArg = null;
136
138
  let formatArg = null;
139
+ let chapterArg = null;
137
140
  const filteredArgs = [];
138
141
  for (let i = 0; i < args.length; i++) {
139
142
  if (args[i] === '-f' || args[i] === '--file') {
@@ -142,6 +145,9 @@ async function main() {
142
145
  else if (args[i] === '--format') {
143
146
  formatArg = args[++i];
144
147
  }
148
+ else if (args[i] === '--chapter') {
149
+ chapterArg = args[++i];
150
+ }
145
151
  else if (args[i] === '-h' || args[i] === '--help') {
146
152
  printHelp();
147
153
  process.exit(0);
@@ -183,7 +189,7 @@ async function main() {
183
189
  case 'list':
184
190
  switch (subcommand) {
185
191
  case 'slices':
186
- listSlices(model, format);
192
+ listSlices(model, format, chapterArg ?? undefined);
187
193
  break;
188
194
  case 'events':
189
195
  listEvents(model, format);
@@ -0,0 +1,13 @@
1
+ import type { Chapter, EventModel, Slice } from '../types.js';
2
+ export interface ChapterWithHierarchy {
3
+ id: string;
4
+ name: string;
5
+ parent?: ChapterWithHierarchy;
6
+ }
7
+ export declare function findParentChapter(model: EventModel, chapter: Chapter): Chapter | null;
8
+ export declare function getChapterHierarchy(model: EventModel, chapter: Chapter): ChapterWithHierarchy;
9
+ export declare function hierarchyToArray(hierarchy: ChapterWithHierarchy): Array<{
10
+ id: string;
11
+ name: string;
12
+ }>;
13
+ export declare function findChapterForSlice(model: EventModel, slice: Slice): Chapter | null;
@@ -0,0 +1,71 @@
1
+ // Check if two chapters have X-overlap
2
+ function hasXOverlap(parent, child) {
3
+ const parentLeft = parent.position.x;
4
+ const parentRight = parent.position.x + parent.size.width;
5
+ const childLeft = child.position.x;
6
+ const childRight = child.position.x + child.size.width;
7
+ return childLeft < parentRight && childRight > parentLeft;
8
+ }
9
+ // Find the immediate parent of a chapter (nearest chapter above with X-overlap)
10
+ export function findParentChapter(model, chapter) {
11
+ let bestParent = null;
12
+ let bestParentY = -Infinity;
13
+ for (const candidate of model.chapters.values()) {
14
+ if (candidate.id === chapter.id)
15
+ continue;
16
+ // Parent must be above (lower y value)
17
+ if (candidate.position.y >= chapter.position.y)
18
+ continue;
19
+ // Must have X-overlap
20
+ if (!hasXOverlap(candidate, chapter))
21
+ continue;
22
+ // Take the closest parent (highest y value that's still above)
23
+ if (candidate.position.y > bestParentY) {
24
+ bestParent = candidate;
25
+ bestParentY = candidate.position.y;
26
+ }
27
+ }
28
+ return bestParent;
29
+ }
30
+ // Build full hierarchy for a chapter (from root to this chapter)
31
+ export function getChapterHierarchy(model, chapter) {
32
+ const parent = findParentChapter(model, chapter);
33
+ const result = {
34
+ id: chapter.id,
35
+ name: chapter.name,
36
+ };
37
+ if (parent) {
38
+ result.parent = getChapterHierarchy(model, parent);
39
+ }
40
+ return result;
41
+ }
42
+ // Convert hierarchy to array (root first)
43
+ export function hierarchyToArray(hierarchy) {
44
+ const result = [];
45
+ function collect(h) {
46
+ if (h.parent)
47
+ collect(h.parent);
48
+ result.push({ id: h.id, name: h.name });
49
+ }
50
+ collect(hierarchy);
51
+ return result;
52
+ }
53
+ // Find which chapter contains a slice (based on horizontal center)
54
+ // Returns the deepest (highest y-value) chapter that contains the slice
55
+ export function findChapterForSlice(model, slice) {
56
+ const sliceCenterX = slice.position.x + slice.size.width / 2;
57
+ let bestChapter = null;
58
+ let bestY = -Infinity;
59
+ for (const chapter of model.chapters.values()) {
60
+ const chapterLeft = chapter.position.x;
61
+ const chapterRight = chapter.position.x + chapter.size.width;
62
+ if (sliceCenterX >= chapterLeft && sliceCenterX <= chapterRight) {
63
+ // Take the deepest (highest y) chapter
64
+ if (chapter.position.y > bestY) {
65
+ bestChapter = chapter;
66
+ bestY = chapter.position.y;
67
+ }
68
+ }
69
+ }
70
+ return bestChapter;
71
+ }
@@ -0,0 +1,52 @@
1
+ import type { EventModel, Slice } from '../types.js';
2
+ export type ElementType = 'screen' | 'command' | 'event' | 'read-model' | 'processor';
3
+ export interface ElementInfo {
4
+ id: string;
5
+ name: string;
6
+ type: ElementType;
7
+ }
8
+ export interface FieldMappingInfo {
9
+ from: string;
10
+ to: string;
11
+ }
12
+ export interface FlowInfo {
13
+ flowId: string;
14
+ flowType: string;
15
+ source: ElementInfo;
16
+ target: ElementInfo;
17
+ fieldMappings: FieldMappingInfo[];
18
+ }
19
+ export interface SliceFlowInfo extends FlowInfo {
20
+ sourceSlice?: {
21
+ id: string;
22
+ name: string;
23
+ };
24
+ targetSlice?: {
25
+ id: string;
26
+ name: string;
27
+ };
28
+ }
29
+ export declare function getElementInfo(model: EventModel, id: string): ElementInfo | null;
30
+ export declare function getSliceComponentIds(model: EventModel, slice: Slice): Set<string>;
31
+ export declare function findSliceForComponent(model: EventModel, componentId: string): Slice | null;
32
+ export declare function getInboundFlows(model: EventModel, slice: Slice): SliceFlowInfo[];
33
+ export declare function getOutboundFlows(model: EventModel, slice: Slice): SliceFlowInfo[];
34
+ export declare function getInternalFlows(model: EventModel, slice: Slice): FlowInfo[];
35
+ export declare function getFlowsForElement(model: EventModel, elementId: string): {
36
+ incoming: FlowInfo[];
37
+ outgoing: FlowInfo[];
38
+ };
39
+ export interface SliceToSliceFlow {
40
+ fromSlice: {
41
+ id: string;
42
+ name: string;
43
+ };
44
+ toSlice: {
45
+ id: string;
46
+ name: string;
47
+ };
48
+ flows: SliceFlowInfo[];
49
+ }
50
+ export declare function findSliceToSliceFlows(model: EventModel, slices: Slice[]): SliceToSliceFlow[];
51
+ export declare function findChapterInboundFlows(model: EventModel, slices: Slice[]): SliceFlowInfo[];
52
+ export declare function findChapterOutboundFlows(model: EventModel, slices: Slice[]): SliceFlowInfo[];
@@ -0,0 +1,305 @@
1
+ // Get element info (name and type) by ID
2
+ export function getElementInfo(model, id) {
3
+ const screen = model.screens.get(id);
4
+ if (screen)
5
+ return { id, name: screen.name, type: 'screen' };
6
+ const command = model.commands.get(id);
7
+ if (command)
8
+ return { id, name: command.name, type: 'command' };
9
+ const event = model.events.get(id);
10
+ if (event)
11
+ return { id, name: event.name, type: 'event' };
12
+ const readModel = model.readModels.get(id);
13
+ if (readModel)
14
+ return { id, name: readModel.name, type: 'read-model' };
15
+ const processor = model.processors.get(id);
16
+ if (processor)
17
+ return { id, name: processor.name, type: 'processor' };
18
+ return null;
19
+ }
20
+ // Get fields for an element
21
+ function getElementFields(model, id) {
22
+ return (model.screens.get(id)?.fields ??
23
+ model.commands.get(id)?.fields ??
24
+ model.events.get(id)?.fields ??
25
+ model.readModels.get(id)?.fields ??
26
+ model.processors.get(id)?.fields ??
27
+ []);
28
+ }
29
+ // Find a field by ID in a fields array (recursive for subfields)
30
+ function findFieldById(fields, fieldId) {
31
+ for (const field of fields) {
32
+ if (field.id === fieldId)
33
+ return field;
34
+ if (field.subfields) {
35
+ const found = findFieldById(field.subfields, fieldId);
36
+ if (found)
37
+ return found;
38
+ }
39
+ }
40
+ return undefined;
41
+ }
42
+ // Enrich field mappings with field names
43
+ function enrichFieldMappings(model, flow) {
44
+ const sourceFields = getElementFields(model, flow.sourceId);
45
+ const targetFields = getElementFields(model, flow.targetId);
46
+ return flow.fieldMappings.map(mapping => {
47
+ const sourceField = findFieldById(sourceFields, mapping.sourceFieldId);
48
+ const targetField = findFieldById(targetFields, mapping.targetFieldId);
49
+ return {
50
+ from: sourceField?.name ?? 'unknown',
51
+ to: targetField?.name ?? 'unknown',
52
+ };
53
+ });
54
+ }
55
+ // Check if an element's center point is within a slice's bounds
56
+ function isElementInSlice(slice, position, width, height) {
57
+ const centerX = position.x + width / 2;
58
+ const centerY = position.y + height / 2;
59
+ return (centerX >= slice.position.x &&
60
+ centerX <= slice.position.x + slice.size.width &&
61
+ centerY >= slice.position.y &&
62
+ centerY <= slice.position.y + slice.size.height);
63
+ }
64
+ // Get all component IDs in a slice
65
+ export function getSliceComponentIds(model, slice) {
66
+ const ids = new Set();
67
+ for (const screen of model.screens.values()) {
68
+ if (isElementInSlice(slice, screen.position, screen.width, screen.height)) {
69
+ ids.add(screen.id);
70
+ }
71
+ }
72
+ for (const command of model.commands.values()) {
73
+ if (isElementInSlice(slice, command.position, command.width, command.height)) {
74
+ ids.add(command.id);
75
+ }
76
+ }
77
+ for (const event of model.events.values()) {
78
+ if (isElementInSlice(slice, event.position, event.width, event.height)) {
79
+ ids.add(event.id);
80
+ }
81
+ }
82
+ for (const readModel of model.readModels.values()) {
83
+ if (isElementInSlice(slice, readModel.position, readModel.width, readModel.height)) {
84
+ ids.add(readModel.id);
85
+ }
86
+ }
87
+ for (const processor of model.processors.values()) {
88
+ if (isElementInSlice(slice, processor.position, processor.width, processor.height)) {
89
+ ids.add(processor.id);
90
+ }
91
+ }
92
+ return ids;
93
+ }
94
+ // Find which slice contains a component
95
+ export function findSliceForComponent(model, componentId) {
96
+ // Get element position and size
97
+ let position = null;
98
+ let width = 0;
99
+ let height = 0;
100
+ const screen = model.screens.get(componentId);
101
+ if (screen) {
102
+ position = screen.position;
103
+ width = screen.width;
104
+ height = screen.height;
105
+ }
106
+ const command = model.commands.get(componentId);
107
+ if (command) {
108
+ position = command.position;
109
+ width = command.width;
110
+ height = command.height;
111
+ }
112
+ const event = model.events.get(componentId);
113
+ if (event) {
114
+ position = event.position;
115
+ width = event.width;
116
+ height = event.height;
117
+ }
118
+ const readModel = model.readModels.get(componentId);
119
+ if (readModel) {
120
+ position = readModel.position;
121
+ width = readModel.width;
122
+ height = readModel.height;
123
+ }
124
+ const processor = model.processors.get(componentId);
125
+ if (processor) {
126
+ position = processor.position;
127
+ width = processor.width;
128
+ height = processor.height;
129
+ }
130
+ if (!position)
131
+ return null;
132
+ for (const slice of model.slices.values()) {
133
+ if (isElementInSlice(slice, position, width, height)) {
134
+ return slice;
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+ // Build FlowInfo from a Flow
140
+ function buildFlowInfo(model, flow) {
141
+ const source = getElementInfo(model, flow.sourceId);
142
+ const target = getElementInfo(model, flow.targetId);
143
+ if (!source || !target)
144
+ return null;
145
+ return {
146
+ flowId: flow.id,
147
+ flowType: flow.flowType,
148
+ source,
149
+ target,
150
+ fieldMappings: enrichFieldMappings(model, flow),
151
+ };
152
+ }
153
+ // Build SliceFlowInfo from a Flow (includes slice info for source/target)
154
+ function buildSliceFlowInfo(model, flow) {
155
+ const base = buildFlowInfo(model, flow);
156
+ if (!base)
157
+ return null;
158
+ const sourceSlice = findSliceForComponent(model, flow.sourceId);
159
+ const targetSlice = findSliceForComponent(model, flow.targetId);
160
+ return {
161
+ ...base,
162
+ sourceSlice: sourceSlice ? { id: sourceSlice.id, name: sourceSlice.name } : undefined,
163
+ targetSlice: targetSlice ? { id: targetSlice.id, name: targetSlice.name } : undefined,
164
+ };
165
+ }
166
+ // Get flows entering a slice (target in slice, source outside)
167
+ export function getInboundFlows(model, slice) {
168
+ const componentIds = getSliceComponentIds(model, slice);
169
+ const flows = [];
170
+ for (const flow of model.flows.values()) {
171
+ if (componentIds.has(flow.targetId) && !componentIds.has(flow.sourceId)) {
172
+ const flowInfo = buildSliceFlowInfo(model, flow);
173
+ if (flowInfo)
174
+ flows.push(flowInfo);
175
+ }
176
+ }
177
+ return flows;
178
+ }
179
+ // Get flows leaving a slice (source in slice, target outside)
180
+ export function getOutboundFlows(model, slice) {
181
+ const componentIds = getSliceComponentIds(model, slice);
182
+ const flows = [];
183
+ for (const flow of model.flows.values()) {
184
+ if (componentIds.has(flow.sourceId) && !componentIds.has(flow.targetId)) {
185
+ const flowInfo = buildSliceFlowInfo(model, flow);
186
+ if (flowInfo)
187
+ flows.push(flowInfo);
188
+ }
189
+ }
190
+ return flows;
191
+ }
192
+ // Get flows within a slice (both source and target in slice)
193
+ export function getInternalFlows(model, slice) {
194
+ const componentIds = getSliceComponentIds(model, slice);
195
+ const flows = [];
196
+ for (const flow of model.flows.values()) {
197
+ if (componentIds.has(flow.sourceId) && componentIds.has(flow.targetId)) {
198
+ const flowInfo = buildFlowInfo(model, flow);
199
+ if (flowInfo)
200
+ flows.push(flowInfo);
201
+ }
202
+ }
203
+ return flows;
204
+ }
205
+ // Get all flows for a specific element (both directions)
206
+ export function getFlowsForElement(model, elementId) {
207
+ const incoming = [];
208
+ const outgoing = [];
209
+ for (const flow of model.flows.values()) {
210
+ if (flow.targetId === elementId) {
211
+ const flowInfo = buildFlowInfo(model, flow);
212
+ if (flowInfo)
213
+ incoming.push(flowInfo);
214
+ }
215
+ if (flow.sourceId === elementId) {
216
+ const flowInfo = buildFlowInfo(model, flow);
217
+ if (flowInfo)
218
+ outgoing.push(flowInfo);
219
+ }
220
+ }
221
+ return { incoming, outgoing };
222
+ }
223
+ export function findSliceToSliceFlows(model, slices) {
224
+ const sliceMap = new Map(slices.map(s => [s.id, s]));
225
+ const sliceComponentMap = new Map();
226
+ // Build component ID sets for each slice
227
+ for (const slice of slices) {
228
+ sliceComponentMap.set(slice.id, getSliceComponentIds(model, slice));
229
+ }
230
+ // Group flows by slice pair
231
+ const flowsBySlicePair = new Map();
232
+ for (const flow of model.flows.values()) {
233
+ let sourceSliceId = null;
234
+ let targetSliceId = null;
235
+ // Find which slice contains source and target
236
+ for (const [sliceId, componentIds] of sliceComponentMap.entries()) {
237
+ if (componentIds.has(flow.sourceId))
238
+ sourceSliceId = sliceId;
239
+ if (componentIds.has(flow.targetId))
240
+ targetSliceId = sliceId;
241
+ }
242
+ // Only include flows between different slices in our set
243
+ if (sourceSliceId && targetSliceId && sourceSliceId !== targetSliceId) {
244
+ const key = `${sourceSliceId}:${targetSliceId}`;
245
+ const flowInfo = buildSliceFlowInfo(model, flow);
246
+ if (flowInfo) {
247
+ if (!flowsBySlicePair.has(key)) {
248
+ flowsBySlicePair.set(key, []);
249
+ }
250
+ flowsBySlicePair.get(key).push(flowInfo);
251
+ }
252
+ }
253
+ }
254
+ // Convert to result format
255
+ const result = [];
256
+ for (const [key, flows] of flowsBySlicePair.entries()) {
257
+ const [fromId, toId] = key.split(':');
258
+ const fromSlice = sliceMap.get(fromId);
259
+ const toSlice = sliceMap.get(toId);
260
+ if (fromSlice && toSlice) {
261
+ result.push({
262
+ fromSlice: { id: fromSlice.id, name: fromSlice.name },
263
+ toSlice: { id: toSlice.id, name: toSlice.name },
264
+ flows,
265
+ });
266
+ }
267
+ }
268
+ return result;
269
+ }
270
+ // Find flows entering a chapter (target in chapter slices, source outside)
271
+ export function findChapterInboundFlows(model, slices) {
272
+ const chapterComponentIds = new Set();
273
+ for (const slice of slices) {
274
+ for (const id of getSliceComponentIds(model, slice)) {
275
+ chapterComponentIds.add(id);
276
+ }
277
+ }
278
+ const flows = [];
279
+ for (const flow of model.flows.values()) {
280
+ if (chapterComponentIds.has(flow.targetId) && !chapterComponentIds.has(flow.sourceId)) {
281
+ const flowInfo = buildSliceFlowInfo(model, flow);
282
+ if (flowInfo)
283
+ flows.push(flowInfo);
284
+ }
285
+ }
286
+ return flows;
287
+ }
288
+ // Find flows leaving a chapter (source in chapter slices, target outside)
289
+ export function findChapterOutboundFlows(model, slices) {
290
+ const chapterComponentIds = new Set();
291
+ for (const slice of slices) {
292
+ for (const id of getSliceComponentIds(model, slice)) {
293
+ chapterComponentIds.add(id);
294
+ }
295
+ }
296
+ const flows = [];
297
+ for (const flow of model.flows.values()) {
298
+ if (chapterComponentIds.has(flow.sourceId) && !chapterComponentIds.has(flow.targetId)) {
299
+ const flowInfo = buildSliceFlowInfo(model, flow);
300
+ if (flowInfo)
301
+ flows.push(flowInfo);
302
+ }
303
+ }
304
+ return flows;
305
+ }
@@ -1,3 +1,10 @@
1
1
  export type OutputFormat = 'xml' | 'json';
2
+ /**
3
+ * Escape string for use in XML attributes (escapes quotes)
4
+ */
2
5
  export declare function escapeXml(str: string): string;
6
+ /**
7
+ * Escape string for use in XML text content (doesn't escape quotes)
8
+ */
9
+ export declare function escapeXmlText(str: string): string;
3
10
  export declare function outputJson(data: unknown): void;
@@ -1,3 +1,6 @@
1
+ /**
2
+ * Escape string for use in XML attributes (escapes quotes)
3
+ */
1
4
  export function escapeXml(str) {
2
5
  return str
3
6
  .replace(/&/g, '&amp;')
@@ -6,6 +9,15 @@ export function escapeXml(str) {
6
9
  .replace(/"/g, '&quot;')
7
10
  .replace(/'/g, '&apos;');
8
11
  }
12
+ /**
13
+ * Escape string for use in XML text content (doesn't escape quotes)
14
+ */
15
+ export function escapeXmlText(str) {
16
+ return str
17
+ .replace(/&/g, '&amp;')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;');
20
+ }
9
21
  export function outputJson(data) {
10
22
  console.log(JSON.stringify(data, null, 2));
11
23
  }
@@ -91,6 +91,17 @@ export function inferFieldMappings(sourceFields, targetFields) {
91
91
  }
92
92
  return mappings;
93
93
  }
94
+ const VALID_FIELD_TYPES = ['UUID', 'Boolean', 'Double', 'Decimal', 'Date', 'DateTime', 'Long', 'Int', 'String', 'Custom'];
95
+ function validateFieldType(type, fieldName) {
96
+ if (!VALID_FIELD_TYPES.includes(type)) {
97
+ const typeLower = type.toLowerCase();
98
+ let hint = '';
99
+ if (typeLower === 'list' || typeLower === 'array') {
100
+ hint = `\n\nHint: To make a field a list/array, use isList="true" with a valid type:\n <field name="${fieldName}" type="String" isList="true"/>`;
101
+ }
102
+ throw new Error(`Invalid field type "${type}" for field "${fieldName}". Valid types: ${VALID_FIELD_TYPES.join(', ')}${hint}`);
103
+ }
104
+ }
94
105
  function getAttr(attrs, name) {
95
106
  const match = attrs.match(new RegExp(`${name}="([^"]*)"`));
96
107
  return match ? match[1] : undefined;
@@ -106,13 +117,15 @@ export function parseFieldsFromXml(xml) {
106
117
  const type = getAttr(attrs, 'type');
107
118
  if (!name || !type)
108
119
  continue;
120
+ // Validate the field type
121
+ validateFieldType(type, name);
109
122
  const field = {
110
123
  name,
111
124
  type,
112
- isList: getAttr(attrs, 'list') === 'true',
113
- isGenerated: getAttr(attrs, 'generated') === 'true',
114
- isOptional: getAttr(attrs, 'optional') === 'true',
115
- isUserInput: getAttr(attrs, 'user-input') === 'true',
125
+ isList: getAttr(attrs, 'isList') === 'true',
126
+ isGenerated: getAttr(attrs, 'isGenerated') === 'true',
127
+ isOptional: getAttr(attrs, 'isOptional') === 'true',
128
+ isUserInput: getAttr(attrs, 'isUserInput') === 'true',
116
129
  };
117
130
  // Parse nested subfields for Custom type
118
131
  if (type === 'Custom' && content) {
@@ -90,6 +90,11 @@ export function addField(model, filePath, options, input) {
90
90
  if (!validFieldTypes.includes(fieldInput.type)) {
91
91
  console.error(`Error: Invalid field type: ${fieldInput.type}`);
92
92
  console.error(`Valid types: ${validFieldTypes.join(', ')}`);
93
+ const typeLower = fieldInput.type.toLowerCase();
94
+ if (typeLower === 'list' || typeLower === 'array') {
95
+ console.error(`\nHint: To make a field a list/array, use isList="true" with a valid type:`);
96
+ console.error(` <field name="items" type="String" isList="true"/>`);
97
+ }
93
98
  process.exit(1);
94
99
  }
95
100
  // Find entity and add field
@@ -1,5 +1,6 @@
1
1
  import { outputJson } from '../../lib/format.js';
2
2
  import { findElementOrExit } from '../../lib/element-lookup.js';
3
+ import { findChapterForSlice, getChapterHierarchy } from '../../lib/chapter-utils.js';
3
4
  // Get components inside a slice by checking if center point is within bounds
4
5
  function getSliceComponents(model, slice) {
5
6
  const bounds = {
@@ -71,18 +72,6 @@ function findActorForScreen(model, screen) {
71
72
  }
72
73
  return undefined;
73
74
  }
74
- // Find which chapter contains a slice (based on horizontal center)
75
- function findChapterForSlice(model, slice) {
76
- const sliceCenterX = slice.position.x + slice.size.width / 2;
77
- for (const chapter of model.chapters.values()) {
78
- const chapterLeft = chapter.position.x;
79
- const chapterRight = chapter.position.x + chapter.size.width;
80
- if (sliceCenterX >= chapterLeft && sliceCenterX <= chapterRight) {
81
- return chapter;
82
- }
83
- }
84
- return null;
85
- }
86
75
  // Convert Field to JSON-friendly format
87
76
  function fieldToJson(field) {
88
77
  const result = {
@@ -351,7 +340,7 @@ export function codegenSlice(model, sliceName) {
351
340
  id: slice.id,
352
341
  name: slice.name,
353
342
  },
354
- ...(chapter && { chapter: { id: chapter.id, name: chapter.name } }),
343
+ ...(chapter && { chapter: getChapterHierarchy(model, chapter) }),
355
344
  elements: {
356
345
  readModels: components.readModels.map(rm => ({
357
346
  id: rm.id,
@@ -1,3 +1,3 @@
1
1
  import type { EventModel } from '../../types.js';
2
2
  import { type OutputFormat } from '../../lib/format.js';
3
- export declare function listSlices(model: EventModel, format: OutputFormat): void;
3
+ export declare function listSlices(model: EventModel, format: OutputFormat, chapterName?: string): void;
@@ -1,9 +1,24 @@
1
1
  import { escapeXml, outputJson } from '../../lib/format.js';
2
- export function listSlices(model, format) {
3
- const slices = [...model.slices.values()];
2
+ import { findElementOrExit } from '../../lib/element-lookup.js';
3
+ function getSlicesUnderChapter(model, chapter) {
4
+ const chapterLeft = chapter.position.x;
5
+ const chapterRight = chapter.position.x + chapter.size.width;
6
+ return [...model.slices.values()].filter(slice => {
7
+ const sliceCenterX = slice.position.x + slice.size.width / 2;
8
+ return sliceCenterX >= chapterLeft && sliceCenterX <= chapterRight;
9
+ });
10
+ }
11
+ export function listSlices(model, format, chapterName) {
12
+ let slices = [...model.slices.values()];
13
+ // Filter by chapter if specified
14
+ if (chapterName) {
15
+ const chapter = findElementOrExit(model.chapters, chapterName, 'chapter');
16
+ slices = getSlicesUnderChapter(model, chapter);
17
+ }
4
18
  const sorted = [...slices].sort((a, b) => a.position.x - b.position.x);
5
19
  if (format === 'json') {
6
20
  outputJson({
21
+ ...(chapterName && { chapter: chapterName }),
7
22
  slices: sorted.map(s => ({ id: s.id, name: s.name, status: s.status }))
8
23
  });
9
24
  return;
@@ -1,5 +1,7 @@
1
1
  import { escapeXml, outputJson } from '../../lib/format.js';
2
2
  import { findElementOrExit } from '../../lib/element-lookup.js';
3
+ import { getChapterHierarchy, findChapterForSlice } from '../../lib/chapter-utils.js';
4
+ import { findSliceToSliceFlows, findChapterInboundFlows, findChapterOutboundFlows, } from '../../lib/flow-utils.js';
3
5
  function getSlicesUnderChapter(model, chapter) {
4
6
  // A slice is "under" a chapter if its horizontal center falls within the chapter's x range
5
7
  const chapterLeft = chapter.position.x;
@@ -9,18 +11,149 @@ function getSlicesUnderChapter(model, chapter) {
9
11
  return sliceCenterX >= chapterLeft && sliceCenterX <= chapterRight;
10
12
  }).sort((a, b) => a.position.x - b.position.x);
11
13
  }
14
+ function formatSliceFlowXml(sliceFlow) {
15
+ let xml = ` <slice-flow from="${escapeXml(sliceFlow.fromSlice.name)}" to="${escapeXml(sliceFlow.toSlice.name)}">\n`;
16
+ for (const flow of sliceFlow.flows) {
17
+ xml += ` <via type="${flow.flowType}">\n`;
18
+ xml += ` <source element="${escapeXml(flow.source.name)}" type="${flow.source.type}"/>\n`;
19
+ xml += ` <target element="${escapeXml(flow.target.name)}" type="${flow.target.type}"/>\n`;
20
+ if (flow.fieldMappings.length > 0) {
21
+ xml += ' <mappings>\n';
22
+ for (const mapping of flow.fieldMappings) {
23
+ xml += ` <map from="${escapeXml(mapping.from)}" to="${escapeXml(mapping.to)}"/>\n`;
24
+ }
25
+ xml += ' </mappings>\n';
26
+ }
27
+ xml += ' </via>\n';
28
+ }
29
+ xml += ' </slice-flow>\n';
30
+ return xml;
31
+ }
32
+ function formatExternalFlowXml(flow, direction, model) {
33
+ let xml = ` <flow ${direction === 'inbound' ? `to-slice="${escapeXml(flow.targetSlice?.name ?? 'unknown')}"` : `from-slice="${escapeXml(flow.sourceSlice?.name ?? 'unknown')}"`} type="${flow.flowType}">\n`;
34
+ // For inbound: source is external, for outbound: target is external
35
+ if (direction === 'inbound') {
36
+ // Find the chapter of the external source
37
+ const sourceSlice = flow.sourceSlice;
38
+ let chapterInfo = '';
39
+ if (sourceSlice) {
40
+ const sourceSliceObj = model.slices.get(sourceSlice.id);
41
+ if (sourceSliceObj) {
42
+ const sourceChapter = findChapterForSlice(model, sourceSliceObj);
43
+ if (sourceChapter) {
44
+ chapterInfo = ` chapter="${escapeXml(sourceChapter.name)}"`;
45
+ }
46
+ }
47
+ }
48
+ xml += ` <from external="true"${chapterInfo}>${escapeXml(flow.source.name)} (${flow.source.type})</from>\n`;
49
+ xml += ` <to>${escapeXml(flow.target.name)} (${flow.target.type})</to>\n`;
50
+ }
51
+ else {
52
+ xml += ` <from>${escapeXml(flow.source.name)} (${flow.source.type})</from>\n`;
53
+ // Find the chapter of the external target
54
+ const targetSlice = flow.targetSlice;
55
+ let chapterInfo = '';
56
+ if (targetSlice) {
57
+ const targetSliceObj = model.slices.get(targetSlice.id);
58
+ if (targetSliceObj) {
59
+ const targetChapter = findChapterForSlice(model, targetSliceObj);
60
+ if (targetChapter) {
61
+ chapterInfo = ` chapter="${escapeXml(targetChapter.name)}"`;
62
+ }
63
+ }
64
+ }
65
+ xml += ` <to external="true"${chapterInfo}>${escapeXml(flow.target.name)} (${flow.target.type})</to>\n`;
66
+ }
67
+ if (flow.fieldMappings.length > 0) {
68
+ xml += ' <mappings>\n';
69
+ for (const mapping of flow.fieldMappings) {
70
+ xml += ` <map from="${escapeXml(mapping.from)}" to="${escapeXml(mapping.to)}"/>\n`;
71
+ }
72
+ xml += ' </mappings>\n';
73
+ }
74
+ xml += ' </flow>\n';
75
+ return xml;
76
+ }
12
77
  export function showChapter(model, name, format) {
13
78
  const chapter = findElementOrExit(model.chapters, name, 'chapter');
14
79
  const slices = getSlicesUnderChapter(model, chapter);
80
+ // Get flow information
81
+ const sliceToSliceFlows = findSliceToSliceFlows(model, slices);
82
+ const chapterInboundFlows = findChapterInboundFlows(model, slices);
83
+ const chapterOutboundFlows = findChapterOutboundFlows(model, slices);
15
84
  if (format === 'json') {
16
- outputJson({
85
+ const result = {
17
86
  id: chapter.id,
18
87
  name: chapter.name,
88
+ parent: chapter ? getChapterHierarchy(model, chapter).parent : undefined,
19
89
  slices: slices.map(s => ({ id: s.id, name: s.name, status: s.status }))
20
- });
90
+ };
91
+ // Add flow graph if there are slice-to-slice flows
92
+ if (sliceToSliceFlows.length > 0) {
93
+ result.flowGraph = sliceToSliceFlows.map(sf => ({
94
+ from: sf.fromSlice.name,
95
+ to: sf.toSlice.name,
96
+ flows: sf.flows.map(f => ({
97
+ type: f.flowType,
98
+ source: { element: f.source.name, type: f.source.type },
99
+ target: { element: f.target.name, type: f.target.type },
100
+ ...(f.fieldMappings.length > 0 && {
101
+ mappings: f.fieldMappings.map(m => ({ from: m.from, to: m.to }))
102
+ })
103
+ }))
104
+ }));
105
+ }
106
+ // Add external dependencies
107
+ if (chapterInboundFlows.length > 0 || chapterOutboundFlows.length > 0) {
108
+ result.externalDependencies = {
109
+ ...(chapterInboundFlows.length > 0 && {
110
+ inbound: chapterInboundFlows.map(f => {
111
+ const sourceSliceObj = f.sourceSlice ? model.slices.get(f.sourceSlice.id) : null;
112
+ const sourceChapter = sourceSliceObj ? findChapterForSlice(model, sourceSliceObj) : null;
113
+ return {
114
+ toSlice: f.targetSlice?.name,
115
+ type: f.flowType,
116
+ from: {
117
+ element: f.source.name,
118
+ type: f.source.type,
119
+ slice: f.sourceSlice?.name,
120
+ chapter: sourceChapter?.name
121
+ },
122
+ to: { element: f.target.name, type: f.target.type },
123
+ ...(f.fieldMappings.length > 0 && {
124
+ mappings: f.fieldMappings.map(m => ({ from: m.from, to: m.to }))
125
+ })
126
+ };
127
+ })
128
+ }),
129
+ ...(chapterOutboundFlows.length > 0 && {
130
+ outbound: chapterOutboundFlows.map(f => {
131
+ const targetSliceObj = f.targetSlice ? model.slices.get(f.targetSlice.id) : null;
132
+ const targetChapter = targetSliceObj ? findChapterForSlice(model, targetSliceObj) : null;
133
+ return {
134
+ fromSlice: f.sourceSlice?.name,
135
+ type: f.flowType,
136
+ from: { element: f.source.name, type: f.source.type },
137
+ to: {
138
+ element: f.target.name,
139
+ type: f.target.type,
140
+ slice: f.targetSlice?.name,
141
+ chapter: targetChapter?.name
142
+ },
143
+ ...(f.fieldMappings.length > 0 && {
144
+ mappings: f.fieldMappings.map(m => ({ from: m.from, to: m.to }))
145
+ })
146
+ };
147
+ })
148
+ })
149
+ };
150
+ }
151
+ outputJson(result);
21
152
  return;
22
153
  }
154
+ // XML format
23
155
  console.log(`<chapter id="${chapter.id}" name="${escapeXml(chapter.name)}">`);
156
+ // Slices section
24
157
  if (slices.length === 0) {
25
158
  console.log(' <slices/>');
26
159
  }
@@ -31,5 +164,32 @@ export function showChapter(model, name, format) {
31
164
  }
32
165
  console.log(' </slices>');
33
166
  }
167
+ // Flow graph section
168
+ if (sliceToSliceFlows.length > 0) {
169
+ console.log(' <flow-graph>');
170
+ for (const sliceFlow of sliceToSliceFlows) {
171
+ process.stdout.write(formatSliceFlowXml(sliceFlow));
172
+ }
173
+ console.log(' </flow-graph>');
174
+ }
175
+ // External dependencies section
176
+ if (chapterInboundFlows.length > 0 || chapterOutboundFlows.length > 0) {
177
+ console.log(' <external-dependencies>');
178
+ if (chapterInboundFlows.length > 0) {
179
+ console.log(' <inbound>');
180
+ for (const flow of chapterInboundFlows) {
181
+ process.stdout.write(formatExternalFlowXml(flow, 'inbound', model));
182
+ }
183
+ console.log(' </inbound>');
184
+ }
185
+ if (chapterOutboundFlows.length > 0) {
186
+ console.log(' <outbound>');
187
+ for (const flow of chapterOutboundFlows) {
188
+ process.stdout.write(formatExternalFlowXml(flow, 'outbound', model));
189
+ }
190
+ console.log(' </outbound>');
191
+ }
192
+ console.log(' </external-dependencies>');
193
+ }
34
194
  console.log('</chapter>');
35
195
  }
@@ -1,5 +1,7 @@
1
- import { escapeXml, outputJson } from '../../lib/format.js';
1
+ import { escapeXml, escapeXmlText, outputJson } from '../../lib/format.js';
2
2
  import { findElementOrExit } from '../../lib/element-lookup.js';
3
+ import { findChapterForSlice, getChapterHierarchy } from '../../lib/chapter-utils.js';
4
+ import { getInboundFlows, getOutboundFlows, getFlowsForElement, } from '../../lib/flow-utils.js';
3
5
  function formatFieldValues(values) {
4
6
  if (!values || Object.keys(values).length === 0)
5
7
  return '';
@@ -102,9 +104,91 @@ function findActorForScreen(model, screen) {
102
104
  }
103
105
  return null;
104
106
  }
107
+ function formatChapterXml(hierarchy, indent) {
108
+ let xml = `${indent}<chapter id="${hierarchy.id}" name="${escapeXml(hierarchy.name)}"`;
109
+ if (hierarchy.parent) {
110
+ xml += '>\n';
111
+ xml += formatChapterXml(hierarchy.parent, indent + ' ');
112
+ xml += `${indent}</chapter>\n`;
113
+ }
114
+ else {
115
+ xml += '/>\n';
116
+ }
117
+ return xml;
118
+ }
119
+ // Format inline flow annotations for a component
120
+ function formatFlowAnnotationsXml(elementId, incoming, outgoing, componentIds, indent) {
121
+ let xml = '';
122
+ // Flows coming into this element
123
+ const flowsFrom = incoming.map(f => {
124
+ const external = !componentIds.has(f.source.id);
125
+ return external
126
+ ? `${escapeXml(f.source.name)} (${f.source.type}) [external]`
127
+ : `${escapeXml(f.source.name)} (${f.source.type})`;
128
+ });
129
+ if (flowsFrom.length > 0) {
130
+ xml += `${indent}<flows-from>${flowsFrom.join(', ')}</flows-from>\n`;
131
+ }
132
+ // Flows going out of this element
133
+ const flowsTo = outgoing.map(f => {
134
+ const external = !componentIds.has(f.target.id);
135
+ return external
136
+ ? `${escapeXml(f.target.name)} (${f.target.type}) [external]`
137
+ : `${escapeXml(f.target.name)} (${f.target.type})`;
138
+ });
139
+ if (flowsTo.length > 0) {
140
+ xml += `${indent}<flows-to>${flowsTo.join(', ')}</flows-to>\n`;
141
+ }
142
+ return xml;
143
+ }
144
+ // Format inbound flows section
145
+ function formatInboundFlowsXml(flows) {
146
+ if (flows.length === 0)
147
+ return '';
148
+ let xml = ' <inbound-flows>\n';
149
+ for (const flow of flows) {
150
+ xml += ` <flow type="${flow.flowType}">\n`;
151
+ const sourceSliceInfo = flow.sourceSlice ? ` slice="${escapeXml(flow.sourceSlice.name)}"` : '';
152
+ xml += ` <from${sourceSliceInfo}>${escapeXml(flow.source.name)} (${flow.source.type})</from>\n`;
153
+ xml += ` <to>${escapeXml(flow.target.name)} (${flow.target.type})</to>\n`;
154
+ if (flow.fieldMappings.length > 0) {
155
+ xml += ' <mappings>\n';
156
+ for (const mapping of flow.fieldMappings) {
157
+ xml += ` <map from="${escapeXml(mapping.from)}" to="${escapeXml(mapping.to)}"/>\n`;
158
+ }
159
+ xml += ' </mappings>\n';
160
+ }
161
+ xml += ' </flow>\n';
162
+ }
163
+ xml += ' </inbound-flows>\n';
164
+ return xml;
165
+ }
166
+ // Format outbound flows section
167
+ function formatOutboundFlowsXml(flows) {
168
+ if (flows.length === 0)
169
+ return '';
170
+ let xml = ' <outbound-flows>\n';
171
+ for (const flow of flows) {
172
+ xml += ` <flow type="${flow.flowType}">\n`;
173
+ xml += ` <from>${escapeXml(flow.source.name)} (${flow.source.type})</from>\n`;
174
+ const targetSliceInfo = flow.targetSlice ? ` slice="${escapeXml(flow.targetSlice.name)}"` : '';
175
+ xml += ` <to${targetSliceInfo}>${escapeXml(flow.target.name)} (${flow.target.type})</to>\n`;
176
+ if (flow.fieldMappings.length > 0) {
177
+ xml += ' <mappings>\n';
178
+ for (const mapping of flow.fieldMappings) {
179
+ xml += ` <map from="${escapeXml(mapping.from)}" to="${escapeXml(mapping.to)}"/>\n`;
180
+ }
181
+ xml += ' </mappings>\n';
182
+ }
183
+ xml += ' </flow>\n';
184
+ }
185
+ xml += ' </outbound-flows>\n';
186
+ return xml;
187
+ }
105
188
  function formatSliceXml(model, slice) {
106
189
  const components = getSliceComponents(model, slice);
107
190
  const scenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
191
+ const chapter = findChapterForSlice(model, slice);
108
192
  const componentIds = new Set();
109
193
  components.commands.forEach(c => componentIds.add(c.id));
110
194
  components.events.forEach(e => componentIds.add(e.id));
@@ -113,6 +197,9 @@ function formatSliceXml(model, slice) {
113
197
  components.processors.forEach(p => componentIds.add(p.id));
114
198
  const flows = [...model.flows.values()].filter(f => componentIds.has(f.sourceId) || componentIds.has(f.targetId));
115
199
  const internalFlows = flows.filter(f => componentIds.has(f.sourceId) && componentIds.has(f.targetId));
200
+ // Get inbound and outbound flows for the slice
201
+ const inboundFlows = getInboundFlows(model, slice);
202
+ const outboundFlows = getOutboundFlows(model, slice);
116
203
  function getName(id) {
117
204
  return (model.commands.get(id)?.name ??
118
205
  model.events.get(id)?.name ??
@@ -122,6 +209,9 @@ function formatSliceXml(model, slice) {
122
209
  id);
123
210
  }
124
211
  let xml = `<slice id="${slice.id}" name="${escapeXml(slice.name)}" status="${slice.status}">\n`;
212
+ if (chapter) {
213
+ xml += formatChapterXml(getChapterHierarchy(model, chapter), ' ');
214
+ }
125
215
  xml += ' <components>\n';
126
216
  for (const screen of components.screens) {
127
217
  // Check if this is a linked copy
@@ -137,6 +227,9 @@ function formatSliceXml(model, slice) {
137
227
  const actor = findActorForScreen(model, screen);
138
228
  const actorAttr = actor ? ` actor="${escapeXml(actor.name)}"` : '';
139
229
  xml += ` <screen id="${screen.id}" name="${escapeXml(screen.name)}"${copyAttr}${originAttr}${actorAttr}>\n`;
230
+ // Add flow annotations
231
+ const screenFlows = getFlowsForElement(model, screen.id);
232
+ xml += formatFlowAnnotationsXml(screen.id, screenFlows.incoming, screenFlows.outgoing, componentIds, ' ');
140
233
  if (screen.fields.length > 0) {
141
234
  xml += ' <fields>\n';
142
235
  for (const field of screen.fields) {
@@ -148,6 +241,9 @@ function formatSliceXml(model, slice) {
148
241
  }
149
242
  for (const command of components.commands) {
150
243
  xml += ` <command id="${command.id}" name="${escapeXml(command.name)}">\n`;
244
+ // Add flow annotations
245
+ const commandFlows = getFlowsForElement(model, command.id);
246
+ xml += formatFlowAnnotationsXml(command.id, commandFlows.incoming, commandFlows.outgoing, componentIds, ' ');
151
247
  if (command.fields.length > 0) {
152
248
  xml += ' <fields>\n';
153
249
  for (const field of command.fields) {
@@ -179,6 +275,9 @@ function formatSliceXml(model, slice) {
179
275
  const aggregate = findAggregateForEvent(model, event);
180
276
  const aggregateAttr = aggregate ? ` aggregate="${escapeXml(aggregate.name)}"` : '';
181
277
  xml += ` <event id="${event.id}" name="${escapeXml(event.name)}"${copyAttr}${originAttr}${aggregateAttr}>\n`;
278
+ // Add flow annotations
279
+ const eventFlows = getFlowsForElement(model, event.id);
280
+ xml += formatFlowAnnotationsXml(event.id, eventFlows.incoming, eventFlows.outgoing, componentIds, ' ');
182
281
  if (event.fields.length > 0) {
183
282
  xml += ' <fields>\n';
184
283
  for (const field of event.fields) {
@@ -210,6 +309,9 @@ function formatSliceXml(model, slice) {
210
309
  }
211
310
  }
212
311
  xml += ` <read-model id="${readModel.id}" name="${escapeXml(readModel.name)}"${copyAttr}${originAttr}>\n`;
312
+ // Add flow annotations
313
+ const rmFlows = getFlowsForElement(model, readModel.id);
314
+ xml += formatFlowAnnotationsXml(readModel.id, rmFlows.incoming, rmFlows.outgoing, componentIds, ' ');
213
315
  if (readModel.fields.length > 0) {
214
316
  xml += ' <fields>\n';
215
317
  for (const field of readModel.fields) {
@@ -221,6 +323,9 @@ function formatSliceXml(model, slice) {
221
323
  }
222
324
  for (const processor of components.processors) {
223
325
  xml += ` <processor id="${processor.id}" name="${escapeXml(processor.name)}">\n`;
326
+ // Add flow annotations
327
+ const procFlows = getFlowsForElement(model, processor.id);
328
+ xml += formatFlowAnnotationsXml(processor.id, procFlows.incoming, procFlows.outgoing, componentIds, ' ');
224
329
  if (processor.fields.length > 0) {
225
330
  xml += ' <fields>\n';
226
331
  for (const field of processor.fields) {
@@ -243,7 +348,7 @@ function formatSliceXml(model, slice) {
243
348
  for (const scenario of scenarios) {
244
349
  xml += ` <scenario name="${escapeXml(scenario.name)}">\n`;
245
350
  if (scenario.description) {
246
- xml += ` <description>${escapeXml(scenario.description)}</description>\n`;
351
+ xml += ` <description>${escapeXmlText(scenario.description)}</description>\n`;
247
352
  }
248
353
  if (scenario.givenEvents.length > 0) {
249
354
  xml += ' <given>\n';
@@ -252,7 +357,7 @@ function formatSliceXml(model, slice) {
252
357
  const name = evt?.name ?? 'UnknownEvent';
253
358
  const values = formatFieldValues(given.fieldValues);
254
359
  xml += values
255
- ? ` <event type="${escapeXml(name)}">${escapeXml(values)}</event>\n`
360
+ ? ` <event type="${escapeXml(name)}">${escapeXmlText(values)}</event>\n`
256
361
  : ` <event type="${escapeXml(name)}"/>\n`;
257
362
  }
258
363
  xml += ' </given>\n';
@@ -263,7 +368,7 @@ function formatSliceXml(model, slice) {
263
368
  const values = formatFieldValues(scenario.whenCommand.fieldValues);
264
369
  xml += ' <when>\n';
265
370
  xml += values
266
- ? ` <command type="${escapeXml(name)}">${escapeXml(values)}</command>\n`
371
+ ? ` <command type="${escapeXml(name)}">${escapeXmlText(values)}</command>\n`
267
372
  : ` <command type="${escapeXml(name)}"/>\n`;
268
373
  xml += ' </when>\n';
269
374
  }
@@ -272,7 +377,7 @@ function formatSliceXml(model, slice) {
272
377
  xml += ` <error`;
273
378
  if (scenario.then.errorType)
274
379
  xml += ` type="${escapeXml(scenario.then.errorType)}"`;
275
- xml += `>${escapeXml(scenario.then.errorMessage ?? '')}</error>\n`;
380
+ xml += `>${escapeXmlText(scenario.then.errorMessage ?? '')}</error>\n`;
276
381
  }
277
382
  else if (scenario.then.type === 'events' && scenario.then.expectedEvents) {
278
383
  for (const expected of scenario.then.expectedEvents) {
@@ -280,7 +385,7 @@ function formatSliceXml(model, slice) {
280
385
  const name = evt?.name ?? 'UnknownEvent';
281
386
  const values = formatFieldValues(expected.fieldValues);
282
387
  xml += values
283
- ? ` <event type="${escapeXml(name)}">${escapeXml(values)}</event>\n`
388
+ ? ` <event type="${escapeXml(name)}">${escapeXmlText(values)}</event>\n`
284
389
  : ` <event type="${escapeXml(name)}"/>\n`;
285
390
  }
286
391
  }
@@ -289,7 +394,7 @@ function formatSliceXml(model, slice) {
289
394
  const rm = model.readModels.get(assertion.readModelStickyId);
290
395
  const name = rm?.name ?? 'UnknownReadModel';
291
396
  xml += ` <read-model-assertion type="${escapeXml(name)}">\n`;
292
- xml += ` <expected>${escapeXml(formatFieldValues(assertion.expectedFieldValues))}</expected>\n`;
397
+ xml += ` <expected>${escapeXmlText(formatFieldValues(assertion.expectedFieldValues))}</expected>\n`;
293
398
  xml += ' </read-model-assertion>\n';
294
399
  }
295
400
  xml += ' </then>\n';
@@ -297,6 +402,9 @@ function formatSliceXml(model, slice) {
297
402
  }
298
403
  xml += ' </scenarios>\n';
299
404
  }
405
+ // Add inbound and outbound flows sections
406
+ xml += formatInboundFlowsXml(inboundFlows);
407
+ xml += formatOutboundFlowsXml(outboundFlows);
300
408
  xml += '</slice>';
301
409
  return xml;
302
410
  }
@@ -321,6 +429,7 @@ function fieldToJson(field) {
321
429
  function formatSliceJson(model, slice) {
322
430
  const components = getSliceComponents(model, slice);
323
431
  const scenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
432
+ const chapter = findChapterForSlice(model, slice);
324
433
  const componentIds = new Set();
325
434
  components.commands.forEach(c => componentIds.add(c.id));
326
435
  components.events.forEach(e => componentIds.add(e.id));
@@ -329,6 +438,9 @@ function formatSliceJson(model, slice) {
329
438
  components.processors.forEach(p => componentIds.add(p.id));
330
439
  const flows = [...model.flows.values()].filter(f => componentIds.has(f.sourceId) || componentIds.has(f.targetId));
331
440
  const internalFlows = flows.filter(f => componentIds.has(f.sourceId) && componentIds.has(f.targetId));
441
+ // Get inbound and outbound flows for the slice
442
+ const inboundFlows = getInboundFlows(model, slice);
443
+ const outboundFlows = getOutboundFlows(model, slice);
332
444
  function getName(id) {
333
445
  return (model.commands.get(id)?.name ??
334
446
  model.events.get(id)?.name ??
@@ -341,6 +453,7 @@ function formatSliceJson(model, slice) {
341
453
  id: slice.id,
342
454
  name: slice.name,
343
455
  status: slice.status,
456
+ ...(chapter && { chapter: getChapterHierarchy(model, chapter) }),
344
457
  components: {
345
458
  screens: components.screens.map(screen => {
346
459
  const screenObj = {
@@ -357,13 +470,47 @@ function formatSliceJson(model, slice) {
357
470
  const actor = findActorForScreen(model, screen);
358
471
  if (actor)
359
472
  screenObj.actor = actor.name;
473
+ // Add flow annotations
474
+ const screenFlows = getFlowsForElement(model, screen.id);
475
+ if (screenFlows.incoming.length > 0) {
476
+ screenObj.flowsFrom = screenFlows.incoming.map(f => ({
477
+ element: f.source.name,
478
+ type: f.source.type,
479
+ external: !componentIds.has(f.source.id)
480
+ }));
481
+ }
482
+ if (screenFlows.outgoing.length > 0) {
483
+ screenObj.flowsTo = screenFlows.outgoing.map(f => ({
484
+ element: f.target.name,
485
+ type: f.target.type,
486
+ external: !componentIds.has(f.target.id)
487
+ }));
488
+ }
360
489
  return screenObj;
361
490
  }),
362
- commands: components.commands.map(cmd => ({
363
- id: cmd.id,
364
- name: cmd.name,
365
- fields: cmd.fields.map(fieldToJson)
366
- })),
491
+ commands: components.commands.map(cmd => {
492
+ const cmdObj = {
493
+ id: cmd.id,
494
+ name: cmd.name,
495
+ fields: cmd.fields.map(fieldToJson)
496
+ };
497
+ const cmdFlows = getFlowsForElement(model, cmd.id);
498
+ if (cmdFlows.incoming.length > 0) {
499
+ cmdObj.flowsFrom = cmdFlows.incoming.map(f => ({
500
+ element: f.source.name,
501
+ type: f.source.type,
502
+ external: !componentIds.has(f.source.id)
503
+ }));
504
+ }
505
+ if (cmdFlows.outgoing.length > 0) {
506
+ cmdObj.flowsTo = cmdFlows.outgoing.map(f => ({
507
+ element: f.target.name,
508
+ type: f.target.type,
509
+ external: !componentIds.has(f.target.id)
510
+ }));
511
+ }
512
+ return cmdObj;
513
+ }),
367
514
  events: components.events.map(event => {
368
515
  const eventObj = {
369
516
  id: event.id,
@@ -379,6 +526,22 @@ function formatSliceJson(model, slice) {
379
526
  const aggregate = findAggregateForEvent(model, event);
380
527
  if (aggregate)
381
528
  eventObj.aggregate = aggregate.name;
529
+ // Add flow annotations
530
+ const eventFlows = getFlowsForElement(model, event.id);
531
+ if (eventFlows.incoming.length > 0) {
532
+ eventObj.flowsFrom = eventFlows.incoming.map(f => ({
533
+ element: f.source.name,
534
+ type: f.source.type,
535
+ external: !componentIds.has(f.source.id)
536
+ }));
537
+ }
538
+ if (eventFlows.outgoing.length > 0) {
539
+ eventObj.flowsTo = eventFlows.outgoing.map(f => ({
540
+ element: f.target.name,
541
+ type: f.target.type,
542
+ external: !componentIds.has(f.target.id)
543
+ }));
544
+ }
382
545
  return eventObj;
383
546
  }),
384
547
  readModels: components.readModels.map(rm => {
@@ -393,13 +556,47 @@ function formatSliceJson(model, slice) {
393
556
  if (originSlice)
394
557
  rmObj.originSlice = originSlice.name;
395
558
  }
559
+ // Add flow annotations
560
+ const rmFlows = getFlowsForElement(model, rm.id);
561
+ if (rmFlows.incoming.length > 0) {
562
+ rmObj.flowsFrom = rmFlows.incoming.map(f => ({
563
+ element: f.source.name,
564
+ type: f.source.type,
565
+ external: !componentIds.has(f.source.id)
566
+ }));
567
+ }
568
+ if (rmFlows.outgoing.length > 0) {
569
+ rmObj.flowsTo = rmFlows.outgoing.map(f => ({
570
+ element: f.target.name,
571
+ type: f.target.type,
572
+ external: !componentIds.has(f.target.id)
573
+ }));
574
+ }
396
575
  return rmObj;
397
576
  }),
398
- processors: components.processors.map(proc => ({
399
- id: proc.id,
400
- name: proc.name,
401
- fields: proc.fields.map(fieldToJson)
402
- }))
577
+ processors: components.processors.map(proc => {
578
+ const procObj = {
579
+ id: proc.id,
580
+ name: proc.name,
581
+ fields: proc.fields.map(fieldToJson)
582
+ };
583
+ const procFlows = getFlowsForElement(model, proc.id);
584
+ if (procFlows.incoming.length > 0) {
585
+ procObj.flowsFrom = procFlows.incoming.map(f => ({
586
+ element: f.source.name,
587
+ type: f.source.type,
588
+ external: !componentIds.has(f.source.id)
589
+ }));
590
+ }
591
+ if (procFlows.outgoing.length > 0) {
592
+ procObj.flowsTo = procFlows.outgoing.map(f => ({
593
+ element: f.target.name,
594
+ type: f.target.type,
595
+ external: !componentIds.has(f.target.id)
596
+ }));
597
+ }
598
+ return procObj;
599
+ })
403
600
  }
404
601
  };
405
602
  if (internalFlows.length > 0) {
@@ -460,6 +657,41 @@ function formatSliceJson(model, slice) {
460
657
  return scenarioObj;
461
658
  });
462
659
  }
660
+ // Add inbound and outbound flows
661
+ if (inboundFlows.length > 0) {
662
+ result.inboundFlows = inboundFlows.map(f => ({
663
+ type: f.flowType,
664
+ from: {
665
+ element: f.source.name,
666
+ type: f.source.type,
667
+ ...(f.sourceSlice && { slice: f.sourceSlice.name })
668
+ },
669
+ to: {
670
+ element: f.target.name,
671
+ type: f.target.type
672
+ },
673
+ ...(f.fieldMappings.length > 0 && {
674
+ mappings: f.fieldMappings.map(m => ({ from: m.from, to: m.to }))
675
+ })
676
+ }));
677
+ }
678
+ if (outboundFlows.length > 0) {
679
+ result.outboundFlows = outboundFlows.map(f => ({
680
+ type: f.flowType,
681
+ from: {
682
+ element: f.source.name,
683
+ type: f.source.type
684
+ },
685
+ to: {
686
+ element: f.target.name,
687
+ type: f.target.type,
688
+ ...(f.targetSlice && { slice: f.targetSlice.name })
689
+ },
690
+ ...(f.fieldMappings.length > 0 && {
691
+ mappings: f.fieldMappings.map(m => ({ from: m.from, to: m.to }))
692
+ })
693
+ }));
694
+ }
463
695
  return result;
464
696
  }
465
697
  export function showSlice(model, name, format) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eventmodeler",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
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": {