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
data/Rakefile ADDED
@@ -0,0 +1,728 @@
1
+ require "bundler/setup"
2
+ require "rake/testtask"
3
+
4
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
5
+ # load "rails/tasks/engine.rake" if File.exist?(APP_RAKEFILE)
6
+
7
+ # load "rails/tasks/statistics.rake"
8
+
9
+ require "bundler/gem_tasks"
10
+
11
+ # Load custom tasks
12
+ Dir.glob("lib/tasks/*.rake").each { |r| load r }
13
+
14
+ Rake::TestTask.new(:test) do |t|
15
+ t.libs << "lib"
16
+ t.libs << "test"
17
+ t.test_files = FileList["test/**/*_test.rb"]
18
+ t.verbose = true
19
+ end
20
+
21
+
22
+ namespace :test do
23
+ desc "Run unit tests only (excludes controller tests)"
24
+ Rake::TestTask.new(:unit) do |t|
25
+ t.libs << "lib"
26
+ t.libs << "test"
27
+ t.test_files = FileList["test/**/*_test.rb"].exclude("test/controllers/**/*_test.rb")
28
+ t.verbose = true
29
+ end
30
+
31
+ desc "Run controller tests only (requires dummy Rails app)"
32
+ Rake::TestTask.new(:controllers) do |t|
33
+ t.libs << "lib"
34
+ t.libs << "test"
35
+ t.test_files = FileList["test/controllers/**/*_test.rb"]
36
+ t.verbose = true
37
+ end
38
+
39
+ desc "Run tests for all components (Lyra, PAM DSL, PetriFlow)"
40
+ task :all do
41
+ puts "\n=== Running Lyra tests ==="
42
+ Rake::Task["test"].invoke
43
+
44
+ puts "\n=== Running PAM DSL tests ==="
45
+ Dir.chdir("gems/pam_dsl") { system("rake test") || raise("PAM DSL tests failed") }
46
+
47
+ puts "\n=== Running PetriFlow tests ==="
48
+ Dir.chdir("gems/petri_flow") { system("rake test") || raise("PetriFlow tests failed") }
49
+
50
+ puts "\n=== All tests passed! ==="
51
+ end
52
+ end
53
+
54
+ task default: :test
55
+
56
+ # =============================================================================
57
+ # Shared constants
58
+ # =============================================================================
59
+ TESTBED_DIR = File.expand_path("examples/aegean_epay_testbed", __dir__)
60
+
61
+ # =============================================================================
62
+ # Benchmark tasks
63
+ # =============================================================================
64
+ namespace :benchmark do
65
+
66
+ desc "Run Lyra mode comparison benchmark (delegates to aegean_epay_testbed)"
67
+ task :modes do
68
+ # Collect all arguments after the task name
69
+ # Usage: rake benchmark:modes -- --scales=1000,5000 --modes=disabled,monitor
70
+ args = ARGV.drop_while { |a| a != "--" }.drop(1).join(" ")
71
+
72
+ Dir.chdir(TESTBED_DIR) do
73
+ # Use bundle exec with testbed's Gemfile to ensure correct dependencies
74
+ cmd = "bundle exec ruby perf/run_mode_comparison_benchmark.rb #{args}"
75
+ puts "Running: RAILS_ENV=test #{cmd}"
76
+ puts "Working directory: #{TESTBED_DIR}"
77
+ puts ""
78
+ env = {
79
+ "RAILS_ENV" => "test",
80
+ "BUNDLE_GEMFILE" => File.join(TESTBED_DIR, "Gemfile")
81
+ }
82
+ system(env, cmd) || exit(1)
83
+ end
84
+
85
+ # Prevent rake from trying to run arguments as tasks
86
+ exit(0)
87
+ end
88
+
89
+ desc "Continue interrupted benchmark run"
90
+ task :continue do
91
+ Dir.chdir(TESTBED_DIR) do
92
+ cmd = "bundle exec ruby perf/run_mode_comparison_benchmark.rb --continue"
93
+ puts "Resuming benchmark from checkpoint..."
94
+ puts "Working directory: #{TESTBED_DIR}"
95
+ puts ""
96
+ env = {
97
+ "RAILS_ENV" => "test",
98
+ "BUNDLE_GEMFILE" => File.join(TESTBED_DIR, "Gemfile")
99
+ }
100
+ system(env, cmd) || exit(1)
101
+ end
102
+ end
103
+
104
+ desc "Run concurrent operations benchmark (thread scaling)"
105
+ task :concurrent do
106
+ args = ARGV.drop_while { |a| a != "--" }.drop(1).join(" ")
107
+
108
+ Dir.chdir(TESTBED_DIR) do
109
+ cmd = "bundle exec ruby perf/run_concurrent_benchmark.rb #{args}"
110
+ puts "Running: RAILS_ENV=test #{cmd}"
111
+ puts "Working directory: #{TESTBED_DIR}"
112
+ puts ""
113
+ env = {
114
+ "RAILS_ENV" => "test",
115
+ "BUNDLE_GEMFILE" => File.join(TESTBED_DIR, "Gemfile")
116
+ }
117
+ system(env, cmd) || exit(1)
118
+ end
119
+
120
+ exit(0)
121
+ end
122
+
123
+ desc "Show benchmark help"
124
+ task :help do
125
+ puts <<~HELP
126
+ Lyra Benchmark Tasks
127
+ ====================
128
+
129
+ rake benchmark:modes Run mode comparison benchmark (7 modes, 4 scenarios)
130
+ rake benchmark:concurrent Run concurrent/thread scaling benchmark
131
+ rake benchmark:continue Resume interrupted mode comparison run
132
+ rake benchmark:help Show this help
133
+
134
+ Mode Comparison (benchmark:modes):
135
+ Compares all 7 Lyra modes across CRUD, batch, query, and mixed scenarios.
136
+ Supports checkpoint/resume for long-running benchmarks.
137
+
138
+ rake benchmark:modes -- --scales=1000,5000 --modes=disabled,monitor
139
+ rake benchmark:modes -- --scenarios=crud,batch
140
+ rake benchmark:modes -- --help
141
+
142
+ Concurrent Benchmark (benchmark:concurrent):
143
+ Tests throughput under concurrent load with multiple threads.
144
+
145
+ rake benchmark:concurrent -- --threads=1,2,4,8 --operations=100
146
+ rake benchmark:concurrent -- --modes=disabled,monitor,hijack
147
+
148
+ Examples:
149
+ # Quick mode comparison
150
+ rake benchmark:modes -- --scales=100,500 --iterations=10
151
+
152
+ # Full benchmark for paper
153
+ rake benchmark:modes -- --scales=500,1000,5000,10000
154
+
155
+ # Resume after interruption
156
+ rake benchmark:continue
157
+
158
+ # Concurrent scaling test
159
+ rake benchmark:concurrent -- --threads=1,2,4,8,16
160
+
161
+ Note: Ensure PostgreSQL is running before benchmarks (rake docker:start).
162
+ HELP
163
+ end
164
+ end
165
+
166
+ # =============================================================================
167
+ # Docker tasks for PostgreSQL container
168
+ # =============================================================================
169
+ namespace :docker do
170
+ CONTAINER_NAME = "lyra_postgres"
171
+
172
+ desc "Ensure PostgreSQL container is running (starts if needed)"
173
+ task :ensure do
174
+ running = `docker ps --filter "name=#{CONTAINER_NAME}" --format "{{.Names}}"`.strip
175
+ if running != CONTAINER_NAME
176
+ Rake::Task["docker:start"].invoke
177
+ else
178
+ # Verify it's healthy
179
+ health = `docker inspect --format='{{.State.Health.Status}}' #{CONTAINER_NAME} 2>/dev/null`.strip
180
+ if health != "healthy"
181
+ print "Waiting for PostgreSQL to be healthy"
182
+ 30.times do
183
+ health = `docker inspect --format='{{.State.Health.Status}}' #{CONTAINER_NAME} 2>/dev/null`.strip
184
+ break if health == "healthy"
185
+ print "."
186
+ sleep 1
187
+ end
188
+ puts ""
189
+ end
190
+ end
191
+ end
192
+
193
+ desc "Start PostgreSQL container for testing and benchmarks"
194
+ task :start do
195
+ puts "Starting PostgreSQL container..."
196
+
197
+ # Check if container already exists
198
+ existing = `docker ps -a --filter "name=#{CONTAINER_NAME}" --format "{{.Names}}"`.strip
199
+
200
+ if existing == CONTAINER_NAME
201
+ # Container exists, check if it's running
202
+ running = `docker ps --filter "name=#{CONTAINER_NAME}" --format "{{.Names}}"`.strip
203
+ if running == CONTAINER_NAME
204
+ puts "Container '#{CONTAINER_NAME}' is already running"
205
+ else
206
+ puts "Starting existing container..."
207
+ system("docker start #{CONTAINER_NAME}")
208
+ puts "Container started"
209
+ end
210
+ else
211
+ # Start new container with docker-compose
212
+ puts "Creating new container..."
213
+ system("docker compose up -d")
214
+ end
215
+
216
+ # Wait for healthcheck
217
+ print "Waiting for PostgreSQL to be ready"
218
+ 30.times do
219
+ result = `docker inspect --format='{{.State.Health.Status}}' #{CONTAINER_NAME} 2>/dev/null`.strip
220
+ if result == "healthy"
221
+ puts "\nPostgreSQL is ready on port 5433"
222
+ break
223
+ end
224
+ print "."
225
+ sleep 1
226
+ end
227
+ end
228
+
229
+ desc "Stop PostgreSQL container"
230
+ task :stop do
231
+ puts "Stopping PostgreSQL container..."
232
+ system("docker compose down")
233
+ puts "Container stopped"
234
+ end
235
+
236
+ desc "Show PostgreSQL container status"
237
+ task :status do
238
+ running = `docker ps --filter "name=#{CONTAINER_NAME}" --format "{{.Names}}"`.strip
239
+ if running == CONTAINER_NAME
240
+ puts "Container '#{CONTAINER_NAME}' is running"
241
+ puts ""
242
+ system("docker ps --filter 'name=#{CONTAINER_NAME}' --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'")
243
+ else
244
+ existing = `docker ps -a --filter "name=#{CONTAINER_NAME}" --format "{{.Names}}"`.strip
245
+ if existing == CONTAINER_NAME
246
+ puts "Container '#{CONTAINER_NAME}' exists but is stopped"
247
+ puts " Run 'rake docker:start' to start it"
248
+ else
249
+ puts "Container '#{CONTAINER_NAME}' does not exist"
250
+ puts " Run 'rake docker:start' to create and start it"
251
+ end
252
+ end
253
+ end
254
+
255
+ desc "View PostgreSQL container logs"
256
+ task :logs do
257
+ system("docker logs #{CONTAINER_NAME} --tail 50")
258
+ end
259
+
260
+ desc "Remove PostgreSQL container and data volume"
261
+ task :clean do
262
+ puts "Stopping and removing container..."
263
+ system("docker compose down -v")
264
+ puts "Container and volume removed"
265
+ end
266
+
267
+ desc "Connect to PostgreSQL with psql"
268
+ task :psql do
269
+ system("docker", "exec", "-it", CONTAINER_NAME, "psql", "-U", "postgres")
270
+ end
271
+ end
272
+
273
+ # =============================================================================
274
+ # Statistics tasks
275
+ # =============================================================================
276
+ namespace :stats do
277
+ desc "Generate comprehensive code statistics for all components"
278
+ task :all do
279
+ require "json"
280
+
281
+ puts "=" * 70
282
+ puts "LYRA PROJECT CODE STATISTICS"
283
+ puts "=" * 70
284
+ puts "Generated: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}"
285
+ puts "=" * 70
286
+ puts ""
287
+
288
+ results = {}
289
+
290
+ # Lyra core
291
+ puts "Analyzing Lyra core..."
292
+ results[:lyra] = analyze_directory("lib", "test", "Lyra Core")
293
+
294
+ # PAM DSL
295
+ puts "Analyzing PAM DSL..."
296
+ results[:pam_dsl] = analyze_directory("gems/pam_dsl/lib", "gems/pam_dsl/test", "PAM DSL")
297
+
298
+ # PetriFlow
299
+ puts "Analyzing PetriFlow..."
300
+ results[:petri_flow] = analyze_directory("gems/petri_flow/lib", "gems/petri_flow/test", "PetriFlow")
301
+
302
+ # Aegean E-Pay Testbed
303
+ puts "Analyzing Aegean E-Pay Testbed..."
304
+ results[:testbed] = analyze_rails_app("examples/aegean_epay_testbed", "Aegean E-Pay Testbed")
305
+
306
+ puts ""
307
+
308
+ # Print individual reports
309
+ results.each do |key, data|
310
+ print_component_report(data)
311
+ end
312
+
313
+ # Print summary
314
+ print_summary(results)
315
+
316
+ # Save to file
317
+ save_report(results)
318
+ end
319
+
320
+ desc "Generate statistics for Lyra core only"
321
+ task :lyra do
322
+ result = analyze_directory("lib", "test", "Lyra Core")
323
+ print_component_report(result)
324
+ end
325
+
326
+ desc "Generate statistics for PAM DSL"
327
+ task :pam_dsl do
328
+ result = analyze_directory("gems/pam_dsl/lib", "gems/pam_dsl/test", "PAM DSL")
329
+ print_component_report(result)
330
+ end
331
+
332
+ desc "Generate statistics for PetriFlow"
333
+ task :petri_flow do
334
+ result = analyze_directory("gems/petri_flow/lib", "gems/petri_flow/test", "PetriFlow")
335
+ print_component_report(result)
336
+ end
337
+
338
+ desc "Generate statistics for Aegean E-Pay Testbed"
339
+ task :testbed do
340
+ result = analyze_rails_app("examples/aegean_epay_testbed", "Aegean E-Pay Testbed")
341
+ print_component_report(result)
342
+ end
343
+
344
+ # Helper methods
345
+ def analyze_directory(lib_path, test_path, name)
346
+ lib_stats = count_ruby_files(lib_path)
347
+ test_stats = count_ruby_files(test_path)
348
+
349
+ {
350
+ name: name,
351
+ lib_path: lib_path,
352
+ test_path: test_path,
353
+ code: lib_stats,
354
+ test: test_stats,
355
+ total_files: lib_stats[:files] + test_stats[:files],
356
+ total_lines: lib_stats[:lines] + test_stats[:lines],
357
+ total_loc: lib_stats[:loc] + test_stats[:loc],
358
+ total_classes: lib_stats[:classes] + test_stats[:classes],
359
+ total_methods: lib_stats[:methods] + test_stats[:methods],
360
+ code_to_test_ratio: lib_stats[:loc] > 0 ? (test_stats[:loc].to_f / lib_stats[:loc]).round(2) : 0
361
+ }
362
+ end
363
+
364
+ def analyze_rails_app(app_path, name)
365
+ # Analyze different parts of the Rails app
366
+ categories = {
367
+ "Controllers" => "#{app_path}/app/controllers",
368
+ "Models" => "#{app_path}/app/models",
369
+ "Views" => "#{app_path}/app/views",
370
+ "Helpers" => "#{app_path}/app/helpers",
371
+ "Jobs" => "#{app_path}/app/jobs",
372
+ "Mailers" => "#{app_path}/app/mailers",
373
+ "Services" => "#{app_path}/app/services",
374
+ "Repositories" => "#{app_path}/app/repositories",
375
+ "Workflows" => "#{app_path}/app/workflows",
376
+ "Libraries" => "#{app_path}/lib"
377
+ }
378
+
379
+ test_categories = {
380
+ "Controller tests" => "#{app_path}/test/controllers",
381
+ "Model tests" => "#{app_path}/test/models",
382
+ "Integration tests" => "#{app_path}/test/integration",
383
+ "System tests" => "#{app_path}/test/system",
384
+ "Other tests" => "#{app_path}/test"
385
+ }
386
+
387
+ code_stats = { files: 0, lines: 0, loc: 0, classes: 0, methods: 0 }
388
+ test_stats = { files: 0, lines: 0, loc: 0, classes: 0, methods: 0 }
389
+ breakdown = {}
390
+
391
+ categories.each do |category, path|
392
+ if Dir.exist?(path)
393
+ stats = count_ruby_files(path)
394
+ if stats[:files] > 0
395
+ breakdown[category] = stats
396
+ code_stats[:files] += stats[:files]
397
+ code_stats[:lines] += stats[:lines]
398
+ code_stats[:loc] += stats[:loc]
399
+ code_stats[:classes] += stats[:classes]
400
+ code_stats[:methods] += stats[:methods]
401
+ end
402
+ end
403
+ end
404
+
405
+ test_categories.each do |category, path|
406
+ if Dir.exist?(path)
407
+ stats = count_ruby_files(path)
408
+ if stats[:files] > 0
409
+ # Avoid double-counting for "Other tests"
410
+ if category == "Other tests"
411
+ other_files = Dir.glob("#{path}/*_test.rb")
412
+ stats = count_files(other_files)
413
+ end
414
+ if stats[:files] > 0
415
+ breakdown[category] = stats
416
+ test_stats[:files] += stats[:files]
417
+ test_stats[:lines] += stats[:lines]
418
+ test_stats[:loc] += stats[:loc]
419
+ test_stats[:classes] += stats[:classes]
420
+ test_stats[:methods] += stats[:methods]
421
+ end
422
+ end
423
+ end
424
+ end
425
+
426
+ {
427
+ name: name,
428
+ lib_path: app_path,
429
+ test_path: "#{app_path}/test",
430
+ code: code_stats,
431
+ test: test_stats,
432
+ breakdown: breakdown,
433
+ total_files: code_stats[:files] + test_stats[:files],
434
+ total_lines: code_stats[:lines] + test_stats[:lines],
435
+ total_loc: code_stats[:loc] + test_stats[:loc],
436
+ total_classes: code_stats[:classes] + test_stats[:classes],
437
+ total_methods: code_stats[:methods] + test_stats[:methods],
438
+ code_to_test_ratio: code_stats[:loc] > 0 ? (test_stats[:loc].to_f / code_stats[:loc]).round(2) : 0
439
+ }
440
+ end
441
+
442
+ def count_ruby_files(path)
443
+ return { files: 0, lines: 0, loc: 0, classes: 0, methods: 0 } unless Dir.exist?(path)
444
+
445
+ files = Dir.glob("#{path}/**/*.rb")
446
+ count_files(files)
447
+ end
448
+
449
+ def count_files(files)
450
+ stats = { files: 0, lines: 0, loc: 0, classes: 0, methods: 0 }
451
+
452
+ files.each do |file|
453
+ next unless File.file?(file)
454
+
455
+ stats[:files] += 1
456
+ content = File.read(file)
457
+ lines = content.lines
458
+
459
+ stats[:lines] += lines.size
460
+ stats[:loc] += lines.count { |l| l.strip.length > 0 && !l.strip.start_with?("#") }
461
+ stats[:classes] += content.scan(/^\s*(class|module)\s+\w+/).size
462
+ stats[:methods] += content.scan(/^\s*def\s+\w+/).size
463
+ end
464
+
465
+ stats
466
+ end
467
+
468
+ def print_component_report(data)
469
+ puts "-" * 70
470
+ puts data[:name].upcase
471
+ puts "-" * 70
472
+ puts ""
473
+
474
+ if data[:breakdown]
475
+ puts "+----------------------+--------+--------+---------+---------+"
476
+ puts "| Category | Files | LOC | Classes | Methods |"
477
+ puts "+----------------------+--------+--------+---------+---------+"
478
+
479
+ data[:breakdown].each do |category, stats|
480
+ printf "| %-20s | %6d | %6d | %7d | %7d |\n",
481
+ category, stats[:files], stats[:loc], stats[:classes], stats[:methods]
482
+ end
483
+
484
+ puts "+----------------------+--------+--------+---------+---------+"
485
+ end
486
+
487
+ puts ""
488
+ puts " Code: #{data[:code][:files]} files, #{data[:code][:loc]} LOC, #{data[:code][:classes]} classes, #{data[:code][:methods]} methods"
489
+ puts " Tests: #{data[:test][:files]} files, #{data[:test][:loc]} LOC, #{data[:test][:classes]} classes, #{data[:test][:methods]} methods"
490
+ puts " Total: #{data[:total_files]} files, #{data[:total_loc]} LOC"
491
+ puts " Code to Test Ratio: 1:#{data[:code_to_test_ratio]}"
492
+ puts ""
493
+ end
494
+
495
+ def print_summary(results)
496
+ puts "=" * 70
497
+ puts "COMBINED SUMMARY"
498
+ puts "=" * 70
499
+ puts ""
500
+
501
+ total = {
502
+ code_files: 0, code_loc: 0, code_classes: 0, code_methods: 0,
503
+ test_files: 0, test_loc: 0, test_classes: 0, test_methods: 0
504
+ }
505
+
506
+ puts "+----------------------+--------+--------+---------+---------+--------+--------+"
507
+ puts "| Component | C.Files| C.LOC | Classes | Methods | T.Files| T.LOC |"
508
+ puts "+----------------------+--------+--------+---------+---------+--------+--------+"
509
+
510
+ results.each do |key, data|
511
+ printf "| %-20s | %6d | %6d | %7d | %7d | %6d | %6d |\n",
512
+ data[:name], data[:code][:files], data[:code][:loc],
513
+ data[:code][:classes], data[:code][:methods],
514
+ data[:test][:files], data[:test][:loc]
515
+
516
+ total[:code_files] += data[:code][:files]
517
+ total[:code_loc] += data[:code][:loc]
518
+ total[:code_classes] += data[:code][:classes]
519
+ total[:code_methods] += data[:code][:methods]
520
+ total[:test_files] += data[:test][:files]
521
+ total[:test_loc] += data[:test][:loc]
522
+ end
523
+
524
+ puts "+----------------------+--------+--------+---------+---------+--------+--------+"
525
+ printf "| %-20s | %6d | %6d | %7d | %7d | %6d | %6d |\n",
526
+ "TOTAL", total[:code_files], total[:code_loc],
527
+ total[:code_classes], total[:code_methods],
528
+ total[:test_files], total[:test_loc]
529
+ puts "+----------------------+--------+--------+---------+---------+--------+--------+"
530
+
531
+ puts ""
532
+ puts "Grand Total: #{total[:code_files] + total[:test_files]} files, #{total[:code_loc] + total[:test_loc]} LOC"
533
+ puts "Code to Test Ratio: 1:#{(total[:test_loc].to_f / total[:code_loc]).round(2)}"
534
+ puts ""
535
+ end
536
+
537
+ def save_report(results)
538
+ require "json"
539
+ require "fileutils"
540
+
541
+ report_dir = "examples/aegean_epay_testbed/reports"
542
+ FileUtils.mkdir_p(report_dir)
543
+
544
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
545
+
546
+ # Calculate totals
547
+ total = {
548
+ code_files: 0, code_loc: 0, code_classes: 0, code_methods: 0,
549
+ test_files: 0, test_loc: 0, test_classes: 0, test_methods: 0
550
+ }
551
+ results.each do |_, data|
552
+ total[:code_files] += data[:code][:files]
553
+ total[:code_loc] += data[:code][:loc]
554
+ total[:code_classes] += data[:code][:classes]
555
+ total[:code_methods] += data[:code][:methods]
556
+ total[:test_files] += data[:test][:files]
557
+ total[:test_loc] += data[:test][:loc]
558
+ end
559
+
560
+ # Save JSON
561
+ json_file = "#{report_dir}/stats_#{timestamp}.json"
562
+ File.write(json_file, JSON.pretty_generate({
563
+ generated_at: Time.now.iso8601,
564
+ components: results,
565
+ totals: total
566
+ }))
567
+
568
+ # Save Markdown
569
+ md_file = "#{report_dir}/stats_#{timestamp}.md"
570
+ File.open(md_file, "w") do |f|
571
+ f.puts "# Lyra Project Code Statistics"
572
+ f.puts ""
573
+ f.puts "Generated: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}"
574
+ f.puts ""
575
+ f.puts "## Summary"
576
+ f.puts ""
577
+ f.puts "| Component | Code Files | Code LOC | Classes | Methods | Test Files | Test LOC | Ratio |"
578
+ f.puts "|-----------|------------|----------|---------|---------|------------|----------|-------|"
579
+
580
+ results.each do |_, data|
581
+ f.puts "| #{data[:name]} | #{data[:code][:files]} | #{data[:code][:loc]} | #{data[:code][:classes]} | #{data[:code][:methods]} | #{data[:test][:files]} | #{data[:test][:loc]} | 1:#{data[:code_to_test_ratio]} |"
582
+ end
583
+
584
+ f.puts "| **TOTAL** | **#{total[:code_files]}** | **#{total[:code_loc]}** | **#{total[:code_classes]}** | **#{total[:code_methods]}** | **#{total[:test_files]}** | **#{total[:test_loc]}** | **1:#{(total[:test_loc].to_f / total[:code_loc]).round(2)}** |"
585
+ f.puts ""
586
+ f.puts "## Component Details"
587
+ f.puts ""
588
+
589
+ results.each do |_, data|
590
+ f.puts "### #{data[:name]}"
591
+ f.puts ""
592
+ f.puts "- **Code:** #{data[:code][:files]} files, #{data[:code][:loc]} LOC, #{data[:code][:classes]} classes, #{data[:code][:methods]} methods"
593
+ f.puts "- **Tests:** #{data[:test][:files]} files, #{data[:test][:loc]} LOC"
594
+ f.puts "- **Path:** `#{data[:lib_path]}`"
595
+ f.puts ""
596
+
597
+ if data[:breakdown]
598
+ f.puts "| Category | Files | LOC | Classes | Methods |"
599
+ f.puts "|----------|-------|-----|---------|---------|"
600
+ data[:breakdown].each do |category, stats|
601
+ f.puts "| #{category} | #{stats[:files]} | #{stats[:loc]} | #{stats[:classes]} | #{stats[:methods]} |"
602
+ end
603
+ f.puts ""
604
+ end
605
+ end
606
+ end
607
+
608
+ puts "Reports saved to:"
609
+ puts " - #{json_file}"
610
+ puts " - #{md_file}"
611
+ end
612
+ end
613
+
614
+ # =============================================================================
615
+ # Demo tasks for running the testbed with sample data
616
+ # =============================================================================
617
+ namespace :demo do
618
+ desc "Start demo server with sample data (seeds DB and starts Rails)"
619
+ task :start do
620
+ mode = ENV.fetch("LYRA_MODE", "monitor")
621
+ port = ENV.fetch("PORT", "3000")
622
+
623
+ Dir.chdir(TESTBED_DIR) do
624
+ env = {
625
+ "RAILS_ENV" => "development",
626
+ "LYRA_MODE" => mode,
627
+ "BUNDLE_GEMFILE" => File.join(TESTBED_DIR, "Gemfile")
628
+ }
629
+
630
+ puts "=" * 60
631
+ puts "LYRA DEMO SERVER"
632
+ puts "=" * 60
633
+ puts ""
634
+ puts "Mode: #{mode}"
635
+ puts "Port: #{port}"
636
+ puts ""
637
+
638
+ # Run seed script
639
+ puts "Seeding demo data..."
640
+ seed_script = File.join(TESTBED_DIR, "db/seeds/demo.rb")
641
+ if File.exist?(seed_script)
642
+ system(env, "bundle exec rails runner #{seed_script}")
643
+ puts ""
644
+ end
645
+
646
+ puts "Starting server..."
647
+ puts ""
648
+ puts "Dashboard: http://localhost:#{port}/lyra/dashboard"
649
+ puts "Dual View: http://localhost:#{port}/lyra/dashboard/compare/Registration/1"
650
+ puts "Timeline: http://localhost:#{port}/lyra/flow/timeline"
651
+ puts ""
652
+ puts "Press Ctrl+C to stop"
653
+ puts "-" * 60
654
+
655
+ exec(env, "bundle exec rails server -p #{port}")
656
+ end
657
+ end
658
+
659
+ desc "Seed demo data only (without starting server)"
660
+ task :seed do
661
+ Dir.chdir(TESTBED_DIR) do
662
+ env = {
663
+ "RAILS_ENV" => "development",
664
+ "LYRA_MODE" => ENV.fetch("LYRA_MODE", "monitor"),
665
+ "BUNDLE_GEMFILE" => File.join(TESTBED_DIR, "Gemfile")
666
+ }
667
+
668
+ seed_script = File.join(TESTBED_DIR, "db/seeds/demo.rb")
669
+ if File.exist?(seed_script)
670
+ puts "Seeding demo data..."
671
+ system(env, "bundle exec rails runner #{seed_script}")
672
+ else
673
+ puts "Error: Seed script not found at #{seed_script}"
674
+ exit 1
675
+ end
676
+ end
677
+ end
678
+
679
+ desc "Reset demo database and reseed"
680
+ task :reset do
681
+ Dir.chdir(TESTBED_DIR) do
682
+ env = {
683
+ "RAILS_ENV" => "development",
684
+ "BUNDLE_GEMFILE" => File.join(TESTBED_DIR, "Gemfile")
685
+ }
686
+
687
+ puts "Resetting database..."
688
+ system(env, "bundle exec rails db:reset")
689
+
690
+ puts ""
691
+ Rake::Task["demo:seed"].invoke
692
+ end
693
+ end
694
+
695
+ desc "Show demo help"
696
+ task :help do
697
+ puts <<~HELP
698
+ Lyra Demo Tasks
699
+ ===============
700
+
701
+ rake demo:start Start demo server with sample data
702
+ rake demo:seed Seed demo data only
703
+ rake demo:reset Reset database and reseed
704
+ rake demo:help Show this help
705
+
706
+ Environment Variables:
707
+ LYRA_MODE Set Lyra mode (default: monitor)
708
+ Options: disabled, monitor, hijack, es_sync, es_async, es_disabled
709
+ PORT Server port (default: 3000)
710
+
711
+ Examples:
712
+ # Start in monitor mode (default)
713
+ rake demo:start
714
+
715
+ # Start in event sourcing mode
716
+ LYRA_MODE=es_sync rake demo:start
717
+
718
+ # Start on different port
719
+ PORT=4000 rake demo:start
720
+
721
+ Dashboard URLs:
722
+ http://localhost:3000/lyra/dashboard
723
+ http://localhost:3000/lyra/dashboard/model/Registration
724
+ http://localhost:3000/lyra/dashboard/compare/Registration/1
725
+ http://localhost:3000/lyra/flow/timeline
726
+ HELP
727
+ end
728
+ end