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.
- package/AGENTS.md +220 -5
- package/DOMAIN_YAML_GUIDE.md +188 -3
- package/FUTURE_FEATURES.md +33 -52
- package/QUICK_REFERENCE.md +8 -4
- package/bin/eva4j.js +70 -2
- package/config/defaults.json +1 -0
- package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
- package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
- package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
- package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
- package/docs/commands/EVALUATE_SYSTEM.md +290 -10
- package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
- package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
- package/docs/commands/INDEX.md +27 -3
- package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
- package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
- package/docs/prototype/system/RISKS.md +277 -0
- package/docs/prototype/system/customers.yaml +133 -0
- package/docs/prototype/system/inventory.yaml +109 -0
- package/docs/prototype/system/notifications.yaml +131 -0
- package/docs/prototype/system/orders.yaml +241 -0
- package/docs/prototype/system/payments.yaml +256 -0
- package/docs/prototype/system/products.yaml +168 -0
- package/docs/prototype/system/system.yaml +269 -0
- package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
- package/examples/domain-events.yaml +26 -0
- package/examples/domain-read-models.yaml +113 -0
- package/examples/system/customer.yaml +89 -0
- package/examples/system/orders.yaml +119 -0
- package/examples/system/product.yaml +27 -0
- package/examples/system/system.yaml +80 -0
- package/package.json +1 -1
- package/read-model-spec.md +664 -0
- package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
- package/src/agents/design-gap-analyst.agent.md +383 -0
- package/src/agents/design-reviewer-temporal.agent.md +412 -0
- package/src/agents/design-reviewer.agent.md +34 -5
- package/src/agents/implement-use-cases.prompt.md +179 -0
- package/src/agents/ux-gap-analyst.agent.md +412 -0
- package/src/commands/add-rabbitmq-client.js +261 -0
- package/src/commands/add-temporal-client.js +22 -2
- package/src/commands/build.js +267 -11
- package/src/commands/evaluate-system.js +700 -13
- package/src/commands/generate-entities.js +560 -24
- package/src/commands/generate-http-exchange.js +3 -0
- package/src/commands/generate-kafka-event.js +3 -0
- package/src/commands/generate-kafka-listener.js +3 -0
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-record.js +2 -2
- package/src/commands/generate-resource.js +4 -1
- package/src/commands/generate-temporal-activity.js +970 -33
- package/src/commands/generate-temporal-flow.js +98 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/commands/generate-usecase.js +4 -1
- package/src/skills/build-system-yaml/SKILL.md +343 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
- package/src/skills/build-system-yaml/references/module-spec.md +90 -9
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
- package/src/skills/build-temporal-system/SKILL.md +752 -0
- package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
- package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
- package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
- package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
- package/src/skills/implement-use-case/SKILL.md +350 -0
- package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
- package/src/skills/requirements-elicitation/SKILL.md +228 -0
- package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
- package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
- package/src/utils/bounded-context-diagram.js +844 -0
- package/src/utils/config-manager.js +4 -2
- package/src/utils/domain-validator.js +495 -17
- package/src/utils/naming.js +20 -0
- package/src/utils/system-validator.js +169 -11
- package/src/utils/system-yaml-parser.js +318 -0
- package/src/utils/temporal-validator.js +497 -0
- package/src/utils/validator.js +3 -1
- package/src/utils/yaml-to-entity.js +281 -9
- package/templates/aggregate/AggregateRepository.java.ejs +4 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
- package/templates/aggregate/AggregateRoot.java.ejs +38 -4
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
- package/templates/aggregate/JpaEntity.java.ejs +2 -2
- package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
- package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
- package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
- package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
- package/templates/base/root/AGENTS.md.ejs +1 -1
- package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
- package/templates/crud/EndpointsController.java.ejs +1 -1
- package/templates/crud/ScaffoldCommand.java.ejs +5 -2
- package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
- package/templates/crud/ScaffoldQuery.java.ejs +5 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
- package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
- package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
- package/templates/evaluate/report.html.ejs +1447 -90
- package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
- package/templates/ports/PortAclMapper.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +7 -22
- package/templates/ports/PortFeignClient.java.ejs +4 -0
- package/templates/ports/PortResponseDto.java.ejs +1 -1
- package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
- package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
- package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
- package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
- package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
- package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
- package/templates/read-model/ReadModelDomain.java.ejs +46 -0
- package/templates/read-model/ReadModelJpa.java.ejs +58 -0
- package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
- package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepository.java.ejs +42 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
- package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
- package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
- package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
- package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
- package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/NestedType.java.ejs +12 -0
- package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
- package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
- package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
- package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
- package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
- 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
|
-
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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) =>
|
|
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
|
|
283
|
-
const consumerConfig = domainConfigs[
|
|
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
|
-
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|