eva4j 1.0.13 → 1.0.15

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 (106) hide show
  1. package/AGENTS.md +314 -10
  2. package/COMMAND_EVALUATION.md +15 -16
  3. package/DOMAIN_YAML_GUIDE.md +576 -10
  4. package/FUTURE_FEATURES.md +1627 -1168
  5. package/README.md +318 -13
  6. package/bin/eva4j.js +34 -0
  7. package/config/defaults.json +1 -0
  8. package/design-system.md +797 -0
  9. package/docs/commands/EVALUATE_SYSTEM.md +994 -0
  10. package/docs/commands/GENERATE_ENTITIES.md +795 -6
  11. package/docs/commands/INDEX.md +10 -1
  12. package/examples/domain-endpoints-relations.yaml +353 -0
  13. package/examples/domain-endpoints-versioned.yaml +144 -0
  14. package/examples/domain-endpoints.yaml +135 -0
  15. package/examples/domain-events.yaml +166 -20
  16. package/examples/domain-listeners.yaml +212 -0
  17. package/examples/domain-one-to-many.yaml +1 -0
  18. package/examples/domain-one-to-one.yaml +1 -0
  19. package/examples/domain-ports.yaml +414 -0
  20. package/examples/domain-soft-delete.yaml +47 -44
  21. package/examples/system/notification.yaml +147 -0
  22. package/examples/system/product.yaml +185 -0
  23. package/examples/system/system.yaml +112 -0
  24. package/examples/system-report.html +971 -0
  25. package/examples/system.yaml +332 -0
  26. package/package.json +2 -1
  27. package/src/commands/build.js +714 -0
  28. package/src/commands/create.js +7 -3
  29. package/src/commands/detach.js +1 -0
  30. package/src/commands/evaluate-system.js +610 -0
  31. package/src/commands/generate-entities.js +1331 -49
  32. package/src/commands/generate-http-exchange.js +2 -0
  33. package/src/commands/generate-kafka-event.js +98 -11
  34. package/src/generators/base-generator.js +8 -1
  35. package/src/generators/postman-generator.js +188 -0
  36. package/src/generators/shared-generator.js +10 -0
  37. package/src/utils/config-manager.js +54 -0
  38. package/src/utils/context-builder.js +1 -0
  39. package/src/utils/domain-diagram.js +192 -0
  40. package/src/utils/domain-validator.js +970 -0
  41. package/src/utils/fake-data.js +376 -0
  42. package/src/utils/naming.js +3 -2
  43. package/src/utils/system-validator.js +434 -0
  44. package/src/utils/yaml-to-entity.js +302 -8
  45. package/templates/aggregate/AggregateMapper.java.ejs +3 -2
  46. package/templates/aggregate/AggregateRepository.java.ejs +8 -2
  47. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +13 -3
  48. package/templates/aggregate/AggregateRoot.java.ejs +60 -2
  49. package/templates/aggregate/DomainEventHandler.java.ejs +27 -20
  50. package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
  51. package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
  52. package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
  53. package/templates/aggregate/JpaRepository.java.ejs +5 -0
  54. package/templates/base/gradle/build.gradle.ejs +3 -2
  55. package/templates/base/root/AGENTS.md.ejs +306 -45
  56. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1663 -0
  57. package/templates/base/root/skill-build-system-yaml.ejs +1446 -0
  58. package/templates/base/root/system.yaml.ejs +97 -0
  59. package/templates/crud/ApplicationMapper.java.ejs +4 -0
  60. package/templates/crud/Controller.java.ejs +4 -4
  61. package/templates/crud/CreateCommand.java.ejs +4 -0
  62. package/templates/crud/CreateItemDto.java.ejs +4 -0
  63. package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
  64. package/templates/crud/DeleteCommandHandler.java.ejs +10 -2
  65. package/templates/crud/EndpointsController.java.ejs +178 -0
  66. package/templates/crud/FindByQuery.java.ejs +17 -0
  67. package/templates/crud/FindByQueryHandler.java.ejs +57 -0
  68. package/templates/crud/ListQuery.java.ejs +1 -1
  69. package/templates/crud/ListQueryHandler.java.ejs +8 -8
  70. package/templates/crud/ScaffoldCommand.java.ejs +12 -0
  71. package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
  72. package/templates/crud/ScaffoldQuery.java.ejs +13 -0
  73. package/templates/crud/ScaffoldQueryHandler.java.ejs +41 -0
  74. package/templates/crud/SubEntityAddCommand.java.ejs +21 -0
  75. package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
  76. package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
  77. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
  78. package/templates/crud/TransitionCommand.java.ejs +9 -0
  79. package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
  80. package/templates/crud/UpdateCommand.java.ejs +4 -0
  81. package/templates/evaluate/report.html.ejs +1363 -0
  82. package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
  83. package/templates/kafka-event/Event.java.ejs +16 -0
  84. package/templates/kafka-listener/KafkaController.java.ejs +1 -1
  85. package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
  86. package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
  87. package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
  88. package/templates/kafka-listener/ListenerCommandHandler.java.ejs +23 -0
  89. package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
  90. package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
  91. package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
  92. package/templates/mock/MockEvent.java.ejs +10 -0
  93. package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
  94. package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
  95. package/templates/mock/SpringEventListener.java.ejs +61 -0
  96. package/templates/ports/PortDomainModel.java.ejs +35 -0
  97. package/templates/ports/PortFeignAdapter.java.ejs +67 -0
  98. package/templates/ports/PortFeignClient.java.ejs +45 -0
  99. package/templates/ports/PortFeignConfig.java.ejs +24 -0
  100. package/templates/ports/PortInterface.java.ejs +45 -0
  101. package/templates/ports/PortNestedType.java.ejs +28 -0
  102. package/templates/ports/PortRequestDto.java.ejs +30 -0
  103. package/templates/ports/PortResponseDto.java.ejs +28 -0
  104. package/templates/postman/Collection.json.ejs +1 -1
  105. package/templates/postman/UnifiedCollection.json.ejs +185 -0
  106. package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
