eva4j 1.0.17 → 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 (134) hide show
  1. package/AGENTS.md +2 -0
  2. package/DOMAIN_YAML_GUIDE.md +3 -1
  3. package/QUICK_REFERENCE.md +8 -4
  4. package/bin/eva4j.js +70 -2
  5. package/config/defaults.json +1 -0
  6. package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
  7. package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
  8. package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
  9. package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
  10. package/docs/commands/EVALUATE_SYSTEM.md +272 -8
  11. package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
  12. package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
  13. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
  14. package/docs/commands/INDEX.md +27 -3
  15. package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
  16. package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
  17. package/docs/prototype/system/RISKS.md +277 -0
  18. package/docs/prototype/system/customers.yaml +133 -0
  19. package/docs/prototype/system/inventory.yaml +109 -0
  20. package/docs/prototype/system/notifications.yaml +131 -0
  21. package/docs/prototype/system/orders.yaml +241 -0
  22. package/docs/prototype/system/payments.yaml +256 -0
  23. package/docs/prototype/system/products.yaml +168 -0
  24. package/docs/prototype/system/system.yaml +269 -0
  25. package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
  26. package/examples/domain-read-models.yaml +2 -2
  27. package/examples/system/customer.yaml +89 -0
  28. package/examples/system/orders.yaml +119 -0
  29. package/examples/system/product.yaml +27 -0
  30. package/examples/system/system.yaml +80 -0
  31. package/package.json +1 -1
  32. package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
  33. package/src/agents/design-gap-analyst.agent.md +383 -0
  34. package/src/agents/design-reviewer-temporal.agent.md +412 -0
  35. package/src/agents/design-reviewer.agent.md +31 -5
  36. package/src/agents/implement-use-cases.prompt.md +179 -0
  37. package/src/agents/ux-gap-analyst.agent.md +412 -0
  38. package/src/commands/add-rabbitmq-client.js +261 -0
  39. package/src/commands/add-temporal-client.js +22 -2
  40. package/src/commands/build.js +267 -11
  41. package/src/commands/evaluate-system.js +700 -13
  42. package/src/commands/generate-entities.js +308 -16
  43. package/src/commands/generate-rabbitmq-event.js +665 -0
  44. package/src/commands/generate-rabbitmq-listener.js +205 -0
  45. package/src/commands/generate-temporal-activity.js +968 -34
  46. package/src/commands/generate-temporal-flow.js +95 -38
  47. package/src/commands/generate-temporal-system.js +708 -0
  48. package/src/skills/build-system-yaml/SKILL.md +222 -2
  49. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +50 -4
  50. package/src/skills/build-system-yaml/references/module-spec.md +57 -8
  51. package/src/skills/build-temporal-system/SKILL.md +752 -0
  52. package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
  53. package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
  54. package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
  55. package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
  56. package/src/skills/implement-use-case/SKILL.md +350 -0
  57. package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
  58. package/src/skills/requirements-elicitation/SKILL.md +228 -0
  59. package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
  60. package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
  61. package/src/utils/bounded-context-diagram.js +844 -0
  62. package/src/utils/domain-validator.js +266 -4
  63. package/src/utils/naming.js +10 -0
  64. package/src/utils/system-validator.js +169 -11
  65. package/src/utils/system-yaml-parser.js +318 -0
  66. package/src/utils/temporal-validator.js +497 -0
  67. package/src/utils/yaml-to-entity.js +10 -7
  68. package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
  69. package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -2
  70. package/templates/aggregate/JpaEntity.java.ejs +2 -2
  71. package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
  72. package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
  73. package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
  74. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
  75. package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
  76. package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
  77. package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
  78. package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
  79. package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
  80. package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
  81. package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
  82. package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
  83. package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
  84. package/templates/base/root/AGENTS.md.ejs +1 -1
  85. package/templates/crud/EndpointsController.java.ejs +1 -1
  86. package/templates/crud/ScaffoldCommand.java.ejs +5 -2
  87. package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
  88. package/templates/crud/ScaffoldQuery.java.ejs +5 -2
  89. package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
  90. package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
  91. package/templates/evaluate/report.html.ejs +1447 -90
  92. package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
  93. package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
  94. package/templates/ports/PortAclMapper.java.ejs +35 -0
  95. package/templates/ports/PortFeignAdapter.java.ejs +7 -22
  96. package/templates/ports/PortFeignClient.java.ejs +4 -0
  97. package/templates/ports/PortResponseDto.java.ejs +1 -1
  98. package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
  99. package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
  100. package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
  101. package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
  102. package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
  103. package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
  104. package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
  105. package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
  106. package/templates/read-model/ReadModelJpa.java.ejs +1 -1
  107. package/templates/read-model/ReadModelJpaRepository.java.ejs +2 -0
  108. package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
  109. package/templates/read-model/ReadModelRepositoryImpl.java.ejs +9 -5
  110. package/templates/read-model/ReadModelSyncHandler.java.ejs +2 -0
  111. package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
  112. package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
  113. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
  114. package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
  115. package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
  116. package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
  117. package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
  118. package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
  119. package/templates/temporal-activity/NestedType.java.ejs +12 -0
  120. package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
  121. package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
  122. package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
  123. package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
  124. package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
  125. package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
  126. package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
  127. package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
  128. package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
  129. package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
  130. package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
  131. package/COMMAND_EVALUATION.md +0 -911
  132. package/test-c2010.js +0 -49
  133. package/test-update-compat.js +0 -109
  134. package/test-update-lifecycle.js +0 -121
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { pluralizeWord, singularizeWord } = 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, singularizeWord } = 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 ─────────────────────────────────────────────────────────
@@ -392,6 +393,7 @@ function runC2(domainConfigs, systemConfig) {
392
393
  'C2-009': { label: 'Evento lifecycle incompatible con configuración de entidad', severity: 'ok', findings: [] },
393
394
  'C2-010': { label: 'Campo de lifecycle event no existe en la entidad raíz', severity: 'ok', findings: [] },
394
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: [] },
395
397
  };
