@specverse/engines 4.3.4 → 5.0.0

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 (66) hide show
  1. package/assets/examples/10-api/README.md +3 -3
  2. package/assets/prompts/core/README.md +1 -1
  3. package/dist/inference/core/rule-engine.d.ts +0 -12
  4. package/dist/inference/core/rule-engine.d.ts.map +1 -1
  5. package/dist/inference/core/rule-engine.js +99 -968
  6. package/dist/inference/core/rule-engine.js.map +1 -1
  7. package/dist/inference/core/template-helpers.d.ts +56 -0
  8. package/dist/inference/core/template-helpers.d.ts.map +1 -0
  9. package/dist/inference/core/template-helpers.js +87 -0
  10. package/dist/inference/core/template-helpers.js.map +1 -0
  11. package/dist/inference/logical/generators/service-generator.d.ts.map +1 -1
  12. package/dist/inference/logical/generators/service-generator.js +0 -4
  13. package/dist/inference/logical/generators/service-generator.js.map +1 -1
  14. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +27 -5
  15. package/dist/libs/instance-factories/scaffolding/templates/generic/package-json-generator.js +1 -1
  16. package/dist/libs/instance-factories/tools/README.md +1 -1
  17. package/dist/libs/instance-factories/tools/mcp.yaml +1 -1
  18. package/dist/libs/instance-factories/tools/templates/mcp/mcp-server-generator.js +336 -116
  19. package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js +172 -8
  20. package/dist/libs/instance-factories/tools/vscode.yaml +1 -1
  21. package/libs/instance-factories/cli/templates/commander/command-generator.ts +27 -5
  22. package/libs/instance-factories/scaffolding/templates/generic/package-json-generator.ts +10 -6
  23. package/libs/instance-factories/tools/README.md +1 -1
  24. package/libs/instance-factories/tools/mcp.yaml +1 -1
  25. package/libs/instance-factories/tools/templates/mcp/mcp-server-generator.ts +386 -141
  26. package/libs/instance-factories/tools/templates/vscode/static/extension.ts +9 -2
  27. package/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.ts +246 -10
  28. package/libs/instance-factories/tools/vscode.yaml +1 -1
  29. package/package.json +5 -4
  30. package/libs/instance-factories/tools/templates/mcp/static/docs/DEPLOYMENT_GUIDE.md +0 -630
  31. package/libs/instance-factories/tools/templates/mcp/static/docs/HYBRID_RESOURCE_SYSTEM.md +0 -330
  32. package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/EXTENSION_DEPLOYMENT.md +0 -552
  33. package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/LOCAL_DEPLOYMENT.md +0 -164
  34. package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/WEB_DEPLOYMENT.md +0 -247
  35. package/libs/instance-factories/tools/templates/mcp/static/package.json +0 -94
  36. package/libs/instance-factories/tools/templates/mcp/static/scripts/build-enterprise.js +0 -284
  37. package/libs/instance-factories/tools/templates/mcp/static/scripts/build-extension.js +0 -139
  38. package/libs/instance-factories/tools/templates/mcp/static/scripts/build-local.js +0 -74
  39. package/libs/instance-factories/tools/templates/mcp/static/scripts/build-web.js +0 -156
  40. package/libs/instance-factories/tools/templates/mcp/static/scripts/copy-canonical-files.js +0 -41
  41. package/libs/instance-factories/tools/templates/mcp/static/scripts/test-deployments.js +0 -259
  42. package/libs/instance-factories/tools/templates/mcp/static/scripts/test-hybrid-resources.js +0 -231
  43. package/libs/instance-factories/tools/templates/mcp/static/scripts/test-hybrid-simple.js +0 -196
  44. package/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.ts +0 -293
  45. package/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.ts +0 -90
  46. package/libs/instance-factories/tools/templates/mcp/static/src/index.ts +0 -24
  47. package/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.ts +0 -15
  48. package/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.ts +0 -106
  49. package/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.ts +0 -75
  50. package/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.ts +0 -239
  51. package/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.ts +0 -1501
  52. package/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.ts +0 -211
  53. package/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.ts +0 -308
  54. package/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.ts +0 -210
  55. package/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.ts +0 -356
  56. package/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.ts +0 -522
  57. package/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.ts +0 -530
  58. package/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.ts +0 -594
  59. package/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.ts +0 -170
  60. package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/CLIProxyService.init.test.ts +0 -544
  61. package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/CLIProxyService.test.ts +0 -189
  62. package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/ResourcesProviderService.test.ts +0 -89
  63. package/libs/instance-factories/tools/templates/mcp/static/src/types/index.ts +0 -110
  64. package/libs/instance-factories/tools/templates/mcp/static/tsconfig.json +0 -28
  65. package/libs/instance-factories/tools/templates/vscode/static/schemas/specverse-v3-schema.json +0 -4279
  66. /package/libs/instance-factories/tools/templates/vscode/static/themes/{specverse-complete-theme.json → specverse-dark-theme.json} +0 -0
@@ -3,6 +3,16 @@
3
3
  * Provides pattern matching and rule application for both logical and deployment inference
4
4
  */
5
5
  import * as yaml from 'js-yaml';
6
+ import Handlebars from 'handlebars';
7
+ import { registerCoreHelpers } from './template-helpers.js';
8
+ // Register Handlebars helpers at module load (idempotent). Phase 1 of
9
+ // the template-engine rewrite — infrastructure lands here; templates
10
+ // continue to run through the old string-substitution apply*Template
11
+ // paths below until Phase 2 switches them over. Registering helpers
12
+ // early costs nothing and makes them available the moment the rewrite
13
+ // swaps in `Handlebars.compile(...)`.
14
+ // See docs/plans/2026-04-17-HANDLEBARS-TEMPLATE-ENGINE-REWRITE.md.
15
+ registerCoreHelpers();
6
16
  export class RuleEngine {
7
17
  debug;
8
18
  rules = new Map();
@@ -357,101 +367,78 @@ export class RuleEngine {
357
367
  };
358
368
  }
