aidp 0.5.0 → 0.8.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +128 -151
  3. data/bin/aidp +1 -1
  4. data/lib/aidp/analysis/kb_inspector.rb +471 -0
  5. data/lib/aidp/analysis/seams.rb +159 -0
  6. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +480 -0
  7. data/lib/aidp/analysis/tree_sitter_scan.rb +686 -0
  8. data/lib/aidp/analyze/error_handler.rb +2 -78
  9. data/lib/aidp/analyze/json_file_storage.rb +292 -0
  10. data/lib/aidp/analyze/progress.rb +12 -0
  11. data/lib/aidp/analyze/progress_visualizer.rb +12 -17
  12. data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
  13. data/lib/aidp/analyze/runner.rb +256 -87
  14. data/lib/aidp/analyze/steps.rb +6 -0
  15. data/lib/aidp/cli/jobs_command.rb +103 -435
  16. data/lib/aidp/cli.rb +317 -191
  17. data/lib/aidp/config.rb +298 -10
  18. data/lib/aidp/debug_logger.rb +195 -0
  19. data/lib/aidp/debug_mixin.rb +187 -0
  20. data/lib/aidp/execute/progress.rb +9 -0
  21. data/lib/aidp/execute/runner.rb +221 -40
  22. data/lib/aidp/execute/steps.rb +17 -7
  23. data/lib/aidp/execute/workflow_selector.rb +211 -0
  24. data/lib/aidp/harness/completion_checker.rb +268 -0
  25. data/lib/aidp/harness/condition_detector.rb +1526 -0
  26. data/lib/aidp/harness/config_loader.rb +373 -0
  27. data/lib/aidp/harness/config_manager.rb +382 -0
  28. data/lib/aidp/harness/config_schema.rb +1006 -0
  29. data/lib/aidp/harness/config_validator.rb +355 -0
  30. data/lib/aidp/harness/configuration.rb +477 -0
  31. data/lib/aidp/harness/enhanced_runner.rb +494 -0
  32. data/lib/aidp/harness/error_handler.rb +616 -0
  33. data/lib/aidp/harness/provider_config.rb +423 -0
  34. data/lib/aidp/harness/provider_factory.rb +306 -0
  35. data/lib/aidp/harness/provider_manager.rb +1269 -0
  36. data/lib/aidp/harness/provider_type_checker.rb +88 -0
  37. data/lib/aidp/harness/runner.rb +411 -0
  38. data/lib/aidp/harness/state/errors.rb +28 -0
  39. data/lib/aidp/harness/state/metrics.rb +219 -0
  40. data/lib/aidp/harness/state/persistence.rb +128 -0
  41. data/lib/aidp/harness/state/provider_state.rb +132 -0
  42. data/lib/aidp/harness/state/ui_state.rb +68 -0
  43. data/lib/aidp/harness/state/workflow_state.rb +123 -0
  44. data/lib/aidp/harness/state_manager.rb +586 -0
  45. data/lib/aidp/harness/status_display.rb +888 -0
  46. data/lib/aidp/harness/ui/base.rb +16 -0
  47. data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
  48. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
  49. data/lib/aidp/harness/ui/error_handler.rb +132 -0
  50. data/lib/aidp/harness/ui/frame_manager.rb +361 -0
  51. data/lib/aidp/harness/ui/job_monitor.rb +500 -0
  52. data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
  53. data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
  54. data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
  55. data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
  56. data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
  57. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
  58. data/lib/aidp/harness/ui/progress_display.rb +280 -0
  59. data/lib/aidp/harness/ui/question_collector.rb +141 -0
  60. data/lib/aidp/harness/ui/spinner_group.rb +184 -0
  61. data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
  62. data/lib/aidp/harness/ui/status_manager.rb +312 -0
  63. data/lib/aidp/harness/ui/status_widget.rb +280 -0
  64. data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
  65. data/lib/aidp/harness/user_interface.rb +2381 -0
  66. data/lib/aidp/provider_manager.rb +131 -7
  67. data/lib/aidp/providers/anthropic.rb +28 -109
  68. data/lib/aidp/providers/base.rb +170 -0
  69. data/lib/aidp/providers/cursor.rb +52 -183
  70. data/lib/aidp/providers/gemini.rb +24 -109
  71. data/lib/aidp/providers/macos_ui.rb +99 -5
  72. data/lib/aidp/providers/opencode.rb +194 -0
  73. data/lib/aidp/storage/csv_storage.rb +172 -0
  74. data/lib/aidp/storage/file_manager.rb +214 -0
  75. data/lib/aidp/storage/json_storage.rb +140 -0
  76. data/lib/aidp/version.rb +1 -1
  77. data/lib/aidp.rb +56 -35
  78. data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
  79. data/templates/COMMON/AGENT_BASE.md +11 -0
  80. data/templates/EXECUTE/00_PRD.md +4 -4
  81. data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
  82. data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
  83. data/templates/EXECUTE/08_TASKS.md +4 -4
  84. data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
  85. data/templates/README.md +279 -0
  86. data/templates/aidp-development.yml.example +373 -0
  87. data/templates/aidp-minimal.yml.example +48 -0
  88. data/templates/aidp-production.yml.example +475 -0
  89. data/templates/aidp.yml.example +598 -0
  90. metadata +106 -64
  91. data/lib/aidp/analyze/agent_personas.rb +0 -71
  92. data/lib/aidp/analyze/agent_tool_executor.rb +0 -445
  93. data/lib/aidp/analyze/data_retention_manager.rb +0 -426
  94. data/lib/aidp/analyze/database.rb +0 -260
  95. data/lib/aidp/analyze/dependencies.rb +0 -335
  96. data/lib/aidp/analyze/export_manager.rb +0 -425
  97. data/lib/aidp/analyze/focus_guidance.rb +0 -517
  98. data/lib/aidp/analyze/incremental_analyzer.rb +0 -543
  99. data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
  100. data/lib/aidp/analyze/large_analysis_progress.rb +0 -504
  101. data/lib/aidp/analyze/memory_manager.rb +0 -365
  102. data/lib/aidp/analyze/metrics_storage.rb +0 -336
  103. data/lib/aidp/analyze/parallel_processor.rb +0 -460
  104. data/lib/aidp/analyze/performance_optimizer.rb +0 -694
  105. data/lib/aidp/analyze/repository_chunker.rb +0 -704
  106. data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
  107. data/lib/aidp/analyze/storage.rb +0 -662
  108. data/lib/aidp/analyze/tool_configuration.rb +0 -456
  109. data/lib/aidp/analyze/tool_modernization.rb +0 -750
  110. data/lib/aidp/database/pg_adapter.rb +0 -148
  111. data/lib/aidp/database_config.rb +0 -69
  112. data/lib/aidp/database_connection.rb +0 -72
  113. data/lib/aidp/database_migration.rb +0 -158
  114. data/lib/aidp/job_manager.rb +0 -41
  115. data/lib/aidp/jobs/base_job.rb +0 -47
  116. data/lib/aidp/jobs/provider_execution_job.rb +0 -96
  117. data/lib/aidp/project_detector.rb +0 -117
  118. data/lib/aidp/providers/agent_supervisor.rb +0 -348
  119. data/lib/aidp/providers/supervised_base.rb +0 -317
  120. data/lib/aidp/providers/supervised_cursor.rb +0 -22
  121. data/lib/aidp/sync.rb +0 -13
  122. data/lib/aidp/workspace.rb +0 -19
