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.
- package/AGENTS.md +2 -0
- package/DOMAIN_YAML_GUIDE.md +3 -1
- 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 +272 -8
- 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-read-models.yaml +2 -2
- 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/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 +31 -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 +308 -16
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-temporal-activity.js +968 -34
- package/src/commands/generate-temporal-flow.js +95 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/skills/build-system-yaml/SKILL.md +222 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +50 -4
- package/src/skills/build-system-yaml/references/module-spec.md +57 -8
- 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/domain-validator.js +266 -4
- package/src/utils/naming.js +10 -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/yaml-to-entity.js +10 -7
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -2
- 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/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/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/ReadModelJpa.java.ejs +1 -1
- package/templates/read-model/ReadModelJpaRepository.java.ejs +2 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +9 -5
- package/templates/read-model/ReadModelSyncHandler.java.ejs +2 -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
- package/test-c2010.js +0 -49
- package/test-update-compat.js +0 -109
- 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
|
-
*
|
|
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 ─────────────────────────────────────────────────────────
|
|
@@ -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
|
|
package/src/utils/naming.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
|