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