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
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs-extra');
2
2
  const path = require('path');
3
+ const { toCamelCase } = require('./naming');
3
4
 
4
5
  class ConfigManager {
5
6
  constructor(projectPath = process.cwd()) {
@@ -88,8 +89,9 @@ class ConfigManager {
88
89
  async moduleExists(moduleName) {
89
90
  const config = await this.loadProjectConfig();
90
91
  if (!config) return false;
91
-
92
- return config.modules.some(module => module.name === moduleName);
92
+
93
+ const normalized = toCamelCase(moduleName);
94
+ return config.modules.some(module => module.name === normalized);
93
95
  }
94
96
 
95
97
  /**
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { pluralizeWord } = require('./naming');
3
+ const { pluralizeWord, singularizeWord, toPascalCase } = require('./naming');
4
4
 
5
5
  /**
6
6
  * Domain-level validator for eva evaluate system --domain
@@ -13,9 +13,10 @@ const { pluralizeWord } = require('./naming');
13
13
  *
14
14
  * Categories:
15
15
  * C1 — Kafka Event Contracts
16
- * C4 — Behavior Gaps
17
- * C5 — Cross-Reference Integrity
18
- * C6 — Audit & Traceability
16
+ * C2 — Behavior Gaps
17
+ * C3 — Cross-Reference Integrity
18
+ * C4 — Audit & Traceability
19
+ * C5 — Temporal Workflow Integrity
19
20
  */
20
21
 
21
22
  // ── Internal helpers ─────────────────────────────────────────────────────────
@@ -119,7 +120,10 @@ function buildSystemAsyncMap(systemConfig) {
119
120
  map[ev.event] = {
120
121
  producer: ev.producer,
121
122
  topic: ev.topic,
122
- consumers: (ev.consumers || []).map((c) => (typeof c === 'string' ? c : c.module)),
123
+ consumers: (ev.consumers || []).map((c) => {
124
+ if (typeof c === 'string') return { module: c, useCase: undefined, readModel: undefined };
125
+ return { module: c.module, useCase: c.useCase, readModel: c.readModel };
126
+ }),
123
127
  };
124
128
  }
125
129
  return map;
@@ -213,8 +217,9 @@ function runC1(domainConfigs, systemConfig) {
213
217
  'C1-002': { label: 'Listener referencia evento que ningún módulo produce', severity: 'ok', findings: [] },
214
218
  'C1-003': { label: 'Campo en listener.fields no existe en el evento del productor', severity: 'ok', findings: [] },
215
219
  'C1-004': { label: 'Campo existe pero con tipo incompatible productor/consumidor', severity: 'ok', findings: [] },
216
- 'C1-005': { label: 'system.yaml registra consumidor pero módulo no tiene listener declarado', severity: 'ok', findings: [] },
220
+ 'C1-005': { label: 'system.yaml registra consumidor pero módulo no tiene listener o readModel.syncedBy declarado', severity: 'ok', findings: [] },
217
221
  'C1-006': { label: 'Listener declara producer: incorrecto', severity: 'ok', findings: [] },
222
+ 'C1-007': { label: 'Campo de readModel no cubierto por eventos UPSERT del productor', severity: 'ok', findings: [] },
218
223
  };
219
224
 
220
225
  // C1-001: produced event in domain but zero consumers in system.yaml
@@ -277,20 +282,38 @@ function runC1(domainConfigs, systemConfig) {
277
282
  }
278
283
  }
279
284
 
280
- // C1-005: system.yaml consumer present but module has no listener
285
+ // C1-005: system.yaml consumer present but module has no listener or readModel.syncedBy
281
286
  for (const [eventName, sysEntry] of Object.entries(systemAsyncMap)) {
282
- for (const consumerModule of sysEntry.consumers) {
283
- const consumerConfig = domainConfigs[consumerModule];
287
+ for (const consumer of sysEntry.consumers) {
288
+ const consumerConfig = domainConfigs[consumer.module];
284
289
  if (!consumerConfig) continue; // module domain.yaml not loaded — skip
285
- const hasListener = (consumerConfig.listeners || []).some((l) => l.event === eventName);
286
- if (!hasListener) {
287
- checks['C1-005'].findings.push(
288
- finding(
289
- consumerModule,
290
- `system.yaml registra '${consumerModule}' como consumidor de '${eventName}' pero el módulo no tiene listener declarado`,
291
- `Evento producido por: ${sysEntry.producer}`
292
- )
290
+
291
+ if (consumer.readModel) {
292
+ // readModel consumer → check readModels[].syncedBy[]
293
+ const hasSync = (consumerConfig.readModels || []).some((rm) =>
294
+ (rm.syncedBy || []).some((s) => s.event === eventName)
293
295
  );
296
+ if (!hasSync) {
297
+ checks['C1-005'].findings.push(
298
+ finding(
299
+ consumer.module,
300
+ `system.yaml registra '${consumer.module}' como consumidor readModel de '${eventName}' pero el módulo no tiene readModels[].syncedBy con ese evento`,
301
+ `Evento producido por: ${sysEntry.producer}, readModel esperado: ${consumer.readModel}`
302
+ )
303
+ );
304
+ }
305
+ } else {
306
+ // useCase consumer → check listeners[]
307
+ const hasListener = (consumerConfig.listeners || []).some((l) => l.event === eventName);
308
+ if (!hasListener) {
309
+ checks['C1-005'].findings.push(
310
+ finding(
311
+ consumer.module,
312
+ `system.yaml registra '${consumer.module}' como consumidor de '${eventName}' pero el módulo no tiene listener declarado`,
313
+ `Evento producido por: ${sysEntry.producer}`
314
+ )
315
+ );
316
+ }
294
317
  }
295
318
  }
296
319
  }
@@ -312,6 +335,35 @@ function runC1(domainConfigs, systemConfig) {
312
335
  }
313
336
  }
314
337
 
338
+ // C1-007: readModel field not covered by any UPSERT event from producer (RM-008)
339
+ for (const [moduleName, config] of Object.entries(domainConfigs)) {
340
+ for (const rm of config.readModels || []) {
341
+ // Collect field names from all UPSERT syncedBy events
342
+ const upsertEventFields = new Set();
343
+ for (const sync of rm.syncedBy || []) {
344
+ if ((sync.action || '').toUpperCase() !== 'UPSERT') continue;
345
+ const producerInfo = producedEvents[sync.event];
346
+ if (!producerInfo) continue; // caught by other checks
347
+ for (const f of producerInfo.fields) {
348
+ upsertEventFields.add(f.name);
349
+ }
350
+ }
351
+ // Check each readModel field (except 'id') is covered
352
+ for (const rmField of rm.fields || []) {
353
+ if (rmField.name === 'id') continue; // mapped to {entityName}Id in events
354
+ if (!upsertEventFields.has(rmField.name)) {
355
+ checks['C1-007'].findings.push(
356
+ finding(
357
+ moduleName,
358
+ `ReadModel '${rm.name}' tiene campo '${rmField.name}' que no aparece en ningún evento UPSERT de syncedBy`,
359
+ `Source: ${rm.source ? rm.source.module : '?'}. El campo siempre será null — agregar a los events del productor o quitar del readModel`
360
+ )
361
+ );
362
+ }
363
+ }
364
+ }
365
+ }
366
+
315
367
  // Assign severities
316
368
  setDefaultSeverities(checks, {
317
369
  'C1-001': 'info',
@@ -320,6 +372,7 @@ function runC1(domainConfigs, systemConfig) {
320
372
  'C1-004': 'error',
321
373
  'C1-005': 'error',
322
374
  'C1-006': 'error',
375
+ 'C1-007': 'warning',
323
376
  });
324
377
 
325
378
  return checks;
@@ -336,6 +389,11 @@ function runC2(domainConfigs, systemConfig) {
336
389
  'C2-005': { label: 'Transición de estado sin Domain Event asociado (sin trigger)', severity: 'ok', findings: [] },
337
390
  'C2-006': { label: 'Colisión de nombre de useCase entre endpoints y listeners', severity: 'ok', findings: [] },
338
391
  'C2-007': { label: 'UseCase FindAll con nombre de agregado sin pluralizar correctamente', severity: 'ok', findings: [] },
392
+ 'C2-008': { label: 'Evento con valor de lifecycle inválido', severity: 'ok', findings: [] },
393
+ 'C2-009': { label: 'Evento lifecycle incompatible con configuración de entidad', severity: 'ok', findings: [] },
394
+ 'C2-010': { label: 'Campo de lifecycle event no existe en la entidad raíz', severity: 'ok', findings: [] },
395
+ 'C2-011': { label: 'Endpoint useCase no se resuelve a ningún agregado del módulo', severity: 'ok', findings: [] },
396
+ 'C2-012': { label: 'Nombre del agregado no coincide con la entidad raíz (causa import incorrecto en ApplicationMapper)', severity: 'ok', findings: [] },
339
397
  };
340
398
 
341
399
  for (const [moduleName, config] of Object.entries(domainConfigs)) {
@@ -405,6 +463,7 @@ function runC2(domainConfigs, systemConfig) {
405
463
  }
406
464
 
407
465
  // C2-004: event trigger references a method that does not exist in any transition
466
+ // Skipped for events that use lifecycle: instead of triggers:
408
467
  const allTransitionMethods = new Set();
409
468
  for (const agg of config.aggregates || []) {
410
469
  for (const en of agg.enums || []) {
@@ -414,7 +473,14 @@ function runC2(domainConfigs, systemConfig) {
414
473
  }
415
474
  }
416
475
  for (const agg of config.aggregates || []) {
476
+ // Skip C2-004 for aggregates that have no enums — stateless entities have no transition
477
+ // methods, so event triggers on creation/registration cannot reference any method.
478
+ const aggHasEnumsWithTransitions = (agg.enums || []).some(
479
+ (en) => Array.isArray(en.transitions) && en.transitions.length > 0
480
+ );
417
481
  for (const ev of agg.events || []) {
482
+ if (ev.lifecycle) continue; // lifecycle events don't reference transition methods
483
+ if (!aggHasEnumsWithTransitions) continue; // stateless aggregate — no transitions to reference
418
484
  for (const trigger of ev.triggers || []) {
419
485
  if (!allTransitionMethods.has(trigger)) {
420
486
  checks['C2-004'].findings.push(
@@ -429,6 +495,74 @@ function runC2(domainConfigs, systemConfig) {
429
495
  }
430
496
  }
431
497
 
498
+ // C2-008: event lifecycle value is not one of the valid options
499
+ const validLifecycleValues = ['create', 'update', 'delete', 'softDelete'];
500
+ for (const agg of config.aggregates || []) {
501
+ for (const ev of agg.events || []) {
502
+ if (ev.lifecycle && !validLifecycleValues.includes(ev.lifecycle)) {
503
+ checks['C2-008'].findings.push(
504
+ finding(
505
+ moduleName,
506
+ `Evento '${ev.name}' tiene lifecycle: '${ev.lifecycle}' que no es un valor válido`,
507
+ `Valores válidos: ${validLifecycleValues.join(', ')}`
508
+ )
509
+ );
510
+ }
511
+ }
512
+ }
513
+
514
+ // C2-009: lifecycle value is incompatible with entity configuration
515
+ for (const agg of config.aggregates || []) {
516
+ const rootEntity = (agg.entities || []).find(e => e.isRoot);
517
+ const hasSoftDelete = rootEntity && rootEntity.hasSoftDelete;
518
+ for (const ev of agg.events || []) {
519
+ if (ev.lifecycle === 'softDelete' && !hasSoftDelete) {
520
+ checks['C2-009'].findings.push(
521
+ finding(
522
+ moduleName,
523
+ `Evento '${ev.name}' tiene lifecycle: 'softDelete' pero la entidad raíz '${rootEntity ? rootEntity.name : agg.name}' no tiene hasSoftDelete: true`,
524
+ `Agregado: ${agg.name}. Agregar hasSoftDelete: true a la entidad raíz o cambiar lifecycle a 'delete'`
525
+ )
526
+ );
527
+ }
528
+ if (ev.lifecycle === 'delete' && hasSoftDelete) {
529
+ checks['C2-009'].findings.push(
530
+ finding(
531
+ moduleName,
532
+ `Evento '${ev.name}' tiene lifecycle: 'delete' pero la entidad raíz '${rootEntity.name}' tiene hasSoftDelete: true`,
533
+ `Agregado: ${agg.name}. Usar lifecycle: 'softDelete' en su lugar o quitar hasSoftDelete`
534
+ )
535
+ );
536
+ }
537
+ }
538
+ }
539
+
540
+ // C2-010: lifecycle event field not found in root entity
541
+ for (const agg of config.aggregates || []) {
542
+ const rootEntityC10 = (agg.entities || []).find(e => e.isRoot);
543
+ if (!rootEntityC10) continue;
544
+ const entityFieldNames = new Set((rootEntityC10.fields || []).map(f => f.name));
545
+ const entityBase = rootEntityC10.name.charAt(0).toLowerCase() + rootEntityC10.name.slice(1);
546
+ for (const ev of agg.events || []) {
547
+ if (!ev.lifecycle) continue;
548
+ for (const ef of ev.fields || []) {
549
+ // Skip {entityName}Id — mapped to aggregateId in DomainEvent
550
+ if (ef.name === entityBase + 'Id') continue;
551
+ // Skip temporal auto-resolved fields (*At + LocalDateTime)
552
+ if (ef.name.endsWith('At') && ef.type === 'LocalDateTime') continue;
553
+ if (!entityFieldNames.has(ef.name)) {
554
+ checks['C2-010'].findings.push(
555
+ finding(
556
+ moduleName,
557
+ `Evento '${ev.name}' (lifecycle: ${ev.lifecycle}) tiene campo '${ef.name}' que no existe en la entidad raíz '${rootEntityC10.name}'`,
558
+ `Agregado: ${agg.name}. Quitar '${ef.name}' del evento o agregar el campo a la entidad`
559
+ )
560
+ );
561
+ }
562
+ }
563
+ }
564
+ }
565
+
432
566
  // C2-005: transition method without any associated domain event trigger
433
567
  for (const agg of config.aggregates || []) {
434
568
  for (const en of agg.enums || []) {
@@ -566,6 +700,108 @@ function runC2(domainConfigs, systemConfig) {
566
700
  }
567
701
  }
568
702
  }
703
+
704
+ // C2-011: Endpoint useCase not semantically resolvable to any aggregate
705
+ // Detects FindAll/Get use cases that won't match any aggregate via exact
706
+ // or fuzzy matching — these silently fall to scaffold with the wrong
707
+ // aggregate's ResponseDto, producing code that doesn't compile.
708
+ const allAggs = config.aggregates || [];
709
+ for (const ver of (config.endpoints && config.endpoints.versions) || []) {
710
+ for (const op of ver.operations || []) {
711
+ const uc = op.useCase || '';
712
+ let resolved = false;
713
+
714
+ for (const agg of allAggs) {
715
+ const aggName = agg.name;
716
+ const aggPlural = pluralizeWord(aggName);
717
+ // Exact standard match
718
+ if (uc === `Create${aggName}` || uc === `Update${aggName}` ||
719
+ uc === `Delete${aggName}` || uc === `Get${aggName}` ||
720
+ uc === `FindAll${aggPlural}`) {
721
+ resolved = true;
722
+ break;
723
+ }
724
+ // Fuzzy FindAll: singular of suffix is prefix of aggregate name (or vice-versa)
725
+ if (uc.startsWith('FindAll')) {
726
+ const suffix = uc.slice(7);
727
+ if (suffix) {
728
+ const singular = singularizeWord(suffix).toLowerCase();
729
+ const aggLower = aggName.toLowerCase();
730
+ if (aggLower.startsWith(singular) || singular.startsWith(aggLower)) {
731
+ resolved = true;
732
+ break;
733
+ }
734
+ }
735
+ }
736
+ // Fuzzy Get: suffix is prefix of aggregate name (or vice-versa)
737
+ if (uc.startsWith('Get') && !uc.startsWith('GetAll')) {
738
+ const suffix = uc.slice(3);
739
+ if (suffix) {
740
+ const suffixLower = suffix.toLowerCase();
741
+ const aggLower = aggName.toLowerCase();
742
+ if (aggLower.startsWith(suffixLower) || suffixLower.startsWith(aggLower)) {
743
+ resolved = true;
744
+ break;
745
+ }
746
+ }
747
+ }
748
+ // Transition match
749
+ const entities = agg.entities || [];
750
+ const rootEntity = entities.find(e => e.isRoot) || entities[0] || {};
751
+ const enums = rootEntity.enums || agg.enums || [];
752
+ for (const enumDef of enums) {
753
+ for (const tr of (enumDef.transitions || [])) {
754
+ const methodPascal = tr.method.charAt(0).toUpperCase() + tr.method.slice(1);
755
+ if (uc === `${methodPascal}${aggName}`) {
756
+ resolved = true;
757
+ }
758
+ }
759
+ }
760
+ if (resolved) break;
761
+ // SubEntity match
762
+ const rels = (rootEntity.relationships || []).filter(r => r.type === 'OneToMany' && !r.isInverse);
763
+ for (const rel of rels) {
764
+ if (uc === `Add${rel.target}` || uc === `Remove${rel.target}`) {
765
+ resolved = true;
766
+ break;
767
+ }
768
+ }
769
+ if (resolved) break;
770
+ // Substring fallback: aggregate name inside useCase
771
+ if (uc.toLowerCase().includes(aggName.toLowerCase())) {
772
+ resolved = true;
773
+ break;
774
+ }
775
+ }
776
+
777
+ if (!resolved) {
778
+ const firstAgg = allAggs.length > 0 ? allAggs[0].name : '(none)';
779
+ checks['C2-011'].findings.push(
780
+ finding(
781
+ moduleName,
782
+ `UseCase '${uc}' no se resuelve a ningún agregado del módulo — se asignará al primero ('${firstAgg}') y generará código con tipos incorrectos`,
783
+ `Versión: ${ver.version}. Considere renombrar el useCase para que contenga el nombre del agregado, o verificar que el agregado destino existe.`
784
+ )
785
+ );
786
+ }
787
+ }
788
+ }
789
+ }
790
+
791
+ // C2-012: Aggregate name ≠ root entity name → ApplicationMapper imports wrong class
792
+ for (const [moduleName, config] of Object.entries(domainConfigs)) {
793
+ for (const agg of config.aggregates || []) {
794
+ const rootEntity = (agg.entities || []).find(e => e.isRoot);
795
+ if (rootEntity && toPascalCase(rootEntity.name) !== agg.name) {
796
+ checks['C2-012'].findings.push(
797
+ finding(
798
+ moduleName,
799
+ `Agregado '${agg.name}' tiene entidad raíz '${rootEntity.name}' (PascalCase: '${toPascalCase(rootEntity.name)}') — los nombres no coinciden. El generador usará '${agg.name}' para imports y mappers pero la clase de dominio se llamará '${toPascalCase(rootEntity.name)}'`,
800
+ `Renombre la entidad raíz a '${agg.name.charAt(0).toLowerCase() + agg.name.slice(1)}' o el agregado a '${toPascalCase(rootEntity.name)}' para que coincidan.`
801
+ )
802
+ );
803
+ }
804
+ }
569
805
  }
570
806
 
571
807
  setDefaultSeverities(checks, {
@@ -576,6 +812,11 @@ function runC2(domainConfigs, systemConfig) {
576
812
  'C2-005': 'info',
577
813
  'C2-006': 'error',
578
814
  'C2-007': 'error',
815
+ 'C2-008': 'error',
816
+ 'C2-009': 'warning',
817
+ 'C2-010': 'error',
818
+ 'C2-011': 'error',
819
+ 'C2-012': 'error',
579
820
  });
580
821
 
581
822
  return checks;
@@ -935,6 +1176,225 @@ function runC4(domainConfigs, systemConfig) {
935
1176
  return checks;
936
1177
  }
937
1178
 
1179
+ // ─── C5 — Temporal Workflow Integrity ────────────────────────────────────────
1180
+
1181
+ function runC5(domainConfigs, systemConfig) {
1182
+ const checks = {
1183
+ 'C5-001': { label: 'Tipo de input de actividad de compensación incompatible con actividad padre', severity: 'ok', findings: [] },
1184
+ 'C5-002': { label: 'Step de workflow referencia actividad no declarada en módulo destino', severity: 'ok', findings: [] },
1185
+ 'C5-003': { label: 'Compensación de workflow referencia actividad no declarada en módulo destino', severity: 'ok', findings: [] },
1186
+ 'C5-004': { label: 'Tipo de input en step incompatible con el output del step que lo provee', severity: 'ok', findings: [] },
1187
+ };
1188
+
1189
+ const workflows = systemConfig.workflows || [];
1190
+ if (workflows.length === 0) return checks;
1191
+
1192
+ // Build map: moduleName → { activityName → activityDef }
1193
+ const moduleActivities = {};
1194
+ for (const [moduleName, config] of Object.entries(domainConfigs)) {
1195
+ const acts = {};
1196
+ for (const act of config.activities || []) {
1197
+ acts[act.name] = act;
1198
+ }
1199
+ moduleActivities[moduleName] = acts;
1200
+ }
1201
+
1202
+ for (const wf of workflows) {
1203
+ for (const step of wf.steps || []) {
1204
+ const target = step.target;
1205
+ const acts = moduleActivities[target] || {};
1206
+
1207
+ // C5-002: activity not found in target module
1208
+ if (step.activity && !acts[step.activity]) {
1209
+ checks['C5-002'].findings.push(
1210
+ finding(
1211
+ target,
1212
+ `Workflow '${wf.name}' step '${step.activity}' no se encuentra en activities de '${target}'`,
1213
+ `Declarar la actividad '${step.activity}' en ${target}.yaml activities[]`
1214
+ )
1215
+ );
1216
+ }
1217
+
1218
+ // C5-003: compensation activity not found in target module
1219
+ if (step.compensation && !acts[step.compensation]) {
1220
+ checks['C5-003'].findings.push(
1221
+ finding(
1222
+ target,
1223
+ `Workflow '${wf.name}' compensación '${step.compensation}' no se encuentra en activities de '${target}'`,
1224
+ `Declarar la actividad '${step.compensation}' en ${target}.yaml activities[]`
1225
+ )
1226
+ );
1227
+ }
1228
+
1229
+ // C5-001: compensation input type mismatch with parent activity
1230
+ if (step.activity && step.compensation && acts[step.activity] && acts[step.compensation]) {
1231
+ const parentAct = acts[step.activity];
1232
+ const compAct = acts[step.compensation];
1233
+ const parentInputs = parentAct.input || [];
1234
+ const compInputs = compAct.input || [];
1235
+
1236
+ // Compare each input field by position and type
1237
+ const maxLen = Math.max(parentInputs.length, compInputs.length);
1238
+ for (let i = 0; i < maxLen; i++) {
1239
+ const pField = parentInputs[i];
1240
+ const cField = compInputs[i];
1241
+
1242
+ if (!pField || !cField) {
1243
+ // Different number of input fields
1244
+ checks['C5-001'].findings.push(
1245
+ finding(
1246
+ target,
1247
+ `Workflow '${wf.name}': '${step.compensation}' tiene ${compInputs.length} campo(s) de input pero '${step.activity}' tiene ${parentInputs.length}`,
1248
+ `La compensación recibe el mismo input que la actividad padre — deben coincidir en cantidad y tipos`
1249
+ )
1250
+ );
1251
+ break; // report once per pair
1252
+ }
1253
+
1254
+ const pType = normalizeType(pField.type);
1255
+ const cType = normalizeType(cField.type);
1256
+ if (pType !== cType && !typesCompatible(pField.type, cField.type)) {
1257
+ checks['C5-001'].findings.push(
1258
+ finding(
1259
+ target,
1260
+ `Workflow '${wf.name}': input '${cField.name}' de compensación '${step.compensation}' es '${cField.type}' pero la actividad padre '${step.activity}' usa '${pField.type}'`,
1261
+ `Campo '${pField.name}' (pos ${i}). La compensación recibe el mismo input en runtime — usar un tipo neutral compartido (ej: nestedType con los campos mínimos necesarios)`
1262
+ )
1263
+ );
1264
+ }
1265
+ }
1266
+ }
1267
+ }
1268
+ }
1269
+
1270
+ // C5-004: workflow data-flow type mismatch
1271
+ // Trace variable types through the step chain: each step's output feeds the pool,
1272
+ // each subsequent step's input is checked against the pool types.
1273
+ for (const wf of workflows) {
1274
+ const pool = {}; // varName → { type, producerStep }
1275
+
1276
+ for (const step of wf.steps || []) {
1277
+ if (!step.activity) continue; // skip non-activity steps (e.g. wait)
1278
+ const target = step.target;
1279
+ const acts = moduleActivities[target] || {};
1280
+ const actDef = acts[step.activity];
1281
+ if (!actDef) continue; // caught by C5-002
1282
+
1283
+ const actInputs = actDef.input || [];
1284
+ const stepInputs = step.input || [];
1285
+
1286
+ // Check each step input against pool (positional: step.input[i] → activity.input[i])
1287
+ for (let i = 0; i < stepInputs.length && i < actInputs.length; i++) {
1288
+ const varName = stepInputs[i];
1289
+ const poolEntry = pool[varName];
1290
+ if (!poolEntry) continue; // workflow-level param with no prior producer — skip
1291
+
1292
+ const expectedType = actInputs[i].type;
1293
+ if (!expectedType) continue;
1294
+
1295
+ const poolType = normalizeType(poolEntry.type);
1296
+ const expType = normalizeType(expectedType);
1297
+ if (poolType !== expType && !typesCompatible(poolEntry.type, expectedType)) {
1298
+ checks['C5-004'].findings.push(
1299
+ finding(
1300
+ target,
1301
+ `Workflow '${wf.name}': step '${step.activity}' espera '${varName}' como '${expectedType}' pero '${poolEntry.producerStep}' lo produce como '${poolEntry.type}'`,
1302
+ `Variable '${varName}' fluye de '${poolEntry.producerStep}' → '${step.activity}'. Los tipos deben coincidir — agregar un campo de proyección con el tipo correcto en el output del productor`
1303
+ )
1304
+ );
1305
+ }
1306
+ }
1307
+
1308
+ // Register step outputs in pool (positional: step.output[i] → activity.output[i])
1309
+ const actOutputs = actDef.output || [];
1310
+ const stepOutputs = step.output || [];
1311
+ for (let i = 0; i < stepOutputs.length && i < actOutputs.length; i++) {
1312
+ pool[stepOutputs[i]] = {
1313
+ type: actOutputs[i].type,
1314
+ producerStep: step.activity,
1315
+ };
1316
+ }
1317
+ }
1318
+ }
1319
+
1320
+ // Also validate domain-level compensation references (activity.compensation within same module)
1321
+ for (const [moduleName, config] of Object.entries(domainConfigs)) {
1322
+ const acts = moduleActivities[moduleName] || {};
1323
+ for (const act of config.activities || []) {
1324
+ if (!act.compensation) continue;
1325
+ const compAct = acts[act.compensation];
1326
+
1327
+ if (!compAct) {
1328
+ // Compensation activity not found — only report if not already caught by C5-003
1329
+ const alreadyCaught = checks['C5-003'].findings.some(
1330
+ (f) => f.module === moduleName && f.message.includes(`'${act.compensation}'`)
1331
+ );
1332
+ if (!alreadyCaught) {
1333
+ checks['C5-003'].findings.push(
1334
+ finding(
1335
+ moduleName,
1336
+ `Actividad '${act.name}' declara compensation: '${act.compensation}' pero no existe en activities de '${moduleName}'`,
1337
+ `Declarar la actividad '${act.compensation}' en ${moduleName}.yaml activities[]`
1338
+ )
1339
+ );
1340
+ }
1341
+ continue;
1342
+ }
1343
+
1344
+ // Type mismatch check at domain level
1345
+ const parentInputs = act.input || [];
1346
+ const compInputs = compAct.input || [];
1347
+ const maxLen = Math.max(parentInputs.length, compInputs.length);
1348
+ for (let i = 0; i < maxLen; i++) {
1349
+ const pField = parentInputs[i];
1350
+ const cField = compInputs[i];
1351
+
1352
+ if (!pField || !cField) {
1353
+ const alreadyCaught = checks['C5-001'].findings.some(
1354
+ (f) => f.module === moduleName && f.message.includes(`'${act.compensation}'`) && f.message.includes(`'${act.name}'`)
1355
+ );
1356
+ if (!alreadyCaught) {
1357
+ checks['C5-001'].findings.push(
1358
+ finding(
1359
+ moduleName,
1360
+ `Actividad '${act.compensation}' tiene ${compInputs.length} campo(s) de input pero '${act.name}' tiene ${parentInputs.length}`,
1361
+ `La compensación recibe el mismo input que la actividad padre — deben coincidir en cantidad y tipos`
1362
+ )
1363
+ );
1364
+ }
1365
+ break;
1366
+ }
1367
+
1368
+ const pType = normalizeType(pField.type);
1369
+ const cType = normalizeType(cField.type);
1370
+ if (pType !== cType && !typesCompatible(pField.type, cField.type)) {
1371
+ const alreadyCaught = checks['C5-001'].findings.some(
1372
+ (f) => f.module === moduleName && f.message.includes(`'${cField.name}'`) && f.message.includes(`'${act.compensation}'`)
1373
+ );
1374
+ if (!alreadyCaught) {
1375
+ checks['C5-001'].findings.push(
1376
+ finding(
1377
+ moduleName,
1378
+ `Input '${cField.name}' de compensación '${act.compensation}' es '${cField.type}' pero la actividad padre '${act.name}' usa '${pField.type}'`,
1379
+ `Campo '${pField.name}' (pos ${i}). La compensación recibe el mismo input en runtime — usar un tipo neutral compartido (ej: nestedType con los campos mínimos necesarios)`
1380
+ )
1381
+ );
1382
+ }
1383
+ }
1384
+ }
1385
+ }
1386
+ }
1387
+
1388
+ setDefaultSeverities(checks, {
1389
+ 'C5-001': 'error',
1390
+ 'C5-002': 'error',
1391
+ 'C5-003': 'error',
1392
+ 'C5-004': 'error',
1393
+ });
1394
+
1395
+ return checks;
1396
+ }
1397
+
938
1398
  // ── Severity finalization ────────────────────────────────────────────────────
939
1399
 
940
1400
  /**
@@ -960,6 +1420,7 @@ function validateDomain(domainConfigs, systemConfig) {
960
1420
  const c2Checks = runC2(domainConfigs, systemConfig);
961
1421
  const c3Checks = runC3(domainConfigs, systemConfig);
962
1422
  const c4Checks = runC4(domainConfigs, systemConfig);
1423
+ const c5Checks = runC5(domainConfigs, systemConfig);
963
1424
 
964
1425
  const categories = [
965
1426
  {
@@ -986,6 +1447,12 @@ function validateDomain(domainConfigs, systemConfig) {
986
1447
  description: 'Verifica que las entidades críticas tengan mecanismos de trazabilidad de cambios.',
987
1448
  checks: checksToArray(c4Checks),
988
1449
  },
1450
+ {
1451
+ id: 'C5',
1452
+ label: 'Integridad de Workflows Temporal',
1453
+ description: 'Verifica que las actividades, compensaciones y contratos de tipos en workflows Temporal sean coherentes.',
1454
+ checks: checksToArray(c5Checks),
1455
+ },
989
1456
  ];
990
1457
 
991
1458
  // Compute summary
@@ -1000,11 +1467,22 @@ function validateDomain(domainConfigs, systemConfig) {
1000
1467
  }
1001
1468
 
1002
1469
  const { generateDomainDiagrams } = require('./domain-diagram');
1470
+ const { generateBlueprintDiagrams } = require('./bounded-context-diagram');
1471
+
1472
+ const blueprintResults = generateBlueprintDiagrams(domainConfigs, systemConfig);
1473
+ const blueprintDiagrams = {};
1474
+ const useCaseDetails = {};
1475
+ for (const [mod, result] of Object.entries(blueprintResults)) {
1476
+ blueprintDiagrams[mod] = result.diagram || '';
1477
+ useCaseDetails[mod] = result.useCases || {};
1478
+ }
1003
1479
 
1004
1480
  return {
1005
1481
  summary: { errors, warnings, info, ok },
1006
1482
  categories,
1007
1483
  diagrams: generateDomainDiagrams(domainConfigs),
1484
+ blueprints: blueprintDiagrams,
1485
+ useCaseDetails,
1008
1486
  };
1009
1487
  }
1010
1488