eva4j 1.0.16 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/AGENTS.md +218 -5
  2. package/DOMAIN_YAML_GUIDE.md +185 -2
  3. package/FUTURE_FEATURES.md +33 -52
  4. package/docs/commands/EVALUATE_SYSTEM.md +18 -2
  5. package/examples/domain-events.yaml +26 -0
  6. package/examples/domain-read-models.yaml +113 -0
  7. package/package.json +1 -1
  8. package/read-model-spec.md +664 -0
  9. package/src/agents/design-reviewer.agent.md +3 -0
  10. package/src/commands/generate-entities.js +254 -10
  11. package/src/commands/generate-http-exchange.js +3 -0
  12. package/src/commands/generate-kafka-event.js +3 -0
  13. package/src/commands/generate-kafka-listener.js +3 -0
  14. package/src/commands/generate-record.js +2 -2
  15. package/src/commands/generate-resource.js +4 -1
  16. package/src/commands/generate-temporal-activity.js +4 -1
  17. package/src/commands/generate-temporal-flow.js +4 -1
  18. package/src/commands/generate-usecase.js +4 -1
  19. package/src/skills/build-system-yaml/SKILL.md +122 -1
  20. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +205 -24
  21. package/src/skills/build-system-yaml/references/module-spec.md +33 -1
  22. package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
  23. package/src/utils/config-manager.js +4 -2
  24. package/src/utils/domain-validator.js +230 -14
  25. package/src/utils/naming.js +10 -0
  26. package/src/utils/validator.js +3 -1
  27. package/src/utils/yaml-to-entity.js +272 -3
  28. package/templates/aggregate/AggregateRepository.java.ejs +4 -0
  29. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
  30. package/templates/aggregate/AggregateRoot.java.ejs +38 -4
  31. package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -2
  32. package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
  33. package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
  34. package/templates/read-model/ReadModelDomain.java.ejs +46 -0
  35. package/templates/read-model/ReadModelJpa.java.ejs +58 -0
  36. package/templates/read-model/ReadModelJpaRepository.java.ejs +11 -0
  37. package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
  38. package/templates/read-model/ReadModelRepository.java.ejs +42 -0
  39. package/templates/read-model/ReadModelRepositoryImpl.java.ejs +81 -0
  40. package/templates/read-model/ReadModelSyncHandler.java.ejs +52 -0
  41. package/test-c2010.js +49 -0
  42. package/test-update-compat.js +109 -0
  43. package/test-update-lifecycle.js +121 -0
@@ -5,9 +5,9 @@ const fs = require('fs-extra');
5
5
  const inquirer = require('inquirer');
6
6
  const ConfigManager = require('../utils/config-manager');
7
7
  const { isEva4jProject } = require('../utils/validator');
8
- const { toPackagePath, toCamelCase, toKebabCase, toPascalCase, getApplicationClassName, pluralizeWord } = require('../utils/naming');
8
+ const { toPackagePath, toCamelCase, toKebabCase, toPascalCase, getApplicationClassName, pluralizeWord, singularizeWord } = require('../utils/naming');
9
9
  const { renderAndWrite, renderTemplate } = require('../utils/template-engine');
10
- const { parseDomainYaml, generateEntityImports, generateValidationImports } = require('../utils/yaml-to-entity');
10
+ const { parseDomainYaml, generateEntityImports, generateValidationImports, resolveLifecycleEventArgs, resolveEventArgs } = require('../utils/yaml-to-entity');
11
11
  const { createOrUpdateUrlsConfig, ensureUrlsImport } = require('./generate-http-exchange');
12
12
  const SharedGenerator = require('../generators/shared-generator');
13
13
  const ChecksumManager = require('../utils/checksum-manager');