package/AGENTS.md CHANGED
@@ -457,9 +457,17 @@ aggregates:
457
457
  fields:
458
458
  - name: userId
459
459
  type: String
460
- # kafka: true # opcional publica a Kafka tras commit
460
+ # Nota: el flag kafka: true ya no es necesario.
461
+ # Si el proyecto tiene un broker instalado (eva add kafka-client),
462
+ # eva g entities cablea automáticamente todos los eventos declarados.
461
463
  ```
462
464
 
465
+ El `domain.yaml` también soporta una sección `endpoints:` opcional (sibling de `aggregates:`) para declarar los endpoints REST. Ver sección [⚡ Características Avanzadas](#-características-avanzadas-del-domainyaml) para detalles.
466
+
467
+ El `domain.yaml` también soporta una sección `listeners:` opcional (sibling de `aggregates:`) para declarar los eventos externos que **consume** este módulo. Ver sección [⚡ Características Avanzadas](#-características-avanzadas-del-domainyaml) para detalles.
468
+
469
+ El `domain.yaml` también soporta una sección `ports:` opcional (sibling de `aggregates:`) para declarar los servicios HTTP síncronos que **llama** este módulo. Ver sección [⚡ Características Avanzadas](#-características-avanzadas-del-domainyaml) para detalles.
470
+
463
471
  ---
464
472
 
465
473
  ## ⚡ Características Avanzadas del domain.yaml
@@ -522,30 +530,298 @@ Genera automáticamente **en el enum**: `VALID_TRANSITIONS`, `canTransitionTo()`
522
530
  aggregates:
523
531
  - name: Order
524
532
  entities: [...]
533
+ enums:
534
+ - name: OrderStatus
535
+ transitions:
536
+ - from: DRAFT
537
+ to: PLACED
538
+ method: place
539
+ - from: PLACED
540
+ to: CANCELLED
541
+ method: cancel
525
542
  events:
526
- - name: OrderConfirmedEvent
543
+ - name: OrderPlaced
544
+ topic: ORDER_PLACED # opcional: sobreescribe el topic auto-derivado
545
+ triggers:
546
+ - place # ← conecta la transición con este evento
527
547
  fields:
528
548
  - name: orderId
529
549
  type: String
530
550
  - name: confirmedAt
531
551
  type: LocalDateTime
532
- kafka: true # opcional — genera publicación a MessageBroker
552
+
553
+ - name: OrderCancelled
554
+ triggers:
555
+ - cancel
556
+ fields:
557
+ - name: reason # campo no resuelto → null /* TODO: provide reason */
558
+ type: String
559
+ ```
560
+
561
+ #### Propiedad `topic` (opcional)
562
+
563
+ Sobreescribe el nombre del topic Kafka auto-derivado para este evento.
564
+
565
+ **Regla de derivación por defecto:** el generador quita el sufijo `Event` del nombre de la clase antes de convertir a SCREAMING_SNAKE_CASE:
566
+ - `ProductPublishedEvent` → `PRODUCT_PUBLISHED` ✓ (no `PRODUCT_PUBLISHED_EVENT`)
567
+ - `OrderCancelled` → `ORDER_CANCELLED` (sin sufijo, sin cambios)
568
+
569
+ **Cuándo usar `topic:` explícito:**
570
+ - El producer y el consumer de otro módulo deben usar exactamente el mismo nombre.
571
+ - Si el consumer en `listeners[]` declara `topic: MY_CUSTOM_TOPIC`, declara el mismo valor aquí para que el match sea garantizado.
572
+
573
+ ```yaml
574
+ events:
575
+ - name: ProductPublishedEvent
576
+ # topic auto-derivado: PRODUCT_PUBLISHED (sufijo 'Event' eliminado)
577
+ triggers: [publish]
578
+ fields: [...]
579
+
580
+ - name: OrderReadyEvent
581
+ topic: ORDER_READY_FOR_PICKUP # override explícito
582
+ triggers: [markReady]
583
+ fields: [...]
584
+ ```
585
+
586
+ > **Nota:** el flag `kafka: true` ya no es necesario — si el proyecto tiene `kafka-client` instalado, todos los eventos se cablearán automáticamente al ejecutar `eva g entities`.
587
+
588
+ #### Propiedad `triggers`
589
+
590
+ Lista de nombres de métodos de transición que publican este evento. El generador emite automáticamente `raise(new XEvent(...))` dentro de cada método listado.
591
+
592
+ **Reglas de resolución de argumentos (en orden):**
593
+
594
+ | Condición del campo del evento | Argumento generado |
595
+ |---|---|
596
+ | Siempre (primer arg, aggregateId del DomainEvent base) | `this.getId()` |
597
+ | Nombre = `{entityName}Id` (ej: `orderId` en `Order`) | **Ignorado** en el Domain Event class — mapeado a `event.getAggregateId()` en el Integration Event |
598
+ | Nombre coincide con un campo de la entidad | `this.get{Field}()` |
599
+ | Nombre termina en `At` + tipo `LocalDateTime` | `LocalDateTime.now()` |
600
+ | No resuelto | `null /* TODO: provide {fieldName} */` |
601
+
602
+ > **Convención:** Sí declarar `{entityName}Id` en `events[].fields` cuando el evento **cruza módulos via Kafka** — es necesario para que el id viaje en el payload del Integration Event. El generador lo mapea automáticamente a `event.getAggregateId()` en el handler, evitando la duplicación en el Domain Event class interno.
603
+
604
+ **Resultado generado:**
605
+
606
+ ```java
607
+ public void place() {
608
+ this.status = this.status.transitionTo(OrderStatus.PLACED);
609
+ raise(new OrderPlaced(this.getId(), this.getId(), LocalDateTime.now()));
610
+ // ^—aggregateId ^—orderId ^—confirmedAt
611
+ }
612
+
613
+ public void cancel() {
614
+ this.status = this.status.transitionTo(OrderStatus.CANCELLED);
615
+ raise(new OrderCancelled(this.getId(), null /* TODO: provide reason */));
616
+ }
533
617
  ```
