eva4j 1.0.16 → 1.0.18

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.
Files changed (151) hide show
  1. package/AGENTS.md +220 -5
  2. package/DOMAIN_YAML_GUIDE.md +188 -3
  3. package/FUTURE_FEATURES.md +33 -52
  4. package/QUICK_REFERENCE.md +8 -4
  5. package/bin/eva4j.js +70 -2
  6. package/config/defaults.json +1 -0
  7. package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
  8. package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
  9. package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
  10. package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
  11. package/docs/commands/EVALUATE_SYSTEM.md +290 -10
  12. package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
  13. package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
  14. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
  15. package/docs/commands/INDEX.md +27 -3
  16. package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
  17. package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
  18. package/docs/prototype/system/RISKS.md +277 -0
  19. package/docs/prototype/system/customers.yaml +133 -0
  20. package/docs/prototype/system/inventory.yaml +109 -0
  21. package/docs/prototype/system/notifications.yaml +131 -0
  22. package/docs/prototype/system/orders.yaml +241 -0
  23. package/docs/prototype/system/payments.yaml +256 -0
  24. package/docs/prototype/system/products.yaml +168 -0
  25. package/docs/prototype/system/system.yaml +269 -0
  26. package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
  27. package/examples/domain-events.yaml +26 -0
  28. package/examples/domain-read-models.yaml +113 -0
  29. package/examples/system/customer.yaml +89 -0
  30. package/examples/system/orders.yaml +119 -0
  31. package/examples/system/product.yaml +27 -0
  32. package/examples/system/system.yaml +80 -0
  33. package/package.json +1 -1
  34. package/read-model-spec.md +664 -0
  35. package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
  36. package/src/agents/design-gap-analyst.agent.md +383 -0
  37. package/src/agents/design-reviewer-temporal.agent.md +412 -0
  38. package/src/agents/design-reviewer.agent.md +34 -5
  39. package/src/agents/implement-use-cases.prompt.md +179 -0
  40. package/src/agents/ux-gap-analyst.agent.md +412 -0
  41. package/src/commands/add-rabbitmq-client.js +261 -0
  42. package/src/commands/add-temporal-client.js +22 -2
  43. package/src/commands/build.js +267 -11
  44. package/src/commands/evaluate-system.js +700 -13
  45. package/src/commands/generate-entities.js +560 -24
  46. package/src/commands/generate-http-exchange.js +3 -0
  47. package/src/commands/generate-kafka-event.js +3 -0
  48. package/src/commands/generate-kafka-listener.js +3 -0
  49. package/src/commands/generate-rabbitmq-event.js +665 -0
  50. package/src/commands/generate-rabbitmq-listener.js +205 -0
  51. package/src/commands/generate-record.js +2 -2
  52. package/src/commands/generate-resource.js +4 -1
  53. package/src/commands/generate-temporal-activity.js +970 -33
  54. package/src/commands/generate-temporal-flow.js +98 -38
  55. package/src/commands/generate-temporal-system.js +708 -0
  56. package/src/commands/generate-usecase.js +4 -1
  57. package/src/skills/build-system-yaml/SKILL.md +343 -2
  58. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
  59. package/src/skills/build-system-yaml/references/module-spec.md +90 -9
  60. package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
  61. package/src/skills/build-temporal-system/SKILL.md +752 -0
  62. package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
  63. package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
  64. package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
  65. package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
  66. package/src/skills/implement-use-case/SKILL.md +350 -0
  67. package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
  68. package/src/skills/requirements-elicitation/SKILL.md +228 -0
  69. package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
  70. package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
  71. package/src/utils/bounded-context-diagram.js +844 -0
  72. package/src/utils/config-manager.js +4 -2
  73. package/src/utils/domain-validator.js +495 -17
  74. package/src/utils/naming.js +20 -0
  75. package/src/utils/system-validator.js +169 -11
  76. package/src/utils/system-yaml-parser.js +318 -0
  77. package/src/utils/temporal-validator.js +497 -0
  78. package/src/utils/validator.js +3 -1
  79. package/src/utils/yaml-to-entity.js +281 -9
  80. package/templates/aggregate/AggregateRepository.java.ejs +4 -0
  81. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
  82. package/templates/aggregate/AggregateRoot.java.ejs +38 -4
  83. package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
  84. package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
  85. package/templates/aggregate/JpaEntity.java.ejs +2 -2
  86. package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
  87. package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
  88. package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
  89. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
  90. package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
  91. package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
  92. package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
  93. package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
  94. package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
  95. package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
  96. package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
  97. package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
  98. package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
  99. package/templates/base/root/AGENTS.md.ejs +1 -1
  100. package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
  101. package/templates/crud/EndpointsController.java.ejs +1 -1
  102. package/templates/crud/ScaffoldCommand.java.ejs +5 -2
  103. package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
  104. package/templates/crud/ScaffoldQuery.java.ejs +5 -2
  105. package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
  106. package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
  107. package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
  108. package/templates/evaluate/report.html.ejs +1447 -90
  109. package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
  110. package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
  111. package/templates/ports/PortAclMapper.java.ejs +35 -0
  112. package/templates/ports/PortFeignAdapter.java.ejs +7 -22
  113. package/templates/ports/PortFeignClient.java.ejs +4 -0
  114. package/templates/ports/PortResponseDto.java.ejs +1 -1
  115. package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
  116. package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
  117. package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
  118. package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
  119. package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
  120. package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
  121. package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
  122. package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
  123. package/templates/read-model/ReadModelDomain.java.ejs +46 -0
  124. package/templates/read-model/ReadModelJpa.java.ejs +58 -0
  125. package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
  126. package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
  127. package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
  128. package/templates/read-model/ReadModelRepository.java.ejs +42 -0
  129. package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
  130. package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
  131. package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
  132. package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
  133. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
  134. package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
  135. package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
  136. package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
  137. package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
  138. package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
  139. package/templates/temporal-activity/NestedType.java.ejs +12 -0
  140. package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
  141. package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
  142. package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
  143. package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
  144. package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
  145. package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
  146. package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
  147. package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
  148. package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
  149. package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
  150. package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
  151. package/COMMAND_EVALUATION.md +0 -911
