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,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
|