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,135 @@
1
+ module Lyra
2
+ # Analyzes collections of events for patterns and metrics
3
+ class EventAnalyzer
4
+ attr_reader :events
5
+
6
+ def initialize(events)
7
+ @events = events
8
+ end
9
+
10
+ # Analyze events and return comprehensive metrics
11
+ def analyze
12
+ {
13
+ timeline: calculate_timeline,
14
+ operations: calculate_operations,
15
+ metrics: calculate_metrics,
16
+ privacy: analyze_privacy
17
+ }
18
+ end
19
+
20
+ private
21
+
22
+ def calculate_timeline
23
+ return {} if @events.empty?
24
+
25
+ first_time = extract_timestamp(@events.first)
26
+ last_time = extract_timestamp(@events.last)
27
+
28
+ return {} if first_time.nil? || last_time.nil?
29
+
30
+ duration_seconds = last_time - first_time
31
+
32
+ {
33
+ first_event: first_time,
34
+ last_event: last_time,
35
+ duration_seconds: duration_seconds,
36
+ duration_minutes: duration_seconds / 60.0,
37
+ duration_hours: duration_seconds / 3600.0,
38
+ duration_days: duration_seconds / 86400.0
39
+ }
40
+ end
41
+
42
+ def extract_timestamp(event)
43
+ if event.respond_to?(:timestamp) && event.timestamp
44
+ event.timestamp
45
+ elsif event.respond_to?(:data) && event.data.is_a?(Hash) && event.data[:timestamp]
46
+ event.data[:timestamp]
47
+ elsif event.respond_to?(:metadata) && event.metadata.is_a?(Hash) && event.metadata[:timestamp]
48
+ event.metadata[:timestamp]
49
+ else
50
+ nil
51
+ end
52
+ end
53
+
54
+ def calculate_operations
55
+ operations = Hash.new(0)
56
+
57
+ @events.each do |event|
58
+ operation = if event.respond_to?(:operation)
59
+ event.operation
60
+ elsif event.respond_to?(:data)
61
+ event.data[:operation]
62
+ end
63
+
64
+ operations[operation] += 1 if operation
65
+ end
66
+
67
+ operations
68
+ end
69
+
70
+ def calculate_metrics
71
+ timeline = calculate_timeline
72
+
73
+ {
74
+ total_events: @events.length,
75
+ operations_per_day: if timeline[:duration_days] && timeline[:duration_days] > 0
76
+ @events.length.to_f / timeline[:duration_days]
77
+ else
78
+ 0
79
+ end,
80
+ average_time_between_events: if @events.length > 1 && timeline[:duration_seconds]
81
+ timeline[:duration_seconds] / (@events.length - 1)
82
+ else
83
+ 0
84
+ end
85
+ }
86
+ end
87
+
88
+ def analyze_privacy
89
+ pii_fields = {}
90
+ events_with_pii = 0
91
+
92
+ @events.each do |event|
93
+ event_pii = detect_event_pii(event)
94
+
95
+ if event_pii.any?
96
+ events_with_pii += 1
97
+ event_pii.each do |field, info|
98
+ pii_fields[field] ||= info
99
+ end
100
+ end
101
+ end
102
+
103
+ percentage = @events.empty? ? 0.0 : (events_with_pii.to_f / @events.length * 100.0)
104
+
105
+ {
106
+ pii_fields: pii_fields,
107
+ events_with_pii: events_with_pii,
108
+ percentage: percentage
109
+ }
110
+ end
111
+
112
+ def detect_event_pii(event)
113
+ attributes = if event.respond_to?(:attributes)
114
+ event.attributes
115
+ elsif event.respond_to?(:data)
116
+ event.data[:attributes] || {}
117
+ else
118
+ {}
119
+ end
120
+
121
+ changes = if event.respond_to?(:changes)
122
+ event.changes
123
+ elsif event.respond_to?(:data)
124
+ event.data[:changes] || {}
125
+ else
126
+ {}
127
+ end
128
+
129
+ return {} unless Lyra.privacy_features_available?
130
+
131
+ all_data = attributes.merge(changes)
132
+ Lyra::Privacy::PIIDetector.detect(all_data)
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,449 @@
1
+ module Lyra
2
+ # Visualizes event flows and chains
3
+ class EventFlow
4
+ attr_reader :subject_id, :subject_type, :time_range
5
+
6
+ def initialize(subject_id: nil, subject_type: nil, time_range: nil)
7
+ @subject_id = subject_id
8
+ @subject_type = subject_type
9
+ @time_range = time_range || (30.days.ago..Time.current)
10
+ end
11
+
12
+ # Get complete event flow for visualization
13
+ def flow_data
14
+ events = load_events
15
+ grouped = group_by_correlation(events)
16
+
17
+ {
18
+ timeline: build_timeline(events),
19
+ flows: build_flows(grouped),
20
+ statistics: calculate_statistics(events),
21
+ privacy_impact: analyze_privacy_impact(events)
22
+ }
23
+ end
24
+
25
+ # Build timeline view showing CRUD operations and their events
26
+ def build_timeline(events)
27
+ events.sort_by { |e| event_timestamp(e) }.map do |event|
28
+ {
29
+ event_id: event.event_id,
30
+ timestamp: event_timestamp(event),
31
+ operation: event_operation(event),
32
+ model_class: event_model_class(event),
33
+ model_id: event_model_id(event),
34
+ correlation_id: event.metadata[:correlation_id],
35
+ action_id: event.metadata[:action_id],
36
+ user_action: event.metadata[:user_action],
37
+ changes: event_changes(event),
38
+ pii_fields: detect_pii(event),
39
+ grouped_with: find_grouped_events(event, events)
40
+ }
41
+ end
42
+ end
43
+
44
+ # Build event flows showing cause and effect
45
+ def build_flows(grouped_events)
46
+ grouped_events.map do |correlation_id, events|
47
+ {
48
+ correlation_id: correlation_id,
49
+ started_at: event_timestamp(events.min_by { |e| event_timestamp(e) }),
50
+ completed_at: event_timestamp(events.max_by { |e| event_timestamp(e) }),
51
+ duration: calculate_duration(events),
52
+ user_action: events.first.metadata[:user_action],
53
+ user_id: events.first.metadata[:user_id],
54
+ events_chain: build_event_chain(events),
55
+ crud_operations: extract_crud_operations(events),
56
+ privacy_impact: calculate_flow_privacy_impact(events)
57
+ }
58
+ end
59
+ end
60
+
61
+ # Show how a single CRUD operation maps to events
62
+ def crud_to_event_mapping(model_class, operation, model_id = nil)
63
+ events = load_events.select do |event|
64
+ matches = event_model_class(event) == model_class && event_operation(event) == operation
65
+ matches &&= event_model_id(event) == model_id if model_id
66
+ matches
67
+ end
68
+
69
+ {
70
+ crud_operation: {
71
+ model: model_class,
72
+ operation: operation,
73
+ record_id: model_id
74
+ },
75
+ generated_events: events.map { |e| event_summary(e) },
76
+ count: events.count,
77
+ timeline: events.sort_by { |e| event_timestamp(e) }.map do |e|
78
+ {
79
+ timestamp: event_timestamp(e),
80
+ event_type: e.class.name,
81
+ event_id: e.event_id,
82
+ data: e.data
83
+ }
84
+ end
85
+ }
86
+ end
87
+
88
+ # Reconstruct state from event chain
89
+ def reconstruct_state_chain(model_class, model_id)
90
+ stream_name = "#{model_class}$#{model_id}"
91
+ events = Lyra.config.event_store.read.stream(stream_name).to_a
92
+
93
+ chain = []
94
+ state = {}
95
+
96
+ events.sort_by { |e| event_timestamp(e) }.each do |event|
97
+ previous_state = state.dup
98
+
99
+ case event_operation(event)
100
+ when :created
101
+ state = event_attributes(event).dup
102
+ when :updated
103
+ event_changes(event).each { |k, (old, new)| state[k] = new }
104
+ when :destroyed
105
+ state[:_deleted] = true
106
+ state[:_deleted_at] = event_timestamp(event)
107
+ end
108
+
109
+ chain << {
110
+ event_id: event.event_id,
111
+ timestamp: event_timestamp(event),
112
+ operation: event_operation(event),
113
+ previous_state: previous_state,
114
+ changes: event_changes(event),
115
+ new_state: state.dup,
116
+ pii_changed: identify_pii_changes(previous_state, state),
117
+ user_action: event.metadata[:user_action]
118
+ }
119
+ end
120
+
121
+ {
122
+ model: { class: model_class, id: model_id },
123
+ initial_state: {},
124
+ final_state: state,
125
+ events_count: events.count,
126
+ state_evolution: chain
127
+ }
128
+ end
129
+
130
+ # Visualize data lineage for GDPR compliance
131
+ def data_lineage(field_name, model_class = nil)
132
+ events = load_events
133
+
134
+ if model_class
135
+ events = events.select { |e| event_model_class(e) == model_class }
136
+ end
137
+
138
+ lineage = []
139
+
140
+ events.each do |event|
141
+ attrs = event_attributes(event)
142
+ changes = event_changes(event)
143
+ if attrs.key?(field_name) || attrs.key?(field_name.to_s) || changes.key?(field_name) || changes.key?(field_name.to_s)
144
+ lineage << {
145
+ timestamp: event_timestamp(event),
146
+ event_id: event.event_id,
147
+ model: event_model_class(event),
148
+ record_id: event_model_id(event),
149
+ operation: event_operation(event),
150
+ old_value: changes.dig(field_name, 0) || changes.dig(field_name.to_s, 0),
151
+ new_value: changes.dig(field_name, 1) || changes.dig(field_name.to_s, 1) || attrs[field_name] || attrs[field_name.to_s],
152
+ source: event.metadata[:source],
153
+ user_id: event.metadata[:user_id],
154
+ action: event.metadata[:user_action]
155
+ }
156
+ end
157
+ end
158
+
159
+ {
160
+ field: field_name,
161
+ model_class: model_class,
162
+ total_modifications: lineage.count,
163
+ first_seen: lineage.min_by { |l| l[:timestamp] }&.dig(:timestamp),
164
+ last_modified: lineage.max_by { |l| l[:timestamp] }&.dig(:timestamp),
165
+ lineage: lineage.sort_by { |l| l[:timestamp] }
166
+ }
167
+ end
168
+
169
+ # Analyze privacy impact of event chains
170
+ def privacy_impact_analysis
171
+ events = load_events
172
+ pii_inventory = extract_pii_from_events(events)
173
+
174
+ {
175
+ total_events: events.count,
176
+ events_with_pii: events.count { |e| has_pii?(e) },
177
+ pii_categories: pii_inventory.keys,
178
+ pii_fields_count: pii_inventory.values.sum(&:count),
179
+ sensitive_operations: identify_sensitive_operations(events),
180
+ data_flows: trace_data_flows(events),
181
+ risk_assessment: assess_privacy_risk(events, pii_inventory)
182
+ }
183
+ end
184
+
185
+ private
186
+
187
+ # Helper methods to extract data from events (works with both Lyra::Event and RubyEventStore::Event)
188
+ def event_operation(event)
189
+ return event.operation if event.respond_to?(:operation)
190
+ op = event.data[:operation] || event.data["operation"]
191
+ op.is_a?(String) ? op.to_sym : op
192
+ end
193
+
194
+ def event_model_class(event)
195
+ return event.model_class if event.respond_to?(:model_class)
196
+ event.data[:model_class] || event.data["model_class"]
197
+ end
198
+
199
+ def event_model_id(event)
200
+ return event.model_id if event.respond_to?(:model_id)
201
+ event.data[:model_id] || event.data["model_id"]
202
+ end
203
+
204
+ def event_timestamp(event)
205
+ return event.timestamp if event.respond_to?(:timestamp) && event.timestamp
206
+ event.data[:timestamp] || event.data["timestamp"] || event.metadata[:timestamp]
207
+ end
208
+
209
+ def event_attributes(event)
210
+ return event.attributes if event.respond_to?(:attributes) && !event.attributes.is_a?(Hash)
211
+ event.data[:attributes] || event.data["attributes"] || {}
212
+ end
213
+
214
+ def event_changes(event)
215
+ return event.changes if event.respond_to?(:changes) && event.method(:changes).owner != ActiveRecord::AttributeMethods::Dirty
216
+ event.data[:changes] || event.data["changes"] || {}
217
+ end
218
+
219
+ def load_events
220
+ events = Lyra.config.event_store.read.to_a
221
+
222
+ # Filter by subject if specified
223
+ if subject_id && subject_type
224
+ events = events.select { |e| event_relates_to_subject?(e) }
225
+ end
226
+
227
+ # Filter by time range
228
+ events = events.select { |e| time_range.cover?(parse_timestamp(event_timestamp(e))) }
229
+
230
+ events
231
+ end
232
+
233
+ def event_relates_to_subject?(event)
234
+ event.metadata[:user_id] == subject_id ||
235
+ event.data[:user_id] == subject_id ||
236
+ (event_model_class(event) == subject_type && event_model_id(event) == subject_id)
237
+ end
238
+
239
+ def group_by_correlation(events)
240
+ events.group_by { |e| e.metadata[:correlation_id] || e.event_id }
241
+ end
242
+
243
+ def build_event_chain(events)
244
+ events.sort_by { |e| event_timestamp(e) }.map do |event|
245
+ {
246
+ event_id: event.event_id,
247
+ timestamp: event_timestamp(event),
248
+ event_type: event.class.name,
249
+ operation: event_operation(event),
250
+ model: "#{event_model_class(event)}##{event_model_id(event)}",
251
+ changes: event_changes(event),
252
+ pii_affected: detect_pii(event).any?
253
+ }
254
+ end
255
+ end
256
+
257
+ def extract_crud_operations(events)
258
+ events.map do |event|
259
+ {
260
+ operation: event_operation(event),
261
+ model: event_model_class(event),
262
+ record_id: event_model_id(event),
263
+ timestamp: event_timestamp(event)
264
+ }
265
+ end
266
+ end
267
+
268
+ def calculate_duration(events)
269
+ return 0 if events.empty?
270
+ max_time = parse_timestamp(event_timestamp(events.max_by { |e| event_timestamp(e) }))
271
+ min_time = parse_timestamp(event_timestamp(events.min_by { |e| event_timestamp(e) }))
272
+ (max_time - min_time).to_f
273
+ end
274
+
275
+ def parse_timestamp(timestamp)
276
+ return timestamp if timestamp.is_a?(Time) || timestamp.is_a?(DateTime)
277
+ return timestamp.to_time if timestamp.respond_to?(:to_time)
278
+ Time.parse(timestamp.to_s)
279
+ end
280
+
281
+ def calculate_statistics(events)
282
+ {
283
+ total_events: events.count,
284
+ operations: events.group_by { |e| event_operation(e) }.transform_values(&:count),
285
+ models: events.group_by { |e| event_model_class(e) }.transform_values(&:count),
286
+ hourly_distribution: hourly_distribution(events),
287
+ users: events.map { |e| e.metadata[:user_id] }.compact.uniq.count
288
+ }
289
+ end
290
+
291
+ def hourly_distribution(events)
292
+ events.group_by { |e| parse_timestamp(event_timestamp(e)).hour }.transform_values(&:count)
293
+ end
294
+
295
+ def detect_pii(event)
296
+ return {} unless Lyra.privacy_features_available?
297
+
298
+ Lyra::Privacy::PIIDetector.detect(event_attributes(event))
299
+ end
300
+
301
+ def has_pii?(event)
302
+ detect_pii(event).any?
303
+ end
304
+
305
+ def find_grouped_events(event, all_events)
306
+ correlation_id = event.metadata[:correlation_id]
307
+ return [] unless correlation_id
308
+
309
+ all_events.select do |e|
310
+ e.metadata[:correlation_id] == correlation_id && e.event_id != event.event_id
311
+ end.map(&:event_id)
312
+ end
313
+
314
+ def calculate_flow_privacy_impact(events)
315
+ pii_count = events.count { |e| has_pii?(e) }
316
+ {
317
+ events_with_pii: pii_count,
318
+ percentage: events.empty? ? 0 : (pii_count.to_f / events.count * 100).round(2),
319
+ pii_types: events.flat_map { |e| detect_pii(e).values.map { |v| v[:type] } }.uniq
320
+ }
321
+ end
322
+
323
+ def identify_pii_changes(old_state, new_state)
324
+ return {} unless Lyra.privacy_features_available?
325
+
326
+ changes = {}
327
+
328
+ new_state.each do |key, new_value|
329
+ old_value = old_state[key]
330
+ if old_value != new_value && Lyra::Privacy::PIIDetector.contains_pii?(key)
331
+ changes[key] = { from: old_value, to: new_value }
332
+ end
333
+ end
334
+
335
+ changes
336
+ end
337
+
338
+ def event_summary(event)
339
+ {
340
+ event_id: event.event_id,
341
+ timestamp: event_timestamp(event),
342
+ operation: event_operation(event),
343
+ has_pii: has_pii?(event)
344
+ }
345
+ end
346
+
347
+ def analyze_privacy_impact(events)
348
+ pii_inventory = extract_pii_from_events(events)
349
+
350
+ {
351
+ pii_categories: pii_inventory.keys,
352
+ total_pii_fields: pii_inventory.values.sum(&:count),
353
+ sensitive_data_present: pii_inventory.keys.any? { |k|
354
+ [:ssn, :credit_card, :health, :biometric].include?(k)
355
+ }
356
+ }
357
+ end
358
+
359
+ def extract_pii_from_events(events)
360
+ # Custom implementation that uses event_attributes helper
361
+ inventory = Hash.new { |h, k| h[k] = [] }
362
+ events.each do |event|
363
+ pii = detect_pii(event)
364
+ pii.each do |field, info|
365
+ inventory[info[:type]] << { field: field, model: event_model_class(event) }
366
+ end
367
+ end
368
+ inventory
369
+ end
370
+
371
+ def identify_sensitive_operations(events)
372
+ events.select { |e| has_pii?(e) && [:destroyed, :updated].include?(event_operation(e)) }
373
+ .map { |e| event_summary(e) }
374
+ end
375
+
376
+ def trace_data_flows(events)
377
+ # Identify how data flows between models
378
+ flows = Hash.new { |h, k| h[k] = [] }
379
+
380
+ events.each do |event|
381
+ pii = detect_pii(event)
382
+ next if pii.empty?
383
+
384
+ source = event_model_class(event)
385
+ pii.each do |field, info|
386
+ flows[info[:type]] << {
387
+ source_model: source,
388
+ field: field,
389
+ timestamp: event_timestamp(event),
390
+ operation: event_operation(event)
391
+ }
392
+ end
393
+ end
394
+
395
+ flows
396
+ end
397
+
398
+ def assess_privacy_risk(events, pii_inventory)
399
+ risk_factors = []
400
+
401
+ # Check for sensitive PII
402
+ if pii_inventory.keys.any? { |k| [:ssn, :credit_card, :health].include?(k) }
403
+ risk_factors << { level: :high, reason: "Contains highly sensitive PII" }
404
+ end
405
+
406
+ # Check for many PII fields
407
+ if pii_inventory.values.sum(&:count) > 10
408
+ risk_factors << { level: :medium, reason: "Large number of PII fields" }
409
+ end
410
+
411
+ # Check for frequent modifications
412
+ update_events = events.count { |e| event_operation(e) == :updated && has_pii?(e) }
413
+ if update_events > 50
414
+ risk_factors << { level: :medium, reason: "Frequent PII modifications" }
415
+ end
416
+
417
+ {
418
+ overall_risk: calculate_overall_risk(risk_factors),
419
+ factors: risk_factors,
420
+ recommendations: generate_recommendations(risk_factors)
421
+ }
422
+ end
423
+
424
+ def calculate_overall_risk(risk_factors)
425
+ return :low if risk_factors.empty?
426
+ return :high if risk_factors.any? { |f| f[:level] == :high }
427
+ return :medium if risk_factors.any? { |f| f[:level] == :medium }
428
+ :low
429
+ end
430
+
431
+ def generate_recommendations(risk_factors)
432
+ recommendations = []
433
+
434
+ if risk_factors.any? { |f| f[:reason].include?("sensitive") }
435
+ recommendations << "Implement field-level encryption for sensitive PII"
436
+ recommendations << "Enable audit logging for all sensitive data access"
437
+ end
438
+
439
+ if risk_factors.any? { |f| f[:reason].include?("modifications") }
440
+ recommendations << "Review data retention policies"
441
+ recommendations << "Implement change approval workflows"
442
+ end
443
+
444
+ recommendations << "Regular GDPR compliance audits recommended" if risk_factors.any?
445
+
446
+ recommendations
447
+ end
448
+ end
449
+ end
@@ -0,0 +1,106 @@
1
+ module Lyra
2
+ # Maps CRUD operations to domain events
3
+ class EventMapper
4
+ class << self
5
+ # Map a CRUD operation to an event
6
+ def map_operation(model_class, operation, data)
7
+ mapper_class = find_mapper(model_class) || DefaultMapper
8
+
9
+ mapper_class.new(model_class, operation, data).to_event
10
+ end
11
+
12
+ # Register a custom mapper for a model
13
+ def register_mapper(model_class, mapper_class)
14
+ mappers[model_class.name] = mapper_class
15
+ end
16
+
17
+ private
18
+
19
+ def mappers
20
+ @mappers ||= {}
21
+ end
22
+
23
+ def find_mapper(model_class)
24
+ mappers[model_class.name]
25
+ end
26
+ end
27
+
28
+ attr_reader :model_class, :operation, :data
29
+
30
+ def initialize(model_class, operation, data)
31
+ @model_class = model_class
32
+ @operation = operation
33
+ @data = data
34
+ end
35
+
36
+ def to_event
37
+ event_class = resolve_event_class
38
+ event_class.new(data: event_data, metadata: event_metadata)
39
+ end
40
+
41
+ def event_data
42
+ {
43
+ model_class: model_class.name,
44
+ model_id: data[:id],
45
+ operation: operation,
46
+ attributes: data[:attributes] || {},
47
+ changes: data[:changes] || {},
48
+ timestamp: Time.current
49
+ }
50
+ end
51
+
52
+ def event_metadata
53
+ {
54
+ user_id: data[:user_id],
55
+ request_id: data[:request_id],
56
+ correlation_id: Lyra::Correlation.current_id,
57
+ causation_id: Lyra::Causation.current_id,
58
+ source: 'lyra_interceptor'
59
+ }
60
+ end
61
+
62
+ def resolve_event_class
63
+ config = Lyra.config.model_config(model_class)
64
+ event_name = config.event_name_for(operation)
65
+ # Sanitize namespaced event names for constant lookup
66
+ sanitized_name = event_name.to_s.gsub("::", "")
67
+
68
+ # First check if it exists in Lyra::Events namespace
69
+ if Lyra::Events.const_defined?(sanitized_name, false)
70
+ return Lyra::Events.const_get(sanitized_name, false)
71
+ end
72
+
73
+ # Try to constantize (for user-defined event classes)
74
+ begin
75
+ event_name.constantize
76
+ rescue NameError
77
+ create_dynamic_event_class(event_name)
78
+ end
79
+ end
80
+
81
+ def create_dynamic_event_class(event_name)
82
+ # Sanitize namespaced event names (e.g., "Spree::OrderCreated" -> "SpreeOrderCreated")
83
+ sanitized_name = event_name.to_s.gsub("::", "")
84
+
85
+ return Lyra::Events.const_get(sanitized_name, false) if Lyra::Events.const_defined?(sanitized_name, false)
86
+
87
+ Lyra::Events.const_set(sanitized_name, Class.new(Lyra::Event))
88
+ end
89
+ end
90
+
91
+ class DefaultMapper < EventMapper
92
+ # Uses the base EventMapper behavior
93
+ end
94
+
95
+ # Example of a custom mapper
96
+ class AuditMapper < EventMapper
97
+ def event_data
98
+ super.merge(
99
+ audit_info: {
100
+ ip_address: data[:ip_address],
101
+ user_agent: data[:user_agent]
102
+ }
103
+ )
104
+ end
105
+ end
106
+ end