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 +13 -4
- package/dist/lib/element-lookup.d.ts +6 -4
- package/dist/lib/element-lookup.js +42 -11
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +12 -0
- package/dist/slices/show-completeness/index.js +16 -5
- package/dist/slices/show-slice/index.js +7 -7
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
17
|
-
*
|
|
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
|
|
3
|
+
* Provides fuzzy lookup with UUID disambiguation for ambiguous matches.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
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
|
|
9
|
-
*
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
return { success: true, element:
|
|
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 (
|
|
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
|
}
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/format.js
CHANGED
|
@@ -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, '&')
|
|
@@ -6,6 +9,15 @@ export function escapeXml(str) {
|
|
|
6
9
|
.replace(/"/g, '"')
|
|
7
10
|
.replace(/'/g, ''');
|
|
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, '&')
|
|
18
|
+
.replace(/</g, '<')
|
|
19
|
+
.replace(/>/g, '>');
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>${
|
|
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)}">${
|
|
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)}">${
|
|
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 += `>${
|
|
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)}">${
|
|
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>${
|
|
292
|
+
xml += ` <expected>${escapeXmlText(formatFieldValues(assertion.expectedFieldValues))}</expected>\n`;
|
|
293
293
|
xml += ' </read-model-assertion>\n';
|
|
294
294
|
}
|
|
295
295
|
xml += ' </then>\n';
|