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,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :lyra do
|
|
4
|
+
namespace :schema do
|
|
5
|
+
desc "Generate initial event schema from current model configuration"
|
|
6
|
+
task create: :environment do
|
|
7
|
+
require_lyra_schema
|
|
8
|
+
|
|
9
|
+
if Lyra::Schema::Store.exists?
|
|
10
|
+
puts "Schema already exists at #{Lyra::Schema::Store.schema_path}"
|
|
11
|
+
puts ""
|
|
12
|
+
puts "Options:"
|
|
13
|
+
puts " - Run 'rake lyra:schema:update' to create a new version"
|
|
14
|
+
puts " - Run 'rake lyra:schema:verify' to check for changes"
|
|
15
|
+
puts " - Delete #{Lyra::Schema::Store.schema_path}/ to start fresh"
|
|
16
|
+
exit 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if Lyra.config.monitored_models.empty?
|
|
20
|
+
puts "No models are configured for monitoring."
|
|
21
|
+
puts "Configure models with Lyra.config.monitor_model(YourModel) first."
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
schema = Lyra::Schema::Generator.generate
|
|
26
|
+
file_path = Lyra::Schema::Store.save(schema)
|
|
27
|
+
|
|
28
|
+
puts ""
|
|
29
|
+
puts "=" * 60
|
|
30
|
+
puts "Lyra Event Schema Created"
|
|
31
|
+
puts "=" * 60
|
|
32
|
+
puts ""
|
|
33
|
+
puts "Version: v#{schema[:version]}"
|
|
34
|
+
puts "File: #{file_path}"
|
|
35
|
+
puts "Models: #{schema[:summary][:models_count]}"
|
|
36
|
+
puts "Columns: #{schema[:summary][:total_columns]}"
|
|
37
|
+
puts "Events: #{schema[:summary][:events_count]}"
|
|
38
|
+
puts "PII Fields: #{schema[:summary][:pii_fields_count]}"
|
|
39
|
+
puts ""
|
|
40
|
+
puts "=" * 60
|
|
41
|
+
puts ""
|
|
42
|
+
puts "Next steps:"
|
|
43
|
+
puts " - Commit #{Lyra::Schema::Store.schema_path}/ to version control"
|
|
44
|
+
puts " - Run 'rake lyra:schema:report' to view mappings"
|
|
45
|
+
puts ""
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
desc "Update schema (creates new version with current configuration)"
|
|
49
|
+
task update: :environment do
|
|
50
|
+
require_lyra_schema
|
|
51
|
+
|
|
52
|
+
validator = Lyra::Schema::Validator.new
|
|
53
|
+
|
|
54
|
+
unless validator.schema_exists?
|
|
55
|
+
puts "No existing schema found."
|
|
56
|
+
puts "Run 'rake lyra:schema:create' first."
|
|
57
|
+
exit 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if validator.valid?
|
|
61
|
+
puts "No changes detected. Schema is up to date."
|
|
62
|
+
puts ""
|
|
63
|
+
puts "Current version: v#{Lyra::Schema::Store.latest_version}"
|
|
64
|
+
puts "Fingerprint: #{Lyra::Schema::Store.load_current[:fingerprint]}"
|
|
65
|
+
exit 0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
puts validator.report
|
|
69
|
+
puts ""
|
|
70
|
+
|
|
71
|
+
print "Create new schema version? [y/N] "
|
|
72
|
+
response = $stdin.gets&.chomp&.downcase
|
|
73
|
+
|
|
74
|
+
unless response == "y"
|
|
75
|
+
puts "Aborted."
|
|
76
|
+
exit 0
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
schema = Lyra::Schema::Generator.generate
|
|
80
|
+
file_path = Lyra::Schema::Store.save(schema)
|
|
81
|
+
|
|
82
|
+
puts ""
|
|
83
|
+
puts "New schema version created!"
|
|
84
|
+
puts "Version: v#{schema[:version]}"
|
|
85
|
+
puts "File: #{file_path}"
|
|
86
|
+
puts ""
|
|
87
|
+
puts "Don't forget to commit the new schema file."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
desc "Verify current schema against stored schema"
|
|
91
|
+
task verify: :environment do
|
|
92
|
+
require_lyra_schema
|
|
93
|
+
|
|
94
|
+
validator = Lyra::Schema::Validator.new
|
|
95
|
+
|
|
96
|
+
unless validator.schema_exists?
|
|
97
|
+
puts "No schema found."
|
|
98
|
+
puts ""
|
|
99
|
+
puts "Run 'rake lyra:schema:create' to generate initial schema."
|
|
100
|
+
exit 1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if validator.valid?
|
|
104
|
+
current = Lyra::Schema::Store.load_current
|
|
105
|
+
puts "Schema verification PASSED"
|
|
106
|
+
puts ""
|
|
107
|
+
puts "Version: v#{current[:version]}"
|
|
108
|
+
puts "Fingerprint: #{current[:fingerprint]}"
|
|
109
|
+
puts "No changes detected."
|
|
110
|
+
exit 0
|
|
111
|
+
else
|
|
112
|
+
puts validator.report
|
|
113
|
+
puts ""
|
|
114
|
+
|
|
115
|
+
if validator.breaking_changes?
|
|
116
|
+
puts "VERIFICATION FAILED: Breaking changes detected!"
|
|
117
|
+
puts ""
|
|
118
|
+
puts "To resolve:"
|
|
119
|
+
puts " 1. Run 'rake lyra:schema:update' to create a new version"
|
|
120
|
+
puts " 2. Or revert the model/database changes"
|
|
121
|
+
exit 1
|
|
122
|
+
else
|
|
123
|
+
puts "Non-breaking changes detected."
|
|
124
|
+
puts "Consider running 'rake lyra:schema:update' to update the schema."
|
|
125
|
+
exit 0
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
desc "Display current schema report (model->event mappings)"
|
|
131
|
+
task report: :environment do
|
|
132
|
+
require_lyra_schema
|
|
133
|
+
|
|
134
|
+
if Lyra.config.monitored_models.empty?
|
|
135
|
+
puts "No models are configured for monitoring."
|
|
136
|
+
puts "Configure models with Lyra.config.monitor_model(YourModel) first."
|
|
137
|
+
exit 1
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
reporter = Lyra::Schema::Reporter.new
|
|
141
|
+
puts reporter.to_s
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
desc "Show schema version history"
|
|
145
|
+
task history: :environment do
|
|
146
|
+
require_lyra_schema
|
|
147
|
+
|
|
148
|
+
history = Lyra::Schema::Store.history
|
|
149
|
+
|
|
150
|
+
if history.empty?
|
|
151
|
+
puts "No schema versions found."
|
|
152
|
+
puts ""
|
|
153
|
+
puts "Run 'rake lyra:schema:create' to generate initial schema."
|
|
154
|
+
exit 0
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
puts ""
|
|
158
|
+
puts "=" * 80
|
|
159
|
+
puts "Lyra Schema Version History"
|
|
160
|
+
puts "=" * 80
|
|
161
|
+
puts ""
|
|
162
|
+
puts "%-10s %-25s %-15s %-10s %-20s" % ["Version", "Created At", "Lyra Version", "Models", "File"]
|
|
163
|
+
puts "-" * 80
|
|
164
|
+
|
|
165
|
+
history.each do |h|
|
|
166
|
+
puts "%-10s %-25s %-15s %-10s %-20s" % [
|
|
167
|
+
"v#{h[:version]}",
|
|
168
|
+
h[:created_at],
|
|
169
|
+
h[:lyra_version],
|
|
170
|
+
h[:models_count],
|
|
171
|
+
h[:file]
|
|
172
|
+
]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
puts "-" * 80
|
|
176
|
+
puts ""
|
|
177
|
+
puts "Current version: v#{Lyra::Schema::Store.latest_version}"
|
|
178
|
+
puts "Schema path: #{Lyra::Schema::Store.schema_path}"
|
|
179
|
+
puts ""
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
desc "Compare two schema versions"
|
|
183
|
+
task :diff, [:v1, :v2] => :environment do |_t, args|
|
|
184
|
+
require_lyra_schema
|
|
185
|
+
|
|
186
|
+
v1 = args[:v1]&.to_i
|
|
187
|
+
v2 = args[:v2]&.to_i || Lyra::Schema::Store.latest_version
|
|
188
|
+
|
|
189
|
+
unless v1
|
|
190
|
+
puts "Usage: rake lyra:schema:diff[v1,v2]"
|
|
191
|
+
puts "Example: rake lyra:schema:diff[1,2]"
|
|
192
|
+
puts " rake lyra:schema:diff[1] (compare v1 to latest)"
|
|
193
|
+
exit 1
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
schema1 = Lyra::Schema::Store.load_version(v1)
|
|
197
|
+
schema2 = Lyra::Schema::Store.load_version(v2)
|
|
198
|
+
|
|
199
|
+
unless schema1
|
|
200
|
+
puts "Could not load schema version #{v1}"
|
|
201
|
+
exit 1
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
unless schema2
|
|
205
|
+
puts "Could not load schema version #{v2}"
|
|
206
|
+
exit 1
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
puts ""
|
|
210
|
+
puts "=" * 60
|
|
211
|
+
puts "Comparing v#{v1} -> v#{v2}"
|
|
212
|
+
puts "=" * 60
|
|
213
|
+
puts ""
|
|
214
|
+
|
|
215
|
+
differences = Lyra::Schema::Diff.compare(schema1, schema2)
|
|
216
|
+
report = Lyra::Schema::Diff.format_report(differences)
|
|
217
|
+
|
|
218
|
+
puts report
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
private
|
|
222
|
+
|
|
223
|
+
def require_lyra_schema
|
|
224
|
+
require "lyra/schema/store"
|
|
225
|
+
require "lyra/schema/generator"
|
|
226
|
+
require "lyra/schema/diff"
|
|
227
|
+
require "lyra/schema/validator"
|
|
228
|
+
require "lyra/schema/reporter"
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :lyra do
|
|
4
|
+
namespace :workflows do
|
|
5
|
+
desc "Generate verification workflow models from Lyra implementation via metaprogramming"
|
|
6
|
+
task generate: :environment do
|
|
7
|
+
require "fileutils"
|
|
8
|
+
|
|
9
|
+
unless Lyra.petri_flow_available?
|
|
10
|
+
puts "Error: PetriFlow gem is required"
|
|
11
|
+
puts "Add 'petri_flow' to your Gemfile and run 'bundle install'"
|
|
12
|
+
exit 1
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require "lyra/verification/workflow_generator"
|
|
16
|
+
|
|
17
|
+
# Parse mode argument: MODE=monitor or MODE=all (default)
|
|
18
|
+
requested_mode = ENV["MODE"]&.to_sym
|
|
19
|
+
if requested_mode && requested_mode != :all
|
|
20
|
+
unless Lyra::Verification::WorkflowGenerator::AVAILABLE_MODES.include?(requested_mode)
|
|
21
|
+
puts "Error: Invalid mode '#{requested_mode}'"
|
|
22
|
+
puts "Available modes: #{Lyra::Verification::WorkflowGenerator::AVAILABLE_MODES.join(', ')}, all"
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Determine Lyra gem root using multiple strategies
|
|
28
|
+
lyra_gem_root = Gem.loaded_specs['lyra']&.gem_dir
|
|
29
|
+
lyra_gem_root ||= Lyra::Engine.root.to_s if defined?(Lyra::Engine)
|
|
30
|
+
lyra_gem_root ||= begin
|
|
31
|
+
# Find lyra.gemspec by traversing up from Rails.root
|
|
32
|
+
dir = Rails.root
|
|
33
|
+
while dir.to_s != "/"
|
|
34
|
+
if File.exist?(File.join(dir, 'lyra.gemspec'))
|
|
35
|
+
break dir.to_s
|
|
36
|
+
end
|
|
37
|
+
dir = dir.parent
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Determine output directory
|
|
42
|
+
# Both Lyra gem and applications use app/workflows with top-level constants
|
|
43
|
+
# This is Zeitwerk-compatible: app/workflows/monitor_mode_workflow.rb -> MonitorModeWorkflow
|
|
44
|
+
is_lyra_context = lyra_gem_root && (
|
|
45
|
+
Rails.root.to_s.start_with?(lyra_gem_root) ||
|
|
46
|
+
File.exist?(File.join(Rails.root, 'lyra.gemspec'))
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if is_lyra_context && lyra_gem_root
|
|
50
|
+
# Lyra gem context - use app/workflows in the gem root
|
|
51
|
+
workflows_dir = File.join(lyra_gem_root, 'app', 'workflows')
|
|
52
|
+
else
|
|
53
|
+
# Normal application
|
|
54
|
+
workflows_dir = Rails.root.join('app', 'workflows')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Always use top-level constants for Zeitwerk compatibility
|
|
58
|
+
@use_lyra_namespace = false
|
|
59
|
+
|
|
60
|
+
reports_dir = Rails.root.join('reports')
|
|
61
|
+
|
|
62
|
+
FileUtils.mkdir_p(workflows_dir)
|
|
63
|
+
FileUtils.mkdir_p(reports_dir)
|
|
64
|
+
|
|
65
|
+
puts "Analyzing Lyra implementation..."
|
|
66
|
+
puts "Mode: #{requested_mode || 'all'}"
|
|
67
|
+
puts "Workflows directory: #{workflows_dir}"
|
|
68
|
+
puts "Reports directory: #{reports_dir}"
|
|
69
|
+
puts ""
|
|
70
|
+
|
|
71
|
+
generator = Lyra::Verification::WorkflowGenerator.new
|
|
72
|
+
result = if requested_mode && requested_mode != :all
|
|
73
|
+
generator.generate!(mode: requested_mode)
|
|
74
|
+
else
|
|
75
|
+
generator.generate!
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Output analysis results
|
|
79
|
+
puts "=" * 60
|
|
80
|
+
puts "LYRA IMPLEMENTATION ANALYSIS"
|
|
81
|
+
puts "=" * 60
|
|
82
|
+
puts ""
|
|
83
|
+
|
|
84
|
+
analysis = result[:analysis]
|
|
85
|
+
|
|
86
|
+
# Modes
|
|
87
|
+
puts "MODES:"
|
|
88
|
+
puts " Current mode: #{analysis[:current_mode] || 'not set'}"
|
|
89
|
+
puts " Available modes: #{analysis[:modes].join(', ')}"
|
|
90
|
+
puts ""
|
|
91
|
+
|
|
92
|
+
# Callbacks
|
|
93
|
+
puts "CALLBACKS DETECTED:"
|
|
94
|
+
hooks = analysis[:callback_hooks]
|
|
95
|
+
if hooks[:after].any?
|
|
96
|
+
puts " After callbacks:"
|
|
97
|
+
hooks[:after].each { |h| puts " - after_#{h[:type]} :#{h[:method]}" }
|
|
98
|
+
end
|
|
99
|
+
if hooks[:before].any?
|
|
100
|
+
puts " Before callbacks:"
|
|
101
|
+
hooks[:before].each { |h| puts " - before_#{h[:type]} :#{h[:method]}" }
|
|
102
|
+
end
|
|
103
|
+
puts ""
|
|
104
|
+
|
|
105
|
+
# Monitored models
|
|
106
|
+
puts "MONITORED MODELS: #{analysis[:models].count}"
|
|
107
|
+
analysis[:models].each do |model|
|
|
108
|
+
puts " - #{model[:name]}"
|
|
109
|
+
puts " Event prefix: #{model[:event_prefix]}" if model[:event_prefix]
|
|
110
|
+
if model[:callbacks]&.any?
|
|
111
|
+
puts " Callbacks:"
|
|
112
|
+
model[:callbacks].each { |k, v| puts " #{k}: #{v.join(', ')}" }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
puts ""
|
|
116
|
+
|
|
117
|
+
# Event types
|
|
118
|
+
puts "EVENT TYPES:"
|
|
119
|
+
analysis[:event_types].each { |e| puts " - #{e[:name]}" }
|
|
120
|
+
puts ""
|
|
121
|
+
|
|
122
|
+
# Generated workflows
|
|
123
|
+
puts "=" * 60
|
|
124
|
+
puts "GENERATED WORKFLOWS"
|
|
125
|
+
puts "=" * 60
|
|
126
|
+
puts ""
|
|
127
|
+
|
|
128
|
+
lifecycle = result[:lifecycle_workflow]
|
|
129
|
+
if lifecycle
|
|
130
|
+
puts "LIFECYCLE WORKFLOW: #{lifecycle[:name]}"
|
|
131
|
+
puts " Places: #{lifecycle[:places].join(' -> ')}"
|
|
132
|
+
puts " Initial: #{lifecycle[:initial_place]}"
|
|
133
|
+
puts " Terminal: #{lifecycle[:terminal_places].join(', ')}"
|
|
134
|
+
puts " Transitions:"
|
|
135
|
+
lifecycle[:transitions].each do |t|
|
|
136
|
+
puts " #{t[:from]} --[#{t[:name]}]--> #{t[:to]}"
|
|
137
|
+
puts " trigger: #{t[:trigger]}"
|
|
138
|
+
end
|
|
139
|
+
puts ""
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Single mode workflow (when specific mode requested)
|
|
143
|
+
if result[:mode_workflow]
|
|
144
|
+
print_workflow(result[:mode_workflow])
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Multiple mode workflows (when all modes requested)
|
|
148
|
+
if result[:mode_workflows]
|
|
149
|
+
result[:mode_workflows].each do |_mode, workflow|
|
|
150
|
+
puts "-" * 40
|
|
151
|
+
print_workflow(workflow)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Save workflow files
|
|
156
|
+
generated_files = []
|
|
157
|
+
|
|
158
|
+
# Generate lifecycle workflow file
|
|
159
|
+
if result[:lifecycle_workflow]
|
|
160
|
+
lifecycle_file = File.join(workflows_dir, "lifecycle_workflow.rb")
|
|
161
|
+
File.write(lifecycle_file, generate_workflow_file(result[:lifecycle_workflow], "LifecycleWorkflow", use_lyra_namespace: @use_lyra_namespace))
|
|
162
|
+
generated_files << lifecycle_file
|
|
163
|
+
puts "Generated: #{lifecycle_file}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Generate individual mode workflow files
|
|
167
|
+
if result[:mode_workflow]
|
|
168
|
+
# Single mode requested
|
|
169
|
+
mode_name = result[:mode_workflow][:name].downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')
|
|
170
|
+
mode_file = File.join(workflows_dir, "#{mode_name}.rb")
|
|
171
|
+
class_name = workflow_class_name(result[:mode_workflow][:name])
|
|
172
|
+
File.write(mode_file, generate_workflow_file(result[:mode_workflow], class_name, use_lyra_namespace: @use_lyra_namespace))
|
|
173
|
+
generated_files << mode_file
|
|
174
|
+
puts "Generated: #{mode_file}"
|
|
175
|
+
elsif result[:mode_workflows]
|
|
176
|
+
# All modes - generate separate files for each
|
|
177
|
+
result[:mode_workflows].each do |mode, workflow|
|
|
178
|
+
mode_file = File.join(workflows_dir, "#{mode}_mode_workflow.rb")
|
|
179
|
+
class_name = "#{mode.to_s.split('_').map(&:capitalize).join}ModeWorkflow"
|
|
180
|
+
File.write(mode_file, generate_workflow_file(workflow, class_name, use_lyra_namespace: @use_lyra_namespace))
|
|
181
|
+
generated_files << mode_file
|
|
182
|
+
puts "Generated: #{mode_file}"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Generate Markdown report
|
|
187
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
188
|
+
md_file = reports_dir.join("workflow_analysis_#{timestamp}.md")
|
|
189
|
+
File.write(md_file, generate_markdown_report(result))
|
|
190
|
+
puts "Generated analysis report: #{md_file}"
|
|
191
|
+
|
|
192
|
+
# Also generate a "latest" report
|
|
193
|
+
latest_md = reports_dir.join("workflow_analysis_latest.md")
|
|
194
|
+
File.write(latest_md, generate_markdown_report(result))
|
|
195
|
+
puts "Generated latest report: #{latest_md}"
|
|
196
|
+
|
|
197
|
+
puts ""
|
|
198
|
+
puts "Generated #{generated_files.count} workflow files in #{workflows_dir}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
desc "Verify generated workflows with PetriFlow"
|
|
202
|
+
task verify: :environment do
|
|
203
|
+
unless Lyra.petri_flow_available?
|
|
204
|
+
puts "Error: PetriFlow gem is required"
|
|
205
|
+
exit 1
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
require "lyra/verification/workflow_generator"
|
|
209
|
+
|
|
210
|
+
generator = Lyra::Verification::WorkflowGenerator.new
|
|
211
|
+
result = generator.generate!
|
|
212
|
+
|
|
213
|
+
puts "=" * 60
|
|
214
|
+
puts "PETRIFLOW VERIFICATION"
|
|
215
|
+
puts "=" * 60
|
|
216
|
+
puts ""
|
|
217
|
+
|
|
218
|
+
if result[:mode_workflows]
|
|
219
|
+
result[:mode_workflows].each do |mode, wf_data|
|
|
220
|
+
puts "Verifying #{mode} mode workflow..."
|
|
221
|
+
|
|
222
|
+
# Create a PetriFlow workflow from the data
|
|
223
|
+
workflow_class = Class.new(PetriFlow::Workflow) do
|
|
224
|
+
workflow_name wf_data[:name]
|
|
225
|
+
places(*wf_data[:places])
|
|
226
|
+
initial_place wf_data[:initial_place]
|
|
227
|
+
terminal_places(*wf_data[:terminal_places])
|
|
228
|
+
|
|
229
|
+
wf_data[:transitions].each do |t|
|
|
230
|
+
transition t[:name], from: t[:from], to: t[:to]
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
workflow = workflow_class.new
|
|
235
|
+
results = workflow.verify!
|
|
236
|
+
|
|
237
|
+
reachability = results[:reachability]
|
|
238
|
+
boundedness = results[:boundedness]
|
|
239
|
+
liveness = results[:liveness]
|
|
240
|
+
|
|
241
|
+
puts " Reachable states: #{reachability[:total_reachable_states]}"
|
|
242
|
+
puts " Terminal states: #{reachability[:terminal_states]}"
|
|
243
|
+
puts " Is safe (1-bounded): #{boundedness[:is_safe]}"
|
|
244
|
+
puts " Is bounded: #{boundedness[:is_bounded]}"
|
|
245
|
+
puts " Max tokens: #{boundedness[:max_tokens]}"
|
|
246
|
+
puts " Deadlock-free: #{liveness[:deadlock_free]}"
|
|
247
|
+
puts " Liveness score: #{liveness[:liveness_score]}"
|
|
248
|
+
puts ""
|
|
249
|
+
end
|
|
250
|
+
else
|
|
251
|
+
puts "No workflows generated"
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Helper methods
|
|
256
|
+
def print_workflow(workflow)
|
|
257
|
+
return unless workflow
|
|
258
|
+
|
|
259
|
+
puts "MODE WORKFLOW: #{workflow[:name]}"
|
|
260
|
+
puts " #{workflow[:description]}" if workflow[:description]
|
|
261
|
+
puts " Places: #{workflow[:places].join(' -> ')}"
|
|
262
|
+
puts " Initial: #{workflow[:initial_place]}"
|
|
263
|
+
puts " Terminal: #{workflow[:terminal_places].join(', ')}"
|
|
264
|
+
puts " Transitions:"
|
|
265
|
+
workflow[:transitions].each do |t|
|
|
266
|
+
puts " #{t[:from]} --[#{t[:name]}]--> #{t[:to]}"
|
|
267
|
+
puts " trigger: #{t[:trigger]}"
|
|
268
|
+
end
|
|
269
|
+
puts ""
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def workflow_class_name(name)
|
|
273
|
+
name.to_s.gsub(/[^a-zA-Z0-9]/, ' ').split.map(&:capitalize).join + "Workflow"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def generate_workflow_file(workflow, class_name, use_lyra_namespace: true)
|
|
277
|
+
lines = []
|
|
278
|
+
lines << "# frozen_string_literal: true"
|
|
279
|
+
lines << "# Auto-generated by Lyra::Verification::WorkflowGenerator"
|
|
280
|
+
lines << "# Generated at: #{Time.current}"
|
|
281
|
+
lines << ""
|
|
282
|
+
|
|
283
|
+
if use_lyra_namespace
|
|
284
|
+
# Lyra gem context - nested namespace
|
|
285
|
+
lines << "module Lyra"
|
|
286
|
+
lines << " module Verification"
|
|
287
|
+
lines << " module Generated"
|
|
288
|
+
lines << ""
|
|
289
|
+
lines << generate_workflow_class(workflow, class_name, indent: 6)
|
|
290
|
+
lines << ""
|
|
291
|
+
lines << " end"
|
|
292
|
+
lines << " end"
|
|
293
|
+
lines << "end"
|
|
294
|
+
else
|
|
295
|
+
# Application context - top-level class for Zeitwerk compatibility
|
|
296
|
+
lines << generate_workflow_class(workflow, class_name, indent: 0)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
lines.join("\n")
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def generate_workflow_class(workflow, class_name, indent: 6)
|
|
303
|
+
pad = " " * indent
|
|
304
|
+
lines = []
|
|
305
|
+
lines << "#{pad}# #{workflow[:name]}"
|
|
306
|
+
lines << "#{pad}# #{workflow[:description]}" if workflow[:description]
|
|
307
|
+
lines << "#{pad}class #{class_name} < PetriFlow::Workflow"
|
|
308
|
+
lines << "#{pad} workflow_name #{workflow[:name].inspect}"
|
|
309
|
+
lines << ""
|
|
310
|
+
lines << "#{pad} places #{workflow[:places].map(&:inspect).join(', ')}"
|
|
311
|
+
lines << "#{pad} initial_place #{workflow[:initial_place].inspect}"
|
|
312
|
+
lines << "#{pad} terminal_places #{workflow[:terminal_places].map(&:inspect).join(', ')}"
|
|
313
|
+
lines << ""
|
|
314
|
+
workflow[:transitions].each do |t|
|
|
315
|
+
lines << "#{pad} transition #{t[:name].inspect},"
|
|
316
|
+
lines << "#{pad} from: #{t[:from].inspect},"
|
|
317
|
+
lines << "#{pad} to: #{t[:to].inspect},"
|
|
318
|
+
lines << "#{pad} trigger: #{t[:trigger].inspect}"
|
|
319
|
+
lines << ""
|
|
320
|
+
end
|
|
321
|
+
lines << "#{pad}end"
|
|
322
|
+
lines.join("\n")
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def generate_markdown_report(result)
|
|
326
|
+
analysis = result[:analysis]
|
|
327
|
+
lifecycle = result[:lifecycle_workflow]
|
|
328
|
+
|
|
329
|
+
md = []
|
|
330
|
+
md << "# Lyra Verification Workflow Analysis"
|
|
331
|
+
md << ""
|
|
332
|
+
md << "**Generated:** #{Time.current}"
|
|
333
|
+
md << "**Method:** Metaprogramming introspection of Lyra implementation"
|
|
334
|
+
md << ""
|
|
335
|
+
|
|
336
|
+
md << "## Implementation Analysis"
|
|
337
|
+
md << ""
|
|
338
|
+
|
|
339
|
+
md << "### Lyra Modes"
|
|
340
|
+
md << ""
|
|
341
|
+
md << "| Mode | Available |"
|
|
342
|
+
md << "|------|-----------|"
|
|
343
|
+
analysis[:modes].each do |mode|
|
|
344
|
+
md << "| #{mode} | Yes |"
|
|
345
|
+
end
|
|
346
|
+
md << ""
|
|
347
|
+
md << "**Current mode:** `#{analysis[:current_mode] || 'not configured'}`"
|
|
348
|
+
md << ""
|
|
349
|
+
|
|
350
|
+
md << "### Detected Callbacks"
|
|
351
|
+
md << ""
|
|
352
|
+
hooks = analysis[:callback_hooks]
|
|
353
|
+
if hooks[:after].any? || hooks[:before].any?
|
|
354
|
+
md << "| Timing | Type | Method |"
|
|
355
|
+
md << "|--------|------|--------|"
|
|
356
|
+
hooks[:before].each { |h| md << "| before | #{h[:type]} | `#{h[:method]}` |" }
|
|
357
|
+
hooks[:after].each { |h| md << "| after | #{h[:type]} | `#{h[:method]}` |" }
|
|
358
|
+
else
|
|
359
|
+
md << "*No callbacks detected in source*"
|
|
360
|
+
end
|
|
361
|
+
md << ""
|
|
362
|
+
|
|
363
|
+
md << "### Monitored Models"
|
|
364
|
+
md << ""
|
|
365
|
+
if analysis[:models].any?
|
|
366
|
+
analysis[:models].each do |model|
|
|
367
|
+
md << "#### #{model[:name]}"
|
|
368
|
+
md << ""
|
|
369
|
+
md << "- **Table:** `#{model[:table_name]}`" if model[:table_name]
|
|
370
|
+
md << "- **Event prefix:** `#{model[:event_prefix]}`" if model[:event_prefix]
|
|
371
|
+
md << ""
|
|
372
|
+
end
|
|
373
|
+
else
|
|
374
|
+
md << "*No models currently monitored*"
|
|
375
|
+
end
|
|
376
|
+
md << ""
|
|
377
|
+
|
|
378
|
+
if lifecycle
|
|
379
|
+
md << generate_workflow_markdown(lifecycle, "Lifecycle")
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Single mode workflow
|
|
383
|
+
if result[:mode_workflow]
|
|
384
|
+
md << generate_workflow_markdown(result[:mode_workflow], result[:mode_workflow][:name])
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Multiple mode workflows
|
|
388
|
+
if result[:mode_workflows]
|
|
389
|
+
md << "## Mode-Specific Workflows"
|
|
390
|
+
md << ""
|
|
391
|
+
result[:mode_workflows].each do |_mode, workflow|
|
|
392
|
+
md << generate_workflow_markdown(workflow, workflow[:name])
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
md << "---"
|
|
397
|
+
md << "*Generated by Lyra::Verification::WorkflowGenerator via metaprogramming*"
|
|
398
|
+
|
|
399
|
+
md.join("\n")
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def generate_workflow_markdown(workflow, title)
|
|
403
|
+
md = []
|
|
404
|
+
md << "## #{title}"
|
|
405
|
+
md << ""
|
|
406
|
+
md << "**Name:** #{workflow[:name]}"
|
|
407
|
+
md << ""
|
|
408
|
+
md << "**Description:** #{workflow[:description]}" if workflow[:description]
|
|
409
|
+
md << ""
|
|
410
|
+
md << "### State Machine"
|
|
411
|
+
md << ""
|
|
412
|
+
md << "```"
|
|
413
|
+
md << "Initial: [#{workflow[:initial_place]}]"
|
|
414
|
+
md << "Terminal: [#{workflow[:terminal_places].join(', ')}]"
|
|
415
|
+
md << ""
|
|
416
|
+
md << "Places: #{workflow[:places].join(' -> ')}"
|
|
417
|
+
md << "```"
|
|
418
|
+
md << ""
|
|
419
|
+
md << "### Transitions"
|
|
420
|
+
md << ""
|
|
421
|
+
md << "| From | Transition | To | Trigger |"
|
|
422
|
+
md << "|------|------------|-----|---------|"
|
|
423
|
+
workflow[:transitions].each do |t|
|
|
424
|
+
md << "| #{t[:from]} | #{t[:name]} | #{t[:to]} | #{t[:trigger]} |"
|
|
425
|
+
end
|
|
426
|
+
md << ""
|
|
427
|
+
|
|
428
|
+
# Generate Mermaid diagram
|
|
429
|
+
md << "### Petri Net Diagram"
|
|
430
|
+
md << ""
|
|
431
|
+
md << "```mermaid"
|
|
432
|
+
md << "flowchart TB"
|
|
433
|
+
workflow[:places].each do |place|
|
|
434
|
+
md << " #{place}((\"#{place.to_s.gsub('_', ' ').split.map(&:capitalize).join(' ')}\"))"
|
|
435
|
+
end
|
|
436
|
+
workflow[:transitions].each do |t|
|
|
437
|
+
md << " t_#{t[:name]}[\"#{t[:name]}\"]"
|
|
438
|
+
md << " #{t[:from]} --> t_#{t[:name]}"
|
|
439
|
+
md << " t_#{t[:name]} --> #{t[:to]}"
|
|
440
|
+
end
|
|
441
|
+
md << "```"
|
|
442
|
+
md << ""
|
|
443
|
+
|
|
444
|
+
md.join("\n")
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# Aliases for convenience
|
|
450
|
+
desc "Generate workflows (alias for lyra:workflows:generate)"
|
|
451
|
+
task generate_workflows: "lyra:workflows:generate"
|
|
452
|
+
end
|