orfeas_lyra 0.6.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 (81) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +222 -0
  3. data/LICENSE +21 -0
  4. data/README.md +1165 -0
  5. data/Rakefile +728 -0
  6. data/app/controllers/lyra/application_controller.rb +23 -0
  7. data/app/controllers/lyra/dashboard_controller.rb +624 -0
  8. data/app/controllers/lyra/flow_controller.rb +224 -0
  9. data/app/controllers/lyra/privacy_controller.rb +182 -0
  10. data/app/views/lyra/dashboard/audit_trail.html.erb +324 -0
  11. data/app/views/lyra/dashboard/discrepancies.html.erb +125 -0
  12. data/app/views/lyra/dashboard/event_graph_view.html.erb +525 -0
  13. data/app/views/lyra/dashboard/heatmap_view.html.erb +155 -0
  14. data/app/views/lyra/dashboard/index.html.erb +119 -0
  15. data/app/views/lyra/dashboard/model_overview.html.erb +115 -0
  16. data/app/views/lyra/dashboard/projections.html.erb +302 -0
  17. data/app/views/lyra/dashboard/schema.html.erb +283 -0
  18. data/app/views/lyra/dashboard/schema_history.html.erb +78 -0
  19. data/app/views/lyra/dashboard/schema_version.html.erb +340 -0
  20. data/app/views/lyra/dashboard/verification.html.erb +370 -0
  21. data/app/views/lyra/flow/crud_mapping.html.erb +125 -0
  22. data/app/views/lyra/flow/timeline.html.erb +260 -0
  23. data/app/views/lyra/privacy/pii_detection.html.erb +148 -0
  24. data/app/views/lyra/privacy/policy.html.erb +188 -0
  25. data/app/workflows/es_async_mode_workflow.rb +80 -0
  26. data/app/workflows/es_sync_mode_workflow.rb +64 -0
  27. data/app/workflows/hijack_mode_workflow.rb +54 -0
  28. data/app/workflows/lifecycle_workflow.rb +43 -0
  29. data/app/workflows/monitor_mode_workflow.rb +39 -0
  30. data/config/privacy_policies.rb +273 -0
  31. data/config/routes.rb +48 -0
  32. data/lib/lyra/aggregate.rb +131 -0
  33. data/lib/lyra/associations/event_aware.rb +225 -0
  34. data/lib/lyra/command.rb +81 -0
  35. data/lib/lyra/command_handler.rb +155 -0
  36. data/lib/lyra/configuration.rb +124 -0
  37. data/lib/lyra/consistency/read_your_writes.rb +91 -0
  38. data/lib/lyra/correlation.rb +144 -0
  39. data/lib/lyra/dual_view.rb +231 -0
  40. data/lib/lyra/engine.rb +67 -0
  41. data/lib/lyra/event.rb +71 -0
  42. data/lib/lyra/event_analyzer.rb +135 -0
  43. data/lib/lyra/event_flow.rb +449 -0
  44. data/lib/lyra/event_mapper.rb +106 -0
  45. data/lib/lyra/event_store_adapter.rb +72 -0
  46. data/lib/lyra/id_generator.rb +137 -0
  47. data/lib/lyra/interceptors/association_interceptor.rb +169 -0
  48. data/lib/lyra/interceptors/crud_interceptor.rb +543 -0
  49. data/lib/lyra/privacy/gdpr_compliance.rb +161 -0
  50. data/lib/lyra/privacy/pii_detector.rb +85 -0
  51. data/lib/lyra/privacy/pii_masker.rb +66 -0
  52. data/lib/lyra/privacy/policy_integration.rb +253 -0
  53. data/lib/lyra/projection.rb +94 -0
  54. data/lib/lyra/projections/async_projection_job.rb +63 -0
  55. data/lib/lyra/projections/cached_projection.rb +322 -0
  56. data/lib/lyra/projections/cached_relation.rb +757 -0
  57. data/lib/lyra/projections/event_store_reader.rb +127 -0
  58. data/lib/lyra/projections/model_projection.rb +143 -0
  59. data/lib/lyra/schema/diff.rb +331 -0
  60. data/lib/lyra/schema/event_class_registrar.rb +63 -0
  61. data/lib/lyra/schema/generator.rb +190 -0
  62. data/lib/lyra/schema/reporter.rb +188 -0
  63. data/lib/lyra/schema/store.rb +156 -0
  64. data/lib/lyra/schema/validator.rb +100 -0
  65. data/lib/lyra/strict_data_access.rb +363 -0
  66. data/lib/lyra/verification/crud_lifecycle_workflow.rb +456 -0
  67. data/lib/lyra/verification/workflow_generator.rb +540 -0
  68. data/lib/lyra/version.rb +3 -0
  69. data/lib/lyra/visualization/activity_heatmap.rb +215 -0
  70. data/lib/lyra/visualization/event_graph.rb +310 -0
  71. data/lib/lyra/visualization/timeline.rb +398 -0
  72. data/lib/lyra.rb +150 -0
  73. data/lib/tasks/dist.rake +391 -0
  74. data/lib/tasks/gems.rake +185 -0
  75. data/lib/tasks/lyra_schema.rake +231 -0
  76. data/lib/tasks/lyra_workflows.rake +452 -0
  77. data/lib/tasks/public_release.rake +351 -0
  78. data/lib/tasks/stats.rake +175 -0
  79. data/lib/tasks/testbed.rake +479 -0
  80. data/lib/tasks/version.rake +159 -0
  81. metadata +221 -0