534
618
 
535
- Genera `OrderConfirmedEvent.java` (en `domain/models/events/`) que extiende `DomainEvent`, y `OrderDomainEventHandler.java` (en `application/usecases/`) con `@TransactionalEventListener(AFTER_COMMIT)`.
619
+ Si un evento **no declara `triggers`**, el desarrollador debe llamar a `raise()` manualmente dentro del método de negocio.
620
+
621
+ **Validaciones generadas:**
622
+ - **C2-004** (error): trigger referencia un método que no existe en ninguna transición del módulo
623
+ - **C2-005** (info): transición sin ningún evento asociado — considera declarar `triggers`
624
+ - **C2-001** se silencia automáticamente para transiciones que ya tienen `triggers`
625
+
626
+ **Auto-wiring de broker:** Si el proyecto tiene un broker de mensajería instalado (`eva add kafka-client`), `eva g entities` genera automáticamente la capa de Integration Events para **todos** los eventos declarados — sin necesidad de ejecutar `eva g kafka-event` por separado:
627
+
628
+ | Archivo generado | Descripción |
629
+ |---|---|
630
+ | `application/events/OrderConfirmedIntegrationEvent.java` | Record broker-facing (Integration Event) |
631
+ | `application/ports/MessageBroker.java` | Puerto broker-agnóstico (creado/actualizado) |
632
+ | `infrastructure/adapters/kafkaMessageBroker/…` | Adaptador Kafka (creado/actualizado) |
633
+ | `shared/…/kafkaConfig/KafkaConfig.java` | Bean `NewTopic` (actualizado) |
634
+ | `parameters/*/kafka.yaml` | Configuración de topic (actualizada) |
635
+
636
+ **Domain Event vs Integration Event:**
637
+ - **Domain Event** (`domain/models/events/OrderConfirmed.java`) — señal interna del bounded context. Nunca depende de infraestructura.
638
+ - **Integration Event** (`application/events/OrderConfirmedIntegrationEvent.java`) — proyección para el broker. Cambiar de Kafka a RabbitMQ solo requiere cambiar el adaptador `MessageBroker`; los Domain Events no se modifican nunca.
639
+
640
+ El `DomainEventHandler` mapea un Domain Event a un Integration Event:
641
+ ```java
642
+ @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
643
+ public void onOrderConfirmed(OrderConfirmed event) {
644
+ messageBroker.publishOrderConfirmedIntegrationEvent(
645
+ new OrderConfirmedIntegrationEvent(event.getOrderId(), event.getConfirmedAt())
646
+ );
647
+ }
648
+ ```
536
649
 
