eventmodeler 0.2.7 → 0.2.9

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
  }
@@ -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
  }
@@ -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';
@@ -1,4 +1,4 @@
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
3
  function formatFieldValues(values) {
4
4
  if (!values || Object.keys(values).length === 0)
@@ -243,7 +243,7 @@ function formatSliceXml(model, slice) {
243
243
  for (const scenario of scenarios) {
244
244
  xml += ` <scenario name="${escapeXml(scenario.name)}">\n`;
245
245
  if (scenario.description) {
246
- xml += ` <description>${escapeXml(scenario.description)}</description>\n`;
246
+ xml += ` <description>${escapeXmlText(scenario.description)}</description>\n`;
247
247
  }
248
248
  if (scenario.givenEvents.length > 0) {
249
249
  xml += ' <given>\n';
@@ -252,7 +252,7 @@ function formatSliceXml(model, slice) {
252
252
  const name = evt?.name ?? 'UnknownEvent';
253
253
  const values = formatFieldValues(given.fieldValues);
254
254
  xml += values
255
- ? ` <event type="${escapeXml(name)}">${escapeXml(values)}</event>\n`
255
+ ? ` <event type="${escapeXml(name)}">${escapeXmlText(values)}</event>\n`
256
256
  : ` <event type="${escapeXml(name)}"/>\n`;
257
257
  }
258
258
  xml += ' </given>\n';
@@ -263,7 +263,7 @@ function formatSliceXml(model, slice) {
263
263
  const values = formatFieldValues(scenario.whenCommand.fieldValues);
264
264
  xml += ' <when>\n';
265
265
  xml += values
266
- ? ` <command type="${escapeXml(name)}">${escapeXml(values)}</command>\n`
266
+ ? ` <command type="${escapeXml(name)}">${escapeXmlText(values)}</command>\n`
267
267
  : ` <command type="${escapeXml(name)}"/>\n`;
268
268
  xml += ' </when>\n';
269
269
  }
@@ -272,7 +272,7 @@ function formatSliceXml(model, slice) {
272
272
  xml += ` <error`;
273
273
  if (scenario.then.errorType)
274
274
  xml += ` type="${escapeXml(scenario.then.errorType)}"`;
275
- xml += `>${escapeXml(scenario.then.errorMessage ?? '')}</error>\n`;
275
+ xml += `>${escapeXmlText(scenario.then.errorMessage ?? '')}</error>\n`;
276
276
  }
277
277
  else if (scenario.then.type === 'events' && scenario.then.expectedEvents) {
278
278
  for (const expected of scenario.then.expectedEvents) {
@@ -280,7 +280,7 @@ function formatSliceXml(model, slice) {
280
280
  const name = evt?.name ?? 'UnknownEvent';
281
281
  const values = formatFieldValues(expected.fieldValues);
282
282
  xml += values
283
- ? ` <event type="${escapeXml(name)}">${escapeXml(values)}</event>\n`
283
+ ? ` <event type="${escapeXml(name)}">${escapeXmlText(values)}</event>\n`
284
284
  : ` <event type="${escapeXml(name)}"/>\n`;
285
285
  }
286
286
  }
@@ -289,7 +289,7 @@ function formatSliceXml(model, slice) {
289
289
  const rm = model.readModels.get(assertion.readModelStickyId);
290
290
  const name = rm?.name ?? 'UnknownReadModel';
291
291
  xml += ` <read-model-assertion type="${escapeXml(name)}">\n`;
292
- xml += ` <expected>${escapeXml(formatFieldValues(assertion.expectedFieldValues))}</expected>\n`;
292
+ xml += ` <expected>${escapeXmlText(formatFieldValues(assertion.expectedFieldValues))}</expected>\n`;
293
293
  xml += ' </read-model-assertion>\n';
294
294
  }
295
295
  xml += ' </then>\n';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eventmodeler",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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": {