359
369
  applyHandlebarsTemplate(rule, templateContext) {
360
- // For now, we'll do a simple template substitution
361
- // In a full implementation, we'd use the Handlebars library
362
- let result = Array.isArray(rule.template.content)
370
+ // Cycle 2: real Handlebars. Previously this method ran one-off
371
+ // hacks (hardcoded parentRelationships[0].targetModel substitution,
372
+ // blanket /each/ strip) because the home-rolled substitutor didn't
373
+ // understand `{{#each}}` or sub-expressions. Handlebars handles
374
+ // those natively — if multiple parentRelationships exist, the
375
+ // iteration emits one block per entry instead of just the first.
376
+ const template = Array.isArray(rule.template.content)
363
377
  ? rule.template.content.join('\n')
364
378
  : rule.template.content;
365
- // Ensure result is a string
366
- if (typeof result !== 'string') {
367
- throw new Error(`Template content must be string or string array, got ${typeof result} in applyHandlebarsTemplate`);
368
- }
369
- // Simple variable substitution for common patterns
370
- result = result.replace(/\{\{modelName\}\}/g, templateContext.modelName || '');
371
- result = result.replace(/\{\{controllerName\}\}/g, templateContext.controllerName || '');
372
- result = result.replace(/\{\{serviceName\}\}/g, templateContext.serviceName || '');
373
- // Handle arrays and objects in a basic way
374
- if (templateContext.parentRelationships?.length > 0) {
375
- result = result.replace(/\{\{#each parentRelationships\}\}/g, '');
376
- result = result.replace(/\{\{\/each\}\}/g, '');
377
- result = result.replace(/\{\{targetModel\}\}/g, templateContext.parentRelationships[0].targetModel);
379
+ if (typeof template !== 'string') {
380
+ throw new Error(`Template content must be string or string array, got ${typeof template} in applyHandlebarsTemplate`);
378
381
  }
382
+ const rendered = Handlebars.compile(template, { strict: false, noEscape: true })(templateContext);
383
+ // If rendered output looks like JSON, parse it; otherwise return
384
+ // as string. Same shape as before, but the renderer now produces
385
+ // valid JSON more often (proper iteration, helpers resolved).
379
386
  try {
380
- // Try to parse as YAML/JSON
381
- if (result.trim().startsWith('{') || result.trim().includes(':')) {
382
- return JSON.parse(result);
387
+ const trimmed = rendered.trim();
388
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
389
+ return JSON.parse(rendered);
383
390
  }
384
391
  }
385
392
  catch {
386
- // If parsing fails, return as string
393
+ // fall through to string return
387
394
  }
388
- return result;
395
+ return rendered;
389
396
  }
390
397
  applyJsonTemplate(rule, templateContext) {
398
+ // Cycle 2 of the template-engine rewrite: JSON templates now go
399
+ // through real Handlebars. Helpers (eq / humanize / pluralize /
400
+ // lowerCase / upperCase) are globally registered at module load.
401
+ // strict:false makes missing variables render as empty strings
402
+ // (old behaviour preserved the literal `{{name}}` — which would
403
+ // break JSON.parse anyway, so this is strictly safer). noEscape
404
+ // because rule output is machine-read JSON, not HTML.
391
405
  try {
392
- let template = Array.isArray(rule.template.content)
406
+ const template = Array.isArray(rule.template.content)
393
407
  ? rule.template.content.join('\n')
394
408
  : rule.template.content;
395
- // Simple variable substitution
396
- template = template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
397
- return templateContext[key] || match;
398
- });
399
- return JSON.parse(template);
409
+ const rendered = Handlebars.compile(template, { strict: false, noEscape: true })(templateContext);
410
+ return JSON.parse(rendered);
400
411
  }
401
412
  catch (error) {
402
413
  throw new Error(`Failed to parse JSON template: ${error instanceof Error ? error.message : String(error)}`);
403
414
  }
404
415
  }
405
416
  applyYamlTemplate(rule, templateContext) {
417
+ // Cycle 3: real Handlebars. Replaces four load-bearing hacks:
418
+ // 1. Hardcoded parentRelationships[0].targetModel substitution
419
+ // — real Handlebars' \`{{#each}}\` iterates every entry.
420
+ // 2. Global /each/ strip — ate nested closers. Real Handlebars
421
+ // pairs opens/closes correctly.
422
+ // 3. Fake \`{{#if required}}\` (always passed through) — real
423
+ // Handlebars evaluates the condition against loop context.
424
+ // 4. Hardcoded \`{{name}}\` → 'sampleAttribute' when parent
425
+ // relationships exist — real Handlebars resolves \`{{name}}\`
426
+ // against the iterated attribute, which is what the template
427
+ // always intended.
428
+ // Plus the processSpeclyHandlebars conditional handler, also gone.
406
429
  try {
407
- let template = Array.isArray(rule.template.content)
430
+ const template = Array.isArray(rule.template.content)
408
431
  ? rule.template.content.join('\n')
409
432
  : rule.template.content;
410
- // Ensure template is a string
411
433
  if (typeof template !== 'string') {
412
434
  throw new Error(`Template content must be string or string array, got ${typeof template}`);
413
435
  }
414
- // Apply basic variable substitution
415
- template = template.replace(/\{\{modelName\}\}/g, templateContext.modelName || '');
416
- template = template.replace(/\{\{controllerName\}\}/g, templateContext.controllerName || '');
417
- template = template.replace(/\{\{serviceName\}\}/g, templateContext.serviceName || '');
418
- // Handle parent relationships processing
419
- if (templateContext.relationships?.parentRelationships?.length > 0) {
420
- const parentRel = templateContext.relationships.parentRelationships[0];
421
- // Process the each loop for parent relationships
422
- template = template.replace(/\{\{#each relationships\.parentRelationships\}\}/g, '');
423
- template = template.replace(/\{\{\/each\}\}/g, '');
424
- // Replace context variables
425
- template = template.replace(/\{\{targetModel\}\}/g, parentRel.targetModel || '');
426
- template = template.replace(/\{\{\.\.\/modelName\}\}/g, templateContext.modelName || '');
427
- // Handle nested attribute loops
428
- template = template.replace(/\{\{#each \.\.\/model\.attributes\}\}/g, '');
429
- // Process the required conditional specifically for attribute loops
430
- template = template.replace(/\{\{#if required\}\}([^]*?)\{\{\/if\}\}/g, (match, content) => {
431
- // For attributes, assume required if we're in parent relationship context
432
- return content;
433
- });
434
- template = template.replace(/\{\{name\}\}/g, 'sampleAttribute');
435
- }
436
- // Handle many-to-many relationships processing
437
- if (templateContext.relationships?.manyToManyRelationships?.length > 0) {
438
- const manyToManyRel = templateContext.relationships.manyToManyRelationships[0];
439
- // Process the each loop for many-to-many relationships
440
- template = template.replace(/\{\{#each relationships\.manyToManyRelationships\}\}/g, '');
441
- template = template.replace(/\{\{\/each\}\}/g, '');
442
- // Replace context variables
443
- template = template.replace(/\{\{targetModel\}\}/g, manyToManyRel.targetModel || '');
444
- template = template.replace(/\{\{\.\.\/modelName\}\}/g, templateContext.modelName || '');
445
- }
446
- // Use the shared handlebars processing that handles all conditionals consistently
447
- template = this.processSpeclyHandlebars(template, templateContext);
448
- // Clean up any double empty lines that might confuse YAML parser
449
- template = template.replace(/\n\n+/g, '\n\n');
450
- // Template processing completed
451
- // Handlebars processing already done above
452
- // Parse the processed template as YAML
453
- const result = yaml.load(template);
454
- return result;
436
+ const rendered = Handlebars.compile(template, { strict: false, noEscape: true })(templateContext);
437
+ // Collapse runs of blank lines — same cleanup the old path did,
438
+ // still useful because YAML parsers reject trailing garbage in
439
+ // some shapes.
440
+ const cleaned = rendered.replace(/\n\n+/g, '\n\n');
441
+ return yaml.load(cleaned);
455
442
  }
456
443
  catch (error) {
457
444
  throw new Error(`Failed to parse YAML template: ${error instanceof Error ? error.message : String(error)}`);
@@ -459,564 +446,55 @@ export class RuleEngine {
459
446
  }
460
447
  applySpeclyTemplate(rule, templateContext) {
461
448
  try {
462
- // For controller rules, return a ControllerSpec object instead of a string
463
- if (rule.pattern === 'StandardCURED') {
464
- return this.generateCuredControllerSpec(templateContext);
465
- }
466
- // For service rules, return ServiceSpec objects
467
- if (rule.pattern === 'ProcessingService') {
468
- return this.generateProcessingServiceSpec(templateContext);
469
- }
470
- if (rule.pattern === 'ValidationService') {
471
- return this.generateValidationServiceSpec(templateContext);
472
- }
473
- // For event rules, return EventSpec objects
474
- if (rule.pattern === 'StandardEvents') {
475
- return this.generateStandardEventsSpec(templateContext);
476
- }
477
- if (rule.pattern === 'RelationshipEvents') {
478
- return this.generateRelationshipEventsSpec(templateContext);
479
- }
480
- // For view rules, return ViewSpec objects
481
- if (rule.pattern === 'EnhancedListView' || rule.pattern === 'AnalyticsView') {
482
- return this.generateViewSpec(rule.pattern, templateContext);
483
- }
484
- // For additional service patterns
485
- if (rule.pattern === 'IntegrationService') {
486
- return this.generateIntegrationServiceSpec(templateContext);
487
- }
488
- if (rule.pattern === 'LifecycleService') {
489
- return this.generateLifecycleServiceSpec(templateContext);
490
- }
491
- if (rule.pattern === 'RelationshipService') {
492
- return this.generateRelationshipServiceSpec(templateContext);
493
- }
494
- // For other specly templates, process as string
495
- let template = Array.isArray(rule.template.content)
449
+ // StandardCURED now has a real Handlebars template in
450
+ // entities/src/core/models/inference/controller-rules.json
451
+ // that renders to the equivalent ControllerSpec. Generator
452
+ // method removed. See Cycle 5 in the Handlebars-rewrite plan.
453
+ //
454
+ // ProcessingService / ValidationService / RelationshipService
455
+ // rules + generators fully retired (Cycle 6). See
456
+ // engines/src/inference/DISABLED-RULES.md for the rationale.
457
+ //
458
+ // StandardEvents (Cycle 7) + RelationshipEvents (Cycle 8) now
459
+ // render from real Handlebars templates in
460
+ // entities/src/core/events/inference/event-rules.json.
461
+ // EnhancedListView / AnalyticsView (Cycle 9): retired.
462
+ // Superseded by the view-triggered specialist-views expansion
463
+ // system (specialist_analytics, specialist_dashboard, etc.) —
464
+ // see entities/src/core/views/inference/specialist-views.json
465
+ // and engines/src/inference/DISABLED-RULES.md.
466
+ // IntegrationService (Cycle 10) + LifecycleService (Cycle 11)
467
+ // now render from real Handlebars templates in service-rules.json.
468
+ // Cycle 5: real Handlebars + yaml.load for specly templates that
469
+ // produce a structured object (like the cured_root_controller
470
+ // template which renders a ControllerSpec). Falls back to the
471
+ // raw string if yaml.load fails — for templates that genuinely
472
+ // produce non-YAML output.
473
+ const template = Array.isArray(rule.template.content)
496
474
  ? rule.template.content.join('\n')
497
475
  : rule.template.content;
498
- // Apply basic variable substitution
499
- template = template.replace(/\{\{modelName\}\}/g, templateContext.modelName || '');
500
- template = template.replace(/\{\{controllerName\}\}/g, templateContext.controllerName || '');
501
- template = template.replace(/\{\{serviceName\}\}/g, templateContext.serviceName || '');
502
- // Process conditionals and loops
503
- template = this.processSpeclyHandlebars(template, templateContext);
504
- return template;
505
- }
506
- catch (error) {
507
- throw new Error(`Failed to process Specly template: ${error instanceof Error ? error.message : String(error)}`);
508
- }
509
- }
510
- generateCuredControllerSpec(context) {
511
- const modelName = context.modelName || 'Unknown';
512
- // Build CURED operations spec
513
- const curedOps = {
514
- create: {
515
- parameters: {
516
- data: `${modelName} required`
517
- },
518
- returns: modelName,
519
- requires: [
520
- `${modelName} data is valid`
521
- ],
522
- ensures: [
523
- `${modelName} created with unique ID`,
524
- `${modelName} persisted to storage`
525
- ],
526
- publishes: [`${modelName}Created`]
527
- },
528
- retrieve: {
529
- parameters: {
530
- id: 'UUID required'
531
- },
532
- returns: modelName,
533
- requires: [`${modelName} with ID exists`],
534
- ensures: [`Returns complete ${modelName} details`]
535
- },
536
- retrieve_many: {
537
- parameters: {
538
- filters: `${modelName}Filter optional`,
539
- limit: 'Integer default=20',
540
- offset: 'Integer default=0'
541
- },
542
- returns: `Array[${modelName}]`,
543
- ensures: [
544
- `Returns paginated list of ${modelName}s`,
545
- `Applies filters if specified`
546
- ]
547
- },
548
- update: {
549
- parameters: {
550
- id: 'UUID required',
551
- updates: `Object required`
552
- },
553
- returns: modelName,
554
- requires: [
555
- `${modelName} with ID exists`,
556
- `Update data is valid`
557
- ],
558
- ensures: [
559
- `${modelName} attributes updated`,
560
- `Version number incremented`
561
- ],
562
- publishes: [`${modelName}Updated`]
563
- },
564
- delete: {
565
- parameters: {
566
- id: 'UUID required',
567
- soft: 'Boolean default=true'
568
- },
569
- returns: 'Boolean',
570
- requires: [`${modelName} with ID exists`],
571
- ensures: [`${modelName} marked as deleted`],
572
- publishes: [`${modelName}Deleted`]
573
- },
574
- validate: {
575
- parameters: {
576
- data: `${modelName} required`,
577
- operation: 'String required'
578
- },
579
- returns: 'ValidationResult',
580
- requires: [
581
- `Operation is one of: create, update, evolve`,
582
- `Data structure matches ${modelName} schema`
583
- ],
584
- ensures: [
585
- `Returns validation status and errors`,
586
- `No side effects - dry run only`
587
- ]
588
- }
589
- };
590
- // Add dynamic required field validation
591
- if (context.model?.attributes) {
592
- const requiredFields = context.model.attributes.filter((attr) => attr.required);
593
- if (requiredFields.length > 0) {
594
- requiredFields.forEach((attr) => {
595
- curedOps.create.requires.push(`${attr.name} is not empty`);
596
- });
597
- }
598
- }
599
- // Add lifecycle operations if model has lifecycle
600
- if (context.model?.lifecycle) {
601
- curedOps.evolve = {
602
- parameters: {
603
- id: 'UUID required',
604
- from: `${modelName}State required`,
605
- to: `${modelName}State required`,
606
- reason: 'String optional'
607
- },
608
- returns: modelName,
609
- requires: [
610
- `${modelName} with ID exists`,
611
- `${modelName} current state is 'from' state`,
612
- `Transition from 'from' to 'to' is valid`
613
- ],
614
- ensures: [
615
- `${modelName} state changed to 'to' state`,
616
- `State transition logged`
617
- ],
618
- publishes: [`${modelName}Evolved`]
619
- };
620
- }
621
- return {
622
- description: `Auto-generated CURED controller for ${modelName}`,
623
- model: modelName,
624
- cured: curedOps
625
- };
626
- }
627
- generateProcessingServiceSpec(context) {
628
- const modelName = context.modelName || 'Unknown';
629
- const operations = {
630
- [`handle${modelName}Creation`]: {
631
- parameters: {
632
- event: `${modelName}CreatedEvent required`
633
- },
634
- returns: 'Void',
635
- requires: ['Event data is valid'],
636
- ensures: [
637
- `${modelName} creation processing completed`,
638
- 'Related systems notified'
639
- ]
640
- },
641
- [`handle${modelName}Update`]: {
642
- parameters: {
643
- event: `${modelName}UpdatedEvent required`
644
- },
645
- returns: 'Void',
646
- requires: ['Event data is valid'],
647
- ensures: [
648
- `${modelName} update processing completed`,
649
- 'Change propagated to dependent systems'
650
- ]
651
- },
652
- [`validate${modelName}`]: {
653
- parameters: {
654
- data: `${modelName} required`,
655
- validationContext: 'Object optional'
656
- },
657
- returns: 'ValidationResult',
658
- requires: [`${modelName} data is provided`],
659
- ensures: [
660
- 'Complete validation performed',
661
- 'Business rules checked'
662
- ]
663
- },
664
- [`process${modelName}BusinessRules`]: {
665
- parameters: {
666
- id: 'UUID required',
667
- ruleContext: 'Object required'
668
- },
669
- returns: 'BusinessRuleResult',
670
- requires: [
671
- `${modelName} exists`,
672
- 'Rule context is valid'
673
- ],
674
- ensures: [
675
- 'All applicable business rules processed',
676
- 'Results documented'
677
- ]
678
- }
679
- };
680
- const subscribes_to = {
681
- [`${modelName}Created`]: `handle${modelName}Creation`,
682
- [`${modelName}Updated`]: `handle${modelName}Update`
683
- };
684
- // Add lifecycle operations if model has lifecycle
685
- if (context.model?.lifecycle) {
686
- operations[`handle${modelName}Evolution`] = {
687
- parameters: {
688
- event: `${modelName}EvolvedEvent required`
689
- },
690
- returns: 'Void',
691
- requires: ['Lifecycle transition is valid'],
692
- ensures: [
693
- 'Lifecycle change processed',
694
- 'State-dependent actions triggered'
695
- ]
696
- };
697
- subscribes_to[`${modelName}Evolved`] = `handle${modelName}Evolution`;
698
- }
699
- return {
700
- description: `Handles complex business logic and processing for ${modelName}`,
701
- subscribes_to,
702
- operations
703
- };
704
- }
705
- generateValidationServiceSpec(context) {
706
- const modelName = context.modelName || 'Unknown';
707
- const operations = {
708
- validateCreate: {
709
- parameters: {
710
- data: `${modelName} required`,
711
- parentContext: 'Object optional'
712
- },
713
- returns: 'ValidationResult',
714
- requires: [`${modelName} data is provided`],
715
- ensures: [
716
- 'All create validations performed',
717
- 'Parent relationships validated',
718
- 'Business constraints checked'
719
- ]
720
- },
721
- validateUpdate: {
722
- parameters: {
723
- id: 'UUID required',
724
- updates: 'Object required',
725
- currentState: `${modelName} required`
726
- },
727
- returns: 'ValidationResult',
728
- requires: [
729
- `${modelName} exists`,
730
- 'Current state is provided'
731
- ],
732
- ensures: [
733
- 'All update validations performed',
734
- 'State transition rules checked'
735
- ]
736
- },
737
- validateBusinessRules: {
738
- parameters: {
739
- data: `${modelName} required`,
740
- ruleSet: 'String optional'
741
- },
742
- returns: 'BusinessRuleValidationResult',
743
- ensures: [
744
- 'All business rules validated',
745
- 'Cross-model dependencies checked'
746
- ]
747
- }
748
- };
749
- // Add lifecycle validation if model has lifecycle
750
- if (context.model?.lifecycle) {
751
- operations.validateStateTransition = {
752
- parameters: {
753
- id: 'UUID required',
754
- fromState: 'String required',
755
- toState: 'String required',
756
- transitionContext: 'Object optional'
757
- },
758
- returns: 'ValidationResult',
759
- requires: [
760
- `${modelName} exists`,
761
- 'States are valid'
762
- ],
763
- ensures: [
764
- 'Transition rules validated',
765
- 'Prerequisites checked'
766
- ]
767
- };
768
- }
769
- return {
770
- description: `Provides comprehensive validation for ${modelName} data`,
771
- operations
772
- };
773
- }
774
- generateStandardEventsSpec(context) {
775
- const modelName = context.modelName || 'Unknown';
776
- const events = {};
777
- // Created event
778
- events[`${modelName}Created`] = {
779
- description: `${modelName} was created`,
780
- attributes: {
781
- id: 'UUID required',
782
- timestamp: 'DateTime required',
783
- createdBy: 'UUID required',
784
- version: 'Integer default=1'
785
- }
786
- };
787
- // Updated event
788
- events[`${modelName}Updated`] = {
789
- description: `${modelName} was updated`,
790
- attributes: {
791
- id: 'UUID required',
792
- changedFields: 'Array required',
793
- previousValues: 'Object optional',
794
- newValues: 'Object required',
795
- timestamp: 'DateTime required',
796
- updatedBy: 'UUID required',
797
- version: 'Integer required'
798
- }
799
- };
800
- // Deleted event
801
- events[`${modelName}Deleted`] = {
802
- description: `${modelName} was deleted`,
803
- attributes: {
804
- id: 'UUID required',
805
- deletedBy: 'UUID required',
806
- timestamp: 'DateTime required',
807
- soft: 'Boolean default=true',
808
- reason: 'String optional'
809
- }
810
- };
811
- // Add key attributes from model to events
812
- if (context.model?.attributes) {
813
- const keyAttributes = context.model.attributes.filter((attr) => attr.name === 'name' || attr.name === 'title' || attr.name === 'status');
814
- keyAttributes.forEach((attr) => {
815
- events[`${modelName}Created`].attributes[attr.name] = `${attr.type} required`;
816
- events[`${modelName}Updated`].attributes[attr.name] = `${attr.type} optional`;
817
- if (attr.name === 'name') {
818
- events[`${modelName}Deleted`].attributes[attr.name] = `${attr.type} optional`;
819
- }
820
- });
821
- }
822
- return events;
823
- }
824
- generateViewSpec(pattern, context) {
825
- const modelName = context.modelName || 'Unknown';
826
- if (pattern === 'EnhancedListView') {
827
- return {
828
- [`${modelName}EnhancedListView`]: {
829
- type: 'list',
830
- model: modelName,
831
- description: `Enhanced list view for ${modelName} with advanced filtering`,
832
- subscribes_to: [
833
- `${modelName}Created`,
834
- `${modelName}Updated`,
835
- `${modelName}Deleted`
836
- ],
837
- uiComponents: {
838
- advancedSearch: {
839
- type: 'AdvancedSearchInput',
840
- properties: {
841
- placeholder: `Search ${modelName}s...`,
842
- debounce: 300,
843
- enableSuggestions: true
844
- }
845
- },
846
- advancedFilters: {
847
- type: 'FilterPanel',
848
- properties: {
849
- model: modelName,
850
- collapsible: true
851
- }
852
- },
853
- [`${modelName.toLowerCase()}Table`]: {
854
- type: 'DataTable',
855
- properties: {
856
- model: modelName,
857
- pagination: {
858
- enabled: true,
859
- pageSize: 25,
860
- showSizeSelector: true
861
- },
862
- selection: {
863
- enabled: true,
864
- multiSelect: true
865
- },
866
- sorting: {
867
- enabled: true,
868
- multiColumn: true
869
- }
870
- }
871
- }
872
- }
873
- }
874
- };
875
- }
876
- else if (pattern === 'AnalyticsView') {
877
- return {
878
- [`${modelName}AnalyticsView`]: {
879
- type: 'dashboard',
880
- model: modelName,
881
- description: `Analytics and reporting dashboard for ${modelName}`,
882
- subscribes_to: [
883
- `${modelName}Created`,
884
- `${modelName}Updated`,
885
- `${modelName}Deleted`
886
- ],
887
- uiComponents: {
888
- keyMetrics: {
889
- type: 'MetricsSummary',
890
- properties: {
891
- title: `${modelName} Key Metrics`,
892
- refreshInterval: 300000
893
- }
894
- },
895
- trendsChart: {
896
- type: 'TimeSeriesChart',
897
- properties: {
898
- title: `${modelName} Trends Over Time`,
899
- chartType: 'line',
900
- dateRange: 'last30Days'
901
- }
902
- }
903
- }
476
+ const rendered = Handlebars.compile(template, { strict: false, noEscape: true })(templateContext);
477
+ try {
478
+ const trimmed = rendered.trim();
479
+ if (trimmed.includes(':') || trimmed.startsWith('[') || trimmed.startsWith('{')) {
480
+ return yaml.load(rendered);
904
481
  }
905
- };
906
- }
907
- return {};
908
- }
909
- processSpeclyHandlebars(template, context) {
910
- // Process {{#if model.lifecycle}} blocks
911
- template = template.replace(/\{\{#if model\.lifecycle\}\}([^]*?)\{\{\/if\}\}/g, (match, content) => {
912
- return context.model?.lifecycle ? content : '';
913
- });
914
- // Process {{#each model.attributes}} blocks
915
- template = template.replace(/\{\{#each model\.attributes\}\}([^]*?)\{\{\/each\}\}/g, (match, content) => {
916
- if (!context.model?.attributes)
917
- return '';
918
- if (typeof content !== 'string') {
919
- throw new Error(`content in model.attributes each loop is not a string: ${typeof content}`);
920
482
  }
921
- return context.model.attributes.map((attr) => {
922
- return content.replace(/\{\{name\}\}/g, attr.name)
923
- .replace(/\{\{type\}\}/g, attr.type)
924
- .replace(/\{\{required\}\}/g, attr.required ? 'true' : 'false');
925
- }).join('');
926
- });
927
- // Process {{#if required}} blocks within attribute loops
928
- template = template.replace(/\{\{#if required\}\}([^]*?)\{\{\/if\}\}/g, (match, content) => {
929
- // This will be processed within the context of each attribute
930
- return content; // Keep the content, the required check happens in the attribute loop
931
- });
932
- // Process {{#each relationships.cascadeDeleteTargets}} blocks
933
- template = template.replace(/\{\{#each relationships\.cascadeDeleteTargets\}\}([^]*?)\{\{\/each\}\}/g, (match, content) => {
934
- if (!context.relationships?.cascadeDeleteTargets)
935
- return '';
936
- return context.relationships.cascadeDeleteTargets.map((target) => {
937
- return content.replace(/\{\{this\}\}/g, target);
938
- }).join('');
939
- });
940
- // Process {{#each relationships.parentRelationships}} blocks
941
- template = template.replace(/\{\{#each relationships\.parentRelationships\}\}([^]*?)\{\{\/each\}\}/g, (match, content) => {
942
- if (!context.relationships?.parentRelationships)
943
- return '';
944
- return context.relationships.parentRelationships.map((rel) => {
945
- let processedContent = content;
946
- // Ensure content is a string
947
- if (typeof processedContent !== 'string') {
948
- throw new Error(`processedContent is not a string: ${typeof processedContent}`);
949
- }
950
- // Replace relationship-specific variables
951
- processedContent = processedContent.replace(/\{\{targetModel\}\}/g, rel.targetModel)
952
- .replace(/\{\{\.\.\/modelName\}\}/g, context.modelName || '')
953
- .replace(/\{\{\.\.\/\.\.\/modelName\}\}/g, context.modelName || '');
954
- // Process nested {{#each ../model.attributes}} blocks within this content
955
- processedContent = processedContent.replace(/\{\{#each \.\.\/model\.attributes\}\}([^]*?)\{\{\/each\}\}/g, (nestedMatch, nestedContent) => {
956
- if (!context.model?.attributes)
957
- return '';
958
- if (typeof nestedContent !== 'string') {
959
- throw new Error(`nestedContent is not a string: ${typeof nestedContent}`);
960
- }
961
- return context.model.attributes.map((attr) => {
962
- if (attr.required) {
963
- return nestedContent.replace(/\{\{name\}\}/g, attr.name)
964
- .replace(/\{\{type\}\}/g, attr.type);
965
- }
966
- return '';
967
- }).join('');
968
- });
969
- // Process nested {{#if required}} blocks
970
- processedContent = processedContent.replace(/\{\{#if required\}\}([^]*?)\{\{\/if\}\}/g, (ifMatch, ifContent) => {
971
- return ifContent; // Keep content, filtering happens in the attributes loop
972
- });
973
- return processedContent;
974
- }).join('');
975
- });
976
- return template;
977
- }
978
- processHandlebarsInJson(obj, context) {
979
- if (typeof obj === 'string') {
980
- // Process Handlebars variables
981
- let result = obj.replace(/\{\{(\w+)\}\}/g, (match, key) => {
982
- return context[key] || match;
983
- });
984
- // Process Handlebars conditionals and loops (basic implementation)
985
- result = this.processHandlebarsConditionals(result, context);
986
- return result;
987
- }
988
- else if (Array.isArray(obj)) {
989
- return obj.map(item => this.processHandlebarsInJson(item, context));
990
- }
991
- else if (obj && typeof obj === 'object') {
992
- const processed = {};
993
- for (const [key, value] of Object.entries(obj)) {
994
- const processedKey = this.processHandlebarsInJson(key, context);
995
- processed[processedKey] = this.processHandlebarsInJson(value, context);
483
+ catch {
484
+ // YAML parse failed — return the raw rendered string.
996
485
  }
997
- return processed;
486
+ return rendered;
487
+ }
488
+ catch (error) {
489
+ throw new Error(`Failed to process Specly template: ${error instanceof Error ? error.message : String(error)}`);
998
490
  }
999
- return obj;
1000
- }
1001
- processHandlebarsConditionals(template, context) {
1002
- // Basic conditional processing for {{#if}} and {{#each}}
1003
- // This is a simplified version - a full implementation would use the Handlebars library
1004
- // Process {{#if model.lifecycle}} blocks
1005
- template = template.replace(/\{\{#if model\.lifecycle\}\}([^]*?)\{\{\/if\}\}/g, (match, content) => {
1006
- return context.model?.lifecycle ? content : '';
1007
- });
1008
- // Process {{#each model.attributes}} blocks
1009
- template = template.replace(/\{\{#each model\.attributes\}\}([^]*?)\{\{\/each\}\}/g, (match, content) => {
1010
- if (!context.model?.attributes)
1011
- return '';
1012
- return context.model.attributes.map((attr) => {
1013
- return content.replace(/\{\{name\}\}/g, attr.name)
1014
- .replace(/\{\{type\}\}/g, attr.type)
1015
- .replace(/\{\{required\}\}/g, attr.required);
1016
- }).join('');
1017
- });
1018
- return template;
1019
491
  }
492
+ // processSpeclyHandlebars / processHandlebarsInJson /
493
+ // processHandlebarsConditionals — removed in Cycle 3. They were
494
+ // the home-rolled string-substitution layer that the four
495
+ // apply*Template methods delegated to. All four methods now go
496
+ // through real Handlebars (Handlebars.compile + render), so these
497
+ // have zero callers and are gone.
1020
498
  convertJsonToSpecly(obj) {
1021
499
  // Convert processed JSON object to .specly syntax
1022
500
  let result = '';
@@ -1062,352 +540,5 @@ export class RuleEngine {
1062
540
  }
1063
541
  return result;
1064
542
  }
1065
- generateIntegrationServiceSpec(context) {
1066
- const modelName = context.modelName || 'Unknown';
1067
- const operations = {
1068
- syncToExternalSystems: {
1069
- parameters: {
1070
- event: `${modelName}CreatedEvent required`
1071
- },
1072
- returns: 'IntegrationResult',
1073
- requires: ['External systems are available'],
1074
- ensures: [
1075
- `${modelName} synchronized to all external systems`,
1076
- 'Integration status tracked'
1077
- ]
1078
- },
1079
- updateExternalSystems: {
1080
- parameters: {
1081
- event: `${modelName}UpdatedEvent required`
1082
- },
1083
- returns: 'IntegrationResult',
1084
- requires: ['External systems are available'],
1085
- ensures: [
1086
- 'Changes synchronized to external systems',
1087
- 'Sync conflicts resolved'
1088
- ]
1089
- },
1090
- removeFromExternalSystems: {
1091
- parameters: {
1092
- event: `${modelName}DeletedEvent required`
1093
- },
1094
- returns: 'IntegrationResult',
1095
- ensures: [
1096
- `${modelName} removed from external systems`,
1097
- 'Cleanup operations completed'
1098
- ]
1099
- },
1100
- pullFromExternalSystem: {
1101
- parameters: {
1102
- externalSystemId: 'String required',
1103
- lastSyncTime: 'DateTime optional'
1104
- },
1105
- returns: `Array[${modelName}]`,
1106
- requires: ['External system is accessible'],
1107
- ensures: [
1108
- 'Latest data retrieved from external system',
1109
- 'Sync timestamp updated'
1110
- ]
1111
- },
1112
- pushToExternalSystem: {
1113
- parameters: {
1114
- id: 'UUID required',
1115
- externalSystemId: 'String required',
1116
- forceSync: 'Boolean default=false'
1117
- },
1118
- returns: 'IntegrationResult',
1119
- requires: [
1120
- `${modelName} exists`,
1121
- 'External system is accessible'
1122
- ],
1123
- ensures: [
1124
- `${modelName} pushed to external system`,
1125
- 'Integration status updated'
1126
- ]
1127
- }
1128
- };
1129
- return {
1130
- description: `Manages external system integration for ${modelName}`,
1131
- subscribes_to: {
1132
- [`${modelName}Created`]: 'syncToExternalSystems',
1133
- [`${modelName}Updated`]: 'updateExternalSystems',
1134
- [`${modelName}Deleted`]: 'removeFromExternalSystems'
1135
- },
1136
- operations
1137
- };
1138
- }
1139
- generateLifecycleServiceSpec(context) {
1140
- const modelName = context.modelName || 'Unknown';
1141
- const operations = {
1142
- handleStateChange: {
1143
- parameters: {
1144
- event: `${modelName}EvolvedEvent required`
1145
- },
1146
- returns: 'Void',
1147
- requires: ['State transition is valid'],
1148
- ensures: [
1149
- 'State-dependent actions triggered',
1150
- 'Transition side-effects processed'
1151
- ]
1152
- },
1153
- validateTransition: {
1154
- parameters: {
1155
- id: 'UUID required',
1156
- fromState: 'String required',
1157
- toState: 'String required',
1158
- context: 'Object optional'
1159
- },
1160
- returns: 'TransitionValidationResult',
1161
- requires: [
1162
- `${modelName} exists`,
1163
- 'States are valid lifecycle states'
1164
- ],
1165
- ensures: [
1166
- 'Transition prerequisites validated',
1167
- 'Business rules checked'
1168
- ]
1169
- },
1170
- getAvailableTransitions: {
1171
- parameters: {
1172
- [`${modelName}Id`]: 'UUID required'
1173
- },
1174
- returns: 'Array[TransitionOption]',
1175
- requires: [`${modelName} exists`],
1176
- ensures: ['Returns all valid transitions from current state']
1177
- },
1178
- getStateHistory: {
1179
- parameters: {
1180
- id: 'UUID required',
1181
- limit: 'Integer default=50'
1182
- },
1183
- returns: 'Array[StateHistoryEntry]',
1184
- requires: [`${modelName} exists`],
1185
- ensures: ['Returns chronological state change history']
1186
- }
1187
- };
1188
- // Add dynamic transition operations if lifecycle exists
1189
- if (context.model?.lifecycle?.transitions) {
1190
- context.model.lifecycle.transitions.forEach((transition) => {
1191
- const operationName = `execute${transition.name}Transition`;
1192
- operations[operationName] = {
1193
- parameters: {
1194
- id: 'UUID required',
1195
- reason: 'String optional',
1196
- context: 'Object optional'
1197
- },
1198
- returns: modelName,
1199
- requires: [
1200
- `${modelName} exists`,
1201
- `${modelName} current state is '${transition.from}'`
1202
- ],
1203
- ensures: [
1204
- `${modelName} state changed to '${transition.to}'`,
1205
- 'Transition side-effects completed'
1206
- ]
1207
- };
1208
- });
1209
- }
1210
- return {
1211
- description: `Manages lifecycle transitions and state-dependent behavior for ${modelName}`,
1212
- subscribes_to: {
1213
- [`${modelName}Evolved`]: 'handleStateChange'
1214
- },
1215
- operations
1216
- };
1217
- }
1218
- generateRelationshipServiceSpec(context) {
1219
- const modelName = context.modelName || 'Unknown';
1220
- const operations = {
1221
- handleChildAdded: {
1222
- parameters: {
1223
- event: 'ChildEntityEvent required'
1224
- },
1225
- returns: 'Void',
1226
- ensures: [
1227
- 'Parent relationship established',
1228
- 'Relationship integrity maintained'
1229
- ]
1230
- },
1231
- handleChildRemoved: {
1232
- parameters: {
1233
- event: 'ChildEntityEvent required'
1234
- },
1235
- returns: 'Void',
1236
- ensures: [
1237
- 'Parent relationship cleaned up',
1238
- 'Cascade rules applied'
1239
- ]
1240
- },
1241
- validateRelationshipIntegrity: {
1242
- parameters: {
1243
- id: 'UUID required',
1244
- relationshipType: 'String optional'
1245
- },
1246
- returns: 'IntegrityValidationResult',
1247
- requires: [`${modelName} exists`],
1248
- ensures: [
1249
- 'All relationships validated for integrity',
1250
- 'Constraint violations identified'
1251
- ]
1252
- },
1253
- repairRelationshipIntegrity: {
1254
- parameters: {
1255
- id: 'UUID required',
1256
- repairOptions: 'RepairOptions required'
1257
- },
1258
- returns: 'RepairResult',
1259
- requires: [
1260
- `${modelName} exists`,
1261
- 'Repair options are valid'
1262
- ],
1263
- ensures: [
1264
- 'Relationship integrity issues resolved',
1265
- 'Repair actions logged'
1266
- ]
1267
- }
1268
- };
1269
- const subscribes_to = {};
1270
- // Add child relationship subscriptions
1271
- if (context.relationships?.childRelationships) {
1272
- context.relationships.childRelationships.forEach((rel) => {
1273
- subscribes_to[`${rel.targetModel}Created`] = 'handleChildAdded';
1274
- subscribes_to[`${rel.targetModel}Deleted`] = 'handleChildRemoved';
1275
- });
1276
- }
1277
- // Add many-to-many relationship subscriptions and operations
1278
- if (context.relationships?.manyToManyRelationships) {
1279
- context.relationships.manyToManyRelationships.forEach((rel) => {
1280
- subscribes_to[`${modelName}AssociatedWith${rel.targetModel}`] = 'handleAssociationAdded';
1281
- subscribes_to[`${modelName}DisassociatedFrom${rel.targetModel}`] = 'handleAssociationRemoved';
1282
- operations.handleAssociationAdded = {
1283
- parameters: {
1284
- event: 'AssociationEvent required'
1285
- },
1286
- returns: 'Void',
1287
- ensures: [
1288
- 'Association established',
1289
- 'Association metadata updated'
1290
- ]
1291
- };
1292
- operations.handleAssociationRemoved = {
1293
- parameters: {
1294
- event: 'DisassociationEvent required'
1295
- },
1296
- returns: 'Void',
1297
- ensures: [
1298
- 'Association removed',
1299
- 'Related data cleaned up'
1300
- ]
1301
- };
1302
- });
1303
- }
1304
- return {
1305
- description: `Manages complex relationships and ensures relationship integrity for ${modelName}`,
1306
- subscribes_to,
1307
- operations
1308
- };
1309
- }
1310
- generateRelationshipEventsSpec(context) {
1311
- const modelName = context.modelName || 'Unknown';
1312
- const events = {};
1313
- // Parent relationship events
1314
- if (context.relationships?.parentRelationships) {
1315
- context.relationships.parentRelationships.forEach((rel) => {
1316
- const targetModel = rel.targetModel;
1317
- // Child created within parent
1318
- events[`${modelName}CreatedIn${targetModel}`] = {
1319
- description: `${modelName} was created within ${targetModel}`,
1320
- attributes: {
1321
- id: 'UUID required',
1322
- [`${targetModel.toLowerCase()}Id`]: 'UUID required',
1323
- [`${targetModel.toLowerCase()}Name`]: 'String optional',
1324
- timestamp: 'DateTime required',
1325
- createdBy: 'UUID required'
1326
- }
1327
- };
1328
- // Add name attribute if model has one
1329
- if (context.model?.attributes?.some((attr) => attr.name === 'name')) {
1330
- events[`${modelName}CreatedIn${targetModel}`].attributes.name = 'String required';
1331
- }
1332
- // Child removed from parent
1333
- events[`${modelName}RemovedFrom${targetModel}`] = {
1334
- description: `${modelName} was removed from ${targetModel}`,
1335
- attributes: {
1336
- id: 'UUID required',
1337
- [`${targetModel.toLowerCase()}Id`]: 'UUID required',
1338
- [`${targetModel.toLowerCase()}Name`]: 'String optional',
1339
- removedBy: 'UUID required',
1340
- timestamp: 'DateTime required',
1341
- reason: 'String optional'
1342
- }
1343
- };
1344
- });
1345
- }
1346
- // Child relationship events
1347
- if (context.relationships?.childRelationships) {
1348
- context.relationships.childRelationships.forEach((rel) => {
1349
- const targetModel = rel.targetModel || rel.name;
1350
- const relType = rel.type || 'hasMany';
1351
- // Child added to parent
1352
- events[`${targetModel}AddedTo${modelName}`] = {
1353
- description: `${targetModel} was added to ${modelName}`,
1354
- attributes: {
1355
- [`${modelName.toLowerCase()}Id`]: 'UUID required',
1356
- [`${targetModel.toLowerCase()}Id`]: 'UUID required',
1357
- relationshipType: `String default=${relType}`,
1358
- addedBy: 'UUID required',
1359
- timestamp: 'DateTime required'
1360
- }
1361
- };
1362
- // Child removed from parent
1363
- const removeEvent = {
1364
- description: `${targetModel} was removed from ${modelName}`,
1365
- attributes: {
1366
- [`${modelName.toLowerCase()}Id`]: 'UUID required',
1367
- [`${targetModel.toLowerCase()}Id`]: 'UUID required',
1368
- relationshipType: `String default=${relType}`,
1369
- removedBy: 'UUID required',
1370
- timestamp: 'DateTime required'
1371
- }
1372
- };
1373
- // Add cascade flag if specified
1374
- if (rel.cascadeDelete) {
1375
- removeEvent.attributes.cascadeTriggered = 'Boolean default=true';
1376
- }
1377
- events[`${targetModel}RemovedFrom${modelName}`] = removeEvent;
1378
- });
1379
- }
1380
- // Many-to-many relationship events
1381
- if (context.relationships?.manyToManyRelationships) {
1382
- context.relationships.manyToManyRelationships.forEach((rel) => {
1383
- const targetModel = rel.targetModel;
1384
- // Association created
1385
- events[`${modelName}AssociatedWith${targetModel}`] = {
1386
- description: `${modelName} was associated with ${targetModel}`,
1387
- attributes: {
1388
- [`${modelName.toLowerCase()}Id`]: 'UUID required',
1389
- [`${targetModel.toLowerCase()}Id`]: 'UUID required',
1390
- associationType: 'String default=manyToMany',
1391
- associationData: 'Object optional',
1392
- associatedBy: 'UUID required',
1393
- timestamp: 'DateTime required'
1394
- }
1395
- };
1396
- // Association removed
1397
- events[`${modelName}DisassociatedFrom${targetModel}`] = {
1398
- description: `${modelName} was disassociated from ${targetModel}`,
1399
- attributes: {
1400
- [`${modelName.toLowerCase()}Id`]: 'UUID required',
1401
- [`${targetModel.toLowerCase()}Id`]: 'UUID required',
1402
- associationType: 'String default=manyToMany',
1403
- disassociatedBy: 'UUID required',
1404
- timestamp: 'DateTime required',
1405
- reason: 'String optional'
1406
- }
1407
- };
1408
- });
1409
- }
1410
- return events;
1411
- }
1412
543
  }
1413
544
  //# sourceMappingURL=rule-engine.js.map