eventmodeler 0.2.6 → 0.2.8

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
@@ -160,7 +160,10 @@ async function main() {
160
160
  : getDefaultFormat();
161
161
  const command = filteredArgs[0];
162
162
  const subcommand = filteredArgs[1];
163
- const target = filteredArgs[2];
163
+ // Join remaining args as target (allows "show chapter Register Products" without quotes)
164
+ // For commands with named args (--flag), those are already extracted by getNamedArg
165
+ const remainingArgs = filteredArgs.slice(2);
166
+ const target = remainingArgs.length > 0 ? remainingArgs.join(' ') : undefined;
164
167
  if (!command) {
165
168
  openApp();
166
169
  return;
@@ -271,14 +274,20 @@ async function main() {
271
274
  }
272
275
  search(model, subcommand, format);
273
276
  break;
274
- case 'mark':
275
- if (!subcommand || !target) {
277
+ case 'mark': {
278
+ // mark <slice-name> <status> - status is last arg, slice name is everything before
279
+ const validStatuses = ['created', 'in-progress', 'blocked', 'done'];
280
+ const lastArg = remainingArgs[remainingArgs.length - 1];
281
+ if (remainingArgs.length < 2 || !validStatuses.includes(lastArg)) {
276
282
  console.error('Usage: eventmodeler mark <slice-name> <status>');
277
283
  console.error('Valid statuses: created, in-progress, blocked, done');
278
284
  process.exit(1);
279
285
  }
280
- markSliceStatus(model, filePath, subcommand, target);
286
+ const sliceName = remainingArgs.slice(0, -1).join(' ');
287
+ const status = lastArg;
288
+ markSliceStatus(model, filePath, sliceName, status);
281
289
  break;
290
+ }
282
291
  case 'summary':
283
292
  showModelSummary(model, format);
284
293
  break;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Element lookup utilities for CLI commands.
3
- * Provides exact-match lookup with UUID disambiguation for duplicate names.
3
+ * Provides fuzzy lookup with UUID disambiguation for ambiguous matches.
4
4
  */
5
5
  export type LookupResult<T> = {
6
6
  success: true;
@@ -11,10 +11,12 @@ export type LookupResult<T> = {
11
11
  matches: T[];
12
12
  };
13
13
  /**
14
- * Find an element by exact name (case-insensitive) or UUID.
14
+ * Find an element by name (fuzzy) or UUID.
15
15
  * - If search starts with "id:", treats rest as UUID/UUID prefix
16
- * - Otherwise, performs case-insensitive exact name match
17
- * - Returns error if multiple elements have the same name
16
+ * - Otherwise, performs fuzzy name matching:
17
+ * 1. Exact match (case-insensitive, normalized spaces)
18
+ * 2. Partial match (search is contained in name)
19
+ * - Returns error if multiple elements match
18
20
  */
19
21
  export declare function findElement<T extends {
20
22
  id: string;
@@ -1,12 +1,23 @@
1
1
  /**
2
2
  * Element lookup utilities for CLI commands.
3
- * Provides exact-match lookup with UUID disambiguation for duplicate names.
3
+ * Provides fuzzy lookup with UUID disambiguation for ambiguous matches.
4
4
  */
5
5
  /**
6
- * Find an element by exact name (case-insensitive) or UUID.
6
+ * Normalize a string for fuzzy matching:
7
+ * - lowercase
8
+ * - collapse multiple spaces to single space
9
+ * - trim whitespace
10
+ */
11
+ function normalize(str) {
12
+ return str.toLowerCase().replace(/\s+/g, ' ').trim();
13
+ }
14
+ /**
15
+ * Find an element by name (fuzzy) or UUID.
7
16
  * - If search starts with "id:", treats rest as UUID/UUID prefix
8
- * - Otherwise, performs case-insensitive exact name match
9
- * - Returns error if multiple elements have the same name
17
+ * - Otherwise, performs fuzzy name matching:
18
+ * 1. Exact match (case-insensitive, normalized spaces)
19
+ * 2. Partial match (search is contained in name)
20
+ * - Returns error if multiple elements match
10
21
  */
11
22
  export function findElement(elements, search) {
12
23
  const elementArray = Array.isArray(elements) ? elements : [...elements.values()];
@@ -28,14 +39,34 @@ export function findElement(elements, search) {
28
39
  }
29
40
  return { success: false, error: 'not_found', matches: [] };
30
41
  }
31
- // Case-insensitive exact name match
32
- const searchLower = search.toLowerCase();
33
- const matches = elementArray.filter(e => e.name.toLowerCase() === searchLower);
34
- if (matches.length === 1) {
35
- return { success: true, element: matches[0] };
42
+ const searchNorm = normalize(search);
43
+ // 1. Try exact match first (case-insensitive, normalized)
44
+ const exactMatches = elementArray.filter(e => normalize(e.name) === searchNorm);
45
+ if (exactMatches.length === 1) {
46
+ return { success: true, element: exactMatches[0] };
47
+ }
48
+ if (exactMatches.length > 1) {
49
+ return { success: false, error: 'ambiguous', matches: exactMatches };
50
+ }
51
+ // 2. Try partial match (search contained in name)
52
+ const partialMatches = elementArray.filter(e => normalize(e.name).includes(searchNorm));
53
+ if (partialMatches.length === 1) {
54
+ return { success: true, element: partialMatches[0] };
55
+ }
56
+ if (partialMatches.length > 1) {
57
+ return { success: false, error: 'ambiguous', matches: partialMatches };
58
+ }
59
+ // 3. Try word-based match (all search words appear in name)
60
+ const searchWords = searchNorm.split(' ').filter(w => w.length > 0);
61
+ const wordMatches = elementArray.filter(e => {
62
+ const nameNorm = normalize(e.name);
63
+ return searchWords.every(word => nameNorm.includes(word));
64
+ });
65
+ if (wordMatches.length === 1) {
66
+ return { success: true, element: wordMatches[0] };
36
67
  }
37
- if (matches.length > 1) {
38
- return { success: false, error: 'ambiguous', matches };
68
+ if (wordMatches.length > 1) {
69
+ return { success: false, error: 'ambiguous', matches: wordMatches };
39
70
  }
40
71
  return { success: false, error: 'not_found', matches: [] };
41
72
  }
@@ -71,6 +71,18 @@ function findActorForScreen(model, screen) {
71
71
  }
72
72
  return undefined;
73
73
  }
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
+ }
74
86
  // Convert Field to JSON-friendly format
75
87
  function fieldToJson(field) {
76
88
  const result = {
@@ -330,13 +342,16 @@ export function codegenSlice(model, sliceName) {
330
342
  const internalFlows = getInternalFlows(model, componentIds);
331
343
  // 7. Get scenarios for this slice
332
344
  const scenarios = [...model.scenarios.values()].filter(s => s.sliceId === slice.id);
333
- // 8. Build output
345
+ // 8. Find which chapter this slice belongs to
346
+ const chapter = findChapterForSlice(model, slice);
347
+ // 9. Build output
334
348
  const output = {
335
349
  sliceType,
336
350
  slice: {
337
351
  id: slice.id,
338
352
  name: slice.name,
339
353
  },
354
+ ...(chapter && { chapter: { id: chapter.id, name: chapter.name } }),
340
355
  elements: {
341
356
  readModels: components.readModels.map(rm => ({
342
357
  id: rm.id,
@@ -379,6 +394,6 @@ export function codegenSlice(model, sliceName) {
379
394
  internalFlows,
380
395
  scenarios: formatScenarios(model, scenarios),
381
396
  };
382
- // 9. Output JSON
397
+ // 10. Output JSON
383
398
  outputJson(output);
384
399
  }
@@ -112,13 +112,22 @@ function getSourceFields(model, sourceId) {
112
112
  return proc.fields;
113
113
  return [];
114
114
  }
115
- function flattenFields(fields, prefix = '') {
115
+ /**
116
+ * Flattens nested Custom fields into a flat list with dot-notation paths.
117
+ * When skipCustomParents is true, Custom fields with subfields are excluded.
118
+ */
119
+ function flattenFields(fields, prefix = '', skipCustomParents = false) {
116
120
  const result = [];
117
121
  for (const field of fields) {
118
122
  const path = prefix ? `${prefix}.${field.name}` : field.name;
119
- result.push({ id: field.id, path, field });
123
+ const isCustomParent = field.fieldType === 'Custom' &&
124
+ field.subfields &&
125
+ field.subfields.length > 0;
126
+ if (!skipCustomParents || !isCustomParent) {
127
+ result.push({ id: field.id, path, field });
128
+ }
120
129
  if (field.fieldType === 'Custom' && field.subfields) {
121
- result.push(...flattenFields(field.subfields, path));
130
+ result.push(...flattenFields(field.subfields, path, skipCustomParents));
122
131
  }
123
132
  }
124
133
  return result;
@@ -158,7 +167,8 @@ function calculateReadModelUnionCompleteness(model, readModelId, readModelFields
158
167
  }
159
168
  }
160
169
  // Check each target field against the union of all sources
161
- const flatTarget = flattenFields(readModelFields);
170
+ // Skip custom parents - only check leaf fields
171
+ const flatTarget = flattenFields(readModelFields, '', true);
162
172
  const untraceableFields = [];
163
173
  const optionalMissingFields = [];
164
174
  const fieldStatuses = [];
@@ -214,7 +224,8 @@ function calculateReadModelUnionCompleteness(model, readModelId, readModelFields
214
224
  }
215
225
  function calculateFlowCompleteness(model, flow, targetFields, sourceFields) {
216
226
  const flatSource = flattenFields(sourceFields);
217
- const flatTarget = flattenFields(targetFields);
227
+ // Skip custom parents for target fields - only check leaf fields
228
+ const flatTarget = flattenFields(targetFields, '', true);
218
229
  const manualMappings = flow.fieldMappings ?? [];
219
230
  // Get source entity name
220
231
  let sourceName = 'Unknown';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eventmodeler",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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": {