@specverse/engines 4.1.13 → 4.1.15

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 (68) hide show
  1. package/assets/prompts/core/standard/v9/behavior.prompt.yaml +126 -0
  2. package/dist/ai/behavior-ai-service.d.ts +65 -0
  3. package/dist/ai/behavior-ai-service.d.ts.map +1 -0
  4. package/dist/ai/behavior-ai-service.js +205 -0
  5. package/dist/ai/behavior-ai-service.js.map +1 -0
  6. package/dist/ai/index.d.ts +27 -0
  7. package/dist/ai/index.d.ts.map +1 -1
  8. package/dist/ai/index.js +30 -0
  9. package/dist/ai/index.js.map +1 -1
  10. package/dist/ai/prompt-loader.js +2 -2
  11. package/dist/inference/quint-transpiler.d.ts.map +1 -1
  12. package/dist/inference/quint-transpiler.js +204 -4
  13. package/dist/inference/quint-transpiler.js.map +1 -1
  14. package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +4 -1
  15. package/dist/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.js +2 -2
  16. package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +1 -0
  17. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +97 -22
  18. package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +31 -31
  19. package/dist/libs/instance-factories/communication/templates/eventemitter/types-generator.js +79 -0
  20. package/dist/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.js +96 -0
  21. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +45 -9
  22. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +20 -2
  23. package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.js +10 -2
  24. package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +249 -0
  25. package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +72 -45
  26. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +61 -54
  27. package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +31 -10
  28. package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +101 -84
  29. package/dist/libs/instance-factories/views/templates/react/components-generator.js +40 -10
  30. package/dist/realize/index.d.ts.map +1 -1
  31. package/dist/realize/index.js +192 -23
  32. package/dist/realize/index.js.map +1 -1
  33. package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +4 -1
  34. package/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.ts +2 -2
  35. package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts +6 -1
  36. package/libs/instance-factories/cli/templates/commander/command-generator.ts +115 -22
  37. package/libs/instance-factories/communication/event-emitter.yaml +16 -12
  38. package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +33 -36
  39. package/libs/instance-factories/communication/templates/eventemitter/types-generator.ts +95 -0
  40. package/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.ts +105 -0
  41. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +57 -11
  42. package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +23 -2
  43. package/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.ts +23 -2
  44. package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +376 -0
  45. package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +116 -45
  46. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +83 -59
  47. package/libs/instance-factories/services/templates/prisma/service-generator.ts +40 -10
  48. package/libs/instance-factories/services/templates/prisma/step-conventions.ts +169 -85
  49. package/libs/instance-factories/views/templates/react/components-generator.ts +50 -10
  50. package/package.json +1 -1
  51. package/dist/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.js +0 -232
  52. package/dist/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.js +0 -49
  53. package/dist/libs/instance-factories/tools/templates/mcp/static/src/index.js +0 -18
  54. package/dist/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.js +0 -0
  55. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.js +0 -97
  56. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.js +0 -64
  57. package/dist/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.js +0 -182
  58. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.js +0 -1210
  59. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.js +0 -172
  60. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.js +0 -240
  61. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.js +0 -147
  62. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.js +0 -281
  63. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.js +0 -409
  64. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.js +0 -414
  65. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.js +0 -467
  66. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.js +0 -135
  67. package/dist/libs/instance-factories/tools/templates/mcp/static/src/types/index.js +0 -0
  68. package/dist/libs/instance-factories/tools/templates/vscode/static/extension.js +0 -965
@@ -44,7 +44,7 @@ export default function generateCommand(context: TemplateContext): string {
44
44
  const alias = flag.alias ? `${flag.alias}, ` : '';
45
45
  const flagType = flag.type?.toLowerCase();
46
46
  const valuePart = flagType === 'boolean' ? '' : ` <${flagName.replace(/^--/, '')}>`;
47
- const defaultVal = flag.default !== undefined ? `, ${JSON.stringify(flag.default)}` : '';
47
+ const defaultVal = flag.default !== undefined ? `, ${commanderDefault(flag.default, flagType)}` : '';
48
48
  const desc = flag.description || `${flagName} option`;
49
49
  return ` .option('${alias}${flagName}${valuePart}', '${desc}'${defaultVal})`;
50
50
  });
