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
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Temporal Workflow Validator — T1/T2/T3 rules
|
|
5
|
+
*
|
|
6
|
+
* Called by evaluate-system.js when orchestration.engine === 'temporal'.
|
|
7
|
+
* Operates on the enriched temporal context produced by extractTemporalReportData().
|
|
8
|
+
*
|
|
9
|
+
* @param {object} systemConfig — Parsed system.yaml
|
|
10
|
+
* @param {Record<string, object>} domainConfigs — moduleName → parsed domain YAML
|
|
11
|
+
* @param {object} temporalCtx — Pre-built temporal context from extractTemporalReportData()
|
|
12
|
+
* @returns {{ summary, categories[], score }}
|
|
13
|
+
*/
|
|
14
|
+
function validateTemporal(systemConfig, domainConfigs, temporalCtx) {
|
|
15
|
+
const { activityCatalog, workflows, moduleRoles } = temporalCtx;
|
|
16
|
+
const rawWorkflows = systemConfig.workflows || [];
|
|
17
|
+
const modules = systemConfig.modules || [];
|
|
18
|
+
|
|
19
|
+
// ── Build quick lookup structures ─────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** module camelCase → { activities: Map<PascalCaseName, actDef> } */
|
|
22
|
+
const activityRegistry = buildActivityRegistry(domainConfigs);
|
|
23
|
+
|
|
24
|
+
/** All domain events keyed by moduleName (camelCase) → Set<eventName> */
|
|
25
|
+
const domainEventsByModule = buildDomainEventsByModule(domainConfigs);
|
|
26
|
+
|
|
27
|
+
/** localWorkflows per module: moduleName → workflow[] */
|
|
28
|
+
const localWorkflowsByModule = buildLocalWorkflowsByModule(domainConfigs);
|
|
29
|
+
|
|
30
|
+
// ── Initialize checks ─────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const checks = {
|
|
33
|
+
// T1 — Activity Contracts
|
|
34
|
+
'T1-001': { label: 'Actividad en step no declarada en domain.yaml del módulo target', severity: 'ok', findings: [] },
|
|
35
|
+
'T1-002': { label: 'Campo output del step no existe en output[] formal de la actividad', severity: 'ok', findings: [] },
|
|
36
|
+
'T1-003': { label: 'Campo input del step no resoluble (no en outputs anteriores ni en trigger)', severity: 'ok', findings: [] },
|
|
37
|
+
'T1-004': { label: 'Compensation del step no declarada como actividad en el módulo', severity: 'ok', findings: [] },
|
|
38
|
+
'T1-005': { label: 'ExternalType referencia módulo que no declara ese tipo en output de actividad', severity: 'ok', findings: [] },
|
|
39
|
+
// T2 — Workflow Design
|
|
40
|
+
'T2-001': { label: 'saga:true sin ningún step con compensation (saga vacía)', severity: 'ok', findings: [] },
|
|
41
|
+
'T2-002': { label: 'Step async con compensation declarada (no se puede compensar async)', severity: 'ok', findings: [] },
|
|
42
|
+
'T2-003': { label: 'Trigger on: apunta a evento no declarado en events[] del módulo', severity: 'ok', findings: [] },
|
|
43
|
+
'T2-004': { label: 'Módulo trigger no tiene evento con notifies apuntando al workflow', severity: 'ok', findings: [] },
|
|
44
|
+
// T3 — Activity Quality
|
|
45
|
+
'T3-001': { label: 'Actividad declarada en domain.yaml pero no usada en ningún workflow (huérfana)', severity: 'ok', findings: [] },
|
|
46
|
+
'T3-002': { label: 'Actividad heavy sin retryPolicy declarada', severity: 'ok', findings: [] },
|
|
47
|
+
'T3-003': { label: 'Actividad compensation con output[] (las compensaciones no deben retornar datos)', severity: 'ok', findings: [] },
|
|
48
|
+
'T3-004': { label: 'ExternalType cuya definición no existe en el módulo fuente declarado', severity: 'ok', findings: [] },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function finding(moduleName, message, context = '') {
|
|
52
|
+
return { module: moduleName, message, context };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── T1 — Activity Contracts ───────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
for (const wf of rawWorkflows) {
|
|
58
|
+
const wfName = wf.name || '(unnamed)';
|
|
59
|
+
const triggerModule = wf.trigger && wf.trigger.module ? camelCase(wf.trigger.module) : null;
|
|
60
|
+
const steps = Array.isArray(wf.steps) ? wf.steps : [];
|
|
61
|
+
|
|
62
|
+
// Build data-flow context: which fields are available at each step
|
|
63
|
+
const available = new Set(); // fields available from trigger or prior step outputs
|
|
64
|
+
|
|
65
|
+
// Seed with trigger-level fields (we can't know exact fields, so we approximate
|
|
66
|
+
// from the domain event that triggers this workflow)
|
|
67
|
+
if (wf.trigger && wf.trigger.on && triggerModule) {
|
|
68
|
+
const eventsForModule = domainEventsByModule.get(triggerModule) || new Map();
|
|
69
|
+
const triggerOn = wf.trigger.on;
|
|
70
|
+
for (const [, evDef] of eventsForModule) {
|
|
71
|
+
const nameMatch =
|
|
72
|
+
normalizeMethodName(evDef.name) === normalizeMethodName(triggerOn) ||
|
|
73
|
+
normalizeMethodName(evDef.name).includes(normalizeMethodName(triggerOn));
|
|
74
|
+
// Also match via explicit triggers[] array declared in the event
|
|
75
|
+
const triggersMatch = (evDef.triggers || []).some(
|
|
76
|
+
(t) => normalizeMethodName(t) === normalizeMethodName(triggerOn)
|
|
77
|
+
);
|
|
78
|
+
if (nameMatch || triggersMatch) {
|
|
79
|
+
for (const f of evDef.fields || []) {
|
|
80
|
+
if (f && f.name) available.add(f.name);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Also add the triggering identifier itself (e.g., cartId)
|
|
85
|
+
if (triggerOn) available.add(triggerOn + 'Id');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < steps.length; i++) {
|
|
89
|
+
const step = steps[i];
|
|
90
|
+
if (!step.activity) continue;
|
|
91
|
+
|
|
92
|
+
const stepNum = i + 1;
|
|
93
|
+
const actName = pascalCase(step.activity);
|
|
94
|
+
const targetMod = step.target ? camelCase(step.target) : triggerModule;
|
|
95
|
+
const moduleActivities = activityRegistry.get(targetMod) || new Map();
|
|
96
|
+
const actDef = moduleActivities.get(actName);
|
|
97
|
+
|
|
98
|
+
// T1-001: Activity not declared in target module
|
|
99
|
+
if (!actDef) {
|
|
100
|
+
checks['T1-001'].findings.push(
|
|
101
|
+
finding(wfName, `Paso ${stepNum} (${actName}): actividad no declarada en activities[] del módulo '${targetMod}'`, `workflow: ${wfName}`)
|
|
102
|
+
);
|
|
103
|
+
// Add step outputs to available anyway for subsequent steps
|
|
104
|
+
for (const o of step.output || []) available.add(typeof o === 'string' ? o : o.name);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// T1-002: Step output field not in activity formal output
|
|
109
|
+
const formalOutputNames = new Set((actDef.output || []).map((f) => (typeof f === 'string' ? f : f.name)));
|
|
110
|
+
for (const o of step.output || []) {
|
|
111
|
+
const oName = typeof o === 'string' ? o : o.name;
|
|
112
|
+
if (oName && !formalOutputNames.has(oName)) {
|
|
113
|
+
checks['T1-002'].findings.push(
|
|
114
|
+
finding(wfName, `Paso ${stepNum} (${actName}): output '${oName}' no existe en output[] de la actividad — disponibles: [${[...formalOutputNames].join(', ')}]`, `workflow: ${wfName}`)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
available.add(oName);
|
|
118
|
+
}
|
|
119
|
+
// If no step.output was declared, add formal outputs anyway (they exist)
|
|
120
|
+
if (!step.output || step.output.length === 0) {
|
|
121
|
+
for (const o of actDef.output || []) {
|
|
122
|
+
available.add(typeof o === 'string' ? o : o.name);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// T1-003: Step input not resolvable
|
|
127
|
+
const stepInputs = Array.isArray(step.input) ? step.input : [];
|
|
128
|
+
for (const inField of stepInputs) {
|
|
129
|
+
const inName = typeof inField === 'string' ? inField : inField.name;
|
|
130
|
+
if (inName && !available.has(inName)) {
|
|
131
|
+
checks['T1-003'].findings.push(
|
|
132
|
+
finding(wfName, `Paso ${stepNum} (${actName}): input '${inName}' no está disponible desde outputs de pasos anteriores ni desde el trigger del workflow`, `workflow: ${wfName}, módulo target: ${targetMod}`)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// T1-004: Compensation not declared as activity in target module
|
|
138
|
+
if (step.compensation) {
|
|
139
|
+
const compName = pascalCase(step.compensation);
|
|
140
|
+
const compDef = moduleActivities.get(compName);
|
|
141
|
+
if (!compDef) {
|
|
142
|
+
checks['T1-004'].findings.push(
|
|
143
|
+
finding(wfName, `Paso ${stepNum} (${actName}): compensación '${compName}' no está declarada en activities[] del módulo '${targetMod}'`, `workflow: ${wfName}`)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// T1-005: ExternalType references module that doesn't declare that type in output
|
|
151
|
+
const externalTypeDeps = temporalCtx.externalTypeDeps || [];
|
|
152
|
+
for (const dep of externalTypeDeps) {
|
|
153
|
+
// Check if sourceModule actually declares this type in any activity output
|
|
154
|
+
const sourceActivities = activityRegistry.get(dep.sourceModule) || new Map();
|
|
155
|
+
let typeFound = false;
|
|
156
|
+
for (const [, actDef] of sourceActivities) {
|
|
157
|
+
for (const nested of actDef.nestedTypes || []) {
|
|
158
|
+
const nestedName = typeof nested === 'string' ? nested : nested.name;
|
|
159
|
+
if (pascalCase(nestedName) === pascalCase(dep.typeName)) {
|
|
160
|
+
typeFound = true;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (typeFound) break;
|
|
165
|
+
}
|
|
166
|
+
if (!typeFound) {
|
|
167
|
+
checks['T1-005'].findings.push(
|
|
168
|
+
finding(dep.consumerModule, `Activity '${dep.activityName}' usa externalType '${dep.typeName}' de módulo '${dep.sourceModule}', pero ese módulo no declara ese tipo en nestedTypes[] de ninguna actividad`, `módulo consumidor: ${dep.consumerModule}`)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── T2 — Workflow Design ──────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
for (const wf of rawWorkflows) {
|
|
176
|
+
const wfName = wf.name || '(unnamed)';
|
|
177
|
+
const triggerModule = wf.trigger && wf.trigger.module ? camelCase(wf.trigger.module) : null;
|
|
178
|
+
const steps = Array.isArray(wf.steps) ? wf.steps : [];
|
|
179
|
+
|
|
180
|
+
// T2-001: saga:true with no step having compensation
|
|
181
|
+
if (wf.saga === true) {
|
|
182
|
+
const hasAnyCompensation = steps.some((s) => !!s.compensation);
|
|
183
|
+
if (!hasAnyCompensation) {
|
|
184
|
+
checks['T2-001'].findings.push(
|
|
185
|
+
finding(wfName, `Workflow '${wfName}' declara saga:true pero ningún step tiene campo compensation — la saga no tiene rollback definido`, `workflow: ${wfName}`)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// T2-002: async step with compensation
|
|
191
|
+
for (let i = 0; i < steps.length; i++) {
|
|
192
|
+
const step = steps[i];
|
|
193
|
+
if (step.type === 'async' && step.compensation) {
|
|
194
|
+
checks['T2-002'].findings.push(
|
|
195
|
+
finding(wfName, `Paso ${i + 1} (${step.activity}): tipo 'async' (fire-and-forget) no puede tener compensation — los pasos async no bloquean y no son compensables`, `workflow: ${wfName}`)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// T2-003: trigger.on not matching any event[] in trigger module
|
|
201
|
+
if (wf.trigger && wf.trigger.on && triggerModule) {
|
|
202
|
+
const eventsForModule = domainEventsByModule.get(triggerModule) || new Map();
|
|
203
|
+
const triggerOn = wf.trigger.on;
|
|
204
|
+
let found = false;
|
|
205
|
+
for (const [, evDef] of eventsForModule) {
|
|
206
|
+
const nameMatch =
|
|
207
|
+
normalizeMethodName(evDef.name).includes(normalizeMethodName(triggerOn)) ||
|
|
208
|
+
normalizeMethodName(triggerOn).includes(normalizeMethodName(evDef.name));
|
|
209
|
+
// Also match via explicit triggers[] array declared in the event
|
|
210
|
+
const triggersMatch = (evDef.triggers || []).some(
|
|
211
|
+
(t) => normalizeMethodName(t) === normalizeMethodName(triggerOn)
|
|
212
|
+
);
|
|
213
|
+
if (nameMatch || triggersMatch) {
|
|
214
|
+
found = true;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Also check if it matches transition method names
|
|
219
|
+
// Use getDomainCfg to handle both kebab-case and camelCase dict keys
|
|
220
|
+
const domainCfg = getDomainCfg(domainConfigs, triggerModule) || {};
|
|
221
|
+
for (const agg of domainCfg.aggregates || []) {
|
|
222
|
+
for (const en of agg.enums || []) {
|
|
223
|
+
for (const tr of en.transitions || []) {
|
|
224
|
+
if (tr.method && normalizeMethodName(tr.method) === normalizeMethodName(triggerOn)) {
|
|
225
|
+
found = true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!found) {
|
|
231
|
+
checks['T2-003'].findings.push(
|
|
232
|
+
finding(wfName, `Trigger on:'${triggerOn}' del workflow '${wfName}' no coincide con ningún evento[] ni transición declarada en el módulo '${triggerModule}'`, `workflow: ${wfName}, módulo: ${triggerModule}`)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// T2-004: trigger module has no event with notifies pointing to this workflow
|
|
238
|
+
if (wf.trigger && wf.trigger.module && wf.name) {
|
|
239
|
+
const trigMod = camelCase(wf.trigger.module);
|
|
240
|
+
// Use getDomainCfg to handle both kebab-case and camelCase dict keys
|
|
241
|
+
const domainCfg = getDomainCfg(domainConfigs, trigMod) || {};
|
|
242
|
+
let notifiesFound = false;
|
|
243
|
+
for (const agg of domainCfg.aggregates || []) {
|
|
244
|
+
for (const ev of agg.events || []) {
|
|
245
|
+
for (const notif of ev.notifies || []) {
|
|
246
|
+
if (notif.workflow && normalizeMethodName(notif.workflow) === normalizeMethodName(wf.name)) {
|
|
247
|
+
notifiesFound = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (!notifiesFound) {
|
|
253
|
+
checks['T2-004'].findings.push(
|
|
254
|
+
finding(wfName, `El módulo trigger '${trigMod}' no tiene ningún event[] con notifies apuntando al workflow '${wf.name}' — considera añadir notifies:[{workflow: '${wf.name}'}] al evento correspondiente`, `workflow: ${wfName}`)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── T3 — Activity Quality ─────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
// Build a set of all activities used in any workflow (cross-module + local)
|
|
263
|
+
const usedActivities = new Set(); // "moduleName::ActivityName"
|
|
264
|
+
for (const wf of rawWorkflows) {
|
|
265
|
+
const triggerModule = wf.trigger && wf.trigger.module ? camelCase(wf.trigger.module) : null;
|
|
266
|
+
for (const step of Array.isArray(wf.steps) ? wf.steps : []) {
|
|
267
|
+
const actName = step.activity ? pascalCase(step.activity) : null;
|
|
268
|
+
if (!actName) continue;
|
|
269
|
+
const targetMod = step.target ? camelCase(step.target) : triggerModule;
|
|
270
|
+
usedActivities.add(`${targetMod}::${actName}`);
|
|
271
|
+
if (step.compensation) {
|
|
272
|
+
usedActivities.add(`${targetMod}::${pascalCase(step.compensation)}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Also count activities used in local workflows (declared in domain.yaml)
|
|
277
|
+
for (const [modName, localWfs] of Object.entries(localWorkflowsByModule)) {
|
|
278
|
+
for (const wf of localWfs) {
|
|
279
|
+
for (const step of wf.steps || []) {
|
|
280
|
+
if (step.activity) usedActivities.add(`${modName}::${pascalCase(step.activity)}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (const [modName, modActivities] of activityRegistry) {
|
|
286
|
+
for (const [actName, actDef] of modActivities) {
|
|
287
|
+
const key = `${modName}::${actName}`;
|
|
288
|
+
|
|
289
|
+
// T3-001: Activity not used in any workflow
|
|
290
|
+
if (!usedActivities.has(key)) {
|
|
291
|
+
checks['T3-001'].findings.push(
|
|
292
|
+
finding(modName, `Actividad '${actName}' del módulo '${modName}' no es usada en ningún workflow (ni cross-module ni local) — podría ser una actividad huérfana o pendiente de conectar`, `módulo: ${modName}`)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// T3-002: heavy activity without retryPolicy
|
|
297
|
+
if ((actDef.type || '').toLowerCase() === 'heavy' && !actDef.retryPolicy) {
|
|
298
|
+
checks['T3-002'].findings.push(
|
|
299
|
+
finding(modName, `Actividad heavy '${actName}' del módulo '${modName}' no declara retryPolicy — las actividades heavy (larga duración) deberían tener una política de reintentos`, `módulo: ${modName}`)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// T3-003: Compensation activity with output[] (compensations should not return data)
|
|
304
|
+
const isCompensation = actDef.isCompensation;
|
|
305
|
+
if (isCompensation && Array.isArray(actDef.output) && actDef.output.length > 0) {
|
|
306
|
+
checks['T3-003'].findings.push(
|
|
307
|
+
finding(modName, `Actividad de compensación '${actName}' del módulo '${modName}' declara output[] — las compensaciones no deben retornar datos ya que Temporal ignora su salida`, `módulo: ${modName}`)
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// T3-004: externalType module not found or type not declared in that module
|
|
314
|
+
for (const dep of externalTypeDeps) {
|
|
315
|
+
// getDomainCfg handles both kebab-case keys ('shopping-carts') and camelCase ('shoppingCarts')
|
|
316
|
+
if (!getDomainCfg(domainConfigs, dep.sourceModule)) {
|
|
317
|
+
checks['T3-004'].findings.push(
|
|
318
|
+
finding(dep.consumerModule, `ExternalType '${dep.typeName}' referencia módulo '${dep.sourceModule}' que no existe en el sistema o no tiene domain.yaml`, `módulo consumidor: ${dep.consumerModule}, actividad: ${dep.activityName}`)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── Assign severities ─────────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
const severityMap = {
|
|
326
|
+
'T1-001': 'error',
|
|
327
|
+
'T1-002': 'warning',
|
|
328
|
+
'T1-003': 'error',
|
|
329
|
+
'T1-004': 'error',
|
|
330
|
+
'T1-005': 'warning',
|
|
331
|
+
'T2-001': 'warning',
|
|
332
|
+
'T2-002': 'error',
|
|
333
|
+
'T2-003': 'warning',
|
|
334
|
+
'T2-004': 'info',
|
|
335
|
+
'T3-001': 'info',
|
|
336
|
+
'T3-002': 'warning',
|
|
337
|
+
'T3-003': 'warning',
|
|
338
|
+
'T3-004': 'error',
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
let errCount = 0, warnCount = 0, infoCount = 0, okCount = 0;
|
|
342
|
+
|
|
343
|
+
for (const [id, check] of Object.entries(checks)) {
|
|
344
|
+
if (check.findings.length > 0) {
|
|
345
|
+
check.severity = severityMap[id] || 'info';
|
|
346
|
+
if (check.severity === 'error') errCount++;
|
|
347
|
+
else if (check.severity === 'warning') warnCount++;
|
|
348
|
+
else infoCount++;
|
|
349
|
+
} else {
|
|
350
|
+
check.severity = 'ok';
|
|
351
|
+
okCount++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const categories = [
|
|
356
|
+
{
|
|
357
|
+
id: 'T1',
|
|
358
|
+
label: 'Contratos de Actividades',
|
|
359
|
+
checks: ['T1-001', 'T1-002', 'T1-003', 'T1-004', 'T1-005'].map((id) => ({ id, ...checks[id] })),
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: 'T2',
|
|
363
|
+
label: 'Diseño de Workflows',
|
|
364
|
+
checks: ['T2-001', 'T2-002', 'T2-003', 'T2-004'].map((id) => ({ id, ...checks[id] })),
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
id: 'T3',
|
|
368
|
+
label: 'Calidad de Actividades',
|
|
369
|
+
checks: ['T3-001', 'T3-002', 'T3-003', 'T3-004'].map((id) => ({ id, ...checks[id] })),
|
|
370
|
+
},
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
const total = okCount + errCount + warnCount * 0.5;
|
|
374
|
+
const score = total > 0 ? Math.round((okCount / total) * 100) : 100;
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
summary: { errors: errCount, warnings: warnCount, info: infoCount, ok: okCount },
|
|
378
|
+
categories,
|
|
379
|
+
score,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Builds a registry: moduleCamelCase → Map<ActivityPascalCase, activityDefinition>
|
|
387
|
+
* Also marks compensation activities (isCompensation=true) based on cross-references.
|
|
388
|
+
*/
|
|
389
|
+
function buildActivityRegistry(domainConfigs) {
|
|
390
|
+
const registry = new Map();
|
|
391
|
+
|
|
392
|
+
// First pass: collect all activities
|
|
393
|
+
for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
|
|
394
|
+
if (!domainCfg || !Array.isArray(domainCfg.activities)) continue;
|
|
395
|
+
const modKey = camelCase(modName);
|
|
396
|
+
const modMap = new Map();
|
|
397
|
+
for (const act of domainCfg.activities) {
|
|
398
|
+
const actKey = pascalCase(act.name);
|
|
399
|
+
modMap.set(actKey, { ...act, isCompensation: false });
|
|
400
|
+
}
|
|
401
|
+
registry.set(modKey, modMap);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Second pass: mark compensation activities
|
|
405
|
+
for (const [, modMap] of registry) {
|
|
406
|
+
for (const [, actDef] of modMap) {
|
|
407
|
+
if (actDef.compensation) {
|
|
408
|
+
const compName = pascalCase(actDef.compensation);
|
|
409
|
+
if (modMap.has(compName)) {
|
|
410
|
+
modMap.get(compName).isCompensation = true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return registry;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Builds: moduleCamelCase → Map<normalizedEventName, { name, fields[], notifies[] }>
|
|
421
|
+
*/
|
|
422
|
+
function buildDomainEventsByModule(domainConfigs) {
|
|
423
|
+
const result = new Map();
|
|
424
|
+
for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
|
|
425
|
+
const modKey = camelCase(modName);
|
|
426
|
+
const evMap = new Map();
|
|
427
|
+
for (const agg of domainCfg?.aggregates || []) {
|
|
428
|
+
for (const ev of agg.events || []) {
|
|
429
|
+
evMap.set(normalizeMethodName(ev.name), {
|
|
430
|
+
name: ev.name,
|
|
431
|
+
fields: ev.fields || [],
|
|
432
|
+
notifies: ev.notifies || [],
|
|
433
|
+
triggers: ev.triggers || [],
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
result.set(modKey, evMap);
|
|
438
|
+
}
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Builds: moduleCamelCase → workflow[] (local workflows in domain.yaml)
|
|
444
|
+
*/
|
|
445
|
+
function buildLocalWorkflowsByModule(domainConfigs) {
|
|
446
|
+
const result = {};
|
|
447
|
+
for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
|
|
448
|
+
if (domainCfg && Array.isArray(domainCfg.workflows) && domainCfg.workflows.length > 0) {
|
|
449
|
+
result[camelCase(modName)] = domainCfg.workflows;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function camelCase(str) {
|
|
456
|
+
if (!str) return '';
|
|
457
|
+
return str
|
|
458
|
+
.replace(/[-_ ]+(.)/g, (_, c) => c.toUpperCase())
|
|
459
|
+
.replace(/^(.)/, (c) => c.toLowerCase());
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Converts camelCase/PascalCase to kebab-case.
|
|
464
|
+
* e.g. shoppingCarts → shopping-carts
|
|
465
|
+
*/
|
|
466
|
+
function kebabCase(str) {
|
|
467
|
+
if (!str) return '';
|
|
468
|
+
return str
|
|
469
|
+
.replace(/([A-Z])/g, '-$1')
|
|
470
|
+
.toLowerCase()
|
|
471
|
+
.replace(/^-/, '');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Looks up domainConfigs by camelCase OR kebab-case key.
|
|
476
|
+
* domainConfigs keys are raw filenames (e.g. 'shopping-carts'),
|
|
477
|
+
* but callers typically compute camelCase module names.
|
|
478
|
+
*/
|
|
479
|
+
function getDomainCfg(domainConfigs, moduleName) {
|
|
480
|
+
return domainConfigs[moduleName]
|
|
481
|
+
|| domainConfigs[kebabCase(moduleName)]
|
|
482
|
+
|| domainConfigs[camelCase(moduleName)]
|
|
483
|
+
|| null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function pascalCase(str) {
|
|
487
|
+
if (!str) return '';
|
|
488
|
+
const c = camelCase(str);
|
|
489
|
+
return c.charAt(0).toUpperCase() + c.slice(1);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function normalizeMethodName(str) {
|
|
493
|
+
if (!str) return '';
|
|
494
|
+
return str.toLowerCase().replace(/[-_\s]/g, '').replace(/event$/, '');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = { validateTemporal };
|
package/src/utils/validator.js
CHANGED
|
@@ -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
|
/**
|
|
5
6
|
* Validate project name
|
|
@@ -107,7 +108,8 @@ async function isEva4jProject(dir) {
|
|
|
107
108
|
* @returns {boolean} True if module exists
|
|
108
109
|
*/
|
|
109
110
|
async function moduleExists(projectDir, packagePath, moduleName) {
|
|
110
|
-
const
|
|
111
|
+
const normalized = toCamelCase(moduleName);
|
|
112
|
+
const modulePath = path.join(projectDir, 'src', 'main', 'java', packagePath, normalized);
|
|
111
113
|
return await fs.pathExists(modulePath);
|
|
112
114
|
}
|
|
113
115
|
|