537
650
  Publicar desde la entidad raíz usando `raise()` heredado:
538
651
 
539
652
  ```java
540
653
  public void confirm() {
541
654
  this.status = this.status.transitionTo(OrderStatus.CONFIRMED);
542
- raise(new OrderConfirmedEvent(this.id, this.id, LocalDateTime.now()));
655
+ raise(new OrderConfirmed(this.id, LocalDateTime.now()));
543
656
  }
544
657
  ```
545
658
 
659
+ **Nota:** el flag `kafka: true` por evento ya no es necesario — todos los eventos se cablearán automáticamente cuando haya un broker instalado.
660
+
661
+ ### Consumo de Eventos Externos (`listeners[]`)
662
+
663
+ ```yaml
664
+ # Nivel raíz, sibling de aggregates:
665
+ listeners:
666
+ - event: PaymentApprovedEvent # PascalCase + sufijo Event
667
+ producer: payments # Módulo que lo produce (referencia documental)
668
+ topic: PAYMENT_APPROVED # Topic Kafka — obligatorio en módulos standalone
669
+ useCase: ConfirmOrder # Caso de uso que maneja el evento (PascalCase)
670
+ fields: # Campos del payload recibido
671
+ - name: orderId
672
+ type: String
673
+ - name: approvedAt
674
+ type: LocalDateTime
675
+ - name: details # Tipo complejo → declarar en nestedTypes
676
+ type: PaymentDetails
677
+ nestedTypes: # Records auxiliares para campos de tipo objeto
678
+ - name: paymentDetails # camelCase → normalizado a PaymentDetails
679
+ fields:
680
+ - name: paymentId
681
+ type: String
682
+ - name: method
683
+ type: String
684
+ - name: amount
685
+ type: BigDecimal
686
+ ```
687
+
688
+ Genera por cada entrada (hasta **6 artefactos**):
689
+
690
+ | Archivo generado | Descripción |
691
+ |---|---|
692
+ | `application/events/PaymentDetails.java` | Record auxiliar (uno por `nestedTypes` entry) |
693
+ | `application/events/PaymentApprovedIntegrationEvent.java` | Record tipado con los `fields` declarados |
694
+ | `infrastructure/kafkaListener/PaymentApprovedKafkaListener.java` | `@KafkaListener` → deserializa y despacha al `useCase` |
695
+ | `parameters/*/kafka.yaml` | Registro del topic en `topics:` |
696
+ | `application/commands/ConfirmOrderCommand.java` | Command tipado para el `useCase` |
697
+ | `application/usecases/ConfirmOrderCommandHandler.java` | Handler stub — implementar la lógica de negocio aquí |
698
+
699
+ **Deserialización:** el listener usa `EventEnvelope<Map<String,Object>>` + `objectMapper.convertValue()` para deserializar cada campo del payload de forma robusta y tipada.
700
+
701
+ **Regla de `topic:`:**
702
+ - Módulo standalone (sin `system.yaml`) → `topic:` **obligatorio**
703
+ - Proyecto con `system.yaml` → puede omitirse; el generador lo infiere de `integrations.async[].topic`
704
+ - Declarado explícitamente → tiene **precedencia** sobre la inferencia
705
+
706
+ **`nestedTypes:` — cuándo usarlo:**
707
+ Declara un `nestedType` cuando un campo del payload es un **objeto anidado** (no un escalar). El generador produce un record en el mismo paquete `application/events/`, que tanto la `IntegrationEvent` como el `Command` y el `KafkaListener` usan directamente.
708
+
709
+ **Colisión de nombres entre módulos:** cuando varios módulos consumen el mismo evento Kafka, el generador produce clases listener con el mismo nombre (ej: `PaymentApprovedKafkaListener` en `orders` y en `notifications`). Esto es seguro porque el generador usa `@Component("<moduleName>.<listenerClassName>")` para calificar el bean y evitar `ConflictingBeanDefinitionException`. **No se requiere acción del agente** — a diferencia de `ports[]`, donde el nombre de `service:` debe ser único por módulo.
710
+
711
+ **Contraste eventos producidos vs. consumidos:**
712
+ ```
713
+ aggregates:
714
+ └── events: → Domain Events que PRODUCE (domain/models/events/)
715
+
716
+ listeners: → Integration Events que CONSUME (infrastructure/kafkaListener/)
717
+ ```
718
+
546
719
  ---