@@ -96,7 +96,18 @@ export default function generateCommand(context: TemplateContext): string {
96
96
  if (ENGINE_HANDLERS[key]?.imports) allImportSets.push(ENGINE_HANDLERS[key].imports);
97
97
  }
98
98
  }
99
- const engineImports = deduplicateImports(allImportSets);
99
+ // Generate the body first so we can filter imports to only those actually used
100
+ const registerBody = hasSubcommands
101
+ ? generateCommandWithSubcommands(name, description, subcommands, optionDefs)
102
+ : generateLeafCommand(name, description, commandStr, optionDefs, actionParams, serviceRef, exitCodes);
103
+
104
+ const bodyForUsage = `${serviceImport}\n${registerBody}\n${subcommandRegistrations}`;
105
+ const commandOptionsUsed = new RegExp('\\bCommandOptions\\b').test(bodyForUsage);
106
+ const engineImports = deduplicateImports(allImportSets, bodyForUsage);
107
+
108
+ const commandOptionsDecl = commandOptionsUsed
109
+ ? `interface CommandOptions {\n${optionTypes.length > 0 ? optionTypes.join('\n') : ' [key: string]: any;'}\n}\n`
110
+ : '';
100
111
 
101
112
  return `/**
102
113
  * ${name} command
@@ -108,17 +119,14 @@ import { Command } from 'commander';
108
119
  ${serviceImport}
109
120
  ${engineImports}
110
121
 
111
- interface CommandOptions {
112
- ${optionTypes.length > 0 ? optionTypes.join('\n') : ' [key: string]: any;'}
113
- }
114
-
122
+ ${commandOptionsDecl}
115
123
  ${exitCodeComments ? `/**\n * Exit codes:\n${exitCodeComments}\n */` : ''}
116
124
 
117
125
  /**
118
126
  * Register the ${name} command on the program.
119
127
  */
120
128
  export function register${capitalize(name)}Command(program: Command): void {
121
- ${hasSubcommands ? generateCommandWithSubcommands(name, description, subcommands, optionDefs) : generateLeafCommand(name, description, commandStr, optionDefs, actionParams, serviceRef, exitCodes)}
129
+ ${registerBody}
122
130
  }
123
131
  ${subcommandRegistrations}
124
132
  `;
@@ -280,6 +288,44 @@ import type { ParserEngine, InferenceEngine, RealizeEngine } from '@specverse/ty
280
288
  }
281
289
  }
282
290
 
291
+ // Merge custom actions from original spec controllers into inferred controllers
292
+ // Inference generates standard CURED; custom actions (openPoll, castVote) come from the spec
293
+ const origControllers = Array.isArray((origComponent as any).controllers)
294
+ ? (origComponent as any).controllers
295
+ : Object.values((origComponent as any).controllers || {});
296
+ const inferredControllers = componentData.controllers || {};
297
+ for (const origCtrl of origControllers) {
298
+ const ctrlName = (origCtrl as any).name;
299
+ if (ctrlName && (origCtrl as any).actions) {
300
+ const inferredCtrl = inferredControllers[ctrlName];
301
+ if (inferredCtrl) {
302
+ inferredCtrl.actions = { ...(inferredCtrl.actions || {}), ...(origCtrl as any).actions };
303
+ }
304
+ }
305
+ }
306
+
307
+ // Merge service operation steps from original spec into inferred services
308
+ // Inference generates service shells with preconditions/returns but drops
309
+ // the declarative steps arrays — we restore them from the parsed AST.
310
+ const origServices = Array.isArray((origComponent as any).services)
311
+ ? (origComponent as any).services
312
+ : Object.values((origComponent as any).services || {});
313
+ const inferredServices = componentData.services || {};
314
+ for (const origSvc of origServices) {
315
+ const svcName = (origSvc as any).name;
316
+ if (!svcName) continue;
317
+ const inferredSvc = inferredServices[svcName];
318
+ if (!inferredSvc) continue;
319
+
320
+ const origOps = (origSvc as any).operations || {};
321
+ const inferredOps = inferredSvc.operations || {};
322
+ for (const [opName, origOp] of Object.entries(origOps) as [string, any][]) {
323
+ if (inferredOps[opName] && origOp.steps) {
324
+ inferredOps[opName].steps = origOp.steps;
325
+ }
326
+ }
327
+ }
328
+
283
329
  // Inject key as name into entity maps, and expand convention-format attributes
