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,456 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lyra
4
+ module Verification
5
+ # Petri Net model for verifying CRUD→Event mapping correctness.
6
+ #
7
+ # This workflow models the lifecycle of an entity through CRUD operations
8
+ # and verifies that:
9
+ # 1. Every CRUD operation generates exactly one event
10
+ # 2. Entity state transitions are valid (can't update/destroy before create)
11
+ # 3. All terminal states are reachable
12
+ # 4. No deadlocks in the event flow
13
+ #
14
+ # @example Verify the workflow
15
+ # workflow = Lyra::Verification::CrudLifecycleWorkflow.new
16
+ # results = workflow.verify!
17
+ # puts results[:liveness][:deadlock_free] # => true
18
+ #
19
+ class CrudLifecycleWorkflow < PetriFlow::Workflow
20
+ workflow_name "CRUD Entity Lifecycle"
21
+
22
+ # Entity lifecycle states
23
+ # - nonexistent: Entity does not exist yet
24
+ # - created: Entity was just created (event pending)
25
+ # - persisted: Entity exists in storage
26
+ # - updated: Entity was just updated (event pending)
27
+ # - destroyed: Entity was just destroyed (event pending)
28
+ # - deleted: Entity no longer exists (terminal)
29
+ places :nonexistent, :created, :persisted, :updated, :destroyed, :deleted
30
+
31
+ # Starting state - entity does not exist
32
+ initial_place :nonexistent
33
+
34
+ # Terminal state - entity is deleted
35
+ terminal_places :deleted
36
+
37
+ # CRUD Operations as Transitions
38
+ # Each transition represents a CRUD operation that:
39
+ # 1. Changes entity state
40
+ # 2. Generates a corresponding event
41
+
42
+ # CREATE: nonexistent → created → persisted
43
+ transition :create, from: :nonexistent, to: :created,
44
+ trigger: "ActiveRecord after_create callback"
45
+ transition :emit_created_event, from: :created, to: :persisted,
46
+ trigger: "Lyra::Events::Created published"
47
+
48
+ # UPDATE: persisted → updated → persisted (cycle)
49
+ transition :update, from: :persisted, to: :updated,
50
+ trigger: "ActiveRecord after_update callback"
51
+ transition :emit_updated_event, from: :updated, to: :persisted,
52
+ trigger: "Lyra::Events::Updated published"
53
+
54
+ # DESTROY: persisted → destroyed → deleted
55
+ transition :destroy, from: :persisted, to: :destroyed,
56
+ trigger: "ActiveRecord after_destroy callback"
57
+ transition :emit_destroyed_event, from: :destroyed, to: :deleted,
58
+ trigger: "Lyra::Events::Destroyed published"
59
+ end
60
+
61
+ # CREATE operation workflow across all Lyra modes
62
+ class CreateModeWorkflow < PetriFlow::Workflow
63
+ workflow_name "Create Mode Verification"
64
+
65
+ places :idle,
66
+ :create_requested,
67
+ :monitor_mode, :hijack_mode, :event_sourcing_mode,
68
+ :event_published, :command_processed,
69
+ :completed
70
+
71
+ initial_place :idle
72
+ terminal_places :completed
73
+
74
+ # Request create
75
+ transition :request_create, from: :idle, to: :create_requested,
76
+ trigger: "model.save (new record)"
77
+
78
+ # Monitor mode: after_create callback
79
+ transition :select_monitor, from: :create_requested, to: :monitor_mode,
80
+ trigger: "Lyra.monitor_mode? == true"
81
+ transition :monitor_publish, from: :monitor_mode, to: :event_published,
82
+ trigger: "lyra_intercept_create"
83
+
84
+ # Hijack mode: before_create callback with command
85
+ transition :select_hijack, from: :create_requested, to: :hijack_mode,
86
+ trigger: "Lyra.hijack_mode? == true"
87
+ transition :hijack_handle, from: :hijack_mode, to: :command_processed,
88
+ trigger: "CommandHandler.handle(CreateCommand)"
89
+
90
+ # Event sourcing mode
91
+ transition :select_es, from: :create_requested, to: :event_sourcing_mode,
92
+ trigger: "Lyra.event_sourcing_mode? == true"
93
+ transition :es_process, from: :event_sourcing_mode, to: :command_processed,
94
+ trigger: "lyra_prepare_event_source + lyra_finalize"
95
+
96
+ # Complete
97
+ transition :complete_from_event, from: :event_published, to: :completed,
98
+ trigger: "Event stored"
99
+ transition :complete_from_command, from: :command_processed, to: :completed,
100
+ trigger: "Command processed"
101
+ end
102
+
103
+ # UPDATE operation workflow across all Lyra modes
104
+ class UpdateModeWorkflow < PetriFlow::Workflow
105
+ workflow_name "Update Mode Verification"
106
+
107
+ places :idle,
108
+ :update_requested,
109
+ :monitor_mode, :hijack_mode, :event_sourcing_mode,
110
+ :event_published, :command_processed,
111
+ :completed
112
+
113
+ initial_place :idle
114
+ terminal_places :completed
115
+
116
+ # Request update
117
+ transition :request_update, from: :idle, to: :update_requested,
118
+ trigger: "model.save (existing record)"
119
+
120
+ # Monitor mode: after_update callback
121
+ transition :select_monitor, from: :update_requested, to: :monitor_mode,
122
+ trigger: "Lyra.monitor_mode? == true"
123
+ transition :monitor_publish, from: :monitor_mode, to: :event_published,
124
+ trigger: "lyra_intercept_update"
125
+
126
+ # Hijack mode: before_update callback with command
127
+ transition :select_hijack, from: :update_requested, to: :hijack_mode,
128
+ trigger: "Lyra.hijack_mode? == true"
129
+ transition :hijack_handle, from: :hijack_mode, to: :command_processed,
130
+ trigger: "CommandHandler.handle(UpdateCommand)"
131
+
132
+ # Event sourcing mode
133
+ transition :select_es, from: :update_requested, to: :event_sourcing_mode,
134
+ trigger: "Lyra.event_sourcing_mode? == true"
135
+ transition :es_process, from: :event_sourcing_mode, to: :command_processed,
136
+ trigger: "lyra_prepare_event_source + lyra_finalize"
137
+
138
+ # Complete
139
+ transition :complete_from_event, from: :event_published, to: :completed,
140
+ trigger: "Event stored"
141
+ transition :complete_from_command, from: :command_processed, to: :completed,
142
+ trigger: "Command processed"
143
+ end
144
+
145
+ # DESTROY operation workflow across all Lyra modes
146
+ class DestroyModeWorkflow < PetriFlow::Workflow
147
+ workflow_name "Destroy Mode Verification"
148
+
149
+ places :idle,
150
+ :destroy_requested,
151
+ :monitor_mode, :hijack_mode, :event_sourcing_mode,
152
+ :event_published, :command_processed,
153
+ :completed
154
+
155
+ initial_place :idle
156
+ terminal_places :completed
157
+
158
+ # Request destroy
159
+ transition :request_destroy, from: :idle, to: :destroy_requested,
160
+ trigger: "model.destroy"
161
+
162
+ # Monitor mode: after_destroy callback
163
+ transition :select_monitor, from: :destroy_requested, to: :monitor_mode,
164
+ trigger: "Lyra.monitor_mode? == true"
165
+ transition :monitor_publish, from: :monitor_mode, to: :event_published,
166
+ trigger: "lyra_intercept_destroy"
167
+
168
+ # Hijack mode: before_destroy callback with command
169
+ transition :select_hijack, from: :destroy_requested, to: :hijack_mode,
170
+ trigger: "Lyra.hijack_mode? == true"
171
+ transition :hijack_handle, from: :hijack_mode, to: :command_processed,
172
+ trigger: "CommandHandler.handle(DestroyCommand)"
173
+
174
+ # Event sourcing mode
175
+ transition :select_es, from: :destroy_requested, to: :event_sourcing_mode,
176
+ trigger: "Lyra.event_sourcing_mode? == true"
177
+ transition :es_process, from: :event_sourcing_mode, to: :command_processed,
178
+ trigger: "lyra_prepare_event_source + lyra_finalize"
179
+
180
+ # Complete
181
+ transition :complete_from_event, from: :event_published, to: :completed,
182
+ trigger: "Event stored"
183
+ transition :complete_from_command, from: :command_processed, to: :completed,
184
+ trigger: "Command processed"
185
+ end
186
+
187
+ # Convenience alias for backwards compatibility
188
+ CrudModeVerificationWorkflow = CreateModeWorkflow
189
+
190
+ # Verification runner for Lyra CRUD workflows
191
+ class CrudVerifier
192
+ attr_reader :results, :report
193
+
194
+ def initialize
195
+ @results = {}
196
+ @report = nil
197
+ end
198
+
199
+ # Verify all CRUD workflows
200
+ def verify_all
201
+ verify_lifecycle
202
+ verify_modes
203
+ verify_generated_workflows
204
+ generate_report
205
+ @report
206
+ end
207
+
208
+ # Verify entity lifecycle workflow
209
+ def verify_lifecycle
210
+ workflow = CrudLifecycleWorkflow.new
211
+ @results[:lifecycle] = {
212
+ workflow: workflow.workflow_name,
213
+ verification: workflow.verify!,
214
+ terminal_reachability: workflow.terminal_reachability.dup,
215
+ diagrams: {
216
+ mermaid: workflow.to_mermaid,
217
+ dot: workflow.to_dot
218
+ }
219
+ }
220
+ end
221
+
222
+ # Verify mode-switching workflows (Create, Update, Destroy)
223
+ def verify_modes
224
+ @results[:modes] = {}
225
+
226
+ {
227
+ create: CreateModeWorkflow,
228
+ update: UpdateModeWorkflow,
229
+ destroy: DestroyModeWorkflow
230
+ }.each do |operation, workflow_class|
231
+ workflow = workflow_class.new
232
+ @results[:modes][operation] = {
233
+ workflow: workflow.workflow_name,
234
+ verification: workflow.verify!,
235
+ terminal_reachability: workflow.terminal_reachability.dup,
236
+ diagrams: {
237
+ mermaid: workflow.to_mermaid,
238
+ dot: workflow.to_dot
239
+ }
240
+ }
241
+ end
242
+ end
243
+
244
+ # Verify generated workflows from app/workflows directories
245
+ # Scans both Lyra gem's workflows AND Rails app's workflows
246
+ def verify_generated_workflows
247
+ @results[:generated_workflows] = {}
248
+
249
+ # Find all workflow directories
250
+ workflows_dirs = find_workflows_dirs
251
+ return if workflows_dirs.empty?
252
+
253
+ # Ensure the Generated module exists
254
+ Lyra::Verification.const_set(:Generated, Module.new) unless Lyra::Verification.const_defined?(:Generated)
255
+
256
+ # Load and verify workflows from all directories
257
+ workflows_dirs.each do |workflows_dir|
258
+ Dir.glob(File.join(workflows_dir, "*_workflow.rb")).each do |file|
259
+ basename = File.basename(file, '.rb')
260
+ # Skip if already loaded from another directory
261
+ next if @results[:generated_workflows].key?(basename.to_sym)
262
+
263
+ begin
264
+ # Use load instead of require to bypass Zeitwerk
265
+ load file
266
+
267
+ # Extract class name from file name
268
+ class_name = basename.split('_').map(&:capitalize).join
269
+
270
+ # Try to find the class - check multiple locations
271
+ workflow_class = find_workflow_class(class_name)
272
+
273
+ if workflow_class
274
+ workflow = workflow_class.new
275
+
276
+ @results[:generated_workflows][basename.to_sym] = {
277
+ workflow: workflow.workflow_name,
278
+ file: file,
279
+ verification: workflow.verify!,
280
+ terminal_reachability: workflow.terminal_reachability.dup,
281
+ diagrams: {
282
+ mermaid: workflow.to_mermaid,
283
+ dot: workflow.to_dot
284
+ }
285
+ }
286
+ end
287
+ rescue StandardError => e
288
+ @results[:generated_workflows][basename.to_sym] = {
289
+ error: "#{e.class}: #{e.message}",
290
+ file: file
291
+ }
292
+ end
293
+ end
294
+ end
295
+ end
296
+
297
+ # Find all workflow directories (both Lyra gem and Rails app)
298
+ def find_workflows_dirs
299
+ dirs = []
300
+
301
+ # Check Lyra gem's app/workflows
302
+ lyra_gem_root = Gem.loaded_specs['lyra']&.gem_dir
303
+ lyra_gem_root ||= File.expand_path('../../..', __dir__)
304
+ lyra_workflows = File.join(lyra_gem_root, 'app', 'workflows')
305
+ dirs << lyra_workflows if Dir.exist?(lyra_workflows)
306
+
307
+ # Check Rails app's app/workflows
308
+ if defined?(Rails) && Rails.root
309
+ rails_workflows = Rails.root.join('app', 'workflows').to_s
310
+ # Add only if different from Lyra gem's path
311
+ dirs << rails_workflows if Dir.exist?(rails_workflows) && !dirs.include?(rails_workflows)
312
+ end
313
+
314
+ dirs
315
+ end
316
+
317
+ # Find workflow class by name, checking multiple locations
318
+ def find_workflow_class(class_name)
319
+ # Check Generated module first (if it exists)
320
+ if defined?(Lyra::Verification::Generated) &&
321
+ Lyra::Verification::Generated.const_defined?(class_name)
322
+ return Lyra::Verification::Generated.const_get(class_name)
323
+ end
324
+
325
+ # Check top-level (for workflows defined without module)
326
+ if Object.const_defined?(class_name)
327
+ klass = Object.const_get(class_name)
328
+ return klass if klass < PetriFlow::Workflow
329
+ end
330
+
331
+ nil
332
+ end
333
+
334
+ # Backwards compatibility
335
+ def find_workflows_dir
336
+ find_workflows_dirs.first
337
+ end
338
+
339
+ # Analyze actual CRUD→Event mapping from Lyra configuration
340
+ def analyze_mapping
341
+ mapping = PetriFlow::Matrix::CrudEventMapping.new
342
+
343
+ Lyra.config.monitored_models.each do |model_class|
344
+ config = Lyra.config.model_config(model_class)
345
+ prefix = config.event_prefix
346
+
347
+ # Record expected mappings
348
+ mapping.record_mapping(:create, "#{prefix}Created".to_sym)
349
+ mapping.record_mapping(:update, "#{prefix}Updated".to_sym)
350
+ mapping.record_mapping(:delete, "#{prefix}Destroyed".to_sym)
351
+ end
352
+
353
+ @results[:mapping] = {
354
+ matrix: mapping.to_table,
355
+ stats: {
356
+ models: Lyra.config.monitored_models.count,
357
+ total_mappings: mapping.to_matrix.to_a.flatten.sum
358
+ }
359
+ }
360
+ end
361
+
362
+ # Generate verification report
363
+ def generate_report
364
+ @report = {
365
+ timestamp: Time.current,
366
+ lyra_version: Lyra::VERSION,
367
+ petri_flow_version: PetriFlow::VERSION,
368
+ summary: build_summary,
369
+ details: @results
370
+ }
371
+ end
372
+
373
+ private
374
+
375
+ def build_summary
376
+ lifecycle = @results[:lifecycle]
377
+ modes = @results[:modes]
378
+
379
+ {
380
+ lifecycle_valid: lifecycle_valid?(lifecycle),
381
+ modes_valid: modes_valid?(modes),
382
+ all_terminals_reachable: all_terminals_reachable?,
383
+ deadlock_free: deadlock_free?,
384
+ recommendations: build_recommendations
385
+ }
386
+ end
387
+
388
+ def lifecycle_valid?(result)
389
+ return false unless result
390
+
391
+ verification = result[:verification]
392
+ # Note: We don't require deadlock_free because the update cycle
393
+ # (persisted ↔ updated) is intentional - entities can be updated
394
+ # indefinitely without being deleted. This is valid CRUD behavior.
395
+ verification[:boundedness][:is_safe] &&
396
+ result[:terminal_reachability].values.all?
397
+ end
398
+
399
+ def modes_valid?(modes_results)
400
+ return false unless modes_results
401
+
402
+ # Check all three CRUD mode workflows
403
+ modes_results.values.all? do |result|
404
+ next false unless result[:verification]
405
+ result[:verification][:boundedness][:is_safe] &&
406
+ result[:terminal_reachability].values.all?
407
+ end
408
+ end
409
+
410
+ def all_terminals_reachable?
411
+ flatten_results.all? do |result|
412
+ next true unless result[:terminal_reachability]
413
+ result[:terminal_reachability].values.all?
414
+ end
415
+ end
416
+
417
+ def deadlock_free?
418
+ flatten_results.all? do |result|
419
+ next true unless result.dig(:verification, :liveness)
420
+ result[:verification][:liveness][:deadlock_free]
421
+ end
422
+ end
423
+
424
+ # Flatten nested results (modes has 3 sub-results)
425
+ def flatten_results
426
+ results = []
427
+ results << @results[:lifecycle] if @results[:lifecycle]
428
+ results += @results[:modes].values if @results[:modes]
429
+ results += @results[:generated_workflows].values if @results[:generated_workflows]
430
+ results
431
+ end
432
+
433
+ def build_recommendations
434
+ recommendations = []
435
+
436
+ unless all_terminals_reachable?
437
+ recommendations << "Some terminal states are unreachable - review state machine design"
438
+ end
439
+
440
+ # Check for cycles (expected in lifecycle due to update loop)
441
+ lifecycle_has_cycle = @results[:lifecycle] &&
442
+ !@results[:lifecycle][:verification][:liveness][:deadlock_free]
443
+
444
+ if lifecycle_has_cycle
445
+ recommendations << "Lifecycle contains update cycle (persisted ↔ updated) - this is expected behavior"
446
+ end
447
+
448
+ if recommendations.empty?
449
+ recommendations << "All verifications passed - CRUD→Event mapping is formally correct"
450
+ end
451
+
452
+ recommendations
453
+ end
454
+ end
455
+ end
456
+ end