547
720
 
548
- ## �️ Soft Delete
721
+ ### Clientes HTTP Síncronos (`ports[]`)
722
+
723
+ ```yaml
724
+ # Nivel raíz, sibling de aggregates: y listeners:
725
+ # Un método = una entrada; entries del mismo service: forman un solo FeignClient.
726
+
727
+ ports:
728
+ - name: findScreeningById # nombre del método (camelCase)
729
+ service: ScreeningService # agrupa en una interfaz/FeignClient (PascalCase)
730
+ target: screenings # módulo destino (referencia documental)
731
+ baseUrl: http://localhost:8081 # → parameters/*/urls.yaml (primera entrada del service)
732
+ http: GET /screenings/{id} # verbo + path (igual que en system.yaml exposes:)
733
+ fields: # campos de respuesta → {MethodPascal}ResponseDto.java
734
+ - name: id
735
+ type: String
736
+ - name: startTime
737
+ type: LocalDateTime
738
+
739
+ - name: findAvailableSeats
740
+ service: ScreeningService # mismo service → mismo FeignClient
741
+ target: screenings
742
+ http: GET /screenings/{id}/seats
743
+ returnList: true # → List<FindAvailableSeatResponseDto>
744
+ fields:
745
+ - name: seatId
746
+ type: String
747
+ - name: seatType
748
+ type: String
749
+
750
+ - name: processPayment
751
+ service: PaymentGateway
752
+ target: payment-gateway-external
753
+ baseUrl: https://api.payments.example.com
754
+ http: POST /payments
755
+ body: # @RequestBody → ProcessPaymentRequestDto.java
756
+ - name: amount
757
+ type: BigDecimal
758
+ - name: paymentMethod
759
+ type: PaymentMethodInput # tipo objeto → declarar en nestedTypes:
760
+ nestedTypes:
761
+ - name: paymentMethodInput
762
+ fields:
763
+ - name: type
764
+ type: String
765
+ - name: cardToken
766
+ type: String
767
+ fields: # respuesta → ProcessPaymentResponseDto.java
768
+ - name: paymentId
769
+ type: String
770
+ - name: status
771
+ type: String
772
+
773
+ - name: cancelPayment
774
+ service: PaymentGateway
775
+ target: payment-gateway-external
776
+ http: DELETE /payments/{id}
777
+ # fields: omitido → retorno void
778
+ ```
779
+
780
+ ### Artefactos generados por `ports[]`
781
+
782
+ Por cada `service:` único:
783
+
784
+ | Archivo generado | Descripción |
785
+ |---|---|
786
+ | `domain/repositories/{ServiceName}.java` | Interfaz del puerto secundario (devuelve modelos de dominio) |
787
+ | `infrastructure/adapters/{service}/{ServiceName}FeignClient.java` | Cliente Feign tipado (devuelve DTOs infra) |
788
+ | `infrastructure/adapters/{service}/{ServiceName}FeignAdapter.java` | `@Component implements {ServiceName}` — actúa como ACL |
789
+ | `infrastructure/adapters/{service}/{ServiceName}FeignConfig.java` | Timeouts Feign |
790
+ | `parameters/*/urls.yaml` | Base URL parametrizada |
791
+
792
+ Por cada modelo de dominio único derivado de los métodos con `fields:`:
793
+
794
+ | Archivo generado | Descripción |
795
+ |---|---|
796
+ | `domain/models/{service}/{DomainType}.java` | Modelo de dominio (ACL) — abstracción interna |
797
+
798
+ Por cada método:
799
+
800
+ | Archivo generado | Condición |
801
+ |---|---|
802
+ | `infrastructure/adapters/{service}/{MethodPascal}Dto.java` | Cuando `fields:` presente — DTO infra (forma externa) |
803
+ | `application/dtos/{MethodPascal}RequestDto.java` | Cuando `body:` presente (POST/PUT/PATCH) |
804
+ | `application/dtos/{NestedTypePascal}.java` | Cuando `nestedTypes:` declarado |
805
+
806
+ **Patrón ACL:** Los DTOs de infraestructura (forma de la API externa) viven en `infrastructure/adapters/{service}/`. Los modelos de dominio (abstracción interna) viven en `domain/models/{service}/`. El `FeignAdapter` mapea `InfraDto → DomainModel` inline con métodos privados `to{DomainType}()`. Si la API externa cambia, solo hay que actualizar el adaptador.
807
+
808
+ ### Reglas de `ports[]`
809
+
810
+ - **`service:`** — PascalCase, agrupa métodos en un mismo FeignClient. **Si varios módulos llaman al mismo servicio externo, cada módulo debe usar un nombre de `service:` propio que refleje su bounded context** (ej: `OrderCustomerService` en `orders`, `DeliveryCustomerService` en `deliveries`). Reutilizar el mismo nombre (`CustomerService`) en módulos distintos causa colisión de beans Spring (`ConflictingBeanDefinitionException`) porque el generador produce un `FeignAdapter` con el mismo nombre de clase en cada módulo
811
+ - **`baseUrl:`** — declarar solo en la primera entrada de cada `service:`; si se omite en todas → warning + `http://localhost:8080`
812
+ - **`body:`** — solo en POST/PUT/PATCH; en GET/DELETE emite warning y se ignora
813
+ - **`domainType:`** — sobrescribe el tipo de dominio auto-derivado del nombre del método (ej: `domainType: Seat` en `findAvailableSeats`)
814
+ - **`returnList: true`** — el tipo de retorno es `List<{DomainType}>` en la interfaz y `List<{InfraDto}>` en el FeignClient (default: `false`)
815
+ - **`nestedTypes:`** — records auxiliares en `application/dtos/`; mismo patrón que `listeners:`
816
+ - **`fields:` omitido** → retorno `void` en interfaz y FeignClient
817
+
818
+ **Contraste async vs sync:**
819
+ ```
820
+ aggregates:
821
+ └── events: → Domain Events que PRODUCE (async, broker)
822
+ listeners: → Integration Events que CONSUME (async, broker)
823
+ ports: → Servicios HTTP que LLAMA (sync, Feign)
824
+ ```
549
825
 
