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
|
@@ -10,6 +10,7 @@ const ora = require('ora');
|
|
|
10
10
|
|
|
11
11
|
const { validateSystem } = require('../utils/system-validator');
|
|
12
12
|
const { validateDomain } = require('../utils/domain-validator');
|
|
13
|
+
const { validateTemporal } = require('../utils/temporal-validator');
|
|
13
14
|
|
|
14
15
|
// ── Module icon heuristic ────────────────────────────────────────────────────
|
|
15
16
|
|
|
@@ -207,25 +208,562 @@ function buildEventFlows(systemConfig, modulesMap) {
|
|
|
207
208
|
return flows;
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
// ── Temporal helpers ──────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
function _camelCase(str) {
|
|
214
|
+
if (!str) return '';
|
|
215
|
+
return str.replace(/[-_ ]+(.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (c) => c.toLowerCase());
|
|
216
|
+
}
|
|
217
|
+
function _pascalCase(str) {
|
|
218
|
+
if (!str) return '';
|
|
219
|
+
const c = _camelCase(str);
|
|
220
|
+
return c.charAt(0).toUpperCase() + c.slice(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Infers a human-readable role description for an activity step.
|
|
225
|
+
* Priority: domain.yaml description → auto-infer from name + type context.
|
|
226
|
+
*/
|
|
227
|
+
function inferActivityRole(actName, actDef, step, wfContext) {
|
|
228
|
+
if (actDef && actDef.description) return actDef.description;
|
|
229
|
+
|
|
230
|
+
const name = (actName || '').toLowerCase();
|
|
231
|
+
const isCompensation = actDef && actDef.isCompensation;
|
|
232
|
+
|
|
233
|
+
// Compensation activities
|
|
234
|
+
if (isCompensation || /^(release|refund|cancel|undo|revert|restore|rollback|mark.*cancel)/i.test(actName)) {
|
|
235
|
+
return `COMPENSATE: Deshace los efectos de la actividad principal en caso de fallo del workflow`;
|
|
236
|
+
}
|
|
237
|
+
// Async notification activities
|
|
238
|
+
if (step && step.type === 'async') {
|
|
239
|
+
return `NOTIFY: Envía notificación async (fire-and-forget) — no bloquea el workflow`;
|
|
240
|
+
}
|
|
241
|
+
// Read activities
|
|
242
|
+
if (/^(get|find|fetch|load|read|query|search|list)/i.test(actName)) {
|
|
243
|
+
const target = _pascalCase((step && step.target) || '');
|
|
244
|
+
return `READ: Obtiene datos de ${target} necesarios para los pasos siguientes del workflow`;
|
|
245
|
+
}
|
|
246
|
+
// Write activities
|
|
247
|
+
if (/^(create|make|build|generate|init)/i.test(actName)) {
|
|
248
|
+
const target = _pascalCase((step && step.target) || '');
|
|
249
|
+
return `WRITE: Crea un nuevo recurso en ${target}`;
|
|
250
|
+
}
|
|
251
|
+
if (/^(confirm|approve|activate|enable|process|execute)/i.test(actName)) {
|
|
252
|
+
const target = _pascalCase((step && step.target) || '');
|
|
253
|
+
return `WRITE: Confirma o actualiza estado en ${target}`;
|
|
254
|
+
}
|
|
255
|
+
if (/^(reserve|block|lock|hold|schedule|assign)/i.test(actName)) {
|
|
256
|
+
const target = _pascalCase((step && step.target) || '');
|
|
257
|
+
return `WRITE: Reserva o bloquea recurso en ${target}`;
|
|
258
|
+
}
|
|
259
|
+
if (/^(clear|clean|remove|purge|convert|mark)/i.test(actName)) {
|
|
260
|
+
const target = _pascalCase((step && step.target) || '');
|
|
261
|
+
return `WRITE: Actualiza o limpia datos en ${target}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return `Ejecuta lógica de negocio en módulo '${_pascalCase((step && step.target) || '')}'`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Classifies an activity step into one of: READ / WRITE / COMPENSATE / NOTIFY
|
|
269
|
+
*/
|
|
270
|
+
function classifyActivityRole(actName, actDef, step) {
|
|
271
|
+
if (actDef && actDef.isCompensation) return 'COMPENSATE';
|
|
272
|
+
if (/^(release|refund|cancel|undo|revert|restore|rollback|mark.*cancel)/i.test(actName)) return 'COMPENSATE';
|
|
273
|
+
if (step && step.type === 'async') return 'NOTIFY';
|
|
274
|
+
if (/^(get|find|fetch|load|read|query|search|list)/i.test(actName)) return 'READ';
|
|
275
|
+
if (/^(notify|send|emit|alert|push|broadcast)/i.test(actName)) return 'NOTIFY';
|
|
276
|
+
return 'WRITE';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Calculates the LIFO compensation chain for a saga workflow.
|
|
281
|
+
* Returns an array of { stepNum, activityName, compensationName, targetModule, type }
|
|
282
|
+
* ordered chronologically (execution order); the LIFO rollback is the reverse.
|
|
283
|
+
*/
|
|
284
|
+
function buildSagaChain(wf) {
|
|
285
|
+
const steps = Array.isArray(wf.steps) ? wf.steps : [];
|
|
286
|
+
const chain = [];
|
|
287
|
+
for (let i = 0; i < steps.length; i++) {
|
|
288
|
+
const step = steps[i];
|
|
289
|
+
if (!step.activity) continue;
|
|
290
|
+
chain.push({
|
|
291
|
+
stepNum: i + 1,
|
|
292
|
+
activityName: step.activity,
|
|
293
|
+
compensationName: step.compensation || null,
|
|
294
|
+
targetModule: step.target || null,
|
|
295
|
+
type: step.type || 'sync',
|
|
296
|
+
timeout: step.timeout || null,
|
|
297
|
+
isCompensable: !!(step.compensation),
|
|
298
|
+
canFail: step.type !== 'async',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return chain;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Resolves the source of each input field in a step:
|
|
306
|
+
* 'trigger' | 'step N: ActivityName' | 'unknown'
|
|
307
|
+
*/
|
|
308
|
+
function resolveInputSources(allSteps, currentIdx, triggerFields) {
|
|
309
|
+
const inputSources = {};
|
|
310
|
+
const available = {}; // fieldName → source label
|
|
311
|
+
|
|
312
|
+
// Seed with trigger/event fields
|
|
313
|
+
for (const f of triggerFields) {
|
|
314
|
+
available[f] = 'trigger';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
for (let i = 0; i < currentIdx; i++) {
|
|
318
|
+
const prev = allSteps[i];
|
|
319
|
+
for (const o of (prev.output || [])) {
|
|
320
|
+
const oName = typeof o === 'string' ? o : o.name;
|
|
321
|
+
if (oName) available[oName] = `paso ${i + 1}: ${prev.activity}`;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const current = allSteps[currentIdx];
|
|
326
|
+
for (const inp of (current.input || [])) {
|
|
327
|
+
const iName = typeof inp === 'string' ? inp : inp.name;
|
|
328
|
+
if (iName) inputSources[iName] = available[iName] || 'no resuelto';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return inputSources;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Builds which downstream steps consume each output field of a given step.
|
|
336
|
+
*/
|
|
337
|
+
function buildOutputConsumers(allSteps, currentIdx) {
|
|
338
|
+
const current = allSteps[currentIdx];
|
|
339
|
+
const consumers = {};
|
|
340
|
+
const outputs = (current.output || []).map((o) => (typeof o === 'string' ? o : o.name)).filter(Boolean);
|
|
341
|
+
|
|
342
|
+
for (const outField of outputs) {
|
|
343
|
+
consumers[outField] = [];
|
|
344
|
+
for (let j = currentIdx + 1; j < allSteps.length; j++) {
|
|
345
|
+
const downstep = allSteps[j];
|
|
346
|
+
const inputs = (downstep.input || []).map((i) => (typeof i === 'string' ? i : i.name));
|
|
347
|
+
if (inputs.includes(outField)) {
|
|
348
|
+
consumers[outField].push(`paso ${j + 1}: ${downstep.activity}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return consumers;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Calculates the module role based on its participation in workflows.
|
|
357
|
+
* Orchestrator: has trigger module; DataProvider: only read activities; Executor: write/compensate; Reactor: only async/notify
|
|
358
|
+
*/
|
|
359
|
+
function calculateModuleRoles(systemConfig, domainConfigs) {
|
|
360
|
+
const roles = {};
|
|
361
|
+
const rawWorkflows = systemConfig.workflows || [];
|
|
362
|
+
const modules = systemConfig.modules || [];
|
|
363
|
+
|
|
364
|
+
for (const mod of modules) {
|
|
365
|
+
const modName = _camelCase(mod.name);
|
|
366
|
+
roles[modName] = { name: modName, roles: new Set(), label: mod.name };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const wf of rawWorkflows) {
|
|
370
|
+
const trigMod = wf.trigger && wf.trigger.module ? _camelCase(wf.trigger.module) : null;
|
|
371
|
+
if (trigMod && roles[trigMod]) roles[trigMod].roles.add('Orchestrator');
|
|
372
|
+
|
|
373
|
+
for (const step of (wf.steps || [])) {
|
|
374
|
+
const targetMod = step.target ? _camelCase(step.target) : trigMod;
|
|
375
|
+
if (!targetMod || !roles[targetMod]) continue;
|
|
376
|
+
|
|
377
|
+
const actName = (step.activity || '').toLowerCase();
|
|
378
|
+
if (step.type === 'async') {
|
|
379
|
+
roles[targetMod].roles.add('Reactor');
|
|
380
|
+
} else if (/^(get|find|fetch|load|read|query|search|list)/.test(actName)) {
|
|
381
|
+
roles[targetMod].roles.add('DataProvider');
|
|
382
|
+
} else {
|
|
383
|
+
roles[targetMod].roles.add('Executor');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Convert Sets to sorted arrays
|
|
389
|
+
const result = {};
|
|
390
|
+
for (const [modName, data] of Object.entries(roles)) {
|
|
391
|
+
const rolesArr = [...data.roles];
|
|
392
|
+
result[modName] = {
|
|
393
|
+
name: modName,
|
|
394
|
+
label: data.label,
|
|
395
|
+
roles: rolesArr,
|
|
396
|
+
primaryRole: rolesArr.includes('Orchestrator') ? 'Orchestrator'
|
|
397
|
+
: rolesArr.includes('Executor') ? 'Executor'
|
|
398
|
+
: rolesArr.includes('DataProvider') ? 'DataProvider'
|
|
399
|
+
: rolesArr.includes('Reactor') ? 'Reactor'
|
|
400
|
+
: 'Standalone',
|
|
401
|
+
roleLabel: rolesArr.length > 0 ? rolesArr.join(' + ') : 'Standalone',
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Builds the activity catalog: all activities across all modules with usage info.
|
|
409
|
+
*/
|
|
410
|
+
function buildActivityCatalog(systemConfig, domainConfigs, modulesMap) {
|
|
411
|
+
const rawWorkflows = systemConfig.workflows || [];
|
|
412
|
+
const localWorkflowsByModule = {};
|
|
413
|
+
|
|
414
|
+
// Collect local workflows
|
|
415
|
+
for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
|
|
416
|
+
if (domainCfg && Array.isArray(domainCfg.workflows)) {
|
|
417
|
+
localWorkflowsByModule[_camelCase(modName)] = domainCfg.workflows;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Build used-in index: "modName::ActivityName" → ["WorkflowName", ...]
|
|
422
|
+
const usedIn = {};
|
|
423
|
+
for (const wf of rawWorkflows) {
|
|
424
|
+
const trigMod = wf.trigger && wf.trigger.module ? _camelCase(wf.trigger.module) : null;
|
|
425
|
+
for (const step of (wf.steps || [])) {
|
|
426
|
+
if (!step.activity) continue;
|
|
427
|
+
const targetMod = step.target ? _camelCase(step.target) : trigMod;
|
|
428
|
+
const key = `${targetMod}::${_pascalCase(step.activity)}`;
|
|
429
|
+
if (!usedIn[key]) usedIn[key] = [];
|
|
430
|
+
usedIn[key].push(wf.name);
|
|
431
|
+
|
|
432
|
+
if (step.compensation) {
|
|
433
|
+
const compKey = `${targetMod}::${_pascalCase(step.compensation)}`;
|
|
434
|
+
if (!usedIn[compKey]) usedIn[compKey] = [];
|
|
435
|
+
usedIn[compKey].push(`${wf.name} (compensation)`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
for (const [modName, localWfs] of Object.entries(localWorkflowsByModule)) {
|
|
440
|
+
for (const wf of localWfs) {
|
|
441
|
+
for (const step of (wf.steps || [])) {
|
|
442
|
+
if (!step.activity) continue;
|
|
443
|
+
const key = `${modName}::${_pascalCase(step.activity)}`;
|
|
444
|
+
if (!usedIn[key]) usedIn[key] = [];
|
|
445
|
+
usedIn[key].push(`${wf.name} (local)`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Build catalog entries
|
|
451
|
+
const catalog = [];
|
|
452
|
+
let compensationNames = new Set();
|
|
453
|
+
|
|
454
|
+
// First pass: collect compensation names
|
|
455
|
+
for (const [, domainCfg] of Object.entries(domainConfigs)) {
|
|
456
|
+
for (const act of (domainCfg?.activities || [])) {
|
|
457
|
+
if (act.compensation) compensationNames.add(_pascalCase(act.compensation));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
|
|
462
|
+
if (!domainCfg || !Array.isArray(domainCfg.activities)) continue;
|
|
463
|
+
const modKey = _camelCase(modName);
|
|
464
|
+
|
|
465
|
+
for (const act of domainCfg.activities) {
|
|
466
|
+
const actPascal = _pascalCase(act.name);
|
|
467
|
+
const key = `${modKey}::${actPascal}`;
|
|
468
|
+
const usedInWorkflows = usedIn[key] || [];
|
|
469
|
+
const isCompensation = compensationNames.has(actPascal);
|
|
470
|
+
const role = classifyActivityRole(act.name, { isCompensation }, null);
|
|
471
|
+
|
|
472
|
+
catalog.push({
|
|
473
|
+
name: actPascal,
|
|
474
|
+
module: modKey,
|
|
475
|
+
moduleLabel: modulesMap[modKey]?.label || modKey,
|
|
476
|
+
type: (act.type || 'light').toLowerCase(),
|
|
477
|
+
role,
|
|
478
|
+
description: act.description || inferActivityRole(act.name, { isCompensation }, null, {}),
|
|
479
|
+
usedInWorkflows,
|
|
480
|
+
isCompensation,
|
|
481
|
+
isOrphan: usedInWorkflows.length === 0,
|
|
482
|
+
hasRetryPolicy: !!(act.retryPolicy),
|
|
483
|
+
retryPolicy: act.retryPolicy || null,
|
|
484
|
+
timeout: act.timeout || null,
|
|
485
|
+
inputFields: (act.input || []).map((f) => (typeof f === 'string' ? { name: f, type: 'String' } : f)),
|
|
486
|
+
outputFields: (act.output || []).map((f) => (typeof f === 'string' ? { name: f, type: 'String' } : f)),
|
|
487
|
+
hasOutput: !!(act.output && act.output.length > 0),
|
|
488
|
+
nestedTypes: act.nestedTypes || [],
|
|
489
|
+
externalTypes: act.externalTypes || [],
|
|
490
|
+
compensation: act.compensation || null,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return catalog;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Builds the external type dependency graph.
|
|
500
|
+
* Returns array of { consumerModule, activityName, typeName, sourceModule }
|
|
501
|
+
*/
|
|
502
|
+
function buildExternalTypeDeps(domainConfigs) {
|
|
503
|
+
const deps = [];
|
|
504
|
+
for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
|
|
505
|
+
if (!domainCfg || !Array.isArray(domainCfg.activities)) continue;
|
|
506
|
+
for (const act of domainCfg.activities) {
|
|
507
|
+
for (const ext of (act.externalTypes || [])) {
|
|
508
|
+
deps.push({
|
|
509
|
+
consumerModule: _camelCase(modName),
|
|
510
|
+
activityName: _pascalCase(act.name),
|
|
511
|
+
typeName: _pascalCase(ext.name || ext),
|
|
512
|
+
sourceModule: _camelCase(ext.module || ext),
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return deps;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Builds queue topology for each module.
|
|
522
|
+
*/
|
|
523
|
+
function buildQueueTopology(systemConfig, domainConfigs) {
|
|
524
|
+
const rawWorkflows = systemConfig.workflows || [];
|
|
525
|
+
const modules = systemConfig.modules || [];
|
|
526
|
+
const topology = {};
|
|
527
|
+
|
|
528
|
+
const participating = new Set();
|
|
529
|
+
for (const wf of rawWorkflows) {
|
|
530
|
+
if (wf.trigger && wf.trigger.module) participating.add(_camelCase(wf.trigger.module));
|
|
531
|
+
for (const step of (wf.steps || [])) {
|
|
532
|
+
if (step.target) participating.add(_camelCase(step.target));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Also modules with activities[] in domain.yaml
|
|
536
|
+
for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
|
|
537
|
+
if (domainCfg && Array.isArray(domainCfg.activities) && domainCfg.activities.length > 0) {
|
|
538
|
+
participating.add(_camelCase(modName));
|
|
539
|
+
}
|
|
540
|
+
if (domainCfg && Array.isArray(domainCfg.workflows) && domainCfg.workflows.length > 0) {
|
|
541
|
+
participating.add(_camelCase(modName));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
for (const mod of modules) {
|
|
546
|
+
const modKey = _camelCase(mod.name);
|
|
547
|
+
if (!participating.has(modKey)) continue;
|
|
548
|
+
const snake = mod.name.toUpperCase().replace(/-/g, '_');
|
|
549
|
+
topology[modKey] = {
|
|
550
|
+
name: modKey,
|
|
551
|
+
label: mod.name,
|
|
552
|
+
flowQueue: `${snake}_WORKFLOW_QUEUE`,
|
|
553
|
+
heavyQueue: `${snake}_HEAVY_TASK_QUEUE`,
|
|
554
|
+
lightQueue: `${snake}_LIGHT_TASK_QUEUE`,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return topology;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Enriches workflows with resolved data flow, role descriptions and saga analysis.
|
|
563
|
+
*/
|
|
564
|
+
function enrichWorkflows(rawWorkflows, domainConfigs, modulesMap) {
|
|
565
|
+
const activityDefs = {};
|
|
566
|
+
for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
|
|
567
|
+
if (!domainCfg || !Array.isArray(domainCfg.activities)) continue;
|
|
568
|
+
const modKey = _camelCase(modName);
|
|
569
|
+
const compensationNames = new Set();
|
|
570
|
+
for (const act of domainCfg.activities) {
|
|
571
|
+
if (act.compensation) compensationNames.add(_pascalCase(act.compensation));
|
|
572
|
+
}
|
|
573
|
+
for (const act of domainCfg.activities) {
|
|
574
|
+
activityDefs[`${modKey}::${_pascalCase(act.name)}`] = {
|
|
575
|
+
...act,
|
|
576
|
+
isCompensation: compensationNames.has(_pascalCase(act.name)),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return rawWorkflows.map((wf) => {
|
|
582
|
+
const triggerModule = wf.trigger && wf.trigger.module ? _camelCase(wf.trigger.module) : null;
|
|
583
|
+
const steps = Array.isArray(wf.steps) ? wf.steps : [];
|
|
584
|
+
|
|
585
|
+
// Approximate trigger fields from domain events
|
|
586
|
+
const triggerFields = [];
|
|
587
|
+
if (wf.trigger && wf.trigger.on && triggerModule && domainConfigs[triggerModule]) {
|
|
588
|
+
const domainCfg = domainConfigs[triggerModule];
|
|
589
|
+
for (const agg of (domainCfg.aggregates || [])) {
|
|
590
|
+
for (const ev of (agg.events || [])) {
|
|
591
|
+
const evLower = (ev.name || '').toLowerCase().replace(/event$/, '');
|
|
592
|
+
const trigLower = (wf.trigger.on || '').toLowerCase().replace(/event$/, '');
|
|
593
|
+
if (evLower.includes(trigLower) || trigLower.includes(evLower)) {
|
|
594
|
+
for (const f of (ev.fields || [])) {
|
|
595
|
+
if (f && f.name) triggerFields.push(f.name);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const enrichedSteps = steps.map((step, idx) => {
|
|
603
|
+
if (!step.activity) return { ...step, _stepNum: idx + 1, _isWait: true };
|
|
604
|
+
|
|
605
|
+
const actPascal = _pascalCase(step.activity);
|
|
606
|
+
const targetMod = step.target ? _camelCase(step.target) : triggerModule;
|
|
607
|
+
const actKey = `${targetMod}::${actPascal}`;
|
|
608
|
+
const actDef = activityDefs[actKey] || null;
|
|
609
|
+
|
|
610
|
+
const modInfo = modulesMap[targetMod] || { label: targetMod, icon: '📁', color: '#888' };
|
|
611
|
+
const roleClass = classifyActivityRole(step.activity, actDef, step);
|
|
612
|
+
const roleDesc = inferActivityRole(step.activity, actDef, step, { wfName: wf.name });
|
|
613
|
+
const inputSources = resolveInputSources(steps, idx, triggerFields);
|
|
614
|
+
const outputConsumers = buildOutputConsumers(steps, idx);
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
_stepNum: idx + 1,
|
|
618
|
+
_isWait: false,
|
|
619
|
+
activity: step.activity,
|
|
620
|
+
activityPascal: actPascal,
|
|
621
|
+
target: targetMod,
|
|
622
|
+
targetLabel: modInfo.label,
|
|
623
|
+
targetIcon: modInfo.icon,
|
|
624
|
+
targetColor: modInfo.color,
|
|
625
|
+
type: step.type || 'sync',
|
|
626
|
+
activityType: (actDef && actDef.type) ? actDef.type.toLowerCase() : 'light',
|
|
627
|
+
timeout: step.timeout || null,
|
|
628
|
+
retryPolicy: (actDef && actDef.retryPolicy) || null,
|
|
629
|
+
compensation: step.compensation || null,
|
|
630
|
+
roleClass,
|
|
631
|
+
roleDesc,
|
|
632
|
+
inputs: (step.input || []).map((f) => {
|
|
633
|
+
const fName = typeof f === 'string' ? f : f.name;
|
|
634
|
+
return { name: fName, source: inputSources[fName] || 'trigger' };
|
|
635
|
+
}),
|
|
636
|
+
outputs: (step.output || []).map((f) => {
|
|
637
|
+
const fName = typeof f === 'string' ? f : f.name;
|
|
638
|
+
return { name: fName, consumers: outputConsumers[fName] || [] };
|
|
639
|
+
}),
|
|
640
|
+
formalOutputCount: actDef ? (actDef.output || []).length : 0,
|
|
641
|
+
};
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const sagaChain = wf.saga === true ? buildSagaChain(wf) : null;
|
|
645
|
+
|
|
646
|
+
// Build data flow table: columns = steps (sync only), rows = all field names
|
|
647
|
+
const dataFlowFields = new Set();
|
|
648
|
+
const dataFlowSources = {}; // fieldName → { origin: 'trigger'|stepIdx, label }
|
|
649
|
+
for (const f of triggerFields) {
|
|
650
|
+
dataFlowFields.add(f);
|
|
651
|
+
dataFlowSources[f] = { origin: 'trigger', label: 'trigger' };
|
|
652
|
+
}
|
|
653
|
+
for (let i = 0; i < enrichedSteps.length; i++) {
|
|
654
|
+
for (const o of (enrichedSteps[i].outputs || [])) {
|
|
655
|
+
if (o.name) {
|
|
656
|
+
dataFlowFields.add(o.name);
|
|
657
|
+
dataFlowSources[o.name] = { origin: i, label: `paso ${i + 1}: ${steps[i].activity}` };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const dataFlowTable = [...dataFlowFields].map((field) => {
|
|
663
|
+
const source = dataFlowSources[field] || { origin: 'trigger', label: 'trigger' };
|
|
664
|
+
const consumed = enrichedSteps
|
|
665
|
+
.map((s, idx) => ({ idx, step: s }))
|
|
666
|
+
.filter(({ step }) => (step.inputs || []).some((inp) => inp.name === field))
|
|
667
|
+
.map(({ idx, step }) => ({ stepNum: idx + 1, activity: step.activity || '(wait)' }));
|
|
668
|
+
return { field, source: source.label, consumed };
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
name: wf.name,
|
|
673
|
+
trigger: wf.trigger || null,
|
|
674
|
+
triggerModule,
|
|
675
|
+
triggerModuleLabel: modulesMap[triggerModule]?.label || triggerModule,
|
|
676
|
+
triggerModuleIcon: modulesMap[triggerModule]?.icon || '📁',
|
|
677
|
+
triggerModuleColor: modulesMap[triggerModule]?.color || '#888',
|
|
678
|
+
saga: wf.saga === true,
|
|
679
|
+
taskQueue: wf.taskQueue || null,
|
|
680
|
+
steps: enrichedSteps,
|
|
681
|
+
sagaChain,
|
|
682
|
+
dataFlowTable,
|
|
683
|
+
triggerFields,
|
|
684
|
+
stepCount: steps.length,
|
|
685
|
+
asyncStepCount: steps.filter((s) => s.type === 'async').length,
|
|
686
|
+
compensableStepCount: steps.filter((s) => !!s.compensation).length,
|
|
687
|
+
};
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Main Temporal report data extraction function.
|
|
693
|
+
* Returns the full temporalData object passed to the HTML template.
|
|
694
|
+
*/
|
|
695
|
+
function extractTemporalReportData(systemConfig, domainConfigs, modulesMap) {
|
|
696
|
+
const rawWorkflows = systemConfig.workflows || [];
|
|
697
|
+
const orchestration = systemConfig.orchestration || {};
|
|
698
|
+
|
|
699
|
+
const moduleRoles = calculateModuleRoles(systemConfig, domainConfigs);
|
|
700
|
+
const activityCatalog = buildActivityCatalog(systemConfig, domainConfigs, modulesMap);
|
|
701
|
+
const externalTypeDeps = buildExternalTypeDeps(domainConfigs);
|
|
702
|
+
const queueTopology = buildQueueTopology(systemConfig, domainConfigs);
|
|
703
|
+
const workflows = enrichWorkflows(rawWorkflows, domainConfigs, modulesMap);
|
|
704
|
+
|
|
705
|
+
// Local workflows (from individual domain.yaml files)
|
|
706
|
+
const localWorkflows = [];
|
|
707
|
+
for (const [modName, domainCfg] of Object.entries(domainConfigs)) {
|
|
708
|
+
if (!domainCfg || !Array.isArray(domainCfg.workflows)) continue;
|
|
709
|
+
const modKey = _camelCase(modName);
|
|
710
|
+
for (const wf of domainCfg.workflows) {
|
|
711
|
+
const enriched = enrichWorkflows([wf], domainConfigs, modulesMap)[0];
|
|
712
|
+
localWorkflows.push({ ...enriched, _ownerModule: modKey, _ownerLabel: modulesMap[modKey]?.label || modKey, _isLocal: true });
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Saga workflows only
|
|
717
|
+
const sagaWorkflows = workflows.filter((wf) => wf.saga);
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
isTemporalMode: true,
|
|
721
|
+
orchestration: {
|
|
722
|
+
target: orchestration.temporal?.target || 'localhost:7233',
|
|
723
|
+
namespace: orchestration.temporal?.namespace || 'default',
|
|
724
|
+
engine: 'temporal',
|
|
725
|
+
},
|
|
726
|
+
workflows,
|
|
727
|
+
localWorkflows,
|
|
728
|
+
sagaWorkflows,
|
|
729
|
+
activityCatalog,
|
|
730
|
+
moduleRoles,
|
|
731
|
+
externalTypeDeps,
|
|
732
|
+
queueTopology,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
210
736
|
// ── Data extraction ──────────────────────────────────────────────────────────
|
|
211
737
|
|
|
212
|
-
|
|
738
|
+
// Builds the shared color/icon modules map used by multiple extractors
|
|
739
|
+
function buildModulesMap(systemConfig) {
|
|
213
740
|
const modulesConfig = systemConfig.modules || [];
|
|
214
|
-
const asyncEvents = (systemConfig.integrations || {}).async || [];
|
|
215
|
-
const syncIntegrations = (systemConfig.integrations || {}).sync || [];
|
|
216
|
-
|
|
217
|
-
// Build modules map with color + icon
|
|
218
741
|
const modulesMap = {};
|
|
219
742
|
for (let i = 0; i < modulesConfig.length; i++) {
|
|
220
743
|
const mod = modulesConfig[i];
|
|
221
|
-
|
|
744
|
+
const key = _camelCase(mod.name);
|
|
745
|
+
modulesMap[key] = {
|
|
222
746
|
id: mod.name,
|
|
223
747
|
label: toPascalCase(mod.name),
|
|
224
748
|
icon: assignIcon(mod.name),
|
|
225
749
|
color: COLOR_PALETTE[i % COLOR_PALETTE.length],
|
|
226
750
|
desc: mod.description || mod.name,
|
|
227
751
|
};
|
|
752
|
+
// Also index by raw name for compatibility
|
|
753
|
+
if (mod.name !== key) {
|
|
754
|
+
modulesMap[mod.name] = modulesMap[key];
|
|
755
|
+
}
|
|
228
756
|
}
|
|
757
|
+
return modulesMap;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function extractReportData(systemConfig, validation, domainValidation) {
|
|
761
|
+
const modulesConfig = systemConfig.modules || [];
|
|
762
|
+
const asyncEvents = (systemConfig.integrations || {}).async || [];
|
|
763
|
+
const syncIntegrations = (systemConfig.integrations || {}).sync || [];
|
|
764
|
+
|
|
765
|
+
// Build modules map with color + icon
|
|
766
|
+
const modulesMap = buildModulesMap(systemConfig);
|
|
229
767
|
|
|
230
768
|
// Normalize events (consumers can be strings or objects with .module)
|
|
231
769
|
const events = asyncEvents.map((ev) => ({
|
|
@@ -252,9 +790,18 @@ function extractReportData(systemConfig, validation, domainValidation) {
|
|
|
252
790
|
// Auto-generate flows
|
|
253
791
|
const flows = buildEventFlows(systemConfig, modulesMap);
|
|
254
792
|
|
|
793
|
+
// Deduplicate modules by id (buildModulesMap indexes both camelCase and raw-name keys,
|
|
794
|
+
// so Object.values() can return the same module object twice for hyphenated names like "shopping-carts")
|
|
795
|
+
const seenModIds = new Set();
|
|
796
|
+
const modulesList = Object.values(modulesMap).filter((m) => {
|
|
797
|
+
if (seenModIds.has(m.id)) return false;
|
|
798
|
+
seenModIds.add(m.id);
|
|
799
|
+
return true;
|
|
800
|
+
});
|
|
801
|
+
|
|
255
802
|
return {
|
|
256
803
|
systemName: (systemConfig.system || {}).name || 'eva4j system',
|
|
257
|
-
modules:
|
|
804
|
+
modules: modulesList,
|
|
258
805
|
events,
|
|
259
806
|
syncIntegrations: syncList,
|
|
260
807
|
endpoints,
|
|
@@ -332,8 +879,74 @@ async function evaluateSystemCommand(type, options = {}) {
|
|
|
332
879
|
domainValidation = validateDomain(domainConfigs, systemConfig);
|
|
333
880
|
}
|
|
334
881
|
|
|
335
|
-
// ── 3.
|
|
882
|
+
// ── 3. Detect orchestration engine + extract report data ────────────────
|
|
883
|
+
const orchestration = systemConfig.orchestration || {};
|
|
884
|
+
const isTemporalMode = !!(orchestration.enabled && orchestration.engine === 'temporal');
|
|
885
|
+
|
|
336
886
|
const reportData = extractReportData(systemConfig, validation, domainValidation);
|
|
887
|
+
reportData.isTemporalMode = isTemporalMode;
|
|
888
|
+
|
|
889
|
+
let temporalValidation = null;
|
|
890
|
+
if (isTemporalMode) {
|
|
891
|
+
const modulesMap = buildModulesMap(systemConfig);
|
|
892
|
+
const temporalCtx = extractTemporalReportData(systemConfig, domainConfigs, modulesMap);
|
|
893
|
+
temporalValidation = validateTemporal(systemConfig, domainConfigs, temporalCtx);
|
|
894
|
+
|
|
895
|
+
Object.assign(reportData, {
|
|
896
|
+
orchestration: temporalCtx.orchestration,
|
|
897
|
+
workflows: temporalCtx.workflows,
|
|
898
|
+
localWorkflows: temporalCtx.localWorkflows,
|
|
899
|
+
sagaWorkflows: temporalCtx.sagaWorkflows,
|
|
900
|
+
activityCatalog: temporalCtx.activityCatalog,
|
|
901
|
+
moduleRoles: temporalCtx.moduleRoles,
|
|
902
|
+
externalTypeDeps: temporalCtx.externalTypeDeps,
|
|
903
|
+
queueTopology: temporalCtx.queueTopology,
|
|
904
|
+
temporalValidation,
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
if (temporalValidation) {
|
|
908
|
+
console.log();
|
|
909
|
+
console.log(chalk.bold('⏱️ Temporal Validation'));
|
|
910
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
911
|
+
const tv = temporalValidation.summary;
|
|
912
|
+
|
|
913
|
+
const tvErrors = [], tvWarnings = [], tvInfos = [];
|
|
914
|
+
for (const category of (temporalValidation.categories || [])) {
|
|
915
|
+
for (const check of (category.checks || [])) {
|
|
916
|
+
if (!check.findings || check.findings.length === 0) continue;
|
|
917
|
+
const prefix = `[${check.id}] ${check.label}`;
|
|
918
|
+
const messages = check.findings.map((f) => {
|
|
919
|
+
const msg = typeof f === 'string' ? f : [f.module && `[${f.module}]`, f.message, f.context && `(${f.context})`].filter(Boolean).join(' ');
|
|
920
|
+
return ` • ${prefix}: ${msg}`;
|
|
921
|
+
});
|
|
922
|
+
if (check.severity === 'error') tvErrors.push(...messages);
|
|
923
|
+
else if (check.severity === 'warning') tvWarnings.push(...messages);
|
|
924
|
+
else tvInfos.push(...messages);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
console.log(` ${chalk.red('🔴 Errors:')} ${chalk.red.bold(tvErrors.length)}`);
|
|
929
|
+
console.log(` ${chalk.yellow('🟡 Warnings:')} ${chalk.yellow.bold(tvWarnings.length)}`);
|
|
930
|
+
console.log(` ${chalk.cyan('🔵 Info:')} ${chalk.cyan.bold(tvInfos.length)}`);
|
|
931
|
+
console.log(` ${chalk.green('🟢 OK:')} ${chalk.green.bold(tv.ok)}`);
|
|
932
|
+
|
|
933
|
+
if (tvErrors.length > 0) {
|
|
934
|
+
console.log();
|
|
935
|
+
console.log(chalk.red('Temporal errors:'));
|
|
936
|
+
tvErrors.forEach((m) => console.log(chalk.red(m)));
|
|
937
|
+
}
|
|
938
|
+
if (tvWarnings.length > 0) {
|
|
939
|
+
console.log();
|
|
940
|
+
console.log(chalk.yellow('Temporal warnings:'));
|
|
941
|
+
tvWarnings.forEach((m) => console.log(chalk.yellow(m)));
|
|
942
|
+
}
|
|
943
|
+
if (tvInfos.length > 0) {
|
|
944
|
+
console.log();
|
|
945
|
+
console.log(chalk.cyan('Temporal info:'));
|
|
946
|
+
tvInfos.forEach((m) => console.log(chalk.cyan(m)));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
337
950
|
|
|
338
951
|
// ── 4. Render HTML ──────────────────────────────────────────────────────
|
|
339
952
|
const templatePath = path.join(__dirname, '../../templates/evaluate/report.html.ejs');
|
|
@@ -353,7 +966,7 @@ async function evaluateSystemCommand(type, options = {}) {
|
|
|
353
966
|
|
|
354
967
|
// ── 5b. Write domain assets ───────────────────────────────────────────────
|
|
355
968
|
if (domainValidation) {
|
|
356
|
-
await writeDomainAssets(domainValidation, process.cwd());
|
|
969
|
+
await writeDomainAssets(domainValidation, process.cwd(), temporalValidation);
|
|
357
970
|
}
|
|
358
971
|
|
|
359
972
|
// ── 5c. Write system-evaluation.md ──────────────────────────────────────
|
|
@@ -492,11 +1105,11 @@ function toPascalCase(str) {
|
|
|
492
1105
|
}
|
|
493
1106
|
|
|
494
1107
|
// ── writeDomainAssets ─────────────────────────────────────────────────────────
|
|
495
|
-
async function writeDomainAssets(domainValidation, cwd) {
|
|
1108
|
+
async function writeDomainAssets(domainValidation, cwd, temporalValidation = null) {
|
|
496
1109
|
const assetsDir = path.join(cwd, 'assets', 'evaluation');
|
|
497
1110
|
await fs.ensureDir(assetsDir);
|
|
498
1111
|
|
|
499
|
-
const { categories, diagrams, summary } = domainValidation;
|
|
1112
|
+
const { categories, diagrams, blueprints, summary } = domainValidation;
|
|
500
1113
|
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
501
1114
|
|
|
502
1115
|
// ── 1. Write per-module .mmd files ──────────────────────────────────────
|
|
@@ -508,6 +1121,15 @@ async function writeDomainAssets(domainValidation, cwd) {
|
|
|
508
1121
|
}
|
|
509
1122
|
}
|
|
510
1123
|
|
|
1124
|
+
// ── 1b. Write per-module blueprint .mmd files ───────────────────────────
|
|
1125
|
+
if (blueprints) {
|
|
1126
|
+
for (const [moduleName, blueprintText] of Object.entries(blueprints)) {
|
|
1127
|
+
if (blueprintText) {
|
|
1128
|
+
await fs.writeFile(path.join(assetsDir, `${moduleName}-blueprint.mmd`), blueprintText, 'utf-8');
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
511
1133
|
// ── 2. Build evaluation.md ──────────────────────────────────────────────
|
|
512
1134
|
|
|
513
1135
|
// Collect all findings grouped by module
|
|
@@ -569,6 +1191,11 @@ async function writeDomainAssets(domainValidation, cwd) {
|
|
|
569
1191
|
|
|
570
1192
|
if (diagrams && diagrams[moduleName]) {
|
|
571
1193
|
lines.push(`> 📊 Diagram: [${moduleName}.mmd](./${moduleName}.mmd)`);
|
|
1194
|
+
}
|
|
1195
|
+
if (blueprints && blueprints[moduleName]) {
|
|
1196
|
+
lines.push(`> 🏗 Blueprint: [${moduleName}-blueprint.mmd](./${moduleName}-blueprint.mmd)`);
|
|
1197
|
+
}
|
|
1198
|
+
if ((diagrams && diagrams[moduleName]) || (blueprints && blueprints[moduleName])) {
|
|
572
1199
|
lines.push('');
|
|
573
1200
|
}
|
|
574
1201
|
|
|
@@ -595,16 +1222,76 @@ async function writeDomainAssets(domainValidation, cwd) {
|
|
|
595
1222
|
lines.push(`## Clean Modules (no findings)`);
|
|
596
1223
|
lines.push('');
|
|
597
1224
|
for (const m of cleanModules) {
|
|
598
|
-
|
|
1225
|
+
const links = [`[${m}.mmd](./${m}.mmd)`];
|
|
1226
|
+
if (blueprints && blueprints[m]) links.push(`[${m}-blueprint.mmd](./${m}-blueprint.mmd)`);
|
|
1227
|
+
lines.push(`- \`${m}\` — 🟢 no findings · ${links.join(' · ')}`);
|
|
599
1228
|
}
|
|
600
1229
|
lines.push('');
|
|
601
1230
|
}
|
|
602
1231
|
}
|
|
603
1232
|
|
|
1233
|
+
// ── 3. Temporal Validation section ────────────────────────────────────────
|
|
1234
|
+
if (temporalValidation) {
|
|
1235
|
+
const tv = temporalValidation.summary;
|
|
1236
|
+
const tvErrors = [], tvWarnings = [], tvInfos = [];
|
|
1237
|
+
for (const category of (temporalValidation.categories || [])) {
|
|
1238
|
+
for (const check of (category.checks || [])) {
|
|
1239
|
+
if (!check.findings || check.findings.length === 0) continue;
|
|
1240
|
+
const prefix = `**${check.id}** ${check.label}`;
|
|
1241
|
+
const messages = check.findings.map((f) => {
|
|
1242
|
+
const msg = typeof f === 'string' ? f : [f.module && `\`${f.module}\``, f.message, f.context && `*(${f.context})*`].filter(Boolean).join(' ');
|
|
1243
|
+
return `| ${prefix} | ${msg} |`;
|
|
1244
|
+
});
|
|
1245
|
+
if (check.severity === 'error') tvErrors.push(...messages);
|
|
1246
|
+
else if (check.severity === 'warning') tvWarnings.push(...messages);
|
|
1247
|
+
else tvInfos.push(...messages);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
lines.push('---');
|
|
1252
|
+
lines.push('');
|
|
1253
|
+
lines.push('## ⏱️ Temporal Validation');
|
|
1254
|
+
lines.push('');
|
|
1255
|
+
lines.push(`| 🔴 Errors | 🟡 Warnings | 🔵 Info | 🟢 OK |`);
|
|
1256
|
+
lines.push(`|-----------|-------------|---------|-------|`);
|
|
1257
|
+
lines.push(`| ${tvErrors.length} | ${tvWarnings.length} | ${tvInfos.length} | ${tv.ok} |`);
|
|
1258
|
+
lines.push('');
|
|
1259
|
+
|
|
1260
|
+
if (tvErrors.length > 0) {
|
|
1261
|
+
lines.push('### 🔴 Temporal Errors');
|
|
1262
|
+
lines.push('');
|
|
1263
|
+
lines.push('| Check | Message |');
|
|
1264
|
+
lines.push('|-------|---------|');
|
|
1265
|
+
tvErrors.forEach((m) => lines.push(m));
|
|
1266
|
+
lines.push('');
|
|
1267
|
+
}
|
|
1268
|
+
if (tvWarnings.length > 0) {
|
|
1269
|
+
lines.push('### 🟡 Temporal Warnings');
|
|
1270
|
+
lines.push('');
|
|
1271
|
+
lines.push('| Check | Message |');
|
|
1272
|
+
lines.push('|-------|---------|');
|
|
1273
|
+
tvWarnings.forEach((m) => lines.push(m));
|
|
1274
|
+
lines.push('');
|
|
1275
|
+
}
|
|
1276
|
+
if (tvInfos.length > 0) {
|
|
1277
|
+
lines.push('### 🔵 Temporal Info');
|
|
1278
|
+
lines.push('');
|
|
1279
|
+
lines.push('| Check | Message |');
|
|
1280
|
+
lines.push('|-------|---------|');
|
|
1281
|
+
tvInfos.forEach((m) => lines.push(m));
|
|
1282
|
+
lines.push('');
|
|
1283
|
+
}
|
|
1284
|
+
if (tvErrors.length === 0 && tvWarnings.length === 0 && tvInfos.length === 0) {
|
|
1285
|
+
lines.push('_✅ All Temporal checks passed._');
|
|
1286
|
+
lines.push('');
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
604
1290
|
await fs.writeFile(path.join(assetsDir, 'evaluation.md'), lines.join('\n'), 'utf-8');
|
|
605
1291
|
|
|
606
1292
|
const mmdFiles = diagrams ? Object.keys(diagrams).filter(m => diagrams[m]) : [];
|
|
607
|
-
|
|
1293
|
+
const bpFiles = blueprints ? Object.keys(blueprints).filter(m => blueprints[m]) : [];
|
|
1294
|
+
console.log(chalk.gray(` Domain assets → assets/evaluation/ (evaluation.md + ${mmdFiles.length} .mmd + ${bpFiles.length} blueprint files)`));
|
|
608
1295
|
}
|
|
609
1296
|
|
|
610
1297
|
module.exports = evaluateSystemCommand;
|