@@ -0,0 +1,540 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lyra
4
+ module Verification
5
+ # Generates Petri net workflows by introspecting Lyra's actual implementation.
6
+ # Uses metaprogramming to analyze callbacks, modes, and model configurations.
7
+ class WorkflowGenerator
8
+ attr_reader :analysis, :options
9
+
10
+ AVAILABLE_MODES = [:monitor, :hijack, :es_sync, :es_async].freeze
11
+
12
+ def initialize(options = {})
13
+ @options = options
14
+ @analysis = {
15
+ modes: [],
16
+ callbacks: {},
17
+ models: [],
18
+ event_types: []
19
+ }
20
+ end
21
+
22
+ # Analyze Lyra implementation and generate workflow
23
+ # @param mode [Symbol, nil] Specific mode to generate workflow for, or nil for all
24
+ def generate!(mode: nil)
25
+ analyze_modes
26
+ analyze_callbacks
27
+ analyze_monitored_models
28
+ analyze_event_types
29
+
30
+ result = {
31
+ lifecycle_workflow: generate_lifecycle_workflow,
32
+ analysis: @analysis
33
+ }
34
+
35
+ if mode
36
+ # Generate workflow for specific mode
37
+ validate_mode!(mode)
38
+ result[:mode_workflow] = generate_workflow_for_mode(mode)
39
+ else
40
+ # Generate workflows for all modes
41
+ result[:mode_workflows] = generate_all_mode_workflows
42
+ end
43
+
44
+ result
45
+ end
46
+
47
+ private
48
+
49
+ def validate_mode!(mode)
50
+ unless AVAILABLE_MODES.include?(mode.to_sym)
51
+ raise ArgumentError, "Invalid mode: #{mode}. Available modes: #{AVAILABLE_MODES.join(', ')}"
52
+ end
53
+ end
54
+
55
+ # Analyze available Lyra modes from configuration
56
+ def analyze_modes
57
+ @analysis[:modes] = []
58
+
59
+ # Check which modes are defined in Lyra
60
+ if Lyra.respond_to?(:config)
61
+ config = Lyra.config
62
+
63
+ # Introspect the mode from config
64
+ if config.respond_to?(:mode)
65
+ current_mode = config.mode
66
+ @analysis[:current_mode] = current_mode
67
+ end
68
+ end
69
+
70
+ # Define all possible modes based on Lyra's design
71
+ @analysis[:modes] = [:monitor, :hijack, :es_sync, :es_async, :disabled]
72
+
73
+ # Check which mode methods exist
74
+ @analysis[:mode_methods] = {
75
+ monitor: Lyra.respond_to?(:monitor_mode?),
76
+ hijack: Lyra.respond_to?(:hijack_mode?),
77
+ event_sourcing: Lyra.respond_to?(:event_sourcing_mode?)
78
+ }
79
+ end
80
+
81
+ # Analyze ActiveRecord callbacks used by Lyra
82
+ def analyze_callbacks
83
+ @analysis[:callbacks] = {
84
+ create: [],
85
+ update: [],
86
+ destroy: []
87
+ }
88
+
89
+ # Introspect the Lyra::Monitorable module for callback definitions
90
+ if defined?(Lyra::Monitorable)
91
+ monitorable = Lyra::Monitorable
92
+
93
+ # Check instance methods for callback patterns
94
+ if monitorable.respond_to?(:instance_methods)
95
+ methods = monitorable.instance_methods(false)
96
+
97
+ methods.each do |method_name|
98
+ name = method_name.to_s
99
+ if name.include?('create')
100
+ @analysis[:callbacks][:create] << method_name
101
+ elsif name.include?('update')
102
+ @analysis[:callbacks][:update] << method_name
103
+ elsif name.include?('destroy')
104
+ @analysis[:callbacks][:destroy] << method_name
105
+ end
106
+ end
107
+ end
108
+
109
+ # Check for callback registrations in ClassMethods
110
+ if monitorable.const_defined?(:ClassMethods)
111
+ class_methods = monitorable::ClassMethods.instance_methods(false)
112
+ @analysis[:class_methods] = class_methods
113
+ end
114
+ end
115
+
116
+ # Analyze the actual callback hooks from ActiveSupport
117
+ @analysis[:callback_hooks] = extract_callback_hooks
118
+ end
119
+
120
+ # Extract callback hook information from Lyra source
121
+ def extract_callback_hooks
122
+ hooks = { before: [], after: [] }
123
+
124
+ # Find Lyra gem root from the loaded gem spec
125
+ lyra_root = Gem.loaded_specs['lyra']&.gem_dir
126
+ lyra_root ||= File.expand_path('../../../..', __FILE__)
127
+
128
+ # Look for callback definitions in Monitorable
129
+ monitorable_file = File.join(lyra_root, 'lib', 'lyra', 'monitorable.rb')
130
+
131
+ if File.exist?(monitorable_file)
132
+ content = File.read(monitorable_file)
133
+
134
+ # Find after_* callbacks
135
+ content.scan(/after_(create|update|destroy|commit|save)\s+:(\w+)/) do |type, method|
136
+ hooks[:after] << { type: type, method: method }
137
+ end
138
+
139
+ # Find before_* callbacks
140
+ content.scan(/before_(create|update|destroy|save)\s+:(\w+)/) do |type, method|
141
+ hooks[:before] << { type: type, method: method }
142
+ end
143
+ end
144
+
145
+ hooks
146
+ end
147
+
148
+ # Analyze monitored models
149
+ def analyze_monitored_models
150
+ @analysis[:models] = []
151
+
152
+ if Lyra.respond_to?(:config) && Lyra.config.respond_to?(:monitored_models)
153
+ Lyra.config.monitored_models.each do |model_class|
154
+ model_info = {
155
+ name: model_class.name,
156
+ table_name: model_class.respond_to?(:table_name) ? model_class.table_name : nil,
157
+ callbacks: extract_model_callbacks(model_class)
158
+ }
159
+
160
+ # Get Lyra-specific configuration
161
+ if Lyra.config.respond_to?(:model_config)
162
+ config = Lyra.config.model_config(model_class)
163
+ model_info[:event_prefix] = config.event_prefix if config.respond_to?(:event_prefix)
164
+ model_info[:excluded_attributes] = config.excluded_attributes if config.respond_to?(:excluded_attributes)
165
+ end
166
+
167
+ @analysis[:models] << model_info
168
+ end
169
+ end
170
+ end
171
+
172
+ # Extract callbacks registered on a specific model
173
+ def extract_model_callbacks(model_class)
174
+ callbacks = {}
175
+
176
+ [:create, :update, :destroy, :save, :commit].each do |type|
177
+ [:before, :after, :around].each do |timing|
178
+ callback_name = "#{timing}_#{type}"
179
+ if model_class.respond_to?("_#{callback_name}_callbacks")
180
+ chain = model_class.send("_#{callback_name}_callbacks")
181
+ callbacks["#{timing}_#{type}"] = chain.map { |cb| cb.filter.to_s } if chain.any?
182
+ end
183
+ end
184
+ end
185
+
186
+ callbacks
187
+ end
188
+
189
+ # Analyze event types published by Lyra
190
+ def analyze_event_types
191
+ @analysis[:event_types] = []
192
+
193
+ # Check for event class definitions
194
+ if defined?(Lyra::Events)
195
+ Lyra::Events.constants.each do |const|
196
+ event_class = Lyra::Events.const_get(const)
197
+ if event_class.is_a?(Class)
198
+ @analysis[:event_types] << {
199
+ name: const.to_s,
200
+ class: event_class.name,
201
+ attributes: event_class.respond_to?(:attribute_names) ? event_class.attribute_names : []
202
+ }
203
+ end
204
+ end
205
+ end
206
+
207
+ # Infer from CRUD operations
208
+ @analysis[:crud_events] = [:Created, :Updated, :Destroyed]
209
+ end
210
+
211
+ # Generate lifecycle workflow from analysis
212
+ def generate_lifecycle_workflow
213
+ return nil unless defined?(PetriFlow)
214
+
215
+ places = [:nonexistent, :created, :persisted, :updated, :destroyed, :deleted]
216
+ transitions = []
217
+
218
+ # Generate transitions based on analyzed callbacks
219
+ hooks = @analysis[:callback_hooks]
220
+
221
+ # CREATE flow
222
+ transitions << {
223
+ name: :create,
224
+ from: :nonexistent,
225
+ to: :created,
226
+ trigger: hooks[:after].find { |h| h[:type] == 'create' }&.dig(:method) || 'after_create'
227
+ }
228
+ transitions << {
229
+ name: :emit_created_event,
230
+ from: :created,
231
+ to: :persisted,
232
+ trigger: 'publish Created event'
233
+ }
234
+
235
+ # UPDATE flow (cycle)
236
+ transitions << {
237
+ name: :update,
238
+ from: :persisted,
239
+ to: :updated,
240
+ trigger: hooks[:after].find { |h| h[:type] == 'update' }&.dig(:method) || 'after_update'
241
+ }
242
+ transitions << {
243
+ name: :emit_updated_event,
244
+ from: :updated,
245
+ to: :persisted,
246
+ trigger: 'publish Updated event'
247
+ }
248
+
249
+ # DESTROY flow
250
+ transitions << {
251
+ name: :destroy,
252
+ from: :persisted,
253
+ to: :destroyed,
254
+ trigger: hooks[:after].find { |h| h[:type] == 'destroy' }&.dig(:method) || 'after_destroy'
255
+ }
256
+ transitions << {
257
+ name: :emit_destroyed_event,
258
+ from: :destroyed,
259
+ to: :deleted,
260
+ trigger: 'publish Destroyed event'
261
+ }
262
+
263
+ {
264
+ name: "CRUD Entity Lifecycle (Generated)",
265
+ places: places,
266
+ initial_place: :nonexistent,
267
+ terminal_places: [:deleted],
268
+ transitions: transitions,
269
+ generated_at: Time.current,
270
+ source: "Lyra::Verification::WorkflowGenerator"
271
+ }
272
+ end
273
+
274
+ # Generate mode workflow from analysis
275
+ def generate_mode_workflow
276
+ return nil unless defined?(PetriFlow)
277
+
278
+ modes = @analysis[:modes].reject { |m| m == :disabled }
279
+
280
+ places = [:idle, :operation_requested]
281
+ transitions = []
282
+
283
+ # Add places for each mode
284
+ modes.each do |mode|
285
+ places << "#{mode}_processing".to_sym
286
+ end
287
+ places += [:event_published, :completed]
288
+
289
+ # Initial transition
290
+ transitions << {
291
+ name: :request_operation,
292
+ from: :idle,
293
+ to: :operation_requested,
294
+ trigger: 'CRUD operation initiated'
295
+ }
296
+
297
+ # Mode-specific transitions
298
+ modes.each do |mode|
299
+ processing_place = "#{mode}_processing".to_sym
300
+
301
+ transitions << {
302
+ name: "select_#{mode}_mode".to_sym,
303
+ from: :operation_requested,
304
+ to: processing_place,
305
+ trigger: "Lyra.config.mode == :#{mode}"
306
+ }
307
+
308
+ transitions << {
309
+ name: "#{mode}_complete".to_sym,
310
+ from: processing_place,
311
+ to: :event_published,
312
+ trigger: "#{mode} mode processing complete"
313
+ }
314
+ end
315
+
316
+ # Final transition
317
+ transitions << {
318
+ name: :finalize,
319
+ from: :event_published,
320
+ to: :completed,
321
+ trigger: 'Event stored'
322
+ }
323
+
324
+ {
325
+ name: "Lyra Mode Selection (Generated)",
326
+ places: places,
327
+ initial_place: :idle,
328
+ terminal_places: [:completed],
329
+ transitions: transitions,
330
+ generated_at: Time.current,
331
+ source: "Lyra::Verification::WorkflowGenerator"
332
+ }
333
+ end
334
+
335
+ # Generate workflows for all available modes
336
+ def generate_all_mode_workflows
337
+ return {} unless defined?(PetriFlow)
338
+
339
+ AVAILABLE_MODES.each_with_object({}) do |mode, workflows|
340
+ workflows[mode] = generate_workflow_for_mode(mode)
341
+ end
342
+ end
343
+
344
+ # Generate workflow for a specific mode
345
+ def generate_workflow_for_mode(mode)
346
+ return nil unless defined?(PetriFlow)
347
+
348
+ case mode.to_sym
349
+ when :monitor
350
+ generate_monitor_workflow
351
+ when :hijack
352
+ generate_hijack_workflow
353
+ when :es_sync
354
+ generate_es_sync_workflow
355
+ when :es_async
356
+ generate_es_async_workflow
357
+ end
358
+ end
359
+
360
+ # Monitor Mode: Passive observation of CRUD operations
361
+ # CRUD executes normally, events published after the fact
362
+ def generate_monitor_workflow
363
+ {
364
+ name: "Monitor Mode Workflow",
365
+ description: "Passively observes CRUD operations without modification",
366
+ places: [
367
+ :idle,
368
+ :crud_executing,
369
+ :crud_completed,
370
+ :event_building,
371
+ :event_publishing,
372
+ :completed
373
+ ],
374
+ initial_place: :idle,
375
+ terminal_places: [:completed],
376
+ transitions: [
377
+ { name: :receive_crud, from: :idle, to: :crud_executing,
378
+ trigger: "ActiveRecord callback triggered" },
379
+ { name: :crud_success, from: :crud_executing, to: :crud_completed,
380
+ trigger: "CRUD operation completes successfully" },
381
+ { name: :build_event, from: :crud_completed, to: :event_building,
382
+ trigger: "Extract changes from model" },
383
+ { name: :publish_event, from: :event_building, to: :event_publishing,
384
+ trigger: "Lyra::Event.publish" },
385
+ { name: :store_event, from: :event_publishing, to: :completed,
386
+ trigger: "RailsEventStore.publish" }
387
+ ],
388
+ generated_at: Time.current,
389
+ source: "Lyra::Verification::WorkflowGenerator"
390
+ }
391
+ end
392
+
393
+ # Hijack Mode: Intercepts CRUD, converts to command/event flow
394
+ # Original CRUD is prevented, replaced with event-sourced operation
395
+ def generate_hijack_workflow
396
+ {
397
+ name: "Hijack Mode Workflow",
398
+ description: "Intercepts CRUD operations and converts to event sourcing",
399
+ places: [
400
+ :idle,
401
+ :crud_intercepted,
402
+ :command_created,
403
+ :command_validating,
404
+ :command_valid,
405
+ :event_created,
406
+ :event_stored,
407
+ :projecting,
408
+ :completed
409
+ ],
410
+ initial_place: :idle,
411
+ terminal_places: [:completed],
412
+ transitions: [
413
+ { name: :intercept_crud, from: :idle, to: :crud_intercepted,
414
+ trigger: "before_* callback intercepts operation" },
415
+ { name: :create_command, from: :crud_intercepted, to: :command_created,
416
+ trigger: "Convert CRUD to Lyra::Command" },
417
+ { name: :validate_command, from: :command_created, to: :command_validating,
418
+ trigger: "CommandHandler.validate" },
419
+ { name: :command_passes, from: :command_validating, to: :command_valid,
420
+ trigger: "Validation passes" },
421
+ { name: :emit_event, from: :command_valid, to: :event_created,
422
+ trigger: "CommandHandler.execute creates event" },
423
+ { name: :store_event, from: :event_created, to: :event_stored,
424
+ trigger: "RailsEventStore.publish" },
425
+ { name: :project_state, from: :event_stored, to: :projecting,
426
+ trigger: "Projection.apply(event)" },
427
+ { name: :projection_complete, from: :projecting, to: :completed,
428
+ trigger: "Model state updated from event" }
429
+ ],
430
+ generated_at: Time.current,
431
+ source: "Lyra::Verification::WorkflowGenerator"
432
+ }
433
+ end
434
+
435
+ # ES Sync Mode: Full event sourcing with synchronous projection
436
+ # Blocks until projection is complete
437
+ def generate_es_sync_workflow
438
+ {
439
+ name: "Event Sourcing Sync Mode Workflow",
440
+ description: "Full event sourcing with synchronous (blocking) projection",
441
+ places: [
442
+ :idle,
443
+ :command_received,
444
+ :aggregate_loading,
445
+ :aggregate_loaded,
446
+ :command_applying,
447
+ :events_generated,
448
+ :events_storing,
449
+ :events_stored,
450
+ :projecting_sync,
451
+ :projection_complete,
452
+ :completed
453
+ ],
454
+ initial_place: :idle,
455
+ terminal_places: [:completed],
456
+ transitions: [
457
+ { name: :receive_command, from: :idle, to: :command_received,
458
+ trigger: "CommandHandler receives command" },
459
+ { name: :load_aggregate, from: :command_received, to: :aggregate_loading,
460
+ trigger: "Load aggregate from event stream" },
461
+ { name: :aggregate_ready, from: :aggregate_loading, to: :aggregate_loaded,
462
+ trigger: "Aggregate hydrated from events" },
463
+ { name: :apply_command, from: :aggregate_loaded, to: :command_applying,
464
+ trigger: "Aggregate.apply(command)" },
465
+ { name: :generate_events, from: :command_applying, to: :events_generated,
466
+ trigger: "Domain events created" },
467
+ { name: :store_events, from: :events_generated, to: :events_storing,
468
+ trigger: "EventStore.append_to_stream" },
469
+ { name: :events_persisted, from: :events_storing, to: :events_stored,
470
+ trigger: "Events committed to store" },
471
+ { name: :project_sync, from: :events_stored, to: :projecting_sync,
472
+ trigger: "ModelProjection.apply (synchronous)" },
473
+ { name: :sync_complete, from: :projecting_sync, to: :projection_complete,
474
+ trigger: "Read model updated" },
475
+ { name: :finalize, from: :projection_complete, to: :completed,
476
+ trigger: "Response returned to caller" }
477
+ ],
478
+ generated_at: Time.current,
479
+ source: "Lyra::Verification::WorkflowGenerator"
480
+ }
481
+ end
482
+
483
+ # ES Async Mode: Full event sourcing with asynchronous projection
484
+ # Returns immediately, projection happens in background
485
+ #
486
+ # Uses Petri net FORK pattern to model true parallelism:
487
+ # After events are stored, a single fork transition places tokens in
488
+ # BOTH response_returned AND job_processing simultaneously.
489
+ def generate_es_async_workflow
490
+ {
491
+ name: "Event Sourcing Async Mode Workflow",
492
+ description: "Full event sourcing with asynchronous (non-blocking) projection",
493
+ places: [
494
+ :idle,
495
+ :command_received,
496
+ :aggregate_loading,
497
+ :aggregate_loaded,
498
+ :command_applying,
499
+ :events_generated,
500
+ :events_storing,
501
+ :events_stored,
502
+ :response_returned, # Terminal: caller gets response immediately
503
+ :job_processing, # Background job starts
504
+ :projecting_async,
505
+ :projection_complete # Terminal: projection eventually completes
506
+ ],
507
+ initial_place: :idle,
508
+ terminal_places: [:response_returned, :projection_complete],
509
+ transitions: [
510
+ { name: :receive_command, from: :idle, to: :command_received,
511
+ trigger: "CommandHandler receives command" },
512
+ { name: :load_aggregate, from: :command_received, to: :aggregate_loading,
513
+ trigger: "Load aggregate from event stream" },
514
+ { name: :aggregate_ready, from: :aggregate_loading, to: :aggregate_loaded,
515
+ trigger: "Aggregate hydrated from events" },
516
+ { name: :apply_command, from: :aggregate_loaded, to: :command_applying,
517
+ trigger: "Aggregate.apply(command)" },
518
+ { name: :generate_events, from: :command_applying, to: :events_generated,
519
+ trigger: "Domain events created" },
520
+ { name: :store_events, from: :events_generated, to: :events_storing,
521
+ trigger: "EventStore.append_to_stream" },
522
+ { name: :events_persisted, from: :events_storing, to: :events_stored,
523
+ trigger: "Events committed to store" },
524
+ # FORK: Parallel split - produces tokens in TWO places simultaneously
525
+ # Models async behavior: response returns AND background job starts
526
+ { name: :async_fork, from: :events_stored, to: [:response_returned, :job_processing],
527
+ trigger: "Enqueue job & return response (parallel)" },
528
+ # Background projection flow (runs independently after fork)
529
+ { name: :project_async, from: :job_processing, to: :projecting_async,
530
+ trigger: "ModelProjection.apply (async)" },
531
+ { name: :async_complete, from: :projecting_async, to: :projection_complete,
532
+ trigger: "Read model eventually consistent" }
533
+ ],
534
+ generated_at: Time.current,
535
+ source: "Lyra::Verification::WorkflowGenerator"
536
+ }
537
+ end
538
+ end
539
+ end
540
+ end
@@ -0,0 +1,3 @@
1
+ module Lyra
2
+ VERSION = "0.6.0"
3
+ end