550
826
  Cuando una entidad tiene `hasSoftDelete: true`, eva4j genera eliminación lógica en lugar de física.
551
827
 
@@ -598,6 +874,8 @@ public class Product {
598
874
 
599
875
  ### Reglas para Agentes
600
876
 
877
+ - **SOLO** aplicar `hasSoftDelete: true` en la **entidad raíz** del agregado (`isRoot: true`)
878
+ - **NUNCA** poner `hasSoftDelete: true` en entidades secundarias — el ciclo de vida de estas lo controla la raíz mediante `cascade`; si se ignora, el generador emite un warning y descarta el flag
601
879
  - **NUNCA** usar `repository.deleteById()` cuando hay soft delete
602
880
  - **SIEMPRE** usar `entity.softDelete()` + `repository.save(entity)`
603
881
  - **NUNCA** exponer `deletedAt` en ResponseDtos
@@ -956,7 +1234,18 @@ private String customerId;
956
1234
  2. **SI** el módulo requiere ciclo de vida → usar `transitions` + `initialValue` en el enum
957
1235
  3. **SI** un valor tiene lógica de negocio → declararlo como `valueObject` con `methods`
958
1236
  4. **SI** ocurren hechos relevantes de negocio → declarar `events[]` en el agregado
959
- 5. **DESPUÉS** de generar el `domain.yaml`ejecutar `eva g entities <module>`
1237
+ 5. **SI** el módulo expone endpoints REST específicos declarar `endpoints:` con versiones y operaciones
1238
+ 6. **DESPUÉS** de generar el `domain.yaml` → ejecutar `eva g entities <module>`
1239
+
1240
+ ### Al Usar `endpoints:` en domain.yaml
1241
+
1242
+ 1. **SIEMPRE** declarar `endpoints:` cuando el API REST tiene comportamientos custom (confirmar, cancelar, activar, etc.)
1243
+ 2. **NUNCA** usar `endpoints:` si solo necesitas CRUD estándar — el flujo interactivo es más simple
1244
+ 3. **SIEMPRE** usar PascalCase para los nombres de `useCase` (ej: `ConfirmOrder`, no `confirmOrder`)
1245
+ 4. **CONOCER** cuáles son los 5 use cases estándar por aggregate: `Create{E}`, `Update{E}`, `Delete{E}`, `Get{E}`, `FindAll{Plural(E)}` — estos generan implementación completa (e.g. `FindAllOrders`, `FindAllDeliveries`, `FindAllCategories`)
1246
+ 5. **SABER** que cualquier otro nombre genera un **scaffold** con `UnsupportedOperationException` — el desarrollador debe implementar el handler
1247
+ 6. **APLICAR** la regla anti-duplicado: si el mismo useCase aparece en v1 y v2, se genera solo una vez
1248
+ 7. **NOMBRAR** los controladores según la convención: `{Aggregate}{VersionCapitalized}Controller` (ej: `OrderV1Controller`)
960
1249
 
961
1250
  ### Al Generar Código de Dominio
962
1251
 
@@ -1188,10 +1477,25 @@ Al generar o modificar código, verificar:
1188
1477
  - [ ] Enum con ciclo de vida → usar `transitions` + `initialValue`, no setters manuales
1189
1478
  - [ ] Value Object con comportamiento → declarar `methods` en lugar de lógica en entidad
1190
1479
  - [ ] Evento de dominio → declarar en `events[]`, publicar con `raise()` en método de negocio
1191
- - [ ] Evento con Kafkaagregar `kafka: true` al evento
1480
+ - [ ] Evento con `triggers: [methodName]` el generador emite `raise()` automáticamente; args no resolubles quedan como `null /* TODO */`
1481
+ - [ ] Sin `triggers` en el evento → el dev llama a `raise()` manualmente
1482
+ - [ ] Evento con broker → **no** usar `kafka: true`; si `eva add kafka-client` está instalado, `eva g entities` auto-cablea todos los eventos
1483
+ - [ ] Distinguir entre Domain Event (`domain/models/events/X.java`) e Integration Event (`application/events/XIntegrationEvent.java`) — cambios de broker solo afectan al adaptador `MessageBroker`
1484
+ - [ ] Consumo de eventos externos → declarar en `listeners[]` (nivel raíz); `topic:` obligatorio en módulos standalone
1485
+ - [ ] Cada `listener` genera hasta 6 artefactos: NestedType(s) → IntegrationEvent → KafkaListener → kafka.yaml → Command → CommandHandler
1486
+ - [ ] Varios módulos pueden consumir el mismo evento Kafka sin colisión — el generador califica el bean automáticamente con `@Component("moduleName.listenerClassName")`
1487
+ - [ ] Campos de tipo objeto en listeners → declarar `nestedTypes:` para generar records auxiliares en `application/events/`
1488
+ - [ ] Endpoints REST específicos → declarar `endpoints:` con versiones y operaciones; usar nombres estándar para implementación completa
1489
+ - [ ] Clientes HTTP síncronos → declarar en `ports[]` (nivel raíz); `baseUrl:` en la primera entrada de cada `service:`
1490
+ - [ ] Si varios módulos llaman al mismo servicio → cada uno usa un `service:` con nombre propio del bounded context (ej: `OrderCustomerService`, `DeliveryCustomerService`) — nunca el mismo nombre genérico en módulos distintos
1491
+ - [ ] Métodos con respuesta → incluir `fields:` en la entrada del puerto; sin `fields:` = retorno `void`
1492
+ - [ ] Respuestas en lista → agregar `returnList: true` en el método correspondiente
1493
+ - [ ] Métodos con cuerpo (POST/PUT/PATCH) → incluir `body:`; campos de tipo objeto en `nestedTypes:`
1494
+ - [ ] Tipo de dominio auto-derivado del nombre del método — usar `domainType:` para sobrescribir si es necesario
1495
+ - [ ] Cada `service:` en `ports[]` genera: interfaz (devuelve modelos de dominio), FeignClient (devuelve DTOs infra), FeignAdapter (mapea ACL), FeignConfig + `urls.yaml`
1192
1496
 
1193
1497
  ---
1194
1498
 
1195
- **Última actualización:** 2026-03-02
1196
- **Versión de eva4j:** 1.0.12
1499
+ **Última actualización:** 2026-03-12
1500
+ **Versión de eva4j:** 1.0.14
1197
1501
  **Estado:** Documento de referencia para agentes IA
@@ -412,20 +412,20 @@ aggregates:
412
412
 
413
413
  ---
414
414
 
415
- ### 4. Soft Delete (0% implementado)
415
+ ### 4. Soft Delete Implementado
416
416
 
417
417
  **Impacto**: Medio - Común en apps business
418
418
 
419
- #### Casos de Uso No Cubiertos
419
+ #### Sintaxis
420
420
  ```yaml
421
- # No soportado actualmente
422
- rootEntity:
423
- name: order
424
- softDelete: true # Debería agregar deletedAt y lógica
421
+ # Soportado solo en la entidad raíz (isRoot: true)
422
+ entities:
423
+ - name: order
424
+ isRoot: true
425
+ hasSoftDelete: true # Genera deletedAt, softDelete(), @SQLRestriction
425
426
  ```
426
427
 
427
- **Esfuerzo para implementar**: Medio (4-5 horas)
428
- **Prioridad**: 🟡 Media
428
+ **Estado**: Implementado en `yaml-to-entity.js` + templates `AggregateRoot`, `JpaAggregateRoot`, repositorios y `DeleteCommandHandler`.
429
429
 
430
430
  ---
431
431
 
@@ -558,7 +558,7 @@ aggregates:
558
558
 
559
559
  1. ❌ **Validaciones JSR-303** (0%)
560
560
  2. ❌ **Auditoría (createdAt, updatedAt)** (0%)
561
- 3. **Soft delete** (0%)
561
+ 3. **Soft delete** (implementado con `hasSoftDelete: true`)
562
562
  4. ❌ **Índices y constraints** (0%)
563
563
  5. ❌ **Herencia de entidades** (0%)
564
564
  6. ❌ **DTOs de aplicación** (0%)
@@ -598,7 +598,7 @@ aggregates:
598
598
 
599
599
  | # | Mejora | Impacto | Esfuerzo | Prioridad |
600
600
  |---|--------|---------|----------|-----------|
601
- | 5 | Soft delete | 🟡 Medio | 5h | 🟡 Media |
601
+ | 5 | ~~Soft delete~~ | Implementado | | |
602
602
  | 6 | ManyToMany completo | 🟡 Medio | 6h | 🟡 Media |
603
603
  | 7 | OneToOne avanzado | 🟡 Bajo | 4h | 🔵 Baja |
604
604
 
@@ -640,9 +640,7 @@ aggregates:
640
640
 
641
641
  ### Mediano Plazo
642
642
 
643
- 4. **Soft Delete**
644
- - Útil para muchas apps
645
- - Buena relación esfuerzo/beneficio
643
+ 4. ~~**Soft Delete**~~ ✅ Implementado con `hasSoftDelete: true`
646
644
 
647
645
  5. **ManyToMany completo**
648
646
  - Completa el soporte de relaciones JPA
@@ -794,15 +792,16 @@ entities:
794
792
 
795
793
  ---
796
794
 
797
- ### 4. Soft Delete
795
+ ### 4. Soft Delete ✅ Implementado
798
796
 
799
797
  ```yaml
800
798
  entities:
801
799
  - name: Order
802
- softDelete: true # Agrega deletedAt y lógica
800
+ isRoot: true
801
+ hasSoftDelete: true # Genera deletedAt, softDelete(), isDeleted(), @SQLRestriction
803
802
  ```
804
803
 
805
- **Implementación**: Campo `deletedAt` + custom queries en repositorio.
804
+ **Implementado**: `deletedAt` inyectado automáticamente, `@SQLRestriction("deleted_at IS NULL")` en JPA, `softDelete()` + `isDeleted()` en dominio, `DeleteCommandHandler` usa borrado lógico.
806
805
 
807
806
  ---
808
807