284
330
  // Iterate all object-valued sections (not hardcoded — covers any entity type)
285
331
  const entitySections = Object.keys(componentData).filter(k =>
@@ -348,7 +394,7 @@ import type { ParserEngine, InferenceEngine, RealizeEngine } from '@specverse/ty
348
394
  if (!realizeEngine) { console.error('No realize engine found.'); process.exit(1); }
349
395
  await realizeEngine.initialize({ manifestPath: effectiveManifestPath, workingDir: (process.env.SPECVERSE_USER_CWD || process.cwd()) });
350
396
 
351
- const outputDir = options.output || resolve((process.env.SPECVERSE_USER_CWD || process.cwd()), 'generated/code');
397
+ const outputDir = resolve((process.env.SPECVERSE_USER_CWD || process.cwd()), options.output || 'generated/code');
352
398
  console.log('Realizing ' + type + ' from ' + file + '...');
353
399
  await (realizeEngine as any).realizeAll(inferredSpec, outputDir);
354
400
 
@@ -826,10 +872,12 @@ import { EngineRegistry } from '@specverse/entities';`,
826
872
  imports: ``,
827
873
  handler: `const { SessionManager } = await import('@specverse/engines/ai');
828
874
  const manager = new SessionManager();
829
- const job = await manager.submit(sessionId, requirements, {
830
- jobId: options.jobId,
831
- outputPath: options.output,
832
- operation: options.operation,
875
+ const job = await manager.submit({
876
+ jobId: options.jobId || ('job-' + Date.now()),
877
+ sessionId,
878
+ operation: (options.operation || 'create') as 'create' | 'analyse' | 'materialise' | 'realize',
879
+ requirements,
880
+ output: options.output,
833
881
  });
834
882
  console.log('Job submitted: ' + job.jobId);
835
883
  console.log('Status: ' + job.status);
@@ -845,8 +893,10 @@ import { EngineRegistry } from '@specverse/entities';`,
845
893
  } else {
846
894
  console.log('ID: ' + (status.sessionId || status.jobId));
847
895
  console.log('Status: ' + status.status);
848
- if (status.created) console.log('Created: ' + new Date(status.created).toLocaleString());
849
- if (status.jobsProcessed !== undefined) console.log('Jobs: ' + status.jobsProcessed);
896
+ if (status.submitted) console.log('Submitted: ' + new Date(status.submitted).toLocaleString());
897
+ if (status.started) console.log('Started: ' + new Date(status.started).toLocaleString());
898
+ if (status.completed) console.log('Completed: ' + new Date(status.completed).toLocaleString());
899
+ if (status.duration) console.log('Duration: ' + status.duration);
850
900
  }`
851
901
  },
852
902
  'session.process': {
@@ -854,7 +904,7 @@ import { EngineRegistry } from '@specverse/entities';`,
854
904
  handler: `const { SessionManager } = await import('@specverse/engines/ai');
855
905
  const manager = new SessionManager();
856
906
  console.log('Processing job: ' + jobId);
857
- await manager.process(jobId);
907
+ await manager.processJob(jobId);
858
908
  console.log('Job processed successfully');`
859
909
  },
860
910
  };
@@ -879,11 +929,15 @@ function generateLeafCommand(
879
929
  : `console.log('Executing ${name}...');
880
930
  // TODO: Wire to service`;
881
931
 
882
- return `const cmd = program
932
+ // Rename unused action params with underscore prefix so tsc's
933
+ // noUnusedParameters doesn't flag them.
934
+ const finalActionParams = renameUnusedActionParams(actionParams, handler);
935
+
936
+ return `program
883
937
  .command('${commandStr}')
884
938
  .description('${description}')
885
939
  ${optionDefs.join('\n')}
886
- .action(async (${actionParams}) => {
940
+ .action(async (${finalActionParams}) => {
887
941
  try {
888
942
  ${handler}
889
943
  } catch (error: any) {
@@ -893,6 +947,35 @@ ${optionDefs.join('\n')}
893
947
  });`;
894
948
  }
895
949
 
950
+ /**
951
+ * Commander's `.option()` default value parameter is typed as
952
+ * `string | boolean | string[] | undefined`. Numeric defaults from the spec
953
+ * need to be stringified unless the option is a boolean flag.
954
+ */
955
+ function commanderDefault(value: unknown, flagType?: string): string {
956
+ if (flagType === 'boolean') return JSON.stringify(Boolean(value));
957
+ if (typeof value === 'number') return JSON.stringify(String(value));
958
+ return JSON.stringify(value);
959
+ }
960
+
961
+ /**
962
+ * Rename each action parameter with a leading underscore if its name isn't
963
+ * referenced in the handler body.
964
+ */
965
+ function renameUnusedActionParams(actionParams: string, handler: string): string {
966
+ if (!actionParams.trim()) return actionParams;
967
+ return actionParams.split(',').map(param => {
968
+ const trimmed = param.trim();
969
+ const nameMatch = trimmed.match(/^(\w+)/);
970
+ if (!nameMatch) return param;
971
+ const name = nameMatch[1];
972
+ if (name.startsWith('_')) return param;
973
+ const re = new RegExp(`\\b${name}\\b`);
974
+ if (re.test(handler)) return param;
975
+ return param.replace(new RegExp(`\\b${name}\\b`), `_${name}`);
976
+ }).join(', ');
977
+ }
978
+
896
979
  function generateCommandWithSubcommands(
897
980
  name: string,
898
981
  description: string,
@@ -915,7 +998,7 @@ function generateCommandWithSubcommands(
915
998
  const alias = flag.alias ? `${flag.alias}, ` : '';
916
999
  const flagType = flag.type?.toLowerCase();
917
1000
  const valuePart = flagType === 'boolean' ? '' : ` <${flagName.replace(/^--/, '')}>`;
918
- const defaultVal = flag.default !== undefined ? `, ${JSON.stringify(flag.default)}` : '';
1001
+ const defaultVal = flag.default !== undefined ? `, ${commanderDefault(flag.default, flagType)}` : '';
919
1002
  return ` .option('${alias}${flagName}${valuePart}', '${flag.description || flagName}'${defaultVal})`;
920
1003
  });
921
1004
 
@@ -935,12 +1018,14 @@ function generateCommandWithSubcommands(
935
1018
  ? engineHandler.handler
936
1019
  : `console.log('${name} ${subName}: not yet implemented via engine');`;
937
1020
 
1021
+ const finalSubActionParams = renameUnusedActionParams(subActionParams, handler);
1022
+
938
1023
  return `
939
1024
  cmd
940
1025
  .command('${subCmdStr}')
941
1026
  .description('${subDesc}')
942
1027
  ${subOptionDefs.join('\n')}
943
- .action(async (${subActionParams}) => {
1028
+ .action(async (${finalSubActionParams}) => {
944
1029
  try {
945
1030
  ${handler}
946
1031
  } catch (error: any) {
@@ -989,11 +1074,16 @@ function capitalize(str: string): string {
989
1074
  * Deduplicate import statements across multiple handler import blocks.
990
1075
  * Merges named imports from the same module and removes exact duplicates.
991
1076
  */
992
- function deduplicateImports(importBlocks: string[]): string {
1077
+ function deduplicateImports(importBlocks: string[], usedIn?: string): string {
993
1078
  const seen = new Map<string, Set<string>>(); // module -> set of named imports
994
1079
  const typeImports = new Map<string, Set<string>>(); // module -> set of type imports
995
1080
  const rawLines = new Set<string>(); // non-mergeable lines
996
1081
 
1082
+ const isUsed = (name: string): boolean => {
1083
+ if (!usedIn) return true;
1084
+ return new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(usedIn);
1085
+ };
1086
+
997
1087
  for (const block of importBlocks) {
998
1088
  for (const line of block.split('\n').map(l => l.trim()).filter(l => l)) {
999
1089
  // Match: import { X, Y } from 'module';
@@ -1020,12 +1110,15 @@ function deduplicateImports(importBlocks: string[]): string {
1020
1110
 
1021
1111
  const result: string[] = [];
1022
1112
  for (const [mod, names] of seen.entries()) {
1023
- result.push(`import { ${[...names].join(', ')} } from '${mod}';`);
1113
+ const used = [...names].filter(isUsed);
1114
+ if (used.length > 0) {
1115
+ result.push(`import { ${used.join(', ')} } from '${mod}';`);
1116
+ }
1024
1117
  }
1025
1118
  for (const [mod, names] of typeImports.entries()) {
1026
1119
  // Remove type imports that are already in regular imports
1027
1120
  const regularNames = seen.get(mod) || new Set();
1028
- const typeOnly = [...names].filter(n => !regularNames.has(n));
1121
+ const typeOnly = [...names].filter(n => !regularNames.has(n)).filter(isUsed);
1029
1122
  if (typeOnly.length > 0) {
1030
1123
  result.push(`import type { ${typeOnly.join(', ')} } from '${mod}';`);
1031
1124
  }
@@ -1,12 +1,12 @@
1
1
  name: EventEmitter
2
- version: "1.0.0"
2
+ version: "2.0.0"
3
3
  category: communication
4
- description: "In-memory event bus using Node.js EventEmitter for development and testing"
4
+ description: "Typed in-memory event bus with WebSocket bridge for real-time frontend updates"
5
5
 
6
6
  metadata:
7
7
  author: "SpecVerse Team"
8
8
  license: "MIT"
9
- tags: [events, in-memory, pubsub, eventemitter, development]
9
+ tags: [events, in-memory, pubsub, websocket, eventemitter, development]
10
10
 
11
11
  compatibility:
12
12
  specverse: ">=3.3.0"
@@ -17,6 +17,7 @@ capabilities:
17
17
  - "messaging.pubsub"
18
18
  - "messaging.events"
19
19
  - "messaging.inmemory"
20
+ - "messaging.websocket"
20
21
  requires: []
21
22
 
22
23
  technology:
@@ -29,28 +30,31 @@ dependencies:
29
30
  runtime:
30
31
  - name: "eventemitter3"
31
32
  version: "^5.0.0"
33
+ - name: "@fastify/websocket"
34
+ version: "^11.0.0"
32
35
 
33
36
  dev:
34
37
  - name: "typescript"
35
38
  version: "^5.2.0"
36
39
 
37
40
  codeTemplates:
38
- bus:
41
+ types:
39
42
  engine: typescript
40
- generator: "libs/instance-factories/communication/templates/eventemitter/bus-generator.ts"
41
- outputPattern: "src/events/eventBus.ts"
43
+ generator: "libs/instance-factories/communication/templates/eventemitter/types-generator.ts"
44
+ outputPattern: "{backendDir}/src/events/event-types.ts"
42
45
 
43
- publisher:
46
+ bus:
44
47
  engine: typescript
45
- generator: "libs/instance-factories/communication/templates/eventemitter/publisher-generator.ts"
46
- outputPattern: "src/events/publishers/{event}Publisher.ts"
48
+ generator: "libs/instance-factories/communication/templates/eventemitter/bus-generator.ts"
49
+ outputPattern: "{backendDir}/src/events/eventBus.ts"
47
50
 
48
- subscriber:
51
+ websocket:
49
52
  engine: typescript
50
- generator: "libs/instance-factories/communication/templates/eventemitter/subscriber-generator.ts"
51
- outputPattern: "src/events/subscribers/{event}Subscriber.ts"
53
+ generator: "libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.ts"
54
+ outputPattern: "{backendDir}/src/events/websocket-bridge.ts"
52
55
 
53
56
  configuration:
54
57
  maxListeners: 100
55
58
  captureRejections: true
56
59
  errorHandling: "log"
60
+ websocket: true
@@ -1,33 +1,36 @@
1
1
  /**
2
2
  * EventEmitter Bus Generator
3
3
  *
4
- * Generates in-memory event bus using EventEmitter3
4
+ * Generates typed in-memory event bus using EventEmitter3.
5
+ * Payload types imported from generated event-types.ts.
5
6
  */
6
7
 
7
8
  import type { TemplateContext } from '@specverse/types';
8
9
 
9
- /**
10
- * Generate EventEmitter bus singleton
11
- */
12
10
  export default function generateEventBus(context: TemplateContext): string {
13
11
  const { spec } = context;
14
-
15
- const events = spec.events ? Object.keys(spec.events) : [];
12
+ const hasEvents = spec.events && Object.keys(spec.events).length > 0;
13
+ const models = spec.models ? Object.keys(spec.models) : [];
16
14
 
17
15
  return `/**
18
16
  * Event Bus
19
- * In-memory event bus using EventEmitter3
17
+ * Typed in-memory event bus using EventEmitter3
20
18
  * Generated from SpecVerse specification
21
19
  */
22
20
 
23
- import EventEmitter from 'eventemitter3';
21
+ import { EventEmitter } from 'eventemitter3';
22
+ ${hasEvents || models.length > 0 ? `import type { EventPayloads } from './event-types.js';` : ''}
24
23
 
25
- // Event type definitions
26
- ${events.map(event => `export type ${event}Payload = any; // TODO: Define payload type`).join('\n')}
24
+ // Re-export types for consumers
25
+ ${hasEvents || models.length > 0 ? `export type { EventPayloads, EventName } from './event-types.js';` : ''}
27
26
 
28
- // Event names enum
29
- export enum EventName {
30
- ${events.map(event => ` ${event} = '${event}',`).join('\n')}
27
+ /**
28
+ * Event history entry
29
+ */
30
+ interface EventHistoryEntry {
31
+ event: string;
32
+ payload: any;
33
+ timestamp: Date;
31
34
  }
32
35
 
33
36
  /**
@@ -35,15 +38,13 @@ ${events.map(event => ` ${event} = '${event}',`).join('\n')}
35
38
  */
36
39
  class EventBus extends EventEmitter {
37
40
  private static instance: EventBus;
41
+ private history: EventHistoryEntry[] = [];
42
+ private maxHistory = 1000;
38
43
 
39
44
  private constructor() {
40
45
  super();
41
- this.setMaxListeners(100); // Configure max listeners
42
46
  }
43
47
 
44
- /**
45
- * Get singleton instance
46
- */
47
48
  public static getInstance(): EventBus {
48
49
  if (!EventBus.instance) {
49
50
  EventBus.instance = new EventBus();
@@ -52,37 +53,33 @@ class EventBus extends EventEmitter {
52
53
  }
53
54
 
54
55
  /**
55
- * Publish an event
56
+ * Publish a typed event
56
57
  */
57
- public publish<T = any>(event: EventName | string, payload: T): void {
58
- console.log(\`[EventBus] Publishing event: \${event}\`, payload);
58
+ public publish<K extends string>(event: K, payload: K extends keyof EventPayloads ? EventPayloads[K] : any): void {
59
+ this.history.push({ event, payload, timestamp: new Date() });
60
+ if (this.history.length > this.maxHistory) this.history.shift();
59
61
  this.emit(event, payload);
60
62
  }
61
63
 
62
64
  /**
63
- * Subscribe to an event
65
+ * Subscribe to a typed event
64
66
  */
65
- public subscribe<T = any>(
66
- event: EventName | string,
67
- handler: (payload: T) => void | Promise<void>
67
+ public subscribe<K extends string>(
68
+ event: K,
69
+ handler: (payload: K extends keyof EventPayloads ? EventPayloads[K] : any) => void | Promise<void>
68
70
  ): () => void {
69
- console.log(\`[EventBus] Subscribing to event: \${event}\`);
70
71
  this.on(event, handler);
71
-
72
- // Return unsubscribe function
73
- return () => {
74
- this.off(event, handler);
75
- };
72
+ return () => { this.off(event, handler); };
76
73
  }
77
74
 
78
75
  /**
79
- * Subscribe to an event (one-time)
76
+ * Get event history
80
77
  */
81
- public subscribeOnce<T = any>(
82
- event: EventName | string,
83
- handler: (payload: T) => void | Promise<void>
84
- ): void {
85
- this.once(event, handler);
78
+ public getHistory(eventName?: string, limit = 50): EventHistoryEntry[] {
79
+ const filtered = eventName
80
+ ? this.history.filter(e => e.event === eventName)
81
+ : this.history;
82
+ return filtered.slice(-limit);
86
83
  }
87
84
  }
88
85
 
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Event Types Generator
3
+ *
4
+ * Generates typed event payload interfaces from spec event definitions.
5
+ * Replaces the `any` payload types in the bus generator with real types.
6
+ */
7
+
8
+ import type { TemplateContext } from '@specverse/types';
9
+
10
+ /** Map SpecVerse types to TypeScript types */
11
+ function tsType(specType: string): string {
12
+ const map: Record<string, string> = {
13
+ String: 'string', Text: 'string', Email: 'string', URL: 'string',
14
+ Integer: 'number', Float: 'number', Number: 'number', Decimal: 'number', Money: 'number',
15
+ Boolean: 'boolean',
16
+ Date: 'string', DateTime: 'string', Timestamp: 'string',
17
+ UUID: 'string',
18
+ Object: 'Record<string, any>', Array: 'any[]',
19
+ };
20
+ return map[specType] || 'any';
21
+ }
22
+
23
+ export default function generateEventTypes(context: TemplateContext): string {
24
+ const { spec } = context;
25
+ const events = spec.events && typeof spec.events === 'object'
26
+ ? Object.entries(spec.events) : [];
27
+
28
+ if (events.length === 0) {
29
+ return `/**
30
+ * Event Types — no events defined in specification
31
+ */
32
+ export type EventPayloads = Record<string, any>;
33
+ `;
34
+ }
35
+
36
+ const interfaces: string[] = [];
37
+ const payloadMapEntries: string[] = [];
38
+
39
+ for (const [eventName, eventDef] of events) {
40
+ const def = eventDef as any;
41
+ const attrs = def.attributes || def.payload || {};
42
+
43
+ // Handle both array and object attribute formats
44
+ const attrEntries: Array<[string, any]> = Array.isArray(attrs)
45
+ ? attrs.map((a: any) => [a.name, a])
46
+ : Object.entries(attrs);
47
+
48
+ const fields = attrEntries.map(([name, attrDef]) => {
49
+ const type = typeof attrDef === 'string'
50
+ ? tsType(attrDef.split(' ')[0])
51
+ : tsType(attrDef?.type || 'String');
52
+ const required = typeof attrDef === 'string'
53
+ ? attrDef.includes('required')
54
+ : attrDef?.required;
55
+ return ` ${name}${required ? '' : '?'}: ${type};`;
56
+ });
57
+
58
+ interfaces.push(`export interface ${eventName}Payload {
59
+ ${fields.join('\n')}
60
+ }`);
61
+
62
+ payloadMapEntries.push(` ${eventName}: ${eventName}Payload;`);
63
+ }
64
+
65
+ // Also add standard CURED events for each model
66
+ const models = spec.models ? Object.keys(spec.models) : [];
67
+ for (const model of models) {
68
+ for (const suffix of ['Created', 'Updated', 'Deleted', 'Evolved']) {
69
+ const name = `${model}${suffix}`;
70
+ if (!payloadMapEntries.some(e => e.includes(name))) {
71
+ payloadMapEntries.push(` ${name}: { id: string; timestamp: string; [key: string]: any };`);
72
+ }
73
+ }
74
+ }
75
+
76
+ return `/**
77
+ * Event Payload Types
78
+ * Generated from SpecVerse specification
79
+ */
80
+
81
+ ${interfaces.join('\n\n')}
82
+
83
+ /**
84
+ * Map of all event names to their payload types
85
+ */
86
+ export type EventPayloads = {
87
+ ${payloadMapEntries.join('\n')}
88
+ };
89
+
90
+ /**
91
+ * All known event names
92
+ */
93
+ export type EventName = keyof EventPayloads;
94
+ `;
95
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * WebSocket Bridge Generator
3
+ *
4
+ * Generates a WebSocket server that bridges the event bus to frontend clients.
5
+ * Clients can subscribe to specific events and receive real-time updates.
6
+ */
7
+
8
+ import type { TemplateContext } from '@specverse/types';
9
+
10
+ export default function generateWebSocketBridge(context: TemplateContext): string {
11
+ const { spec } = context;
12
+ const models = spec.models ? Object.keys(spec.models) : [];
13
+
14
+ // Build list of all events the bridge should forward
15
+ const specEvents = spec.events ? Object.keys(spec.events) : [];
16
+ const curedEvents = models.flatMap(m =>
17
+ ['Created', 'Updated', 'Deleted', 'Evolved'].map(s => `${m}${s}`)
18
+ );
19
+ const allEvents = [...new Set([...specEvents, ...curedEvents])];
20
+
21
+ return `/**
22
+ * WebSocket Bridge
23
+ * Bridges the event bus to frontend clients for real-time updates.
24
+ * Generated from SpecVerse specification
25
+ */
26
+
27
+ import type { FastifyInstance } from 'fastify';
28
+ import type { WebSocket } from '@fastify/websocket';
29
+ import { eventBus } from './eventBus.js';
30
+
31
+ interface ClientSubscription {
32
+ ws: WebSocket;
33
+ events: Set<string>;
34
+ }
35
+
36
+ const clients = new Map<WebSocket, ClientSubscription>();
37
+
38
+ // All events this bridge knows about
39
+ const ALL_EVENTS = ${JSON.stringify(allEvents, null, 2)};
40
+
41
+ /**
42
+ * Register WebSocket bridge with Fastify
43
+ */
44
+ export async function registerWebSocketBridge(fastify: FastifyInstance): Promise<void> {
45
+ // Register @fastify/websocket plugin
46
+ const websocketPlugin = await import('@fastify/websocket');
47
+ await fastify.register(websocketPlugin.default || websocketPlugin);
48
+
49
+ // WebSocket route
50
+ fastify.get('/ws', { websocket: true }, (socket: WebSocket) => {
51
+ const subscription: ClientSubscription = { ws: socket, events: new Set() };
52
+ clients.set(socket, subscription);
53
+
54
+ socket.on('message', (raw: Buffer) => {
55
+ try {
56
+ const msg = JSON.parse(raw.toString());
57
+
58
+ if (msg.type === 'subscribe' && msg.event) {
59
+ subscription.events.add(msg.event);
60
+ socket.send(JSON.stringify({ type: 'subscribed', event: msg.event }));
61
+ }
62
+
63
+ if (msg.type === 'unsubscribe' && msg.event) {
64
+ subscription.events.delete(msg.event);
65
+ socket.send(JSON.stringify({ type: 'unsubscribed', event: msg.event }));
66
+ }
67
+
68
+ if (msg.type === 'subscribe-all') {
69
+ ALL_EVENTS.forEach(e => subscription.events.add(e));
70
+ socket.send(JSON.stringify({ type: 'subscribed-all', events: ALL_EVENTS }));
71
+ }
72
+ } catch { /* ignore malformed messages */ }
73
+ });
74
+
75
+ socket.on('close', () => {
76
+ clients.delete(socket);
77
+ });
78
+
79
+ // Send initial connection confirmation
80
+ socket.send(JSON.stringify({
81
+ type: 'connected',
82
+ availableEvents: ALL_EVENTS,
83
+ }));
84
+ });
85
+
86
+ // Subscribe to all events and broadcast to interested clients
87
+ for (const eventName of ALL_EVENTS) {
88
+ eventBus.subscribe(eventName, (payload: any) => {
89
+ const message = JSON.stringify({
90
+ type: 'event',
91
+ event: eventName,
92
+ payload,
93
+ timestamp: new Date().toISOString(),
94
+ });
95
+
96
+ for (const [ws, sub] of clients) {
97
+ if (sub.events.has(eventName) && ws.readyState === 1) {
98
+ try { ws.send(message); } catch { /* client disconnected */ }
99
+ }
100
+ }
101
+ });
102
+ }
103
+ }
104
+ `;
105
+ }