@@ -10,6 +10,7 @@ const ora = require('ora');
10
10
 
11
11
  const { validateSystem } = require('../utils/system-validator');
12
12
  const { validateDomain } = require('../utils/domain-validator');
13
+ const { validateTemporal } = require('../utils/temporal-validator');
13
14
 
14
15
  // ── Module icon heuristic ────────────────────────────────────────────────────
15
16
 
@@ -207,25 +208,562 @@ function buildEventFlows(systemConfig, modulesMap) {
207
208
  return flows;
208
209
  }
209
210
 
211
+ // ── Temporal helpers ──────────────────────────────────────────────────────────
212
+
213
+ function _camelCase(str) {
214
+ if (!str) return '';
215
+ return str.replace(/[-_ ]+(.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (c) => c.toLowerCase());
216
+ }
217
+ function _pascalCase(str) {
218
+ if (!str) return '';
219
+ const c = _camelCase(str);
220
+ return c.charAt(0).toUpperCase() + c.slice(1);
221
+ }
222
+
223
+ /**
224
+ * Infers a human-readable role description for an activity step.
225
+ * Priority: domain.yaml description → auto-infer from name + type context.
226
+ */
227
+ function inferActivityRole(actName, actDef, step, wfContext) {
228
+ if (actDef && actDef.description) return actDef.description;
229
+
230
+ const name = (actName || '').toLowerCase();
231
+ const isCompensation = actDef && actDef.isCompensation;
232
+
233
+ // Compensation activities
234
+ if (isCompensation || /^(release|refund|cancel|undo|revert|restore|rollback|mark.*cancel)/i.test(actName)) {
235
+ return `COMPENSATE: Deshace los efectos de la actividad principal en caso de fallo del workflow`;
236
+ }
237
+ // Async notification activities
238
+ if (step && step.type === 'async') {
239
+ return `NOTIFY: Envía notificación async (fire-and-forget) — no bloquea el workflow`;
240
+ }
241
+ // Read activities
242
+ if (/^(get|find|fetch|load|read|query|search|list)/i.test(actName)) {
243
+ const target = _pascalCase((step && step.target) || '');
244
+ return `READ: Obtiene datos de ${target} necesarios para los pasos siguientes del workflow`;
245
+ }
246
+ // Write activities
247
+ if (/^(create|make|build|generate|init)/i.test(actName)) {
248
+ const target = _pascalCase((step && step.target) || '');
249
+ return `WRITE: Crea un nuevo recurso en ${target}`;
250
+ }
251
+ if (/^(confirm|approve|activate|enable|process|execute)/i.test(actName)) {
252
+ const target = _pascalCase((step && step.target) || '');
253
+ return `WRITE: Confirma o actualiza estado en ${target}`;
254
+ }
255
+ if (/^(reserve|block|lock|hold|schedule|assign)/i.test(actName)) {
256
+ const target = _pascalCase((step && step.target) || '');
257
+ return `WRITE: Reserva o bloquea recurso en ${target}`;
258
+ }
259
+ if (/^(clear|clean|remove|purge|convert|mark)/i.test(actName)) {
260
+ const target = _pascalCase((step && step.target) || '');
261
+ return `WRITE: Actualiza o limpia datos en ${target}`;
262
+ }
263
+
264
+ return `Ejecuta lógica de negocio en módulo '${_pascalCase((step && step.target) || '')}'`;
265
+ }
266
+
267
+ /**
268
+ * Classifies an activity step into one of: READ / WRITE / COMPENSATE / NOTIFY
269
+ */
270
+ function classifyActivityRole(actName, actDef, step) {
271
+ if (actDef && actDef.isCompensation) return 'COMPENSATE';
272
+ if (/^(release|refund|cancel|undo|revert|restore|rollback|mark.*cancel)/i.test(actName)) return 'COMPENSATE';
273
+ if (step && step.type === 'async') return 'NOTIFY';
274
+ if (/^(get|find|fetch|load|read|query|search|list)/i.test(actName)) return 'READ';
275
+ if (/^(notify|send|emit|alert|push|broadcast)/i.test(actName)) return 'NOTIFY';
276
+ return 'WRITE';
277
+ }
278
+
279
+ /**
280
+ * Calculates the LIFO compensation chain for a saga workflow.
281
+ * Returns an array of { stepNum, activityName, compensationName, targetModule, type }
282
+ * ordered chronologically (execution order); the LIFO rollback is the reverse.
283
+ */
284
+ function buildSagaChain(wf) {
285
+ const steps = Array.isArray(wf.steps) ? wf.steps : [];
286
+ const chain = [];
287
+ for (let i = 0; i < steps.length; i++) {
288
+ const step = steps[i];
289
+ if (!step.activity) continue;
290
+ chain.push({
291
+ stepNum: i + 1,
292
+ activityName: step.activity,
293
+ compensationName: step.compensation || null,
294
+ targetModule: step.target || null,
295
+ type: step.type || 'sync',
296
+ timeout: step.timeout || null,
297
+ isCompensable: !!(step.compensation),
298
+ canFail: step.type !== 'async',
299
+ });
300
+ }
301
+ return chain;
302
+ }
303
+
304
+ /**
305
+ * Resolves the source of each input field in a step:
306
+ * 'trigger' | 'step N: ActivityName' | 'unknown'
307
+ */
308
+ function resolveInputSources(allSteps, currentIdx, triggerFields) {
309
+ const inputSources = {};
310
+ const available = {}; // fieldName → source label
311
+
312
+ // Seed with trigger/event fields
313
+ for (const f of triggerFields) {
314
+ available[f] = 'trigger';
315
+ }
316
+
317
+ for (let i = 0; i < currentIdx; i++) {
318
+ const prev = allSteps[i];
319
+ for (const o of (prev.output || [])) {
320
+ const oName = typeof o === 'string' ? o : o.name;
321
+ if (oName) available[oName] = `paso ${i + 1}: ${prev.activity}`;
322
+ }
323
+ }
324
+
325
+ const current = allSteps[currentIdx];
326
+ for (const inp of (current.input || [])) {
327
+ const iName = typeof inp === 'string' ? inp : inp.name;
328
+ if (iName) inputSources[iName] = available[iName] || 'no resuelto';
329
+ }
330
+
331
+ return inputSources;
332
+ }
333
+
334
+ /**
335
+ * Builds which downstream steps consume each output field of a given step.
336
+ */
337
+ function buildOutputConsumers(allSteps, currentIdx) {
338
+ const current = allSteps[currentIdx];
339
+ const consumers = {};
340
+ const outputs = (current.output || []).map((o) => (typeof o === 'string' ? o : o.name)).filter(Boolean);
341
+
342
+ for (const outField of outputs) {
343
+ consumers[outField] = [];
344
+ for (let j = currentIdx + 1; j < allSteps.length; j++) {
345
+ const downstep = allSteps[j];
346
+ const inputs = (downstep.input || []).map((i) => (typeof i === 'string' ? i : i.name));
347
+ if (inputs.includes(outField)) {
348
+ consumers[outField].push(`paso ${j + 1}: ${downstep.activity}`);
349
+ }
350
+ }
351
+ }
352
+ return consumers;
353
+ }
354
+
355
+ /**
356
+ * Calculates the module role based on its participation in workflows.
357
+ * Orchestrator: has trigger module; DataProvider: only read activities; Executor: write/compensate; Reactor: only async/notify
358
+ */
359
+ function calculateModuleRoles(systemConfig, domainConfigs) {
360
+ const roles = {};
361
+ const rawWorkflows = systemConfig.workflows || [];
362
+ const modules = systemConfig.modules || [];
363
+
364
+ for (const mod of modules) {
365
+ const modName = _camelCase(mod.name);
366
+ roles[modName] = { name: modName, roles: new Set(), label: mod.name };
367
+ }
368
+
369
+ for (const wf of rawWorkflows) {
370
+ const trigMod = wf.trigger && wf.trigger.module ? _camelCase(wf.trigger.module) : null;
371
+ if (trigMod && roles[trigMod]) roles[trigMod].roles.add('Orchestrator');
372
+
373
+ for (const step of (wf.steps || [])) {
374
+ const targetMod = step.target ? _camelCase(step.target) : trigMod;
375
+ if (!targetMod || !roles[targetMod]) continue;
376
+
377
+ const actName = (step.activity || '').toLowerCase();
378
+ if (step.type === 'async') {
379
+ roles[targetMod].roles.add('Reactor');
380
+ } else if (/^(get|find|fetch|load|read|query|search|list)/.test(actName)) {
381
+ roles[targetMod].roles.add('DataProvider');
382
+ } else {
383
+ roles[targetMod].roles.add('Executor');
384
+ }
385
+ }
386
+ }
387
+
388
+ // Convert Sets to sorted arrays
389
+ const result = {};
390
+ for (const [modName, data] of Object.entries(roles)) {
391
+ const rolesArr = [...data.roles];
392
+ result[modName] = {
393
+ name: modName,
394
+ label: data.label,
395
+ roles: rolesArr,
396
+ primaryRole: rolesArr.includes('Orchestrator') ? 'Orchestrator'
397
+ : rolesArr.includes('Executor') ? 'Executor'
398
+ : rolesArr.includes('DataProvider') ? 'DataProvider'
399
+ : rolesArr.includes('Reactor') ? 'Reactor'
400
+ : 'Standalone',
401
+ roleLabel: rolesArr.length > 0 ? rolesArr.join(' + ') : 'Standalone',
402
+ };
403
+ }
404
+ return result;
405
+ }
406
+
407
+ /**
408
+ * Builds the activity catalog: all activities across all modules with usage info.
409
+ */
410
+ function buildActivityCatalog(systemConfig, domainConfigs, modulesMap) {
411
+ const rawWorkflows = systemConfig.workflows || [];
412
+ const localWorkflowsByModule = {};
413
+
414
+ // Collect local workflows
415
+ for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
416
+ if (domainCfg && Array.isArray(domainCfg.workflows)) {
417
+ localWorkflowsByModule[_camelCase(modName)] = domainCfg.workflows;
418
+ }
419
+ }
420
+
421
+ // Build used-in index: "modName::ActivityName" → ["WorkflowName", ...]
422
+ const usedIn = {};
423
+ for (const wf of rawWorkflows) {
424
+ const trigMod = wf.trigger && wf.trigger.module ? _camelCase(wf.trigger.module) : null;
425
+ for (const step of (wf.steps || [])) {
426
+ if (!step.activity) continue;
427
+ const targetMod = step.target ? _camelCase(step.target) : trigMod;
428
+ const key = `${targetMod}::${_pascalCase(step.activity)}`;
429
+ if (!usedIn[key]) usedIn[key] = [];
430
+ usedIn[key].push(wf.name);
431
+
432
+ if (step.compensation) {
433
+ const compKey = `${targetMod}::${_pascalCase(step.compensation)}`;
434
+ if (!usedIn[compKey]) usedIn[compKey] = [];
435
+ usedIn[compKey].push(`${wf.name} (compensation)`);
436
+ }
437
+ }
438
+ }
439
+ for (const [modName, localWfs] of Object.entries(localWorkflowsByModule)) {
440
+ for (const wf of localWfs) {
441
+ for (const step of (wf.steps || [])) {
442
+ if (!step.activity) continue;
443
+ const key = `${modName}::${_pascalCase(step.activity)}`;
444
+ if (!usedIn[key]) usedIn[key] = [];
445
+ usedIn[key].push(`${wf.name} (local)`);
446
+ }
447
+ }
448
+ }
449
+
450
+ // Build catalog entries
451
+ const catalog = [];
452
+ let compensationNames = new Set();
453
+
454
+ // First pass: collect compensation names
455
+ for (const [, domainCfg] of Object.entries(domainConfigs)) {
456
+ for (const act of (domainCfg?.activities || [])) {
457
+ if (act.compensation) compensationNames.add(_pascalCase(act.compensation));
458
+ }
459
+ }
460
+
461
+ for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
462
+ if (!domainCfg || !Array.isArray(domainCfg.activities)) continue;
463
+ const modKey = _camelCase(modName);
464
+
465
+ for (const act of domainCfg.activities) {
466
+ const actPascal = _pascalCase(act.name);
467
+ const key = `${modKey}::${actPascal}`;
468
+ const usedInWorkflows = usedIn[key] || [];
469
+ const isCompensation = compensationNames.has(actPascal);
470
+ const role = classifyActivityRole(act.name, { isCompensation }, null);
471
+
472
+ catalog.push({
473
+ name: actPascal,
474
+ module: modKey,
475
+ moduleLabel: modulesMap[modKey]?.label || modKey,
476
+ type: (act.type || 'light').toLowerCase(),
477
+ role,
478
+ description: act.description || inferActivityRole(act.name, { isCompensation }, null, {}),
479
+ usedInWorkflows,
480
+ isCompensation,
481
+ isOrphan: usedInWorkflows.length === 0,
482
+ hasRetryPolicy: !!(act.retryPolicy),
483
+ retryPolicy: act.retryPolicy || null,
484
+ timeout: act.timeout || null,
485
+ inputFields: (act.input || []).map((f) => (typeof f === 'string' ? { name: f, type: 'String' } : f)),
486
+ outputFields: (act.output || []).map((f) => (typeof f === 'string' ? { name: f, type: 'String' } : f)),
487
+ hasOutput: !!(act.output && act.output.length > 0),
488
+ nestedTypes: act.nestedTypes || [],
489
+ externalTypes: act.externalTypes || [],
490
+ compensation: act.compensation || null,
491
+ });
492
+ }
493
+ }
494
+
495
+ return catalog;
496
+ }
497
+
498
+ /**
499
+ * Builds the external type dependency graph.
500
+ * Returns array of { consumerModule, activityName, typeName, sourceModule }
501
+ */
502
+ function buildExternalTypeDeps(domainConfigs) {
503
+ const deps = [];
504
+ for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
505
+ if (!domainCfg || !Array.isArray(domainCfg.activities)) continue;
506
+ for (const act of domainCfg.activities) {
507
+ for (const ext of (act.externalTypes || [])) {
508
+ deps.push({
509
+ consumerModule: _camelCase(modName),
510
+ activityName: _pascalCase(act.name),
511
+ typeName: _pascalCase(ext.name || ext),
512
+ sourceModule: _camelCase(ext.module || ext),
513
+ });
514
+ }
515
+ }
516
+ }
517
+ return deps;
518
+ }
519
+
520
+ /**
521
+ * Builds queue topology for each module.
522
+ */
523
+ function buildQueueTopology(systemConfig, domainConfigs) {
524
+ const rawWorkflows = systemConfig.workflows || [];
525
+ const modules = systemConfig.modules || [];
526
+ const topology = {};
527
+
528
+ const participating = new Set();
529
+ for (const wf of rawWorkflows) {
530
+ if (wf.trigger && wf.trigger.module) participating.add(_camelCase(wf.trigger.module));
531
+ for (const step of (wf.steps || [])) {
532
+ if (step.target) participating.add(_camelCase(step.target));
533
+ }
534
+ }
535
+ // Also modules with activities[] in domain.yaml
536
+ for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
537
+ if (domainCfg && Array.isArray(domainCfg.activities) && domainCfg.activities.length > 0) {
538
+ participating.add(_camelCase(modName));
539
+ }
540
+ if (domainCfg && Array.isArray(domainCfg.workflows) && domainCfg.workflows.length > 0) {
541
+ participating.add(_camelCase(modName));
542
+ }
543
+ }
544
+
545
+ for (const mod of modules) {
546
+ const modKey = _camelCase(mod.name);
547
+ if (!participating.has(modKey)) continue;
548
+ const snake = mod.name.toUpperCase().replace(/-/g, '_');
549
+ topology[modKey] = {
550
+ name: modKey,
551
+ label: mod.name,
552
+ flowQueue: `${snake}_WORKFLOW_QUEUE`,
553
+ heavyQueue: `${snake}_HEAVY_TASK_QUEUE`,
554
+ lightQueue: `${snake}_LIGHT_TASK_QUEUE`,
555
+ };
556
+ }
557
+
558
+ return topology;
559
+ }
560
+
561
+ /**
562
+ * Enriches workflows with resolved data flow, role descriptions and saga analysis.
563
+ */
564
+ function enrichWorkflows(rawWorkflows, domainConfigs, modulesMap) {
565
+ const activityDefs = {};
566
+ for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
567
+ if (!domainCfg || !Array.isArray(domainCfg.activities)) continue;
568
+ const modKey = _camelCase(modName);
569
+ const compensationNames = new Set();
570
+ for (const act of domainCfg.activities) {
571
+ if (act.compensation) compensationNames.add(_pascalCase(act.compensation));
572
+ }
573
+ for (const act of domainCfg.activities) {
574
+ activityDefs[`${modKey}::${_pascalCase(act.name)}`] = {
575
+ ...act,
576
+ isCompensation: compensationNames.has(_pascalCase(act.name)),
577
+ };
578
+ }
579
+ }
580
+
581
+ return rawWorkflows.map((wf) => {
582
+ const triggerModule = wf.trigger && wf.trigger.module ? _camelCase(wf.trigger.module) : null;
583
+ const steps = Array.isArray(wf.steps) ? wf.steps : [];
584
+
585
+ // Approximate trigger fields from domain events
586
+ const triggerFields = [];
587
+ if (wf.trigger && wf.trigger.on && triggerModule && domainConfigs[triggerModule]) {
588
+ const domainCfg = domainConfigs[triggerModule];
589
+ for (const agg of (domainCfg.aggregates || [])) {
590
+ for (const ev of (agg.events || [])) {
591
+ const evLower = (ev.name || '').toLowerCase().replace(/event$/, '');
592
+ const trigLower = (wf.trigger.on || '').toLowerCase().replace(/event$/, '');
593
+ if (evLower.includes(trigLower) || trigLower.includes(evLower)) {
594
+ for (const f of (ev.fields || [])) {
595
+ if (f && f.name) triggerFields.push(f.name);
596
+ }
597
+ }
598
+ }
599
+ }
600
+ }
601
+
602
+ const enrichedSteps = steps.map((step, idx) => {
603
+ if (!step.activity) return { ...step, _stepNum: idx + 1, _isWait: true };
604
+
605
+ const actPascal = _pascalCase(step.activity);
606
+ const targetMod = step.target ? _camelCase(step.target) : triggerModule;
607
+ const actKey = `${targetMod}::${actPascal}`;
608
+ const actDef = activityDefs[actKey] || null;
609
+
610
+ const modInfo = modulesMap[targetMod] || { label: targetMod, icon: '📁', color: '#888' };
611
+ const roleClass = classifyActivityRole(step.activity, actDef, step);
612
+ const roleDesc = inferActivityRole(step.activity, actDef, step, { wfName: wf.name });
613
+ const inputSources = resolveInputSources(steps, idx, triggerFields);
614
+ const outputConsumers = buildOutputConsumers(steps, idx);
615
+
616
+ return {
617
+ _stepNum: idx + 1,
618
+ _isWait: false,
619
+ activity: step.activity,
620
+ activityPascal: actPascal,
621
+ target: targetMod,
622
+ targetLabel: modInfo.label,
623
+ targetIcon: modInfo.icon,
624
+ targetColor: modInfo.color,
625
+ type: step.type || 'sync',
626
+ activityType: (actDef && actDef.type) ? actDef.type.toLowerCase() : 'light',
627
+ timeout: step.timeout || null,
628
+ retryPolicy: (actDef && actDef.retryPolicy) || null,
629
+ compensation: step.compensation || null,
630
+ roleClass,
631
+ roleDesc,
632
+ inputs: (step.input || []).map((f) => {
633
+ const fName = typeof f === 'string' ? f : f.name;
634
+ return { name: fName, source: inputSources[fName] || 'trigger' };
635
+ }),
636
+ outputs: (step.output || []).map((f) => {
637
+ const fName = typeof f === 'string' ? f : f.name;
638
+ return { name: fName, consumers: outputConsumers[fName] || [] };
639
+ }),
640
+ formalOutputCount: actDef ? (actDef.output || []).length : 0,
641
+ };
642
+ });
643
+
644
+ const sagaChain = wf.saga === true ? buildSagaChain(wf) : null;
645
+
646
+ // Build data flow table: columns = steps (sync only), rows = all field names
647
+ const dataFlowFields = new Set();
648
+ const dataFlowSources = {}; // fieldName → { origin: 'trigger'|stepIdx, label }
649
+ for (const f of triggerFields) {
650
+ dataFlowFields.add(f);
651
+ dataFlowSources[f] = { origin: 'trigger', label: 'trigger' };
652
+ }
653
+ for (let i = 0; i < enrichedSteps.length; i++) {
654
+ for (const o of (enrichedSteps[i].outputs || [])) {
655
+ if (o.name) {
656
+ dataFlowFields.add(o.name);
657
+ dataFlowSources[o.name] = { origin: i, label: `paso ${i + 1}: ${steps[i].activity}` };
658
+ }
659
+ }
660
+ }
661
+
662
+ const dataFlowTable = [...dataFlowFields].map((field) => {
663
+ const source = dataFlowSources[field] || { origin: 'trigger', label: 'trigger' };
664
+ const consumed = enrichedSteps
665
+ .map((s, idx) => ({ idx, step: s }))
666
+ .filter(({ step }) => (step.inputs || []).some((inp) => inp.name === field))
667
+ .map(({ idx, step }) => ({ stepNum: idx + 1, activity: step.activity || '(wait)' }));
668
+ return { field, source: source.label, consumed };
669
+ });
670
+
671
+ return {
672
+ name: wf.name,
673
+ trigger: wf.trigger || null,
674
+ triggerModule,
675
+ triggerModuleLabel: modulesMap[triggerModule]?.label || triggerModule,
676
+ triggerModuleIcon: modulesMap[triggerModule]?.icon || '📁',
677
+ triggerModuleColor: modulesMap[triggerModule]?.color || '#888',
678
+ saga: wf.saga === true,
679
+ taskQueue: wf.taskQueue || null,
680
+ steps: enrichedSteps,
681
+ sagaChain,
682
+ dataFlowTable,
683
+ triggerFields,
684
+ stepCount: steps.length,
685
+ asyncStepCount: steps.filter((s) => s.type === 'async').length,
686
+ compensableStepCount: steps.filter((s) => !!s.compensation).length,
687
+ };
688
+ });
689
+ }
690
+
691
+ /**
692
+ * Main Temporal report data extraction function.
693
+ * Returns the full temporalData object passed to the HTML template.
694
+ */
695
+ function extractTemporalReportData(systemConfig, domainConfigs, modulesMap) {
696
+ const rawWorkflows = systemConfig.workflows || [];
697
+ const orchestration = systemConfig.orchestration || {};
698
+
699
+ const moduleRoles = calculateModuleRoles(systemConfig, domainConfigs);
700
+ const activityCatalog = buildActivityCatalog(systemConfig, domainConfigs, modulesMap);
701
+ const externalTypeDeps = buildExternalTypeDeps(domainConfigs);
702
+ const queueTopology = buildQueueTopology(systemConfig, domainConfigs);
703
+ const workflows = enrichWorkflows(rawWorkflows, domainConfigs, modulesMap);
704
+
705
+ // Local workflows (from individual domain.yaml files)
706
+ const localWorkflows = [];
707
+ for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
708
+ if (!domainCfg || !Array.isArray(domainCfg.workflows)) continue;
709
+ const modKey = _camelCase(modName);
710
+ for (const wf of domainCfg.workflows) {
711
+ const enriched = enrichWorkflows([wf], domainConfigs, modulesMap)[0];
712
+ localWorkflows.push({ ...enriched, _ownerModule: modKey, _ownerLabel: modulesMap[modKey]?.label || modKey, _isLocal: true });
713
+ }
714
+ }
715
+
716
+ // Saga workflows only
717
+ const sagaWorkflows = workflows.filter((wf) => wf.saga);
718
+
719
+ return {
720
+ isTemporalMode: true,
721
+ orchestration: {
722
+ target: orchestration.temporal?.target || 'localhost:7233',
723
+ namespace: orchestration.temporal?.namespace || 'default',
724
+ engine: 'temporal',
725
+ },
726
+ workflows,
727
+ localWorkflows,
728
+ sagaWorkflows,
729
+ activityCatalog,
730
+ moduleRoles,
731
+ externalTypeDeps,
732
+ queueTopology,
733
+ };
734
+ }
735
+
210
736
  // ── Data extraction ──────────────────────────────────────────────────────────