396
398
 
397
399
  for (const [moduleName, config] of Object.entries(domainConfigs)) {
@@ -471,8 +473,14 @@ function runC2(domainConfigs, systemConfig) {
471
473
  }
472
474
  }
473
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
+ );
474
481
  for (const ev of agg.events || []) {
475
482
  if (ev.lifecycle) continue; // lifecycle events don't reference transition methods
483
+ if (!aggHasEnumsWithTransitions) continue; // stateless aggregate — no transitions to reference
476
484
  for (const trigger of ev.triggers || []) {
477
485
  if (!allTransitionMethods.has(trigger)) {
478
486
  checks['C2-004'].findings.push(
@@ -780,6 +788,22 @@ function runC2(domainConfigs, systemConfig) {
780
788
  }
781
789
  }
782
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
+ }
805
+ }
806
+
783
807
  setDefaultSeverities(checks, {
784
808
  'C2-001': 'warning',
785
809
  'C2-002': 'info',
@@ -792,6 +816,7 @@ function runC2(domainConfigs, systemConfig) {
792
816
  'C2-009': 'warning',
793
817
  'C2-010': 'error',
794
818
  'C2-011': 'error',
819
+ 'C2-012': 'error',
795
820
  });
796
821
 
797
822
  return checks;
@@ -1151,6 +1176,225 @@ function runC4(domainConfigs, systemConfig) {
1151
1176
  return checks;
1152
1177
  }
1153
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
+
1154
1398
  // ── Severity finalization ────────────────────────────────────────────────────
1155
1399
 
1156
1400
  /**
@@ -1176,6 +1420,7 @@ function validateDomain(domainConfigs, systemConfig) {
1176
1420
  const c2Checks = runC2(domainConfigs, systemConfig);
1177
1421
  const c3Checks = runC3(domainConfigs, systemConfig);
1178
1422
  const c4Checks = runC4(domainConfigs, systemConfig);
1423
+ const c5Checks = runC5(domainConfigs, systemConfig);
1179
1424
 
1180
1425
  const categories = [
1181
1426
  {
@@ -1202,6 +1447,12 @@ function validateDomain(domainConfigs, systemConfig) {
1202
1447
  description: 'Verifica que las entidades críticas tengan mecanismos de trazabilidad de cambios.',
1203
1448
  checks: checksToArray(c4Checks),
1204
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
+ },
1205
1456
  ];
1206
1457
 
1207
1458
  // Compute summary
@@ -1216,11 +1467,22 @@ function validateDomain(domainConfigs, systemConfig) {
1216
1467
  }
1217
1468
 
1218
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
+ }
1219
1479
 
1220
1480
  return {
1221
1481
  summary: { errors, warnings, info, ok },
1222
1482
  categories,
1223
1483
  diagrams: generateDomainDiagrams(domainConfigs),
1484
+ blueprints: blueprintDiagrams,
1485
+ useCaseDetails,
1224
1486
  };
1225
1487
  }
1226
1488
 
@@ -47,6 +47,15 @@ function toKebabCase(str) {
47
47
  .toLowerCase();
48
48
  }
49
49
 
50
+ /**
51
+ * Convert string to SCREAMING_SNAKE_CASE
52
+ * @param {string} str - Input string
53
+ * @returns {string} SCREAMING_SNAKE_CASE string
54
+ */
55
+ function toScreamingSnakeCase(str) {
56
+ return toSnakeCase(str).toUpperCase();
57
+ }
58
+
50
59
  /**
51
60
  * Pluralize a word
52
61
  * @param {string} word - Word to pluralize
@@ -137,6 +146,7 @@ module.exports = {
137
146
  toCamelCase,
138
147
  toSnakeCase,
139
148
  toKebabCase,
149
+ toScreamingSnakeCase,
140
150
  pluralizeWord,
141
151
  singularizeWord,
142
152
  toPackagePath,
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { toPascalCase, toCamelCase } = require('./naming');
4
+
3
5
  /**
4
6
  * Validates a parsed system.yaml object against the S1–S5 static evaluation rules.
5
7
  *
@@ -21,6 +23,10 @@ function validateSystem(systemConfig, domainConfigs = {}) {
21
23
  const messaging = systemConfig.messaging || {};
22
24
  const topicPrefix = (messaging.kafka || {}).topicPrefix || null;
23
25
 
26
+ // Detect Temporal orchestration mode — suppresses Kafka/Feign-specific rules
27
+ const orchestration = systemConfig.orchestration || {};
28
+ const isTemporalMode = !!(orchestration.enabled && orchestration.engine === 'temporal');
29
+
24
30
  // Helper: normalize a consumer entry to its module name string
25
31
  const consumerModule = (c) => (typeof c === 'string' ? c : c.module);
26
32
 
@@ -57,7 +63,17 @@ function validateSystem(systemConfig, domainConfigs = {}) {
57
63
  const consumesEvents = asyncEvents.some((e) =>
58
64
  (e.consumers || []).some((c) => consumerModule(c) === mod.name)
59
65
  );
60
- if (!hasExposes && !producesEvents && !consumesEvents) {
66
+ // In Temporal mode: a module with activities or local workflows is a valid participant
67
+ const hasTemporalRole = isTemporalMode && (() => {
68
+ const domCfg = domainConfigs[mod.name] || {};
69
+ return (Array.isArray(domCfg.activities) && domCfg.activities.length > 0) ||
70
+ (Array.isArray(domCfg.workflows) && domCfg.workflows.length > 0) ||
71
+ (systemConfig.workflows || []).some((wf) =>
72
+ (wf.trigger && toCamelCase(wf.trigger.module) === toCamelCase(mod.name)) ||
73
+ (wf.steps || []).some((s) => s.target && toCamelCase(s.target) === toCamelCase(mod.name))
74
+ );
75
+ })();
76
+ if (!hasExposes && !producesEvents && !consumesEvents && !hasTemporalRole) {
61
77
  errors.push(`[S1-002] Módulo '${mod.name}' no tiene ninguna responsabilidad — no expone endpoints, no produce ni consume eventos`);
62
78
  s1_002_found = true;
63
79
  }
@@ -346,10 +362,13 @@ function validateSystem(systemConfig, domainConfigs = {}) {
346
362
  // ── S5 — Coherencia del sistema global ───────────────────────────────────
347
363
 
348
364
  // S5-001: messaging.enabled: false with async events declared
349
- if (messaging.enabled === false && asyncEvents.length > 0) {
350
- warnings.push(`[S5-001] messaging.enabled está en false pero hay ${asyncEvents.length} eventos declarados en integrations.async`);
351
- } else if (messaging.enabled !== false && asyncEvents.length > 0) {
352
- ok.push('[S5-001] Configuración de messaging es coherente con los eventos declarados ✓');
365
+ // Not applicable to Temporal-based systems
366
+ if (!isTemporalMode) {
367
+ if (messaging.enabled === false && asyncEvents.length > 0) {
368
+ warnings.push(`[S5-001] messaging.enabled está en false pero hay ${asyncEvents.length} eventos declarados en integrations.async`);
369
+ } else if (messaging.enabled !== false && asyncEvents.length > 0) {
370
+ ok.push('[S5-001] Configuración de messaging es coherente con los eventos declarados ✓');
371
+ }
353
372
  }
354
373
 
355
374
  // S5-002: success event without matching failure event for same subject
@@ -399,12 +418,151 @@ function validateSystem(systemConfig, domainConfigs = {}) {
399
418
  }
400
419
 
401
420
  // S5-004: module with no connection to system graph (info)
402
- for (const mod of modules) {
403
- const hasAnyConnection =
404
- asyncEvents.some((e) => e.producer === mod.name || (e.consumers || []).some((c) => consumerModule(c) === mod.name)) ||
405
- syncIntegrations.some((s) => s.caller === mod.name || s.calls === mod.name);
406
- if (!hasAnyConnection) {
407
- info.push(`[S5-004] Módulo '${mod.name}' no tiene ninguna conexión al grafo del sistema — ni async ni sync`);
421
+ // Suppressed for Temporal mode connectivity is via workflows[], not integrations.async[]/sync[]
422
+ if (!isTemporalMode) {
423
+ for (const mod of modules) {
424
+ const hasAnyConnection =
425
+ asyncEvents.some((e) => e.producer === mod.name || (e.consumers || []).some((c) => consumerModule(c) === mod.name)) ||
426
+ syncIntegrations.some((s) => s.caller === mod.name || s.calls === mod.name);
427
+ if (!hasAnyConnection) {
428
+ info.push(`[S5-004] Módulo '${mod.name}' no tiene ninguna conexión al grafo del sistema — ni async ni sync`);
429
+ }
430
+ }
431
+ }
432
+
433
+ // ── T1 — Temporal Workflow Integrity ──────────────────────────────────────
434
+ // Skipped in Temporal mode — handled exclusively by temporal-validator.js
435
+ if (!isTemporalMode && orchestration.enabled && Array.isArray(systemConfig.workflows)) {
436
+ // Build activity registry from domainConfigs: PascalCase name → { module, input[], output[] }
437
+ const activityMap = new Map();
438
+ for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
439
+ if (!domainCfg || !Array.isArray(domainCfg.activities)) continue;
440
+ const modCamel = toCamelCase(modName);
441
+ for (const act of domainCfg.activities) {
442
+ const actPascal = toPascalCase(act.name);
443
+ const inputNames = Array.isArray(act.input) ? act.input.map((f) => f.name || f) : [];
444
+ const outputNames = Array.isArray(act.output) ? act.output.map((f) => f.name || f) : [];
445
+ activityMap.set(`${modCamel}::${actPascal}`, { module: modCamel, inputNames, outputNames });
446
+ }
447
+ }
448
+
449
+ let t1_001_found = false;
450
+ let t1_002_found = false;
451
+ let t1_004_found = false;
452
+ let t1_006_found = false;
453
+
454
+ for (const wf of systemConfig.workflows) {
455
+ const wfName = wf.name || '(unnamed)';
456
+ const hostModule = wf.trigger && wf.trigger.module ? toCamelCase(wf.trigger.module) : null;
457
+ const rawSteps = Array.isArray(wf.steps) ? wf.steps : [];
458
+
459
+ // Track available output names from prior steps for T1-002
460
+ const availableOutputNames = new Set();
461
+
462
+ // Compute workflow-level input names: step inputs not satisfied by prior step outputs
463
+ // (first pass to collect all step outputs)
464
+ const allStepOutputs = new Map(); // stepIndex → rawOutputNames
465
+ for (let i = 0; i < rawSteps.length; i++) {
466
+ const rawOut = Array.isArray(rawSteps[i].output) ? rawSteps[i].output : [];
467
+ allStepOutputs.set(i, new Set(rawOut));
468
+ }
469
+
470
+ // Compute workflow-level inputs: inputs not provided by any prior step's output
471
+ const workflowInputNames = new Set();
472
+ const priorOutputs = new Set();
473
+ for (let i = 0; i < rawSteps.length; i++) {
474
+ const stepInputs = Array.isArray(rawSteps[i].input) ? rawSteps[i].input : [];
475
+ for (const inName of stepInputs) {
476
+ if (!priorOutputs.has(inName)) {
477
+ workflowInputNames.add(inName);
478
+ }
479
+ }
480
+ const stepOutputs = allStepOutputs.get(i) || new Set();
481
+ for (const outName of stepOutputs) {
482
+ priorOutputs.add(outName);
483
+ }
484
+ }
485
+
486
+ for (let i = 0; i < rawSteps.length; i++) {
487
+ const step = rawSteps[i];
488
+ const actName = step.activity ? toPascalCase(step.activity) : null;
489
+ if (!actName) continue;
490
+
491
+ const targetModule = step.target ? toCamelCase(step.target) : hostModule;
492
+ const key = `${targetModule}::${actName}`;
493
+ const registered = activityMap.get(key);
494
+
495
+ // T1-004: Activity not found in target module
496
+ if (!registered) {
497
+ warnings.push(`[T1-004] Workflow '${wfName}' paso ${i + 1}: actividad '${actName}' no encontrada en activities[] de '${targetModule}'`);
498
+ t1_004_found = true;
499
+ }
500
+
501
+ // T1-001: Step output name not in activity's formal output fields
502
+ const rawOutputNames = Array.isArray(step.output) ? step.output : [];
503
+ if (registered && rawOutputNames.length > 0) {
504
+ const formalOutputSet = new Set(registered.outputNames);
505
+ for (const outName of rawOutputNames) {
506
+ if (!formalOutputSet.has(outName)) {
507
+ errors.push(`[T1-001] Workflow '${wfName}' paso ${i + 1} (${actName}): output '${outName}' no existe en los campos de salida de la actividad — campos disponibles: [${registered.outputNames.join(', ')}]`);
508
+ t1_001_found = true;
509
+ }
510
+ }
511
+
512
+ // T1-003: Step output count exceeds activity formal output count
513
+ if (rawOutputNames.length > registered.outputNames.length) {
514
+ warnings.push(`[T1-003] Workflow '${wfName}' paso ${i + 1} (${actName}): declara ${rawOutputNames.length} outputs pero la actividad solo define ${registered.outputNames.length} campos de salida`);
515
+ }
516
+ }
517
+
518
+ // T1-002: Step input name not resolvable
519
+ const rawInputNames = Array.isArray(step.input) ? step.input : [];
520
+ if (rawInputNames.length > 0) {
521
+ for (const inName of rawInputNames) {
522
+ if (!availableOutputNames.has(inName)) {
523
+ // Not from a prior step — must come from workflow input (OK, no error)
524
+ // But if we detect it's truly orphaned, we'd need full data-flow analysis.
525
+ // For now, we skip pure workflow-input names (they're always valid).
526
+ }
527
+ }
528
+ }
529
+
530
+ // T1-005: Sync step with no output declaration
531
+ if (step.type !== 'async' && rawOutputNames.length === 0 && registered && registered.outputNames.length > 0) {
532
+ info.push(`[T1-005] Workflow '${wfName}' paso ${i + 1} (${actName}): paso síncrono sin output: declarado — la actividad define ${registered.outputNames.length} campo(s) de salida que no se propagan`);
533
+ }
534
+
535
+ // Add this step's outputs to available set for subsequent steps
536
+ for (const outName of rawOutputNames) {
537
+ availableOutputNames.add(outName);
538
+ }
539
+
540
+ // T1-006: Compensation input field not resolvable
541
+ if (step.compensation) {
542
+ const compName = toPascalCase(step.compensation);
543
+ const compTarget = step.target ? toCamelCase(step.target) : hostModule;
544
+ const compKey = `${compTarget}::${compName}`;
545
+ const compRegistered = activityMap.get(compKey);
546
+ if (compRegistered) {
547
+ for (const compIn of compRegistered.inputNames) {
548
+ if (!availableOutputNames.has(compIn) && !workflowInputNames.has(compIn)) {
549
+ errors.push(`[T1-006] Workflow '${wfName}' paso ${i + 1} (${actName}): compensación '${compName}' requiere input '${compIn}' pero no está disponible en outputs de pasos previos ni en inputs del workflow`);
550
+ t1_006_found = true;
551
+ }
552
+ }
553
+ }
554
+ }
555
+ }
556
+ }
557
+
558
+ if (!t1_001_found) {
559
+ ok.push('[T1-001] Todos los outputs de pasos de workflow coinciden con campos formales de la actividad ✓');
560
+ }
561
+ if (!t1_004_found) {
562
+ ok.push('[T1-004] Todas las actividades referenciadas en workflows existen en sus módulos target ✓');
563
+ }
564
+ if (!t1_006_found) {
565
+ ok.push('[T1-006] Todos los inputs de compensaciones son resolubles desde outputs de pasos previos o inputs del workflow ✓');
408
566
  }
409
567
  }
410
568