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
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
|
|
@@ -56,6 +65,15 @@ function pluralizeWord(word) {
|
|
|
56
65
|
return pluralize(word);
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Singularize a word
|
|
70
|
+
* @param {string} word - Word to singularize
|
|
71
|
+
* @returns {string} Singularized word
|
|
72
|
+
*/
|
|
73
|
+
function singularizeWord(word) {
|
|
74
|
+
return pluralize.singular(word);
|
|
75
|
+
}
|
|
76
|
+
|
|
59
77
|
/**
|
|
60
78
|
* Convert package name to path
|
|
61
79
|
* @param {string} packageName - Package name (e.g., com.company.project)
|
|
@@ -128,7 +146,9 @@ module.exports = {
|
|
|
128
146
|
toCamelCase,
|
|
129
147
|
toSnakeCase,
|
|
130
148
|
toKebabCase,
|
|
149
|
+
toScreamingSnakeCase,
|
|
131
150
|
pluralizeWord,
|
|
151
|
+
singularizeWord,
|
|
132
152
|
toPackagePath,
|
|
133
153
|
getBaseEntity,
|
|
134
154
|
artifactIdToPackageName,
|
|
@@ -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
|
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* system-yaml-parser.js
|
|
3
|
+
*
|
|
4
|
+
* Parses system.yaml and resolves cross-module workflow definitions by
|
|
5
|
+
* cross-referencing each workflow step's activity with its target module's
|
|
6
|
+
* domain.yaml. Produces a rich context tree for template generation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const yaml = require('js-yaml');
|
|
10
|
+
const fs = require('fs-extra');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const chalk = require('chalk');
|
|
13
|
+
const { toPascalCase, toCamelCase, toScreamingSnakeCase } = require('./naming');
|
|
14
|
+
|
|
15
|
+
// ─── Java type → import mapping (shared with generate-temporal-activity) ────
|
|
16
|
+
const JAVA_TYPE_IMPORTS = {
|
|
17
|
+
BigDecimal: 'java.math.BigDecimal',
|
|
18
|
+
LocalDateTime: 'java.time.LocalDateTime',
|
|
19
|
+
LocalDate: 'java.time.LocalDate',
|
|
20
|
+
LocalTime: 'java.time.LocalTime',
|
|
21
|
+
Instant: 'java.time.Instant',
|
|
22
|
+
UUID: 'java.util.UUID',
|
|
23
|
+
List: 'java.util.List',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function resolveFieldImports(fields) {
|
|
27
|
+
const imports = new Set();
|
|
28
|
+
for (const field of fields) {
|
|
29
|
+
for (const [type, imp] of Object.entries(JAVA_TYPE_IMPORTS)) {
|
|
30
|
+
if (field.javaType && field.javaType.includes(type)) {
|
|
31
|
+
imports.add(`import ${imp};`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return Array.from(imports).sort();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mapField(field) {
|
|
39
|
+
let javaType = field.type || 'String';
|
|
40
|
+
// Default bare collection types to generic <String>
|
|
41
|
+
if (javaType === 'List' || javaType === 'Set') {
|
|
42
|
+
javaType = javaType + '<String>';
|
|
43
|
+
}
|
|
44
|
+
return { name: field.name, javaType };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse a timeout string like "5s", "10m", "2h" into a Duration descriptor.
|
|
49
|
+
* @param {string|null} timeout
|
|
50
|
+
* @returns {{ value: number, unit: string }}
|
|
51
|
+
*/
|
|
52
|
+
function parseTimeout(timeout) {
|
|
53
|
+
if (!timeout) return { value: 30, unit: 'Seconds' };
|
|
54
|
+
const match = String(timeout).match(/^(\d+)(s|m|h)$/);
|
|
55
|
+
if (!match) return { value: 30, unit: 'Seconds' };
|
|
56
|
+
const units = { s: 'Seconds', m: 'Minutes', h: 'Hours' };
|
|
57
|
+
return { value: parseInt(match[1], 10), unit: units[match[2]] || 'Seconds' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Core parser ────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Load and parse system.yaml.
|
|
64
|
+
* @param {string} systemYamlPath - Absolute path to system.yaml
|
|
65
|
+
* @returns {object} Raw parsed YAML data
|
|
66
|
+
*/
|
|
67
|
+
async function loadSystemYaml(systemYamlPath) {
|
|
68
|
+
if (!(await fs.pathExists(systemYamlPath))) {
|
|
69
|
+
throw new Error(`system.yaml not found at ${systemYamlPath}`);
|
|
70
|
+
}
|
|
71
|
+
const content = await fs.readFile(systemYamlPath, 'utf-8');
|
|
72
|
+
return yaml.load(content);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load the activities section from a module's domain.yaml.
|
|
77
|
+
* @param {string} domainYamlPath - Absolute path to domain.yaml
|
|
78
|
+
* @returns {Map<string, object>} Map of PascalCase activity name → raw activity definition
|
|
79
|
+
*/
|
|
80
|
+
async function loadModuleActivities(domainYamlPath) {
|
|
81
|
+
const activities = new Map();
|
|
82
|
+
if (!(await fs.pathExists(domainYamlPath))) return activities;
|
|
83
|
+
|
|
84
|
+
const content = await fs.readFile(domainYamlPath, 'utf-8');
|
|
85
|
+
const data = yaml.load(content);
|
|
86
|
+
if (!data || !Array.isArray(data.activities)) return activities;
|
|
87
|
+
|
|
88
|
+
for (const act of data.activities) {
|
|
89
|
+
activities.set(toPascalCase(act.name), act);
|
|
90
|
+
}
|
|
91
|
+
return activities;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parse system.yaml and resolve all workflow steps against their target module
|
|
96
|
+
* domain.yaml files.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} systemDir - Directory containing system.yaml and domain yamls
|
|
99
|
+
* @returns {object} Parsed system context:
|
|
100
|
+
* {
|
|
101
|
+
* system: { name, groupId, javaVersion, springBootVersion, database },
|
|
102
|
+
* orchestration: { enabled, engine, temporal: { target, namespace } },
|
|
103
|
+
* modules: [{ name, description, exposes }],
|
|
104
|
+
* workflows: [ResolvedWorkflow],
|
|
105
|
+
* activityRegistry: Map<activityName, { module, definition }>,
|
|
106
|
+
* warnings: string[]
|
|
107
|
+
* }
|
|
108
|
+
*/
|
|
109
|
+
async function parseSystemYaml(systemDir) {
|
|
110
|
+
const systemYamlPath = path.join(systemDir, 'system.yaml');
|
|
111
|
+
const data = await loadSystemYaml(systemYamlPath);
|
|
112
|
+
|
|
113
|
+
const warnings = [];
|
|
114
|
+
|
|
115
|
+
// ── 1. Basic sections ──────────────────────────────────────────────────
|
|
116
|
+
const system = data.system || {};
|
|
117
|
+
const orchestration = data.orchestration || {};
|
|
118
|
+
const modules = Array.isArray(data.modules) ? data.modules : [];
|
|
119
|
+
const rawWorkflows = Array.isArray(data.workflows) ? data.workflows : [];
|
|
120
|
+
|
|
121
|
+
// ── 2. Load all module activities ──────────────────────────────────────
|
|
122
|
+
// Map: moduleName (camelCase) → Map<ActivityPascalCase, rawDefinition>
|
|
123
|
+
const moduleActivitiesMap = new Map();
|
|
124
|
+
|
|
125
|
+
for (const mod of modules) {
|
|
126
|
+
const modCamel = toCamelCase(mod.name);
|
|
127
|
+
// Domain YAML files can be in the system dir (prototype layout)
|
|
128
|
+
// or in the project's module directory. Try both.
|
|
129
|
+
const candidates = [
|
|
130
|
+
path.join(systemDir, `${mod.name}.yaml`),
|
|
131
|
+
path.join(systemDir, `${modCamel}.yaml`),
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
let activities = new Map();
|
|
135
|
+
for (const candidate of candidates) {
|
|
136
|
+
activities = await loadModuleActivities(candidate);
|
|
137
|
+
if (activities.size > 0) break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
moduleActivitiesMap.set(modCamel, activities);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── 3. Build activity registry (flat, for easy lookup) ─────────────────
|
|
144
|
+
// Map: activityPascalCase → { module (camelCase), definition, inputFields, outputFields }
|
|
145
|
+
const activityRegistry = new Map();
|
|
146
|
+
|
|
147
|
+
for (const [modName, activities] of moduleActivitiesMap) {
|
|
148
|
+
for (const [actName, actDef] of activities) {
|
|
149
|
+
activityRegistry.set(actName, {
|
|
150
|
+
module: modName,
|
|
151
|
+
definition: actDef,
|
|
152
|
+
inputFields: Array.isArray(actDef.input) ? actDef.input.map(mapField) : [],
|
|
153
|
+
outputFields: Array.isArray(actDef.output) ? actDef.output.map(mapField) : [],
|
|
154
|
+
type: (actDef.type || 'light').toLowerCase(),
|
|
155
|
+
compensation: actDef.compensation ? toPascalCase(actDef.compensation) : null,
|
|
156
|
+
timeout: actDef.timeout || null,
|
|
157
|
+
description: actDef.description || '',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── 4. Resolve workflows ───────────────────────────────────────────────
|
|
163
|
+
const workflows = rawWorkflows.map((wf) =>
|
|
164
|
+
resolveWorkflow(wf, activityRegistry, warnings)
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
system,
|
|
169
|
+
orchestration,
|
|
170
|
+
modules,
|
|
171
|
+
workflows,
|
|
172
|
+
activityRegistry,
|
|
173
|
+
warnings,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolve a single workflow definition: enrich each step with activity
|
|
179
|
+
* definitions, detect parallel groups, and derive the host module.
|
|
180
|
+
*
|
|
181
|
+
* @param {object} wf - Raw workflow from system.yaml
|
|
182
|
+
* @param {Map} activityRegistry - Global activity lookup
|
|
183
|
+
* @param {string[]} warnings - Mutable array for warnings
|
|
184
|
+
* @returns {object} Resolved workflow
|
|
185
|
+
*/
|
|
186
|
+
function resolveWorkflow(wf, activityRegistry, warnings) {
|
|
187
|
+
const name = wf.name;
|
|
188
|
+
const namePascal = toPascalCase(name);
|
|
189
|
+
const trigger = wf.trigger || {};
|
|
190
|
+
const hostModule = toCamelCase(trigger.module || '');
|
|
191
|
+
const taskQueue = wf.taskQueue || `${toScreamingSnakeCase(hostModule)}_WORKFLOW_QUEUE`;
|
|
192
|
+
const isSaga = wf.saga === true;
|
|
193
|
+
|
|
194
|
+
const steps = [];
|
|
195
|
+
const rawSteps = Array.isArray(wf.steps) ? wf.steps : [];
|
|
196
|
+
|
|
197
|
+
for (let i = 0; i < rawSteps.length; i++) {
|
|
198
|
+
const rawStep = rawSteps[i];
|
|
199
|
+
const activityName = toPascalCase(rawStep.activity);
|
|
200
|
+
const targetModule = toCamelCase(rawStep.target || hostModule);
|
|
201
|
+
const isLocal = targetModule === hostModule;
|
|
202
|
+
const isAsync = rawStep.type === 'async';
|
|
203
|
+
const isParallel = rawStep.parallel === true;
|
|
204
|
+
const isOptional = rawStep.optional === true;
|
|
205
|
+
|
|
206
|
+
// Resolve from registry
|
|
207
|
+
const registered = activityRegistry.get(activityName);
|
|
208
|
+
if (!registered) {
|
|
209
|
+
warnings.push(
|
|
210
|
+
`Workflow '${name}' step ${i + 1}: activity '${activityName}' not found in any module's domain.yaml`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const inputFields = registered ? registered.inputFields : (rawStep.input || []).map((n) => ({ name: n, javaType: 'String' }));
|
|
215
|
+
const outputFields = registered ? registered.outputFields : (rawStep.output || []).map((n) => ({ name: n, javaType: 'String' }));
|
|
216
|
+
const actType = registered ? registered.type : (rawStep.type === 'async' ? 'light' : 'light');
|
|
217
|
+
const timeout = rawStep.timeout || (registered ? registered.timeout : null);
|
|
218
|
+
|
|
219
|
+
// Resolve compensation
|
|
220
|
+
let compensation = null;
|
|
221
|
+
if (rawStep.compensation) {
|
|
222
|
+
const compName = toPascalCase(rawStep.compensation);
|
|
223
|
+
const compRegistered = activityRegistry.get(compName);
|
|
224
|
+
compensation = {
|
|
225
|
+
name: compName,
|
|
226
|
+
module: compRegistered ? compRegistered.module : targetModule,
|
|
227
|
+
inputFields: compRegistered ? compRegistered.inputFields : [],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Queue for this step's target module
|
|
232
|
+
const stepQueue = actType === 'heavy'
|
|
233
|
+
? `${toScreamingSnakeCase(targetModule)}_HEAVY_TASK_QUEUE`
|
|
234
|
+
: `${toScreamingSnakeCase(targetModule)}_LIGHT_TASK_QUEUE`;
|
|
235
|
+
|
|
236
|
+
// Raw field names from system.yaml (wiring-level names)
|
|
237
|
+
const rawInputNames = Array.isArray(rawStep.input) ? rawStep.input : [];
|
|
238
|
+
const rawOutputNames = Array.isArray(rawStep.output) ? rawStep.output : [];
|
|
239
|
+
|
|
240
|
+
steps.push({
|
|
241
|
+
index: i,
|
|
242
|
+
activityName,
|
|
243
|
+
activityCamel: activityName.charAt(0).toLowerCase() + activityName.slice(1),
|
|
244
|
+
targetModule,
|
|
245
|
+
targetModulePascal: toPascalCase(targetModule),
|
|
246
|
+
targetModuleScreamingSnake: toScreamingSnakeCase(targetModule),
|
|
247
|
+
stepQueue,
|
|
248
|
+
isLocal,
|
|
249
|
+
isAsync,
|
|
250
|
+
isParallel,
|
|
251
|
+
isOptional,
|
|
252
|
+
actType,
|
|
253
|
+
timeout,
|
|
254
|
+
inputFields,
|
|
255
|
+
outputFields,
|
|
256
|
+
rawInputNames,
|
|
257
|
+
rawOutputNames,
|
|
258
|
+
hasInput: inputFields.length > 0,
|
|
259
|
+
hasOutput: outputFields.length > 0,
|
|
260
|
+
compensation,
|
|
261
|
+
inputImports: resolveFieldImports(inputFields),
|
|
262
|
+
outputImports: resolveFieldImports(outputFields),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Detect parallel groups ─────────────────────────────────────────────
|
|
267
|
+
// Consecutive steps with parallel: true form a group
|
|
268
|
+
const parallelGroups = [];
|
|
269
|
+
let currentGroup = null;
|
|
270
|
+
for (const step of steps) {
|
|
271
|
+
if (step.isParallel) {
|
|
272
|
+
if (!currentGroup) {
|
|
273
|
+
currentGroup = { startIndex: step.index, steps: [] };
|
|
274
|
+
}
|
|
275
|
+
currentGroup.steps.push(step);
|
|
276
|
+
} else {
|
|
277
|
+
if (currentGroup) {
|
|
278
|
+
parallelGroups.push(currentGroup);
|
|
279
|
+
currentGroup = null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (currentGroup) parallelGroups.push(currentGroup);
|
|
284
|
+
|
|
285
|
+
// ── Collect unique target modules (for imports) ────────────────────────
|
|
286
|
+
const targetModules = [...new Set(steps.map((s) => s.targetModule))];
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
name,
|
|
290
|
+
namePascal,
|
|
291
|
+
nameScreamingSnake: toScreamingSnakeCase(name.replace(/Workflow$/, '')),
|
|
292
|
+
trigger,
|
|
293
|
+
hostModule,
|
|
294
|
+
hostModulePascal: toPascalCase(hostModule),
|
|
295
|
+
hostModuleScreamingSnake: toScreamingSnakeCase(hostModule),
|
|
296
|
+
taskQueue,
|
|
297
|
+
isSaga,
|
|
298
|
+
steps,
|
|
299
|
+
parallelGroups,
|
|
300
|
+
targetModules,
|
|
301
|
+
hasParallelSteps: parallelGroups.length > 0,
|
|
302
|
+
hasCompensations: steps.some((s) => s.compensation !== null),
|
|
303
|
+
hasAsyncSteps: steps.some((s) => s.isAsync),
|
|
304
|
+
hasOptionalSteps: steps.some((s) => s.isOptional),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ─── Exports ────────────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
module.exports = {
|
|
311
|
+
parseSystemYaml,
|
|
312
|
+
loadSystemYaml,
|
|
313
|
+
loadModuleActivities,
|
|
314
|
+
resolveWorkflow,
|
|
315
|
+
resolveFieldImports,
|
|
316
|
+
mapField,
|
|
317
|
+
parseTimeout,
|
|
318
|
+
};
|