211
737
 
212
- function extractReportData(systemConfig, validation, domainValidation) {
738
+ // Builds the shared color/icon modules map used by multiple extractors
739
+ function buildModulesMap(systemConfig) {
213
740
  const modulesConfig = systemConfig.modules || [];
214
- const asyncEvents = (systemConfig.integrations || {}).async || [];
215
- const syncIntegrations = (systemConfig.integrations || {}).sync || [];
216
-
217
- // Build modules map with color + icon
218
741
  const modulesMap = {};
219
742
  for (let i = 0; i < modulesConfig.length; i++) {
220
743
  const mod = modulesConfig[i];
221
- modulesMap[mod.name] = {
744
+ const key = _camelCase(mod.name);
745
+ modulesMap[key] = {
222
746
  id: mod.name,
223
747
  label: toPascalCase(mod.name),
224
748
  icon: assignIcon(mod.name),
225
749
  color: COLOR_PALETTE[i % COLOR_PALETTE.length],
226
750
  desc: mod.description || mod.name,
227
751
  };
752
+ // Also index by raw name for compatibility
753
+ if (mod.name !== key) {
754
+ modulesMap[mod.name] = modulesMap[key];
755
+ }
228
756
  }
757
+ return modulesMap;
758
+ }
759
+
760
+ function extractReportData(systemConfig, validation, domainValidation) {
761
+ const modulesConfig = systemConfig.modules || [];
762
+ const asyncEvents = (systemConfig.integrations || {}).async || [];
763
+ const syncIntegrations = (systemConfig.integrations || {}).sync || [];
764
+
765
+ // Build modules map with color + icon
766
+ const modulesMap = buildModulesMap(systemConfig);
229
767
 
230
768
  // Normalize events (consumers can be strings or objects with .module)
231
769
  const events = asyncEvents.map((ev) => ({
@@ -252,9 +790,18 @@ function extractReportData(systemConfig, validation, domainValidation) {
252
790
  // Auto-generate flows
253
791
  const flows = buildEventFlows(systemConfig, modulesMap);
254
792
 
793
+ // Deduplicate modules by id (buildModulesMap indexes both camelCase and raw-name keys,
794
+ // so Object.values() can return the same module object twice for hyphenated names like "shopping-carts")
795
+ const seenModIds = new Set();
796
+ const modulesList = Object.values(modulesMap).filter((m) => {
797
+ if (seenModIds.has(m.id)) return false;
798
+ seenModIds.add(m.id);
799
+ return true;
800
+ });
801
+
255
802
  return {
256
803
  systemName: (systemConfig.system || {}).name || 'eva4j system',
257
- modules: Object.values(modulesMap),
804
+ modules: modulesList,
258
805
  events,
259
806
  syncIntegrations: syncList,
260
807
  endpoints,
@@ -332,8 +879,74 @@ async function evaluateSystemCommand(type, options = {}) {
332
879
  domainValidation = validateDomain(domainConfigs, systemConfig);
333
880
  }
334
881
 
335
- // ── 3. Extract report data ──────────────────────────────────────────────
882
+ // ── 3. Detect orchestration engine + extract report data ────────────────
883
+ const orchestration = systemConfig.orchestration || {};
884
+ const isTemporalMode = !!(orchestration.enabled && orchestration.engine === 'temporal');
885
+
336
886
  const reportData = extractReportData(systemConfig, validation, domainValidation);
887
+ reportData.isTemporalMode = isTemporalMode;
888
+
889
+ let temporalValidation = null;
890
+ if (isTemporalMode) {
891
+ const modulesMap = buildModulesMap(systemConfig);
892
+ const temporalCtx = extractTemporalReportData(systemConfig, domainConfigs, modulesMap);
893
+ temporalValidation = validateTemporal(systemConfig, domainConfigs, temporalCtx);
894
+
895
+ Object.assign(reportData, {
896
+ orchestration: temporalCtx.orchestration,
897
+ workflows: temporalCtx.workflows,
898
+ localWorkflows: temporalCtx.localWorkflows,
899
+ sagaWorkflows: temporalCtx.sagaWorkflows,
900
+ activityCatalog: temporalCtx.activityCatalog,
901
+ moduleRoles: temporalCtx.moduleRoles,
902
+ externalTypeDeps: temporalCtx.externalTypeDeps,
903
+ queueTopology: temporalCtx.queueTopology,
904
+ temporalValidation,
905
+ });
906
+
907
+ if (temporalValidation) {
908
+ console.log();
909
+ console.log(chalk.bold('⏱️ Temporal Validation'));
910
+ console.log(chalk.gray('─'.repeat(40)));
911
+ const tv = temporalValidation.summary;
912
+
913
+ const tvErrors = [], tvWarnings = [], tvInfos = [];
914
+ for (const category of (temporalValidation.categories || [])) {
915
+ for (const check of (category.checks || [])) {
916
+ if (!check.findings || check.findings.length === 0) continue;
917
+ const prefix = `[${check.id}] ${check.label}`;
918
+ const messages = check.findings.map((f) => {
919
+ const msg = typeof f === 'string' ? f : [f.module && `[${f.module}]`, f.message, f.context && `(${f.context})`].filter(Boolean).join(' ');
920
+ return ` • ${prefix}: ${msg}`;
921
+ });
922
+ if (check.severity === 'error') tvErrors.push(...messages);
923
+ else if (check.severity === 'warning') tvWarnings.push(...messages);
924
+ else tvInfos.push(...messages);
925
+ }
926
+ }
927
+
928
+ console.log(` ${chalk.red('🔴 Errors:')} ${chalk.red.bold(tvErrors.length)}`);
929
+ console.log(` ${chalk.yellow('🟡 Warnings:')} ${chalk.yellow.bold(tvWarnings.length)}`);
930
+ console.log(` ${chalk.cyan('🔵 Info:')} ${chalk.cyan.bold(tvInfos.length)}`);
931
+ console.log(` ${chalk.green('🟢 OK:')} ${chalk.green.bold(tv.ok)}`);
932
+
933
+ if (tvErrors.length > 0) {
934
+ console.log();
935
+ console.log(chalk.red('Temporal errors:'));
936
+ tvErrors.forEach((m) => console.log(chalk.red(m)));
937
+ }
938
+ if (tvWarnings.length > 0) {
939
+ console.log();
940
+ console.log(chalk.yellow('Temporal warnings:'));
941
+ tvWarnings.forEach((m) => console.log(chalk.yellow(m)));
942
+ }
943
+ if (tvInfos.length > 0) {
944
+ console.log();
945
+ console.log(chalk.cyan('Temporal info:'));
946
+ tvInfos.forEach((m) => console.log(chalk.cyan(m)));
947
+ }
948
+ }
949
+ }
337
950
 
338
951
  // ── 4. Render HTML ──────────────────────────────────────────────────────
339
952
  const templatePath = path.join(__dirname, '../../templates/evaluate/report.html.ejs');
@@ -353,7 +966,7 @@ async function evaluateSystemCommand(type, options = {}) {
353
966
 
354
967
  // ── 5b. Write domain assets ───────────────────────────────────────────────
355
968
  if (domainValidation) {
356
- await writeDomainAssets(domainValidation, process.cwd());
969
+ await writeDomainAssets(domainValidation, process.cwd(), temporalValidation);
357
970
  }
358
971
 
359
972
  // ── 5c. Write system-evaluation.md ──────────────────────────────────────
@@ -492,11 +1105,11 @@ function toPascalCase(str) {
492
1105
  }
493
1106
 
494
1107
  // ── writeDomainAssets ─────────────────────────────────────────────────────────
495
- async function writeDomainAssets(domainValidation, cwd) {
1108
+ async function writeDomainAssets(domainValidation, cwd, temporalValidation = null) {
496
1109
  const assetsDir = path.join(cwd, 'assets', 'evaluation');
497
1110
  await fs.ensureDir(assetsDir);
498
1111
 
499
- const { categories, diagrams, summary } = domainValidation;
1112
+ const { categories, diagrams, blueprints, summary } = domainValidation;
500
1113
  const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
501
1114
 
502
1115
  // ── 1. Write per-module .mmd files ──────────────────────────────────────
@@ -508,6 +1121,15 @@ async function writeDomainAssets(domainValidation, cwd) {
508
1121
  }
509
1122
  }
510
1123
 
1124
+ // ── 1b. Write per-module blueprint .mmd files ───────────────────────────
1125
+ if (blueprints) {
1126
+ for (const [moduleName, blueprintText] of Object.entries(blueprints)) {
1127
+ if (blueprintText) {
1128
+ await fs.writeFile(path.join(assetsDir, `${moduleName}-blueprint.mmd`), blueprintText, 'utf-8');
1129
+ }
1130
+ }
1131
+ }
1132
+
511
1133
  // ── 2. Build evaluation.md ──────────────────────────────────────────────
512
1134
 
513
1135
  // Collect all findings grouped by module
@@ -569,6 +1191,11 @@ async function writeDomainAssets(domainValidation, cwd) {
569
1191
 
570
1192
  if (diagrams && diagrams[moduleName]) {
571
1193
  lines.push(`> 📊 Diagram: [${moduleName}.mmd](./${moduleName}.mmd)`);
1194
+ }
1195
+ if (blueprints && blueprints[moduleName]) {
1196
+ lines.push(`> 🏗 Blueprint: [${moduleName}-blueprint.mmd](./${moduleName}-blueprint.mmd)`);
1197
+ }
1198
+ if ((diagrams && diagrams[moduleName]) || (blueprints && blueprints[moduleName])) {
572
1199
  lines.push('');
573
1200
  }
574
1201
 
@@ -595,16 +1222,76 @@ async function writeDomainAssets(domainValidation, cwd) {
595
1222
  lines.push(`## Clean Modules (no findings)`);
596
1223
  lines.push('');
597
1224
  for (const m of cleanModules) {
598
- lines.push(`- \`${m}\` 🟢 no findings · [${m}.mmd](./${m}.mmd)`);
1225
+ const links = [`[${m}.mmd](./${m}.mmd)`];
1226
+ if (blueprints && blueprints[m]) links.push(`[${m}-blueprint.mmd](./${m}-blueprint.mmd)`);
1227
+ lines.push(`- \`${m}\` — 🟢 no findings · ${links.join(' · ')}`);
599
1228
  }
600
1229
  lines.push('');
601
1230
  }
602
1231
  }
603
1232
 
1233
+ // ── 3. Temporal Validation section ────────────────────────────────────────
1234
+ if (temporalValidation) {
1235
+ const tv = temporalValidation.summary;
1236
+ const tvErrors = [], tvWarnings = [], tvInfos = [];
1237
+ for (const category of (temporalValidation.categories || [])) {
1238
+ for (const check of (category.checks || [])) {
1239
+ if (!check.findings || check.findings.length === 0) continue;
1240
+ const prefix = `**${check.id}** ${check.label}`;
1241
+ const messages = check.findings.map((f) => {
1242
+ const msg = typeof f === 'string' ? f : [f.module && `\`${f.module}\``, f.message, f.context && `*(${f.context})*`].filter(Boolean).join(' ');
1243
+ return `| ${prefix} | ${msg} |`;
1244
+ });
1245
+ if (check.severity === 'error') tvErrors.push(...messages);
1246
+ else if (check.severity === 'warning') tvWarnings.push(...messages);
1247
+ else tvInfos.push(...messages);
1248
+ }
1249
+ }
1250
+
1251
+ lines.push('---');
1252
+ lines.push('');
1253
+ lines.push('## ⏱️ Temporal Validation');
1254
+ lines.push('');
1255
+ lines.push(`| 🔴 Errors | 🟡 Warnings | 🔵 Info | 🟢 OK |`);
1256
+ lines.push(`|-----------|-------------|---------|-------|`);
1257
+ lines.push(`| ${tvErrors.length} | ${tvWarnings.length} | ${tvInfos.length} | ${tv.ok} |`);
1258
+ lines.push('');
1259
+
1260
+ if (tvErrors.length > 0) {
1261
+ lines.push('### 🔴 Temporal Errors');
1262
+ lines.push('');
1263
+ lines.push('| Check | Message |');
1264
+ lines.push('|-------|---------|');
1265
+ tvErrors.forEach((m) => lines.push(m));
1266
+ lines.push('');
1267
+ }
1268
+ if (tvWarnings.length > 0) {
1269
+ lines.push('### 🟡 Temporal Warnings');
1270
+ lines.push('');
1271
+ lines.push('| Check | Message |');
1272
+ lines.push('|-------|---------|');
1273
+ tvWarnings.forEach((m) => lines.push(m));
1274
+ lines.push('');
1275
+ }
1276
+ if (tvInfos.length > 0) {
1277
+ lines.push('### 🔵 Temporal Info');
1278
+ lines.push('');
1279
+ lines.push('| Check | Message |');
1280
+ lines.push('|-------|---------|');
1281
+ tvInfos.forEach((m) => lines.push(m));
1282
+ lines.push('');
1283
+ }
1284
+ if (tvErrors.length === 0 && tvWarnings.length === 0 && tvInfos.length === 0) {
1285
+ lines.push('_✅ All Temporal checks passed._');
1286
+ lines.push('');
1287
+ }
1288
+ }
1289
+
604
1290
  await fs.writeFile(path.join(assetsDir, 'evaluation.md'), lines.join('\n'), 'utf-8');
605
1291
 
606
1292
  const mmdFiles = diagrams ? Object.keys(diagrams).filter(m => diagrams[m]) : [];
607
- console.log(chalk.gray(` Domain assets assets/evaluation/ (evaluation.md + ${mmdFiles.length} .mmd files)`));
1293
+ const bpFiles = blueprints ? Object.keys(blueprints).filter(m => blueprints[m]) : [];
1294
+ console.log(chalk.gray(` Domain assets → assets/evaluation/ (evaluation.md + ${mmdFiles.length} .mmd + ${bpFiles.length} blueprint files)`));
608
1295
  }
609
1296
 
610
1297
  module.exports = evaluateSystemCommand;