@@ -216,6 +216,9 @@ async function generateEntitiesCommand(moduleName, options = {}) {
216
216
  const { packageName, artifactId } = projectConfig;
217
217
  const packagePath = toPackagePath(packageName);
218
218
 
219
+ // Normalise module name to camelCase (system.yaml uses kebab-case, .eva4j.json stores camelCase)
220
+ moduleName = toCamelCase(moduleName);
221
+
219
222
  // Validate module exists
220
223
  if (!(await configManager.moduleExists(moduleName))) {
221
224
  console.error(chalk.red(`❌ Module '${moduleName}' not found`));
@@ -244,7 +247,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
244
247
 
245
248
  try {
246
249
  // Parse domain.yaml
247
- const { aggregates, allEnums, endpoints, listeners, ports } = await parseDomainYaml(domainYamlPath, packageName, moduleName);
250
+ const { aggregates, allEnums, endpoints, listeners, ports, readModels } = await parseDomainYaml(domainYamlPath, packageName, moduleName);
248
251
 
249
252
  spinner.succeed(chalk.green(`Found ${aggregates.length} aggregate(s) and ${allEnums.length} enum(s)`));
250
253
 
@@ -273,7 +276,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
273
276
  }
274
277
 
275
278
  // Detect installed message broker for auto-wiring integration events
276
- const installedBroker = (hasDomainEventsInModule || (listeners && listeners.length > 0))
279
+ const installedBroker = (hasDomainEventsInModule || (listeners && listeners.length > 0) || (readModels && readModels.length > 0))
277
280
  ? await getInstalledBroker(configManager)
278
281
  : null;
279
282
  // When brokerMode:'mock' is requested AND a broker is installed, use mock Spring-Events adapter
@@ -369,6 +372,9 @@ async function generateEntitiesCommand(moduleName, options = {}) {
369
372
  const { name: aggregateName, rootEntity, secondaryEntities, valueObjects } = aggregate;
370
373
 
371
374
  // 1. Generate Domain Aggregate Root
375
+ const resolvedLifecycle = resolveLifecycleEventArgs(
376
+ aggregate.lifecycleEventsMap || {}, rootEntity.name, rootEntity.fields, valueObjects
377
+ );
372
378
  const rootDomainContext = {
373
379
  packageName,
374
380
  moduleName,
@@ -381,7 +387,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
381
387
  auditable: rootEntity.auditable,
382
388
  hasSoftDelete: rootEntity.hasSoftDelete || false,
383
389
  domainEvents: aggregate.domainEvents || [],
384
- triggeredEventsMap: aggregate.triggeredEventsMap || {}
390
+ triggeredEventsMap: aggregate.triggeredEventsMap || {},
391
+ lifecycleEventsMap: resolvedLifecycle
385
392
  };
386
393
 
387
394
  await renderAndWrite(
@@ -393,6 +400,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
393
400
  generatedFiles.push({ type: 'Domain Entity', name: rootEntity.name, path: `${moduleName}/domain/models/entities/${rootEntity.name}.java` });
394
401
 
395
402
  // 2. Generate JPA Aggregate Root
403
+ const hasCreateLifecycle = !!(aggregate.lifecycleEventsMap && aggregate.lifecycleEventsMap.create && aggregate.lifecycleEventsMap.create.length > 0);
396
404
  const rootJpaContext = {
397
405
  packageName,
398
406
  moduleName,
@@ -405,7 +413,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
405
413
  enums: allEnums,
406
414
  auditable: rootEntity.auditable,
407
415
  audit: rootEntity.audit,
408
- hasSoftDelete: rootEntity.hasSoftDelete || false
416
+ hasSoftDelete: rootEntity.hasSoftDelete || false,
417
+ hasCreateLifecycle
409
418
  };
410
419
 
411
420
  await renderAndWrite(
@@ -524,6 +533,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
524
533
  moduleName,
525
534
  rootEntity,
526
535
  hasSoftDelete: rootEntity.hasSoftDelete || false,
536
+ hasDomainEvents: (aggregate.domainEvents || []).length > 0,
537
+ hasDeleteLifecycle: !!(aggregate.lifecycleEventsMap || {}).delete,
527
538
  findByOps: []
528
539
  };
529
540
 
@@ -551,6 +562,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
551
562
  aggregateName,
552
563
  rootEntity,
553
564
  hasDomainEvents: (aggregate.domainEvents || []).length > 0,
565
+ hasDeleteLifecycle: !!(aggregate.lifecycleEventsMap || {}).delete,
554
566
  hasSoftDelete: rootEntity.hasSoftDelete || false,
555
567
  findByOps: []
556
568
  };
@@ -1072,6 +1084,190 @@ async function generateEntitiesCommand(moduleName, options = {}) {
1072
1084
  spinner.succeed(chalk.green(`HTTP ports generated! ✨`));
1073
1085
  }
1074
1086
 
1087
+ // ── Generate Read Models (local projections of external data) ────────────
1088
+ if (readModels && readModels.length > 0) {
1089
+ if (broker === 'kafka' || broker === 'mock') {
1090
+ spinner.start(`Generating ${readModels.length} read model(s)...`);
1091
+
1092
+ const readModelTemplatesDir = path.join(__dirname, '..', '..', 'templates', 'read-model');
1093
+
1094
+ for (const rm of readModels) {
1095
+ const rmContext = {
1096
+ packageName,
1097
+ moduleName,
1098
+ ...rm
1099
+ };
1100
+
1101
+ // 1. Domain read model class
1102
+ const domainPath = path.join(
1103
+ moduleBasePath, 'domain', 'models', 'readmodels',
1104
+ `${rm.domainClassName}.java`
1105
+ );
1106
+ await renderAndWrite(
1107
+ path.join(readModelTemplatesDir, 'ReadModelDomain.java.ejs'),
1108
+ domainPath,
1109
+ rmContext,
1110
+ writeOptions
1111
+ );
1112
+ generatedFiles.push({
1113
+ type: 'Read Model',
1114
+ name: rm.domainClassName,
1115
+ path: `${moduleName}/domain/models/readmodels/${rm.domainClassName}.java`
1116
+ });
1117
+
1118
+ // 2. JPA entity
1119
+ const jpaPath = path.join(
1120
+ moduleBasePath, 'infrastructure', 'database', 'entities',
1121
+ `${rm.jpaEntityName}.java`
1122
+ );
1123
+ await renderAndWrite(
1124
+ path.join(readModelTemplatesDir, 'ReadModelJpa.java.ejs'),
1125
+ jpaPath,
1126
+ rmContext,
1127
+ writeOptions
1128
+ );
1129
+ generatedFiles.push({
1130
+ type: 'Read Model',
1131
+ name: rm.jpaEntityName,
1132
+ path: `${moduleName}/infrastructure/database/entities/${rm.jpaEntityName}.java`
1133
+ });
1134
+
1135
+ // 3. JPA repository
1136
+ const jpaRepoPath = path.join(
1137
+ moduleBasePath, 'infrastructure', 'database', 'repositories',
1138
+ `${rm.jpaRepositoryName}.java`
1139
+ );
1140
+ await renderAndWrite(
1141
+ path.join(readModelTemplatesDir, 'ReadModelJpaRepository.java.ejs'),
1142
+ jpaRepoPath,
1143
+ rmContext,
1144
+ writeOptions
1145
+ );
1146
+ generatedFiles.push({
1147
+ type: 'Read Model',
1148
+ name: rm.jpaRepositoryName,
1149
+ path: `${moduleName}/infrastructure/database/repositories/${rm.jpaRepositoryName}.java`
1150
+ });
1151
+
1152
+ // 4. Domain repository interface
1153
+ const repoPath = path.join(
1154
+ moduleBasePath, 'domain', 'repositories',
1155
+ `${rm.repositoryName}.java`
1156
+ );
1157
+ await renderAndWrite(
1158
+ path.join(readModelTemplatesDir, 'ReadModelRepository.java.ejs'),
1159
+ repoPath,
1160
+ rmContext,
1161
+ writeOptions
1162
+ );
1163
+ generatedFiles.push({
1164
+ type: 'Read Model',
1165
+ name: rm.repositoryName,
1166
+ path: `${moduleName}/domain/repositories/${rm.repositoryName}.java`
1167
+ });
1168
+
1169
+ // 5. Repository implementation
1170
+ const repoImplPath = path.join(
1171
+ moduleBasePath, 'infrastructure', 'database', 'repositories',
1172
+ `${rm.repositoryImplName}.java`
1173
+ );
1174
+ await renderAndWrite(
1175
+ path.join(readModelTemplatesDir, 'ReadModelRepositoryImpl.java.ejs'),
1176
+ repoImplPath,
1177
+ rmContext,
1178
+ writeOptions
1179
+ );
1180
+ generatedFiles.push({
1181
+ type: 'Read Model',
1182
+ name: rm.repositoryImplName,
1183
+ path: `${moduleName}/infrastructure/database/repositories/${rm.repositoryImplName}.java`
1184
+ });
1185
+
1186
+ // 6. Sync handler
1187
+ const handlerPath = path.join(
1188
+ moduleBasePath, 'application', 'usecases',
1189
+ `${rm.syncHandlerName}.java`
1190
+ );
1191
+ await renderAndWrite(
1192
+ path.join(readModelTemplatesDir, 'ReadModelSyncHandler.java.ejs'),
1193
+ handlerPath,
1194
+ rmContext,
1195
+ writeOptions
1196
+ );
1197
+ generatedFiles.push({
1198
+ type: 'Read Model',
1199
+ name: rm.syncHandlerName,
1200
+ path: `${moduleName}/application/usecases/${rm.syncHandlerName}.java`
1201
+ });
1202
+
1203
+ // 7. Per syncedBy event: integration event + Kafka listener + topic registration
1204
+ for (const sync of rm.syncedBy) {
1205
+ const listenerContext = {
1206
+ packageName,
1207
+ moduleName,
1208
+ ...sync,
1209
+ syncHandlerName: rm.syncHandlerName,
1210
+ domainClassName: rm.domainClassName,
1211
+ repositoryName: rm.repositoryName
1212
+ };
1213
+
1214
+ // Integration event (reuse if already exists from listeners: section)
1215
+ // Only generate for UPSERT — DELETE/SOFT_DELETE bypass IntegrationEvent entirely
1216
+ const integrationEventPath = path.join(
1217
+ moduleBasePath, 'application', 'events',
1218
+ `${sync.integrationEventClassName}.java`
1219
+ );
1220
+ if (sync.action === 'UPSERT' && !(await fs.pathExists(integrationEventPath))) {
1221
+ await renderAndWrite(
1222
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerIntegrationEvent.java.ejs'),
1223
+ integrationEventPath,
1224
+ {
1225
+ packageName,
1226
+ moduleName,
1227
+ integrationEventClassName: sync.integrationEventClassName,
1228
+ topicConstant: sync.topicConstant,
1229
+ producer: rm.sourceModule,
1230
+ fields: sync.fields
1231
+ },
1232
+ writeOptions
1233
+ );
1234
+ generatedFiles.push({
1235
+ type: 'Read Model Integration Event',
1236
+ name: sync.integrationEventClassName,
1237
+ path: `${moduleName}/application/events/${sync.integrationEventClassName}.java`
1238
+ });
1239
+ }
1240
+
1241
+ // Kafka listener
1242
+ if (broker === 'kafka') {
1243
+ const kafkaListenerPath = path.join(
1244
+ moduleBasePath, 'infrastructure', 'kafkaListener',
1245
+ `${sync.listenerClassName}.java`
1246
+ );
1247
+ await renderAndWrite(
1248
+ path.join(readModelTemplatesDir, 'ReadModelKafkaListener.java.ejs'),
1249
+ kafkaListenerPath,
1250
+ listenerContext,
1251
+ writeOptions
1252
+ );
1253
+ generatedFiles.push({
1254
+ type: 'Read Model Kafka Listener',
1255
+ name: sync.listenerClassName,
1256
+ path: `${moduleName}/infrastructure/kafkaListener/${sync.listenerClassName}.java`
1257
+ });
1258
+
1259
+ // Register topic in kafka.yaml
1260
+ await updateKafkaYml(projectDir, sync.topicKey, sync.topicConstant);
1261
+ }
1262
+ }
1263
+ }
1264
+
1265
+ spinner.succeed(chalk.green(`Read models generated! ✨`));
1266
+ } else if (readModels.length > 0) {
1267
+ console.log(chalk.yellow(`⚠ readModels: section found but no broker is installed. Run 'eva add kafka-client' to generate listener classes.`));
1268
+ }
1269
+ }
1270
+
1075
1271
  console.log(chalk.blue('\n📦 Generated files:'));
1076
1272
  const groupedFiles = generatedFiles.reduce((acc, file) => {
1077
1273
  if (!acc[file.type]) acc[file.type] = [];
@@ -1113,8 +1309,18 @@ async function generateEntitiesCommand(moduleName, options = {}) {
1113
1309
  }
1114
1310
  }
1115
1311
  if (!op._ownerAggregate) {
1116
- // True scaffold: heuristic — find aggregate whose name appears inside the use case name
1117
- const matched = aggregates.find(agg =>
1312
+ // True scaffold: heuristic — strip CRUD verb prefix and singularize to find
1313
+ // the best aggregate match via prefix comparison.
1314
+ // e.g. "FindAllGuarantees" → suffix "Guarantees" → singular "Guarantee"
1315
+ // → "GuaranteeCatalog".startsWith("guarantee") ✓
1316
+ const ucSuffix = op.useCase.replace(/^(FindAll|GetAll|Get|Create|Update|Delete|Add|Remove)/, '');
1317
+ const singularSuffix = ucSuffix ? singularizeWord(ucSuffix).toLowerCase() : '';
1318
+ const matched = (singularSuffix &&
1319
+ aggregates.find(agg => {
1320
+ const aggLower = agg.name.toLowerCase();
1321
+ return aggLower.startsWith(singularSuffix) || singularSuffix.startsWith(aggLower);
1322
+ })
1323
+ ) || aggregates.find(agg =>
1118
1324
  op.useCase.toLowerCase().includes(agg.name.toLowerCase())
1119
1325
  );
1120
1326
  const owner = matched || aggregates[0];
@@ -1434,6 +1640,34 @@ function classifyUseCase(op, aggregateName, aggregate) {
1434
1640
  }
1435
1641
  }
1436
1642
 
1643
+ // 5. Fuzzy FindAll — useCase starts with "FindAll" and the singular of the
1644
+ // suffix is a prefix of the aggregate name (or vice-versa).
1645
+ // e.g. FindAllGuarantees → singular "Guarantee" → "GuaranteeCatalog" starts with it ✓
1646
+ if (op.useCase.startsWith('FindAll')) {
1647
+ const suffix = op.useCase.slice(7);
1648
+ if (suffix) {
1649
+ const singularSuffix = singularizeWord(suffix).toLowerCase();
1650
+ const aggLower = aggregateName.toLowerCase();
1651
+ if (aggLower.startsWith(singularSuffix) || singularSuffix.startsWith(aggLower)) {
1652
+ return { category: 'standard', variant: 'findAll' };
1653
+ }
1654
+ }
1655
+ }
1656
+
1657
+ // 6. Fuzzy Get — useCase starts with "Get" and the suffix matches the
1658
+ // aggregate name as a prefix (or vice-versa).
1659
+ // e.g. GetCatTariff → "CatTariff" matches aggregate "CatTariff" ✓
1660
+ if (op.useCase.startsWith('Get') && !op.useCase.startsWith('GetAll')) {
1661
+ const suffix = op.useCase.slice(3);
1662
+ if (suffix) {
1663
+ const suffixLower = suffix.toLowerCase();
1664
+ const aggLower = aggregateName.toLowerCase();
1665
+ if (aggLower.startsWith(suffixLower) || suffixLower.startsWith(aggLower)) {
1666
+ return { category: 'standard', variant: 'getById' };
1667
+ }
1668
+ }
1669
+ }
1670
+
1437
1671
  return { category: 'scaffold' };
1438
1672
  }
1439
1673
 
@@ -1552,13 +1786,18 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
1552
1786
  const oneToManyRelationshipsApp = enrichRelsWithSchemaExamples(
1553
1787
  transformRelsForApp(oneToManyRelationships, validatedVoNames), localAllEnums, valueObjects);
1554
1788
 
1789
+ const resolvedLifecycleEndpoints = resolveLifecycleEventArgs(
1790
+ aggregate.lifecycleEventsMap || {}, aggregateName, rootEntity.fields, valueObjects
1791
+ );
1555
1792
  const baseContext = {
1556
1793
  packageName, moduleName, aggregateName, aggregateNamePlural, rootEntity, secondaryEntities,
1557
1794
  responseFields, responseSecondaryEntities, idType,
1558
1795
  commandFields: commandFieldsApp, oneToManyRelationships, oneToOneRelationships,
1559
1796
  hasValueObjects, hasEnums, imports: rootEntity.imports,
1560
1797
  resourceNameCamel, resourceNameKebab,
1561
- hasSoftDelete: rootEntity.hasSoftDelete || false
1798
+ hasSoftDelete: rootEntity.hasSoftDelete || false,
1799
+ domainEvents: aggregate.domainEvents || [],
1800
+ lifecycleEventsMap: resolvedLifecycleEndpoints
1562
1801
  };
1563
1802
 
1564
1803
  // ── Step 1: Validated VO Dtos ────────────────────────────────────────
@@ -2065,6 +2304,9 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
2065
2304
  transformRelsForApp(oneToManyRelationships, validatedVoNames), localAllEnums, valueObjects);
2066
2305
 
2067
2306
  // Base context for all templates
2307
+ const resolvedLifecycleCrud = resolveLifecycleEventArgs(
2308
+ aggregate.lifecycleEventsMap || {}, aggregateName, rootEntity.fields, valueObjects
2309
+ );
2068
2310
  const baseContext = {
2069
2311
  packageName,
2070
2312
  moduleName,
@@ -2085,7 +2327,9 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
2085
2327
  resourceNameCamel,
2086
2328
  resourceNameKebab,
2087
2329
  hasSoftDelete: rootEntity.hasSoftDelete || false,
2088
- hasCreateOperation: true // In interactive CRUD flow, Create is always generated
2330
+ hasCreateOperation: true, // In interactive CRUD flow, Create is always generated
2331
+ domainEvents: aggregate.domainEvents || [],
2332
+ lifecycleEventsMap: resolvedLifecycleCrud
2089
2333
  };
2090
2334
 
2091
2335
  // 0. Generate Create<VoName>Dto for validated Value Objects
@@ -32,6 +32,9 @@ async function generateHttpExchangeCommand(moduleName, portName) {
32
32
  const { packageName } = projectConfig;
33
33
  const packagePath = toPackagePath(packageName);
34
34
 
35
+ // Normalise module name to camelCase (system.yaml uses kebab-case, .eva4j.json stores camelCase)
36
+ moduleName = toCamelCase(moduleName);
37
+
35
38
  // Validate module exists
36
39
  if (!(await configManager.moduleExists(moduleName))) {
37
40
  console.error(chalk.red(`❌ Module '${moduleName}' not found in project configuration`));
@@ -40,6 +40,9 @@ async function generateKafkaEventCommand(moduleName, eventName) {
40
40
  const { packageName } = projectConfig;
41
41
  const packagePath = toPackagePath(packageName);
42
42
 
43
+ // Normalise module name to camelCase (system.yaml uses kebab-case, .eva4j.json stores camelCase)
44
+ moduleName = toCamelCase(moduleName);
45
+
43
46
  // Validate module exists
44
47
  if (!(await configManager.moduleExists(moduleName))) {
45
48
  console.error(chalk.red(`❌ Module '${moduleName}' not found in project configuration`));
@@ -36,6 +36,9 @@ async function generateKafkaListenerCommand(moduleName) {
36
36
  const { packageName, projectName } = projectConfig;
37
37
  const packagePath = toPackagePath(packageName);
38
38
 
39
+ // Normalise module name to camelCase (system.yaml uses kebab-case, .eva4j.json stores camelCase)
40
+ moduleName = toCamelCase(moduleName);
41
+
39
42
  // Validate module exists
40
43
  if (!(await configManager.moduleExists(moduleName))) {
41
44
  console.error(chalk.red(`❌ Module '${moduleName}' not found in project`));
@@ -6,7 +6,7 @@ const inquirer = require('inquirer');
6
6
  const clipboardy = require('clipboardy');
7
7
  const ConfigManager = require('../utils/config-manager');
8
8
  const { isEva4jProject } = require('../utils/validator');
9
- const { toPackagePath, toPascalCase } = require('../utils/naming');
9
+ const { toPackagePath, toPascalCase, toCamelCase } = require('../utils/naming');
10
10
  const { renderAndWrite } = require('../utils/template-engine');
11
11
  const { parseJsonToRecords } = require('../utils/json-to-java');
12
12
 
@@ -107,7 +107,7 @@ async function generateRecordCommand(options = {}) {
107
107
  const { recordName, moduleName, targetFolder } = answers;
108
108
 
109
109
  // Validate module exists in filesystem
110
- const modulePath = path.join(projectDir, 'src', 'main', 'java', packagePath, moduleName);
110
+ const modulePath = path.join(projectDir, 'src', 'main', 'java', packagePath, toCamelCase(moduleName));
111
111
  if (!(await fs.pathExists(modulePath))) {
112
112
  console.error(chalk.red(`❌ Module '${moduleName}' not found in filesystem`));
113
113
  process.exit(1);
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const fs = require('fs-extra');
6
6
  const ConfigManager = require('../utils/config-manager');
7
7
  const { isEva4jProject } = require('../utils/validator');
8
- const { toPackagePath, toPascalCase, pluralizeWord, toKebabCase } = require('../utils/naming');
8
+ const { toPackagePath, toPascalCase, pluralizeWord, toKebabCase, toCamelCase } = require('../utils/naming');
9
9
  const { renderAndWrite } = require('../utils/template-engine');
10
10
  const ChecksumManager = require('../utils/checksum-manager');
11
11
 
@@ -30,6 +30,9 @@ async function generateResourceCommand(moduleName, options = {}) {
30
30
  const { packageName } = projectConfig;
31
31
  const packagePath = toPackagePath(packageName);
32
32
 
33
+ // Normalise module name to camelCase (system.yaml uses kebab-case, .eva4j.json stores camelCase)
34
+ moduleName = toCamelCase(moduleName);
35
+
33
36
  // Validate module exists
34
37
  if (!(await configManager.moduleExists(moduleName))) {
35
38
  console.error(chalk.red(`❌ Module '${moduleName}' not found in project`));
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const fs = require('fs-extra');
6
6
  const ConfigManager = require('../utils/config-manager');
7
7
  const { isEva4jProject, moduleExists } = require('../utils/validator');
8
- const { toPackagePath, toPascalCase } = require('../utils/naming');
8
+ const { toPackagePath, toPascalCase, toCamelCase } = require('../utils/naming');
9
9
  const { renderAndWrite } = require('../utils/template-engine');
10
10
  const ChecksumManager = require('../utils/checksum-manager');
11
11
 
@@ -46,6 +46,9 @@ async function generateTemporalActivityCommand(moduleName, activityName, options
46
46
  const { packageName } = projectConfig;
47
47
  const packagePath = toPackagePath(packageName);
48
48
 
49
+ // Normalise module name to camelCase (system.yaml uses kebab-case, .eva4j.json stores camelCase)
50
+ moduleName = toCamelCase(moduleName);
51
+
49
52
  if (!(await configManager.moduleExists(moduleName))) {
50
53
  console.error(chalk.red(`❌ Module '${moduleName}' does not exist`));
51
54
  console.error(chalk.gray(`Create it first using: eva add module ${moduleName}`));
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const fs = require('fs-extra');
6
6
  const ConfigManager = require('../utils/config-manager');
7
7
  const { isEva4jProject, moduleExists } = require('../utils/validator');
8
- const { toPackagePath, toPascalCase } = require('../utils/naming');
8
+ const { toPackagePath, toPascalCase, toCamelCase } = require('../utils/naming');
9
9
  const { renderAndWrite } = require('../utils/template-engine');
10
10
  const ChecksumManager = require('../utils/checksum-manager');
11
11
 
@@ -48,6 +48,9 @@ async function generateTemporalFlowCommand(moduleName, flowName, options = {}) {
48
48
  const { packageName } = projectConfig;
49
49
  const packagePath = toPackagePath(packageName);
50
50
 
51
+ // Normalise module name to camelCase (system.yaml uses kebab-case, .eva4j.json stores camelCase)
52
+ moduleName = toCamelCase(moduleName);
53
+
51
54
  if (!(await configManager.moduleExists(moduleName))) {
52
55
  console.error(chalk.red(`❌ Module '${moduleName}' does not exist`));
53
56
  console.error(chalk.gray(`Create it first using: eva add module ${moduleName}`));
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const fs = require('fs-extra');
6
6
  const ConfigManager = require('../utils/config-manager');
7
7
  const { isEva4jProject, moduleExists } = require('../utils/validator');
8
- const { toPackagePath, toPascalCase } = require('../utils/naming');
8
+ const { toPackagePath, toPascalCase, toCamelCase } = require('../utils/naming');
9
9
  const { renderAndWrite } = require('../utils/template-engine');
10
10
  const ChecksumManager = require('../utils/checksum-manager');
11
11
 
@@ -32,6 +32,9 @@ async function generateUsecaseCommand(moduleName, usecaseName, options = {}) {
32
32
  const { packageName } = projectConfig;
33
33
  const packagePath = toPackagePath(packageName);
34
34
 
35
+ // Normalise module name to camelCase (system.yaml uses kebab-case, .eva4j.json stores camelCase)
36
+ moduleName = toCamelCase(moduleName);
37
+
35
38
  // Validate module exists
36
39
  if (!(await configManager.moduleExists(moduleName))) {
37
40
  console.error(chalk.red(`❌ Module '${moduleName}' not found in project configuration`));