@@ -0,0 +1,471 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "tty-box"
5
+
6
+ module Aidp
7
+ module Analysis
8
+ class KBInspector
9
+ def initialize(kb_dir = ".aidp/kb")
10
+ @kb_dir = File.expand_path(kb_dir)
11
+ @data = load_kb_data
12
+ end
13
+
14
+ def show(type, format: "summary")
15
+ case type
16
+ when "seams"
17
+ show_seams(format)
18
+ when "hotspots"
19
+ show_hotspots(format)
20
+ when "cycles"
21
+ show_cycles(format)
22
+ when "apis"
23
+ show_apis(format)
24
+ when "symbols"
25
+ show_symbols(format)
26
+ when "imports"
27
+ show_imports(format)
28
+ when "summary"
29
+ show_summary(format)
30
+ else
31
+ puts "Unknown KB type: #{type}"
32
+ puts "Available types: seams, hotspots, cycles, apis, symbols, imports, summary"
33
+ end
34
+ end
35
+
36
+ def generate_graph(type, format: "dot", output: nil)
37
+ case type
38
+ when "imports"
39
+ generate_import_graph(format, output)
40
+ when "calls"
41
+ generate_call_graph(format, output)
42
+ when "cycles"
43
+ generate_cycle_graph(format, output)
44
+ else
45
+ puts "Unknown graph type: #{type}"
46
+ puts "Available types: imports, calls, cycles"
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def truncate_text(text, max_length = 50)
53
+ return nil unless text
54
+ return text if text.length <= max_length
55
+
56
+ text[0..max_length - 4] + "..."
57
+ end
58
+
59
+ def create_table(header, rows)
60
+ # Use TTY::Box for table display
61
+ content = []
62
+ rows.each_with_index do |row, index|
63
+ row_content = []
64
+ header.each_with_index do |col_header, col_index|
65
+ row_content << "#{col_header}: #{row[col_index]}"
66
+ end
67
+ content << "Row #{index + 1}:\n#{row_content.join("\n")}"
68
+ end
69
+
70
+ box = TTY::Box.frame(
71
+ content.join("\n\n"),
72
+ title: {top_left: "Knowledge Base Data"},
73
+ border: :thick,
74
+ padding: [1, 2]
75
+ )
76
+ puts box
77
+ end
78
+
79
+ def load_kb_data
80
+ data = {}
81
+
82
+ %w[symbols imports calls metrics seams hotspots tests cycles].each do |type|
83
+ file_path = File.join(@kb_dir, "#{type}.json")
84
+ if File.exist?(file_path)
85
+ begin
86
+ data[type.to_sym] = JSON.parse(File.read(file_path), symbolize_names: true)
87
+ rescue JSON::ParserError => e
88
+ # Suppress warnings in test mode to avoid CI failures
89
+ unless ENV["RACK_ENV"] == "test" || defined?(RSpec)
90
+ puts "Warning: Could not parse #{file_path}: #{e.message}"
91
+ end
92
+ data[type.to_sym] = []
93
+ end
94
+ else
95
+ data[type.to_sym] = []
96
+ end
97
+ end
98
+
99
+ data
100
+ end
101
+
102
+ def show_summary(_format)
103
+ puts "\n📊 Knowledge Base Summary"
104
+ puts "=" * 50
105
+
106
+ puts "📁 KB Directory: #{@kb_dir}"
107
+ puts "📄 Files analyzed: #{count_files}"
108
+ puts "🏗️ Symbols: #{@data[:symbols]&.length || 0}"
109
+ puts "📦 Imports: #{@data[:imports]&.length || 0}"
110
+ puts "🔗 Calls: #{@data[:calls]&.length || 0}"
111
+ puts "📏 Metrics: #{@data[:metrics]&.length || 0}"
112
+ puts "🔧 Seams: #{@data[:seams]&.length || 0}"
113
+ puts "🔥 Hotspots: #{@data[:hotspots]&.length || 0}"
114
+ puts "🧪 Tests: #{@data[:tests]&.length || 0}"
115
+ puts "🔄 Cycles: #{@data[:cycles]&.length || 0}"
116
+
117
+ if @data[:seams]&.any?
118
+ puts "\n🔧 Seam Types:"
119
+ seam_types = @data[:seams].group_by { |s| s[:kind] }
120
+ seam_types.each do |type, seams|
121
+ puts " #{type}: #{seams.length}"
122
+ end
123
+ end
124
+
125
+ if @data[:hotspots]&.any?
126
+ puts "\n🔥 Top 5 Hotspots:"
127
+ @data[:hotspots].first(5).each_with_index do |hotspot, i|
128
+ puts " #{i + 1}. #{hotspot[:file]}:#{hotspot[:method]} (score: #{hotspot[:score]})"
129
+ end
130
+ end
131
+ end
132
+
133
+ def show_seams(format)
134
+ return puts "No seams data available" unless @data[:seams]&.any?
135
+
136
+ case format
137
+ when "json"
138
+ puts JSON.pretty_generate(@data[:seams])
139
+ when "table"
140
+ show_seams_table
141
+ else
142
+ show_seams_summary
143
+ end
144
+ end
145
+
146
+ def show_seams_table
147
+ puts "\n🔧 Seams Analysis"
148
+ puts "=" * 80
149
+
150
+ create_table(
151
+ ["Type", "File", "Line", "Symbol", "Suggestion"],
152
+ @data[:seams].map do |seam|
153
+ [
154
+ seam[:kind],
155
+ seam[:file],
156
+ seam[:line],
157
+ seam[:symbol_id]&.split(":")&.last || "N/A",
158
+ truncate_text(seam[:suggestion], 50) || "N/A"
159
+ ]
160
+ end
161
+ )
162
+ end
163
+
164
+ def show_seams_summary
165
+ puts "\n🔧 Seams Analysis"
166
+ puts "=" * 50
167
+
168
+ seam_types = @data[:seams].group_by { |s| s[:kind] }
169
+
170
+ seam_types.each do |type, seams|
171
+ puts "\n📌 #{type.upcase} (#{seams.length} found)"
172
+ puts "-" * 30
173
+
174
+ seams.first(10).each do |seam|
175
+ puts " #{seam[:file]}:#{seam[:line]}"
176
+ puts " Symbol: #{seam[:symbol_id]&.split(":")&.last}"
177
+ puts " Suggestion: #{seam[:suggestion]}"
178
+ puts
179
+ end
180
+
181
+ if seams.length > 10
182
+ puts " ... and #{seams.length - 10} more"
183
+ end
184
+ end
185
+ end
186
+
187
+ def show_hotspots(format)
188
+ return puts "No hotspots data available" unless @data[:hotspots]&.any?
189
+
190
+ case format
191
+ when "json"
192
+ puts JSON.pretty_generate(@data[:hotspots])
193
+ when "table"
194
+ show_hotspots_table
195
+ else
196
+ show_hotspots_summary
197
+ end
198
+ end
199
+
200
+ def show_hotspots_table
201
+ puts "\n🔥 Code Hotspots"
202
+ puts "=" * 80
203
+
204
+ create_table(
205
+ ["Rank", "File", "Method", "Score", "Complexity", "Touches"],
206
+ @data[:hotspots].map.with_index do |hotspot, i|
207
+ [
208
+ i + 1,
209
+ hotspot[:file],
210
+ hotspot[:method],
211
+ hotspot[:score],
212
+ hotspot[:complexity],
213
+ hotspot[:touches]
214
+ ]
215
+ end
216
+ )
217
+ end
218
+
219
+ def show_hotspots_summary
220
+ puts "\n🔥 Code Hotspots (Top 20)"
221
+ puts "=" * 50
222
+
223
+ @data[:hotspots].each_with_index do |hotspot, i|
224
+ puts "#{i + 1}. #{hotspot[:file]}:#{hotspot[:method]}"
225
+ puts " Score: #{hotspot[:score]} (Complexity: #{hotspot[:complexity]}, Touches: #{hotspot[:touches]})"
226
+ puts
227
+ end
228
+ end
229
+
230
+ def show_cycles(format)
231
+ return puts "No cycles data available" unless @data[:cycles]&.any?
232
+
233
+ case format
234
+ when "json"
235
+ puts JSON.pretty_generate(@data[:cycles])
236
+ else
237
+ show_cycles_summary
238
+ end
239
+ end
240
+
241
+ def show_cycles_summary
242
+ puts "\n🔄 Import Cycles"
243
+ puts "=" * 50
244
+
245
+ @data[:cycles].each_with_index do |cycle, i|
246
+ puts "Cycle #{i + 1}:"
247
+ cycle[:members].each do |member|
248
+ puts " - #{member}"
249
+ end
250
+ puts " Weight: #{cycle[:weight]}" if cycle[:weight]
251
+ puts
252
+ end
253
+ end
254
+
255
+ def show_apis(format)
256
+ return puts "No APIs data available" unless @data[:tests]&.any?
257
+
258
+ untested_apis = @data[:tests].select { |t| t[:tests].empty? }
259
+
260
+ case format
261
+ when "json"
262
+ puts JSON.pretty_generate(untested_apis)
263
+ else
264
+ show_apis_summary(untested_apis)
265
+ end
266
+ end
267
+
268
+ def show_apis_summary(untested_apis)
269
+ puts "\n🧪 Untested Public APIs"
270
+ puts "=" * 50
271
+
272
+ if untested_apis.empty?
273
+ puts "✅ All public APIs have associated tests!"
274
+ else
275
+ puts "Found #{untested_apis.length} untested public APIs:"
276
+ puts
277
+
278
+ untested_apis.each do |api|
279
+ symbol = @data[:symbols]&.find { |s| s[:id] == api[:symbol_id] }
280
+ if symbol
281
+ puts " #{symbol[:file]}:#{symbol[:line]} - #{symbol[:name]}"
282
+ puts " Suggestion: Create characterization tests"
283
+ puts
284
+ end
285
+ end
286
+ end
287
+ end
288
+
289
+ def show_symbols(format)
290
+ return puts "No symbols data available" unless @data[:symbols]&.any?
291
+
292
+ case format
293
+ when "json"
294
+ puts JSON.pretty_generate(@data[:symbols])
295
+ when "table"
296
+ show_symbols_table
297
+ else
298
+ show_symbols_summary
299
+ end
300
+ end
301
+
302
+ def show_symbols_table
303
+ puts "\n🏗️ Symbols"
304
+ puts "=" * 80
305
+
306
+ create_table(
307
+ ["Type", "Name", "File", "Line", "Visibility"],
308
+ @data[:symbols].map do |symbol|
309
+ [
310
+ symbol[:kind],
311
+ symbol[:name],
312
+ symbol[:file],
313
+ symbol[:line],
314
+ symbol[:visibility]
315
+ ]
316
+ end
317
+ )
318
+ end
319
+
320
+ def show_symbols_summary
321
+ puts "\n🏗️ Symbols Summary"
322
+ puts "=" * 50
323
+
324
+ symbol_types = @data[:symbols].group_by { |s| s[:kind] }
325
+
326
+ symbol_types.each do |type, symbols|
327
+ puts "#{type.capitalize}: #{symbols.length}"
328
+ end
329
+ end
330
+
331
+ def show_imports(format)
332
+ return puts "No imports data available" unless @data[:imports]&.any?
333
+
334
+ case format
335
+ when "json"
336
+ puts JSON.pretty_generate(@data[:imports])
337
+ when "table"
338
+ show_imports_table
339
+ else
340
+ show_imports_summary
341
+ end
342
+ end
343
+
344
+ def show_imports_table
345
+ puts "\n📦 Imports"
346
+ puts "=" * 80
347
+
348
+ create_table(
349
+ ["Type", "Target", "File", "Line"],
350
+ @data[:imports].map do |import|
351
+ [
352
+ import[:kind],
353
+ import[:target],
354
+ import[:file],
355
+ import[:line]
356
+ ]
357
+ end
358
+ )
359
+ end
360
+
361
+ def show_imports_summary
362
+ puts "\n📦 Imports Summary"
363
+ puts "=" * 50
364
+
365
+ import_types = @data[:imports].group_by { |i| i[:kind] }
366
+
367
+ import_types.each do |type, imports|
368
+ puts "#{type.capitalize}: #{imports.length}"
369
+ end
370
+ end
371
+
372
+ def generate_import_graph(format, output)
373
+ puts "Generating import graph in #{format} format..."
374
+
375
+ case format
376
+ when "dot"
377
+ generate_dot_graph(output)
378
+ when "mermaid"
379
+ generate_mermaid_graph(output)
380
+ when "json"
381
+ generate_json_graph(output)
382
+ else
383
+ puts "Unsupported graph format: #{format}"
384
+ end
385
+ end
386
+
387
+ def generate_dot_graph(output)
388
+ content = ["digraph ImportGraph {"]
389
+ content << " rankdir=LR;"
390
+ content << " node [shape=box];"
391
+
392
+ @data[:imports]&.each do |import|
393
+ from = import[:file].gsub(/[^a-zA-Z0-9]/, "_")
394
+ to = import[:target].gsub(/[^a-zA-Z0-9]/, "_")
395
+ content << " \"#{from}\" -> \"#{to}\" [label=\"#{import[:kind]}\"];"
396
+ end
397
+
398
+ content << "}"
399
+
400
+ if output
401
+ File.write(output, content.join("\n"))
402
+ puts "Graph written to #{output}"
403
+ else
404
+ puts content.join("\n")
405
+ end
406
+ end
407
+
408
+ def generate_mermaid_graph(output)
409
+ content = ["graph LR"]
410
+
411
+ @data[:imports]&.each do |import|
412
+ from = import[:file].gsub(/[^a-zA-Z0-9]/, "_")
413
+ to = import[:target].gsub(/[^a-zA-Z0-9]/, "_")
414
+ content << " #{from} --> #{to}"
415
+ end
416
+
417
+ if output
418
+ File.write(output, content.join("\n"))
419
+ puts "Graph written to #{output}"
420
+ else
421
+ puts content.join("\n")
422
+ end
423
+ end
424
+
425
+ def generate_json_graph(output)
426
+ graph_data = {
427
+ nodes: [],
428
+ edges: []
429
+ }
430
+
431
+ # Add nodes
432
+ files = (@data[:imports]&.map { |i| i[:file] } || []).uniq
433
+ targets = (@data[:imports]&.map { |i| i[:target] } || []).uniq
434
+
435
+ (files + targets).uniq.each do |node|
436
+ graph_data[:nodes] << {id: node, label: node}
437
+ end
438
+
439
+ # Add edges
440
+ @data[:imports]&.each do |import|
441
+ graph_data[:edges] << {
442
+ from: import[:file],
443
+ to: import[:target],
444
+ label: import[:kind]
445
+ }
446
+ end
447
+
448
+ if output
449
+ File.write(output, JSON.pretty_generate(graph_data))
450
+ puts "Graph written to #{output}"
451
+ else
452
+ puts JSON.pretty_generate(graph_data)
453
+ end
454
+ end
455
+
456
+ def generate_call_graph(_format, _output)
457
+ # Similar to import graph but for method calls
458
+ puts "Call graph generation not yet implemented"
459
+ end
460
+
461
+ def generate_cycle_graph(_format, _output)
462
+ # Generate graph showing only the cycles
463
+ puts "Cycle graph generation not yet implemented"
464
+ end
465
+
466
+ def count_files
467
+ @data[:symbols]&.map { |s| s[:file] }&.uniq&.length || 0
468
+ end
469
+ end
470
+ end
471
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Analysis
5
+ module Seams
6
+ # I/O and OS integration patterns
7
+ IO_PATTERNS = [
8
+ /^File\./, /^IO\./, /^Kernel\.system$/, /^Open3\./, /^Net::HTTP/,
9
+ /Socket|TCPSocket|UDPSocket/, /^Dir\./, /^ENV/, /^ARGV/,
10
+ /^STDIN|^STDOUT|^STDERR/, /^Process\./, /^Thread\./, /^Timeout\./
11
+ ].freeze
12
+
13
+ # Database and external service patterns
14
+ EXTERNAL_SERVICE_PATTERNS = [
15
+ /ActiveRecord|Sequel|DataMapper/, /Redis|Memcached/, /Elasticsearch/,
16
+ /AWS::|Google::|Azure::/, /HTTParty|Faraday|Net::HTTP/,
17
+ /Sidekiq|Resque|DelayedJob/, /ActionMailer|Mail/
18
+ ].freeze
19
+
20
+ # Global and singleton patterns
21
+ GLOBAL_PATTERNS = [
22
+ /^\$[a-zA-Z_]/, /^@@[a-zA-Z_]/, /^::[A-Z]/, /^Kernel\./,
23
+ /include Singleton/, /extend Singleton/, /@singleton/
24
+ ].freeze
25
+
26
+ def self.detect_seams_in_ast(ast_nodes, file_path)
27
+ seams = []
28
+
29
+ ast_nodes.each do |node|
30
+ case node[:type]
31
+ when "method"
32
+ seams.concat(detect_method_seams(node, file_path))
33
+ when "class", "module"
34
+ seams.concat(detect_class_module_seams(node, file_path))
35
+ end
36
+ end
37
+
38
+ seams
39
+ end
40
+
41
+ def self.detect_method_seams(method_node, file_path)
42
+ seams = []
43
+
44
+ # Check for I/O calls in method body
45
+ if (io_calls = extract_io_calls(method_node))
46
+ io_calls.each do |call|
47
+ seams << {
48
+ kind: "io_integration",
49
+ file: file_path,
50
+ line: call[:line],
51
+ symbol_id: "#{file_path}:#{method_node[:line]}:#{method_node[:name]}",
52
+ detail: {
53
+ call: call[:call],
54
+ receiver: call[:receiver],
55
+ method: call[:method]
56
+ },
57
+ suggestion: "Consider extracting I/O operations to a separate service class"
58
+ }
59
+ end
60
+ end
61
+
62
+ # Check for external service calls
63
+ if (service_calls = extract_external_service_calls(method_node))
64
+ service_calls.each do |call|
65
+ seams << {
66
+ kind: "external_service",
67
+ file: file_path,
68
+ line: call[:line],
69
+ symbol_id: "#{file_path}:#{method_node[:line]}:#{method_node[:name]}",
70
+ detail: {
71
+ call: call[:call],
72
+ service: call[:service]
73
+ },
74
+ suggestion: "Consider using dependency injection for external service calls"
75
+ }
76
+ end
77
+ end
78
+
79
+ # Check for global/singleton usage
80
+ if (global_usage = extract_global_usage(method_node))
81
+ global_usage.each do |usage|
82
+ seams << {
83
+ kind: "global_singleton",
84
+ file: file_path,
85
+ line: usage[:line],
86
+ symbol_id: "#{file_path}:#{method_node[:line]}:#{method_node[:name]}",
87
+ detail: {
88
+ usage: usage[:usage],
89
+ type: usage[:type]
90
+ },
91
+ suggestion: "Consider passing global state as parameters or using dependency injection"
92
+ }
93
+ end
94
+ end
95
+
96
+ seams
97
+ end
98
+
99
+ def self.detect_class_module_seams(class_node, file_path)
100
+ seams = []
101
+
102
+ # Check for singleton pattern
103
+ if class_node[:content]&.include?("include Singleton")
104
+ seams << {
105
+ kind: "singleton_pattern",
106
+ file: file_path,
107
+ line: class_node[:line],
108
+ symbol_id: "#{file_path}:#{class_node[:line]}:#{class_node[:name]}",
109
+ detail: {
110
+ pattern: "Singleton",
111
+ class_name: class_node[:name]
112
+ },
113
+ suggestion: "Consider using dependency injection instead of singleton pattern"
114
+ }
115
+ end
116
+
117
+ # Check for global state in class/module
118
+ if (global_state = extract_global_state(class_node))
119
+ global_state.each do |state|
120
+ seams << {
121
+ kind: "global_state",
122
+ file: file_path,
123
+ line: state[:line],
124
+ symbol_id: "#{file_path}:#{class_node[:line]}:#{class_node[:name]}",
125
+ detail: {
126
+ state: state[:state],
127
+ type: state[:type]
128
+ },
129
+ suggestion: "Consider encapsulating global state or using configuration objects"
130
+ }
131
+ end
132
+ end
133
+
134
+ seams
135
+ end
136
+
137
+ private_class_method def self.extract_io_calls(_method_node)
138
+ # Extract I/O calls from the method's AST
139
+ # TODO: Implement actual AST analysis
140
+ []
141
+ end
142
+
143
+ private_class_method def self.extract_external_service_calls(_method_node)
144
+ # This would extract external service calls from the method's AST
145
+ []
146
+ end
147
+
148
+ private_class_method def self.extract_global_usage(_method_node)
149
+ # This would extract global variable usage from the method's AST
150
+ []
151
+ end
152
+
153
+ private_class_method def self.extract_global_state(_class_node)
154
+ # This would extract global state from the class/module AST
155
+ []
156
+ end
157
+ end
158
+ end
159
+ end