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,398 @@
1
+ module Lyra
2
+ module Visualization
3
+ # Timeline visualization for event flows
4
+ class Timeline
5
+ def initialize(events)
6
+ @events = events.sort_by { |e| event_timestamp(e) }
7
+ end
8
+
9
+ private
10
+
11
+ # Helper methods to extract data from events (works with both Lyra::Event and RubyEventStore::Event)
12
+ def event_timestamp(event)
13
+ return event.timestamp if event.respond_to?(:timestamp) && event.timestamp
14
+ data = event.respond_to?(:data) ? event.data : nil
15
+ return nil unless data
16
+ data[:timestamp] || data["timestamp"] || event.metadata[:timestamp]
17
+ end
18
+
19
+ def event_operation(event)
20
+ return event.operation if event.respond_to?(:operation)
21
+ data = event.respond_to?(:data) ? event.data : nil
22
+ return nil unless data
23
+ op = data[:operation] || data["operation"]
24
+ op.is_a?(String) ? op.to_sym : op
25
+ end
26
+
27
+ def event_model_class(event)
28
+ return event.model_class if event.respond_to?(:model_class)
29
+ data = event.respond_to?(:data) ? event.data : nil
30
+ return nil unless data
31
+ data[:model_class] || data["model_class"]
32
+ end
33
+
34
+ def event_model_id(event)
35
+ return event.model_id if event.respond_to?(:model_id)
36
+ data = event.respond_to?(:data) ? event.data : nil
37
+ return nil unless data
38
+ data[:model_id] || data["model_id"]
39
+ end
40
+
41
+ def event_attributes(event)
42
+ return event.attributes if event.respond_to?(:attributes) && !event.attributes.is_a?(Hash)
43
+ data = event.respond_to?(:data) ? event.data : nil
44
+ return {} unless data
45
+ data[:attributes] || data["attributes"] || {}
46
+ end
47
+
48
+ def event_changes(event)
49
+ return event.changes if event.respond_to?(:changes) && event.method(:changes).owner != ActiveRecord::AttributeMethods::Dirty rescue event.changes
50
+ data = event.respond_to?(:data) ? event.data : nil
51
+ return {} unless data
52
+ data[:changes] || data["changes"] || {}
53
+ end
54
+
55
+ public
56
+
57
+ # Generate timeline data for visualization
58
+ def to_data
59
+ {
60
+ events: timeline_events,
61
+ groups: event_groups,
62
+ metadata: timeline_metadata
63
+ }
64
+ end
65
+
66
+ # Generate timeline HTML
67
+ def to_html
68
+ TimelineHtmlBuilder.new(@events).build
69
+ end
70
+
71
+ # Generate Mermaid diagram
72
+ def to_mermaid
73
+ lines = ["gantt"]
74
+ lines << " title Event Timeline"
75
+ lines << " dateFormat YYYY-MM-DD HH:mm:ss"
76
+ lines << ""
77
+
78
+ @events.group_by { |e| event_model_class(e) }.each do |model_class, events|
79
+ lines << " section #{model_class}"
80
+ events.each do |event|
81
+ label = "#{event_operation(event)} ##{event_model_id(event)}"
82
+ timestamp = event_timestamp(event).strftime("%Y-%m-%d %H:%M:%S")
83
+ lines << " #{label} :#{timestamp}, 1s"
84
+ end
85
+ end
86
+
87
+ lines.join("\n")
88
+ end
89
+
90
+ # Generate ASCII timeline
91
+ def to_ascii
92
+ lines = ["Event Timeline", "=" * 80, ""]
93
+
94
+ @events.each do |event|
95
+ time_str = event_timestamp(event).strftime("%Y-%m-%d %H:%M:%S")
96
+ pii_marker = has_pii?(event) ? " [PII]" : ""
97
+
98
+ lines << "#{time_str} | #{event_model_class(event)}##{event_model_id(event)}"
99
+ lines << " └─ #{event_operation(event).to_s.upcase}#{pii_marker}"
100
+
101
+ changes = event_changes(event)
102
+ if changes.any?
103
+ changes.each do |field, (old_val, new_val)|
104
+ lines << " • #{field}: #{old_val} → #{new_val}"
105
+ end
106
+ end
107
+
108
+ lines << ""
109
+ end
110
+
111
+ lines.join("\n")
112
+ end
113
+
114
+ # Generate JSON for D3.js visualization
115
+ def to_d3_json
116
+ {
117
+ nodes: @events.map.with_index do |event, i|
118
+ {
119
+ id: event.event_id,
120
+ index: i,
121
+ timestamp: event_timestamp(event).to_i,
122
+ label: "#{event_model_class(event)}##{event_model_id(event)}",
123
+ operation: event_operation(event),
124
+ has_pii: has_pii?(event),
125
+ group: event_model_class(event),
126
+ user_id: event.metadata[:user_id]
127
+ }
128
+ end,
129
+ links: build_links(@events)
130
+ }.to_json
131
+ end
132
+
133
+ private
134
+
135
+ def timeline_events
136
+ @events.map do |event|
137
+ {
138
+ id: event.event_id,
139
+ start: event_timestamp(event),
140
+ content: event_label(event),
141
+ group: event_model_class(event),
142
+ className: event_css_class(event),
143
+ title: event_tooltip(event)
144
+ }
145
+ end
146
+ end
147
+
148
+ def event_groups
149
+ @events.map { |e| event_model_class(e) }.uniq.map.with_index do |model_class, i|
150
+ {
151
+ id: model_class,
152
+ content: model_class,
153
+ order: i
154
+ }
155
+ end
156
+ end
157
+
158
+ def timeline_metadata
159
+ {
160
+ start: @events.first ? event_timestamp(@events.first) : nil,
161
+ end: @events.last ? event_timestamp(@events.last) : nil,
162
+ count: @events.count,
163
+ models: @events.map { |e| event_model_class(e) }.uniq
164
+ }
165
+ end
166
+
167
+ def event_label(event)
168
+ "#{event_operation(event).to_s.capitalize} ##{event_model_id(event)}"
169
+ end
170
+
171
+ def event_css_class(event)
172
+ classes = ["event", "event-#{event_operation(event)}"]
173
+ classes << "event-pii" if has_pii?(event)
174
+ classes.join(" ")
175
+ end
176
+
177
+ def event_tooltip(event)
178
+ parts = [
179
+ "Operation: #{event_operation(event)}",
180
+ "Model: #{event_model_class(event)}",
181
+ "ID: #{event_model_id(event)}",
182
+ "Time: #{event_timestamp(event)}"
183
+ ]
184
+ parts << "Contains PII" if has_pii?(event)
185
+ parts.join("\n")
186
+ end
187
+
188
+ def has_pii?(event)
189
+ return false unless Lyra.privacy_features_available?
190
+
191
+ Lyra::Privacy::PIIDetector.detect(event_attributes(event)).any?
192
+ end
193
+
194
+ def build_links(events)
195
+ links = []
196
+
197
+ # Link events by correlation ID
198
+ events.group_by { |e| e.metadata[:correlation_id] }.each do |corr_id, group|
199
+ next if group.size < 2
200
+
201
+ group.sort_by { |e| event_timestamp(e) }.each_cons(2) do |source, target|
202
+ links << {
203
+ source: source.event_id,
204
+ target: target.event_id,
205
+ type: 'correlation'
206
+ }
207
+ end
208
+ end
209
+
210
+ # Link events on same record
211
+ events.group_by { |e| "#{event_model_class(e)}-#{event_model_id(e)}" }.each do |key, group|
212
+ next if group.size < 2
213
+
214
+ group.sort_by { |e| event_timestamp(e) }.each_cons(2) do |source, target|
215
+ links << {
216
+ source: source.event_id,
217
+ target: target.event_id,
218
+ type: 'same_record'
219
+ }
220
+ end
221
+ end
222
+
223
+ links
224
+ end
225
+ end
226
+
227
+ # HTML builder for timeline visualization (replaces Phlex dependency)
228
+ class TimelineHtmlBuilder
229
+ def initialize(events)
230
+ @events = events.sort_by { |e| event_timestamp(e) }
231
+ end
232
+
233
+ def build
234
+ html = []
235
+ html << '<div class="lyra-timeline">'
236
+ html << "<style>#{timeline_styles}</style>"
237
+ html << build_header
238
+ html << build_body
239
+ html << '</div>'
240
+ html.join("\n")
241
+ end
242
+
243
+ private
244
+
245
+ def event_timestamp(event)
246
+ return event.timestamp if event.respond_to?(:timestamp) && event.timestamp
247
+ data = event.respond_to?(:data) ? event.data : nil
248
+ return Time.current unless data
249
+ data[:timestamp] || data["timestamp"] || event.metadata[:timestamp] || Time.current
250
+ end
251
+
252
+ def event_operation(event)
253
+ return event.operation if event.respond_to?(:operation)
254
+ data = event.respond_to?(:data) ? event.data : nil
255
+ return :unknown unless data
256
+ op = data[:operation] || data["operation"]
257
+ op.is_a?(String) ? op.to_sym : (op || :unknown)
258
+ end
259
+
260
+ def event_model_class(event)
261
+ return event.model_class if event.respond_to?(:model_class)
262
+ data = event.respond_to?(:data) ? event.data : nil
263
+ return "Unknown" unless data
264
+ data[:model_class] || data["model_class"] || "Unknown"
265
+ end
266
+
267
+ def event_model_id(event)
268
+ return event.model_id if event.respond_to?(:model_id)
269
+ data = event.respond_to?(:data) ? event.data : nil
270
+ return "?" unless data
271
+ data[:model_id] || data["model_id"] || "?"
272
+ end
273
+
274
+ def event_attributes(event)
275
+ return event.attributes if event.respond_to?(:attributes) && event.attributes.is_a?(Hash)
276
+ data = event.respond_to?(:data) ? event.data : nil
277
+ return {} unless data
278
+ data[:attributes] || data["attributes"] || {}
279
+ end
280
+
281
+ def event_changes(event)
282
+ return event.changes if event.respond_to?(:changes) && event.changes.is_a?(Hash)
283
+ data = event.respond_to?(:data) ? event.data : nil
284
+ return {} unless data
285
+ data[:changes] || data["changes"] || {}
286
+ end
287
+
288
+ def has_pii?(event)
289
+ return false unless Lyra.privacy_features_available?
290
+ Lyra::Privacy::PIIDetector.detect(event_attributes(event)).any?
291
+ end
292
+
293
+ def h(text)
294
+ ERB::Util.html_escape(text.to_s)
295
+ end
296
+
297
+ def build_header
298
+ model_count = @events.map { |e| event_model_class(e) }.uniq.count
299
+ <<~HTML
300
+ <div class="timeline-header">
301
+ <h2>Event Timeline</h2>
302
+ <div class="timeline-stats">
303
+ <span>#{h(@events.count)} events</span>
304
+ <span>#{h(model_count)} models</span>
305
+ </div>
306
+ </div>
307
+ HTML
308
+ end
309
+
310
+ def build_body
311
+ html = ['<div class="timeline-body">']
312
+ @events.each { |event| html << build_event(event) }
313
+ html << '</div>'
314
+ html.join("\n")
315
+ end
316
+
317
+ def build_event(event)
318
+ pii = has_pii?(event)
319
+ op = event_operation(event)
320
+ pii_class = pii ? ' with-pii' : ''
321
+
322
+ html = []
323
+ html << %(<div class="timeline-event #{h(op)}#{pii_class}">)
324
+ html << %(<div class="event-time">#{h(event_timestamp(event).strftime("%Y-%m-%d %H:%M:%S"))}</div>)
325
+ html << '<div class="event-content">'
326
+ html << build_event_header(event, pii)
327
+ html << build_event_changes(event)
328
+ html << build_user_action(event)
329
+ html << '</div>'
330
+ html << '</div>'
331
+ html.join("\n")
332
+ end
333
+
334
+ def build_event_header(event, has_pii)
335
+ pii_badge = has_pii ? '<span class="pii-badge">PII</span>' : ''
336
+ <<~HTML
337
+ <div class="event-header">
338
+ <span class="event-operation">#{h(event_operation(event).to_s.upcase)}</span>
339
+ <span class="event-model">#{h(event_model_class(event))}##{h(event_model_id(event))}</span>
340
+ #{pii_badge}
341
+ </div>
342
+ HTML
343
+ end
344
+
345
+ def build_event_changes(event)
346
+ changes = event_changes(event)
347
+ return '' if changes.empty?
348
+
349
+ html = ['<div class="event-changes">']
350
+ changes.each do |field, values|
351
+ old_val, new_val = values.is_a?(Array) ? values : [nil, values]
352
+ html << <<~HTML
353
+ <div class="change">
354
+ <span class="field">#{h(field)}</span>
355
+ <span class="arrow">→</span>
356
+ <span class="value">#{h(new_val)}</span>
357
+ </div>
358
+ HTML
359
+ end
360
+ html << '</div>'
361
+ html.join("\n")
362
+ end
363
+
364
+ def build_user_action(event)
365
+ user_action = event.respond_to?(:metadata) ? event.metadata[:user_action] : nil
366
+ return '' unless user_action
367
+ %(<div class="event-action">User Action: #{h(user_action)}</div>)
368
+ end
369
+
370
+ def timeline_styles
371
+ <<~CSS
372
+ .lyra-timeline { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 1200px; margin: 0 auto; padding: 2rem; }
373
+ .timeline-header { margin-bottom: 2rem; }
374
+ .timeline-header h2 { margin: 0 0 1rem 0; color: #1f2937; }
375
+ .timeline-stats { display: flex; gap: 2rem; color: #6b7280; font-size: 0.875rem; }
376
+ .timeline-body { position: relative; padding-left: 2rem; border-left: 2px solid #e5e7eb; }
377
+ .timeline-event { position: relative; margin-bottom: 2rem; padding: 1rem; background: white; border-radius: 8px; border: 1px solid #e5e7eb; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
378
+ .timeline-event::before { content: ''; position: absolute; left: -2.5rem; top: 1.5rem; width: 12px; height: 12px; border-radius: 50%; background: #667eea; border: 2px solid white; }
379
+ .timeline-event.with-pii::before { background: #f59e0b; }
380
+ .event-time { font-size: 0.75rem; color: #6b7280; margin-bottom: 0.5rem; }
381
+ .event-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.5rem; }
382
+ .event-operation { font-weight: 600; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; }
383
+ .timeline-event.created .event-operation { background: #d1fae5; color: #065f46; }
384
+ .timeline-event.updated .event-operation { background: #dbeafe; color: #1e40af; }
385
+ .timeline-event.destroyed .event-operation { background: #fee2e2; color: #991b1b; }
386
+ .event-model { font-family: monospace; color: #4b5563; }
387
+ .pii-badge { background: #fef3c7; color: #92400e; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
388
+ .event-changes { margin-top: 0.5rem; padding-left: 1rem; }
389
+ .change { display: flex; align-items: center; gap: 0.5rem; margin: 0.25rem 0; font-size: 0.875rem; }
390
+ .change .field { font-weight: 500; color: #374151; }
391
+ .change .arrow { color: #9ca3af; }
392
+ .change .value { font-family: monospace; background: #f3f4f6; padding: 0.125rem 0.375rem; border-radius: 3px; }
393
+ .event-action { margin-top: 0.5rem; font-size: 0.875rem; color: #6b7280; font-style: italic; }
394
+ CSS
395
+ end
396
+ end
397
+ end
398
+ end
data/lib/lyra.rb ADDED
@@ -0,0 +1,150 @@
1
+ require "lyra/version"
2
+ require "rails_event_store"
3
+
4
+ # PAM DSL is optional - required for privacy features
5
+ # Set LYRA_DISABLE_PAM_DSL=true to test Lyra without PAM DSL
6
+ PAM_DSL_AVAILABLE = if ENV["LYRA_DISABLE_PAM_DSL"] == "true"
7
+ false
8
+ else
9
+ begin
10
+ require "pam_dsl"
11
+ true
12
+ rescue LoadError
13
+ false
14
+ end
15
+ end
16
+
17
+ # PetriFlow is optional - required for formal verification
18
+ PETRI_FLOW_AVAILABLE = begin
19
+ require "petri_flow"
20
+ true
21
+ rescue LoadError
22
+ false
23
+ end
24
+
25
+ # Only load engine when Rails is available
26
+ if defined?(Rails)
27
+ require "lyra/engine"
28
+ end
29
+
30
+ # Core components
31
+ require "lyra/configuration"
32
+ require "lyra/correlation"
33
+ require "lyra/event"
34
+ require "lyra/event_store_adapter"
35
+ require "lyra/event_mapper"
36
+ require "lyra/projection"
37
+ require "lyra/command"
38
+ require "lyra/aggregate"
39
+ require "lyra/command_handler"
40
+ require "lyra/dual_view"
41
+ require "lyra/event_flow"
42
+ require "lyra/event_analyzer"
43
+ require "lyra/id_generator"
44
+
45
+ # Strict data access (prevents callback-bypassing operations)
46
+ # Loaded before projections because projections use bypass methods
47
+ require "lyra/strict_data_access"
48
+
49
+ # Event sourcing projections
50
+ require "lyra/projections/model_projection"
51
+ require "lyra/projections/async_projection_job"
52
+ require "lyra/projections/cached_projection"
53
+ require "lyra/projections/cached_relation"
54
+ require "lyra/projections/event_store_reader"
55
+
56
+ # Event-aware associations
57
+ require "lyra/associations/event_aware"
58
+
59
+ # Consistency helpers
60
+ require "lyra/consistency/read_your_writes"
61
+
62
+ # Privacy and compliance (requires PAM DSL)
63
+ if PAM_DSL_AVAILABLE
64
+ require "lyra/privacy/pii_detector"
65
+ require "lyra/privacy/pii_masker"
66
+ require "lyra/privacy/gdpr_compliance"
67
+ require "lyra/privacy/policy_integration"
68
+ end
69
+
70
+ # Schema validation
71
+ require "lyra/schema/store"
72
+ require "lyra/schema/generator"
73
+ require "lyra/schema/diff"
74
+ require "lyra/schema/validator"
75
+ require "lyra/schema/reporter"
76
+ require "lyra/schema/event_class_registrar"
77
+
78
+ # Visualization
79
+ require "lyra/visualization/timeline"
80
+ require "lyra/visualization/event_graph"
81
+ require "lyra/visualization/activity_heatmap"
82
+
83
+ # Formal verification (requires PetriFlow)
84
+ if PETRI_FLOW_AVAILABLE
85
+ require "lyra/verification/crud_lifecycle_workflow"
86
+ require "lyra/verification/workflow_generator"
87
+ end
88
+
89
+ module Lyra
90
+ class Error < StandardError; end
91
+
92
+ def self.configure
93
+ yield config if block_given?
94
+ end
95
+
96
+ def self.monitor_mode?
97
+ config.monitor_mode?
98
+ end
99
+
100
+ def self.hijack_mode?
101
+ config.hijack_mode?
102
+ end
103
+
104
+ def self.event_sourcing_mode?
105
+ config.event_sourcing_mode?
106
+ end
107
+
108
+ def self.disabled_mode?
109
+ config.disabled_mode?
110
+ end
111
+
112
+ # Delegate event_store to configuration
113
+ def self.event_store
114
+ config.event_store
115
+ end
116
+
117
+ def self.event_store=(store)
118
+ config.event_store = store
119
+ end
120
+
121
+ # Check if PAM DSL is available for privacy features
122
+ def self.pam_dsl_available?
123
+ PAM_DSL_AVAILABLE
124
+ end
125
+
126
+ # Check if privacy features are available
127
+ def self.privacy_features_available?
128
+ pam_dsl_available?
129
+ end
130
+
131
+ # Check if PetriFlow is available for formal verification
132
+ def self.petri_flow_available?
133
+ PETRI_FLOW_AVAILABLE
134
+ end
135
+
136
+ # Check if formal verification features are available
137
+ def self.verification_available?
138
+ petri_flow_available?
139
+ end
140
+
141
+ # Run formal verification of CRUD→Event mapping
142
+ # @return [Hash] Verification results
143
+ # @raise [RuntimeError] if PetriFlow is not available
144
+ def self.verify_crud_mapping
145
+ raise "PetriFlow is required for formal verification" unless petri_flow_available?
146
+
147
+ verifier = Verification::CrudVerifier.new
148
+ verifier.verify_all
149
+ end
150
+ end