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.
- checksums.yaml +4 -4
- data/README.md +128 -151
- data/bin/aidp +1 -1
- data/lib/aidp/analysis/kb_inspector.rb +471 -0
- data/lib/aidp/analysis/seams.rb +159 -0
- data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +480 -0
- data/lib/aidp/analysis/tree_sitter_scan.rb +686 -0
- data/lib/aidp/analyze/error_handler.rb +2 -78
- data/lib/aidp/analyze/json_file_storage.rb +292 -0
- data/lib/aidp/analyze/progress.rb +12 -0
- data/lib/aidp/analyze/progress_visualizer.rb +12 -17
- data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
- data/lib/aidp/analyze/runner.rb +256 -87
- data/lib/aidp/analyze/steps.rb +6 -0
- data/lib/aidp/cli/jobs_command.rb +103 -435
- data/lib/aidp/cli.rb +317 -191
- data/lib/aidp/config.rb +298 -10
- data/lib/aidp/debug_logger.rb +195 -0
- data/lib/aidp/debug_mixin.rb +187 -0
- data/lib/aidp/execute/progress.rb +9 -0
- data/lib/aidp/execute/runner.rb +221 -40
- data/lib/aidp/execute/steps.rb +17 -7
- data/lib/aidp/execute/workflow_selector.rb +211 -0
- data/lib/aidp/harness/completion_checker.rb +268 -0
- data/lib/aidp/harness/condition_detector.rb +1526 -0
- data/lib/aidp/harness/config_loader.rb +373 -0
- data/lib/aidp/harness/config_manager.rb +382 -0
- data/lib/aidp/harness/config_schema.rb +1006 -0
- data/lib/aidp/harness/config_validator.rb +355 -0
- data/lib/aidp/harness/configuration.rb +477 -0
- data/lib/aidp/harness/enhanced_runner.rb +494 -0
- data/lib/aidp/harness/error_handler.rb +616 -0
- data/lib/aidp/harness/provider_config.rb +423 -0
- data/lib/aidp/harness/provider_factory.rb +306 -0
- data/lib/aidp/harness/provider_manager.rb +1269 -0
- data/lib/aidp/harness/provider_type_checker.rb +88 -0
- data/lib/aidp/harness/runner.rb +411 -0
- data/lib/aidp/harness/state/errors.rb +28 -0
- data/lib/aidp/harness/state/metrics.rb +219 -0
- data/lib/aidp/harness/state/persistence.rb +128 -0
- data/lib/aidp/harness/state/provider_state.rb +132 -0
- data/lib/aidp/harness/state/ui_state.rb +68 -0
- data/lib/aidp/harness/state/workflow_state.rb +123 -0
- data/lib/aidp/harness/state_manager.rb +586 -0
- data/lib/aidp/harness/status_display.rb +888 -0
- data/lib/aidp/harness/ui/base.rb +16 -0
- data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
- data/lib/aidp/harness/ui/error_handler.rb +132 -0
- data/lib/aidp/harness/ui/frame_manager.rb +361 -0
- data/lib/aidp/harness/ui/job_monitor.rb +500 -0
- data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
- data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
- data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
- data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
- data/lib/aidp/harness/ui/progress_display.rb +280 -0
- data/lib/aidp/harness/ui/question_collector.rb +141 -0
- data/lib/aidp/harness/ui/spinner_group.rb +184 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
- data/lib/aidp/harness/ui/status_manager.rb +312 -0
- data/lib/aidp/harness/ui/status_widget.rb +280 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
- data/lib/aidp/harness/user_interface.rb +2381 -0
- data/lib/aidp/provider_manager.rb +131 -7
- data/lib/aidp/providers/anthropic.rb +28 -109
- data/lib/aidp/providers/base.rb +170 -0
- data/lib/aidp/providers/cursor.rb +52 -183
- data/lib/aidp/providers/gemini.rb +24 -109
- data/lib/aidp/providers/macos_ui.rb +99 -5
- data/lib/aidp/providers/opencode.rb +194 -0
- data/lib/aidp/storage/csv_storage.rb +172 -0
- data/lib/aidp/storage/file_manager.rb +214 -0
- data/lib/aidp/storage/json_storage.rb +140 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +56 -35
- data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
- data/templates/COMMON/AGENT_BASE.md +11 -0
- data/templates/EXECUTE/00_PRD.md +4 -4
- data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
- data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
- data/templates/EXECUTE/08_TASKS.md +4 -4
- data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
- data/templates/README.md +279 -0
- data/templates/aidp-development.yml.example +373 -0
- data/templates/aidp-minimal.yml.example +48 -0
- data/templates/aidp-production.yml.example +475 -0
- data/templates/aidp.yml.example +598 -0
- metadata +106 -64
- data/lib/aidp/analyze/agent_personas.rb +0 -71
- data/lib/aidp/analyze/agent_tool_executor.rb +0 -445
- data/lib/aidp/analyze/data_retention_manager.rb +0 -426
- data/lib/aidp/analyze/database.rb +0 -260
- data/lib/aidp/analyze/dependencies.rb +0 -335
- data/lib/aidp/analyze/export_manager.rb +0 -425
- data/lib/aidp/analyze/focus_guidance.rb +0 -517
- data/lib/aidp/analyze/incremental_analyzer.rb +0 -543
- data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
- data/lib/aidp/analyze/large_analysis_progress.rb +0 -504
- data/lib/aidp/analyze/memory_manager.rb +0 -365
- data/lib/aidp/analyze/metrics_storage.rb +0 -336
- data/lib/aidp/analyze/parallel_processor.rb +0 -460
- data/lib/aidp/analyze/performance_optimizer.rb +0 -694
- data/lib/aidp/analyze/repository_chunker.rb +0 -704
- data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
- data/lib/aidp/analyze/storage.rb +0 -662
- data/lib/aidp/analyze/tool_configuration.rb +0 -456
- data/lib/aidp/analyze/tool_modernization.rb +0 -750
- data/lib/aidp/database/pg_adapter.rb +0 -148
- data/lib/aidp/database_config.rb +0 -69
- data/lib/aidp/database_connection.rb +0 -72
- data/lib/aidp/database_migration.rb +0 -158
- data/lib/aidp/job_manager.rb +0 -41
- data/lib/aidp/jobs/base_job.rb +0 -47
- data/lib/aidp/jobs/provider_execution_job.rb +0 -96
- data/lib/aidp/project_detector.rb +0 -117
- data/lib/aidp/providers/agent_supervisor.rb +0 -348
- data/lib/aidp/providers/supervised_base.rb +0 -317
- data/lib/aidp/providers/supervised_cursor.rb +0 -22
- data/lib/aidp/sync.rb +0 -13
- 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
|