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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +222 -0
- data/LICENSE +21 -0
- data/README.md +1165 -0
- data/Rakefile +728 -0
- data/app/controllers/lyra/application_controller.rb +23 -0
- data/app/controllers/lyra/dashboard_controller.rb +624 -0
- data/app/controllers/lyra/flow_controller.rb +224 -0
- data/app/controllers/lyra/privacy_controller.rb +182 -0
- data/app/views/lyra/dashboard/audit_trail.html.erb +324 -0
- data/app/views/lyra/dashboard/discrepancies.html.erb +125 -0
- data/app/views/lyra/dashboard/event_graph_view.html.erb +525 -0
- data/app/views/lyra/dashboard/heatmap_view.html.erb +155 -0
- data/app/views/lyra/dashboard/index.html.erb +119 -0
- data/app/views/lyra/dashboard/model_overview.html.erb +115 -0
- data/app/views/lyra/dashboard/projections.html.erb +302 -0
- data/app/views/lyra/dashboard/schema.html.erb +283 -0
- data/app/views/lyra/dashboard/schema_history.html.erb +78 -0
- data/app/views/lyra/dashboard/schema_version.html.erb +340 -0
- data/app/views/lyra/dashboard/verification.html.erb +370 -0
- data/app/views/lyra/flow/crud_mapping.html.erb +125 -0
- data/app/views/lyra/flow/timeline.html.erb +260 -0
- data/app/views/lyra/privacy/pii_detection.html.erb +148 -0
- data/app/views/lyra/privacy/policy.html.erb +188 -0
- data/app/workflows/es_async_mode_workflow.rb +80 -0
- data/app/workflows/es_sync_mode_workflow.rb +64 -0
- data/app/workflows/hijack_mode_workflow.rb +54 -0
- data/app/workflows/lifecycle_workflow.rb +43 -0
- data/app/workflows/monitor_mode_workflow.rb +39 -0
- data/config/privacy_policies.rb +273 -0
- data/config/routes.rb +48 -0
- data/lib/lyra/aggregate.rb +131 -0
- data/lib/lyra/associations/event_aware.rb +225 -0
- data/lib/lyra/command.rb +81 -0
- data/lib/lyra/command_handler.rb +155 -0
- data/lib/lyra/configuration.rb +124 -0
- data/lib/lyra/consistency/read_your_writes.rb +91 -0
- data/lib/lyra/correlation.rb +144 -0
- data/lib/lyra/dual_view.rb +231 -0
- data/lib/lyra/engine.rb +67 -0
- data/lib/lyra/event.rb +71 -0
- data/lib/lyra/event_analyzer.rb +135 -0
- data/lib/lyra/event_flow.rb +449 -0
- data/lib/lyra/event_mapper.rb +106 -0
- data/lib/lyra/event_store_adapter.rb +72 -0
- data/lib/lyra/id_generator.rb +137 -0
- data/lib/lyra/interceptors/association_interceptor.rb +169 -0
- data/lib/lyra/interceptors/crud_interceptor.rb +543 -0
- data/lib/lyra/privacy/gdpr_compliance.rb +161 -0
- data/lib/lyra/privacy/pii_detector.rb +85 -0
- data/lib/lyra/privacy/pii_masker.rb +66 -0
- data/lib/lyra/privacy/policy_integration.rb +253 -0
- data/lib/lyra/projection.rb +94 -0
- data/lib/lyra/projections/async_projection_job.rb +63 -0
- data/lib/lyra/projections/cached_projection.rb +322 -0
- data/lib/lyra/projections/cached_relation.rb +757 -0
- data/lib/lyra/projections/event_store_reader.rb +127 -0
- data/lib/lyra/projections/model_projection.rb +143 -0
- data/lib/lyra/schema/diff.rb +331 -0
- data/lib/lyra/schema/event_class_registrar.rb +63 -0
- data/lib/lyra/schema/generator.rb +190 -0
- data/lib/lyra/schema/reporter.rb +188 -0
- data/lib/lyra/schema/store.rb +156 -0
- data/lib/lyra/schema/validator.rb +100 -0
- data/lib/lyra/strict_data_access.rb +363 -0
- data/lib/lyra/verification/crud_lifecycle_workflow.rb +456 -0
- data/lib/lyra/verification/workflow_generator.rb +540 -0
- data/lib/lyra/version.rb +3 -0
- data/lib/lyra/visualization/activity_heatmap.rb +215 -0
- data/lib/lyra/visualization/event_graph.rb +310 -0
- data/lib/lyra/visualization/timeline.rb +398 -0
- data/lib/lyra.rb +150 -0
- data/lib/tasks/dist.rake +391 -0
- data/lib/tasks/gems.rake +185 -0
- data/lib/tasks/lyra_schema.rake +231 -0
- data/lib/tasks/lyra_workflows.rake +452 -0
- data/lib/tasks/public_release.rake +351 -0
- data/lib/tasks/stats.rake +175 -0
- data/lib/tasks/testbed.rake +479 -0
- data/lib/tasks/version.rake +159 -0
- 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
|