aidp 0.3.0 → 0.7.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +191 -5
  3. data/lib/aidp/analysis/kb_inspector.rb +456 -0
  4. data/lib/aidp/analysis/seams.rb +188 -0
  5. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +493 -0
  6. data/lib/aidp/analysis/tree_sitter_scan.rb +703 -0
  7. data/lib/aidp/analyze/agent_personas.rb +1 -1
  8. data/lib/aidp/analyze/agent_tool_executor.rb +5 -11
  9. data/lib/aidp/analyze/data_retention_manager.rb +0 -5
  10. data/lib/aidp/analyze/database.rb +99 -82
  11. data/lib/aidp/analyze/error_handler.rb +12 -79
  12. data/lib/aidp/analyze/export_manager.rb +0 -7
  13. data/lib/aidp/analyze/focus_guidance.rb +2 -2
  14. data/lib/aidp/analyze/incremental_analyzer.rb +1 -11
  15. data/lib/aidp/analyze/large_analysis_progress.rb +0 -5
  16. data/lib/aidp/analyze/memory_manager.rb +34 -60
  17. data/lib/aidp/analyze/metrics_storage.rb +336 -0
  18. data/lib/aidp/analyze/parallel_processor.rb +0 -6
  19. data/lib/aidp/analyze/performance_optimizer.rb +0 -3
  20. data/lib/aidp/analyze/prioritizer.rb +2 -2
  21. data/lib/aidp/analyze/repository_chunker.rb +14 -21
  22. data/lib/aidp/analyze/ruby_maat_integration.rb +6 -102
  23. data/lib/aidp/analyze/runner.rb +107 -191
  24. data/lib/aidp/analyze/steps.rb +35 -30
  25. data/lib/aidp/analyze/storage.rb +233 -178
  26. data/lib/aidp/analyze/tool_configuration.rb +21 -36
  27. data/lib/aidp/cli/jobs_command.rb +489 -0
  28. data/lib/aidp/cli/terminal_io.rb +52 -0
  29. data/lib/aidp/cli.rb +160 -45
  30. data/lib/aidp/core_ext/class_attribute.rb +36 -0
  31. data/lib/aidp/database/pg_adapter.rb +148 -0
  32. data/lib/aidp/database_config.rb +69 -0
  33. data/lib/aidp/database_connection.rb +72 -0
  34. data/lib/aidp/execute/runner.rb +65 -92
  35. data/lib/aidp/execute/steps.rb +81 -82
  36. data/lib/aidp/job_manager.rb +41 -0
  37. data/lib/aidp/jobs/base_job.rb +45 -0
  38. data/lib/aidp/jobs/provider_execution_job.rb +83 -0
  39. data/lib/aidp/provider_manager.rb +25 -0
  40. data/lib/aidp/providers/agent_supervisor.rb +348 -0
  41. data/lib/aidp/providers/anthropic.rb +160 -3
  42. data/lib/aidp/providers/base.rb +153 -6
  43. data/lib/aidp/providers/cursor.rb +245 -43
  44. data/lib/aidp/providers/gemini.rb +164 -3
  45. data/lib/aidp/providers/supervised_base.rb +317 -0
  46. data/lib/aidp/providers/supervised_cursor.rb +22 -0
  47. data/lib/aidp/version.rb +1 -1
  48. data/lib/aidp.rb +31 -34
  49. data/templates/ANALYZE/01_REPOSITORY_ANALYSIS.md +4 -4
  50. data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
  51. metadata +91 -36
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg"
4
+ require "json"
5
+
6
+ module Aidp
7
+ module Analyze
8
+ class MetricsStorage
9
+ # Database schema version
10
+ SCHEMA_VERSION = 1
11
+
12
+ def initialize(project_dir = Dir.pwd, db_config = nil)
13
+ @project_dir = project_dir
14
+ @db_config = db_config || default_db_config
15
+ @db = nil
16
+
17
+ ensure_database_exists
18
+ end
19
+
20
+ # Store step execution metrics
21
+ def store_step_metrics(step_name, provider_name, duration, success, metadata = {})
22
+ ensure_connection
23
+
24
+ timestamp = Time.now
25
+
26
+ result = @db.exec_params(
27
+ "INSERT INTO step_executions (step_name, provider_name, duration, success, metadata, created_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id",
28
+ [step_name, provider_name, duration, success, metadata.to_json, timestamp]
29
+ )
30
+
31
+ {
32
+ id: result[0]["id"],
33
+ step_name: step_name,
34
+ provider_name: provider_name,
35
+ duration: duration,
36
+ success: success,
37
+ stored_at: timestamp
38
+ }
39
+ end
40
+
41
+ # Store provider activity metrics
42
+ def store_provider_activity(provider_name, step_name, activity_summary)
43
+ ensure_connection
44
+
45
+ timestamp = Time.now
46
+
47
+ result = @db.exec_params(
48
+ "INSERT INTO provider_activities (provider_name, step_name, start_time, end_time, duration, final_state, stuck_detected, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id",
49
+ [
50
+ provider_name,
51
+ step_name,
52
+ activity_summary[:start_time],
53
+ activity_summary[:end_time],
54
+ activity_summary[:duration],
55
+ activity_summary[:final_state].to_s,
56
+ activity_summary[:stuck_detected],
57
+ timestamp
58
+ ]
59
+ )
60
+
61
+ {
62
+ id: result[0]["id"],
63
+ provider_name: provider_name,
64
+ step_name: step_name,
65
+ stored_at: timestamp
66
+ }
67
+ end
68
+
69
+ # Get step execution statistics
70
+ def get_step_statistics(step_name = nil, provider_name = nil, limit = 100)
71
+ ensure_connection
72
+
73
+ query = "SELECT * FROM step_executions WHERE 1=1"
74
+ params = []
75
+ param_index = 1
76
+
77
+ if step_name
78
+ query += " AND step_name = $#{param_index}"
79
+ params << step_name
80
+ param_index += 1
81
+ end
82
+
83
+ if provider_name
84
+ query += " AND provider_name = $#{param_index}"
85
+ params << provider_name
86
+ param_index += 1
87
+ end
88
+
89
+ query += " ORDER BY created_at DESC LIMIT $#{param_index}"
90
+ params << limit
91
+
92
+ results = @db.exec_params(query, params)
93
+ results.map { |row| parse_step_execution(row) }
94
+ end
95
+
96
+ # Get provider activity statistics
97
+ def get_provider_activity_statistics(provider_name = nil, step_name = nil, limit = 100)
98
+ ensure_connection
99
+
100
+ query = "SELECT * FROM provider_activities WHERE 1=1"
101
+ params = []
102
+ param_index = 1
103
+
104
+ if provider_name
105
+ query += " AND provider_name = $#{param_index}"
106
+ params << provider_name
107
+ param_index += 1
108
+ end
109
+
110
+ if step_name
111
+ query += " AND step_name = $#{param_index}"
112
+ params << step_name
113
+ param_index += 1
114
+ end
115
+
116
+ query += " ORDER BY created_at DESC LIMIT $#{param_index}"
117
+ params << limit
118
+
119
+ results = @db.exec_params(query, params)
120
+ results.map { |row| parse_provider_activity(row) }
121
+ end
122
+
123
+ # Calculate timeout recommendations based on p95 of execution times
124
+ def calculate_timeout_recommendations
125
+ ensure_connection
126
+
127
+ recommendations = {}
128
+
129
+ # Get all step names
130
+ step_names = @db.exec("SELECT DISTINCT step_name FROM step_executions WHERE success = true")
131
+
132
+ step_names.each do |row|
133
+ step_name = row["step_name"]
134
+
135
+ # Get successful executions for this step
136
+ durations = @db.exec_params(
137
+ "SELECT duration FROM step_executions WHERE step_name = $1 AND success = true ORDER BY duration",
138
+ [step_name]
139
+ ).map { |r| r["duration"].to_f }
140
+
141
+ next if durations.empty?
142
+
143
+ # Calculate p95
144
+ p95_index = (durations.length * 0.95).ceil - 1
145
+ p95_duration = durations[p95_index]
146
+
147
+ # Round up to nearest second and add 10% buffer
148
+ recommended_timeout = (p95_duration * 1.1).ceil
149
+
150
+ recommendations[step_name] = {
151
+ p95_duration: p95_duration,
152
+ recommended_timeout: recommended_timeout,
153
+ sample_count: durations.length,
154
+ min_duration: durations.first,
155
+ max_duration: durations.last,
156
+ avg_duration: durations.sum.to_f / durations.length
157
+ }
158
+ end
159
+
160
+ recommendations
161
+ end
162
+
163
+ # Get overall metrics summary
164
+ def get_metrics_summary
165
+ ensure_connection
166
+
167
+ summary = {}
168
+
169
+ # Total executions
170
+ total_executions = @db.exec("SELECT COUNT(*) FROM step_executions").first["count"].to_i
171
+ summary[:total_executions] = total_executions
172
+
173
+ # Successful executions
174
+ successful_executions = @db.exec("SELECT COUNT(*) FROM step_executions WHERE success = true").first["count"].to_i
175
+ summary[:successful_executions] = successful_executions
176
+
177
+ # Success rate
178
+ summary[:success_rate] = (total_executions > 0) ? (successful_executions.to_f / total_executions * 100).round(2) : 0
179
+
180
+ # Average duration
181
+ avg_duration = @db.exec("SELECT AVG(duration) FROM step_executions WHERE success = true").first["avg"]
182
+ summary[:average_duration] = avg_duration ? avg_duration.to_f.round(2) : 0
183
+
184
+ # Stuck detections
185
+ stuck_count = @db.exec("SELECT COUNT(*) FROM provider_activities WHERE stuck_detected = true").first["count"].to_i
186
+ summary[:stuck_detections] = stuck_count
187
+
188
+ # Date range
189
+ date_range = @db.exec("SELECT MIN(created_at), MAX(created_at) FROM step_executions").first
190
+ if date_range && date_range["min"]
191
+ summary[:date_range] = {
192
+ start: Time.parse(date_range["min"]),
193
+ end: Time.parse(date_range["max"])
194
+ }
195
+ end
196
+
197
+ summary
198
+ end
199
+
200
+ # Clean up old metrics data
201
+ def cleanup_old_metrics(retention_days = 30)
202
+ ensure_connection
203
+
204
+ cutoff_time = Time.now - (retention_days * 24 * 60 * 60)
205
+
206
+ # Delete old step executions
207
+ deleted_executions = @db.exec_params(
208
+ "DELETE FROM step_executions WHERE created_at < $1 RETURNING id",
209
+ [cutoff_time]
210
+ ).ntuples
211
+
212
+ # Delete old provider activities
213
+ deleted_activities = @db.exec_params(
214
+ "DELETE FROM provider_activities WHERE created_at < $1 RETURNING id",
215
+ [cutoff_time]
216
+ ).ntuples
217
+
218
+ {
219
+ deleted_executions: deleted_executions,
220
+ deleted_activities: deleted_activities,
221
+ cutoff_time: cutoff_time
222
+ }
223
+ end
224
+
225
+ # Export metrics data
226
+ def export_metrics(format = :json)
227
+ ensure_connection
228
+
229
+ case format
230
+ when :json
231
+ {
232
+ step_executions: get_step_statistics(nil, nil, 1000),
233
+ provider_activities: get_provider_activity_statistics(nil, nil, 1000),
234
+ summary: get_metrics_summary,
235
+ recommendations: calculate_timeout_recommendations,
236
+ exported_at: Time.now.iso8601
237
+ }
238
+ when :csv
239
+ # TODO: Implement CSV export
240
+ raise NotImplementedError, "CSV export not yet implemented"
241
+ else
242
+ raise ArgumentError, "Unsupported export format: #{format}"
243
+ end
244
+ end
245
+
246
+ private
247
+
248
+ def default_db_config
249
+ {
250
+ host: ENV["AIDP_DB_HOST"] || "localhost",
251
+ port: ENV["AIDP_DB_PORT"] || 5432,
252
+ dbname: ENV["AIDP_DB_NAME"] || "aidp",
253
+ user: ENV["AIDP_DB_USER"] || ENV["USER"],
254
+ password: ENV["AIDP_DB_PASSWORD"]
255
+ }
256
+ end
257
+
258
+ def ensure_connection
259
+ return if @db
260
+
261
+ @db = PG.connect(@db_config)
262
+ @db.type_map_for_results = PG::BasicTypeMapForResults.new(@db)
263
+ end
264
+
265
+ def ensure_database_exists
266
+ ensure_connection
267
+
268
+ # Create step_executions table if it doesn't exist
269
+ @db.exec(<<~SQL)
270
+ CREATE TABLE IF NOT EXISTS step_executions (
271
+ id SERIAL PRIMARY KEY,
272
+ step_name TEXT NOT NULL,
273
+ provider_name TEXT NOT NULL,
274
+ duration REAL NOT NULL,
275
+ success BOOLEAN NOT NULL,
276
+ metadata JSONB,
277
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL
278
+ )
279
+ SQL
280
+
281
+ # Create provider_activities table if it doesn't exist
282
+ @db.exec(<<~SQL)
283
+ CREATE TABLE IF NOT EXISTS provider_activities (
284
+ id SERIAL PRIMARY KEY,
285
+ provider_name TEXT NOT NULL,
286
+ step_name TEXT NOT NULL,
287
+ start_time TIMESTAMP WITH TIME ZONE,
288
+ end_time TIMESTAMP WITH TIME ZONE,
289
+ duration REAL,
290
+ final_state TEXT,
291
+ stuck_detected BOOLEAN DEFAULT FALSE,
292
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL
293
+ )
294
+ SQL
295
+
296
+ # Create indexes separately
297
+ @db.exec("CREATE INDEX IF NOT EXISTS idx_step_executions_step_name ON step_executions(step_name)")
298
+ @db.exec("CREATE INDEX IF NOT EXISTS idx_step_executions_provider_name ON step_executions(provider_name)")
299
+ @db.exec("CREATE INDEX IF NOT EXISTS idx_step_executions_created_at ON step_executions(created_at)")
300
+ @db.exec("CREATE INDEX IF NOT EXISTS idx_provider_activities_provider_name ON provider_activities(provider_name)")
301
+ @db.exec("CREATE INDEX IF NOT EXISTS idx_provider_activities_step_name ON provider_activities(step_name)")
302
+ @db.exec("CREATE INDEX IF NOT EXISTS idx_provider_activities_created_at ON provider_activities(created_at)")
303
+
304
+ # Create metrics_schema_version table if it doesn't exist
305
+ @db.exec("CREATE TABLE IF NOT EXISTS metrics_schema_version (version INTEGER NOT NULL)")
306
+ @db.exec_params("INSERT INTO metrics_schema_version (version) VALUES ($1) ON CONFLICT DO NOTHING", [SCHEMA_VERSION])
307
+ end
308
+
309
+ def parse_step_execution(row)
310
+ {
311
+ id: row["id"].to_i,
312
+ step_name: row["step_name"],
313
+ provider_name: row["provider_name"],
314
+ duration: row["duration"].to_f,
315
+ success: row["success"],
316
+ metadata: row["metadata"] ? JSON.parse(row["metadata"]) : {},
317
+ created_at: Time.parse(row["created_at"])
318
+ }
319
+ end
320
+
321
+ def parse_provider_activity(row)
322
+ {
323
+ id: row["id"].to_i,
324
+ provider_name: row["provider_name"],
325
+ step_name: row["step_name"],
326
+ start_time: row["start_time"] ? Time.parse(row["start_time"]) : nil,
327
+ end_time: row["end_time"] ? Time.parse(row["end_time"]) : nil,
328
+ duration: row["duration"].to_f,
329
+ final_state: row["final_state"]&.to_sym,
330
+ stuck_detected: row["stuck_detected"],
331
+ created_at: Time.parse(row["created_at"])
332
+ }
333
+ end
334
+ end
335
+ end
336
+ end
@@ -236,12 +236,6 @@ module Aidp
236
236
  index: future_info[:index]
237
237
  }
238
238
  @progress.increment
239
- rescue => e
240
- @errors << {
241
- chunk_id: future_info[:chunk][:id],
242
- error: e.message,
243
- index: future_info[:index]
244
- }
245
239
  end
246
240
 
247
241
  completed_futures
@@ -663,9 +663,6 @@ module Aidp
663
663
  futures = items.map do |item|
664
664
  Concurrent::Future.execute do
665
665
  processor ? processor.call(item) : item
666
- rescue => e
667
- @statistics[:errors] += 1
668
- {error: e.message, item: item}
669
666
  end
670
667
  end
671
668
 
@@ -12,9 +12,9 @@ module Aidp
12
12
  @feature_analyzer = Aidp::Analyze::FeatureAnalyzer.new(project_dir)
13
13
  end
14
14
 
15
- # Generate prioritized analysis recommendations based on Code Maat data
15
+ # Generate prioritized analysis recommendations based on ruby-maat data
16
16
  def generate_prioritized_recommendations
17
- # Get Code Maat analysis data
17
+ # Get ruby-maat analysis data
18
18
  code_maat_data = @code_maat.run_comprehensive_analysis
19
19
 
20
20
  # Get feature analysis data
@@ -264,29 +264,22 @@ module Aidp
264
264
  status: "running"
265
265
  }
266
266
 
267
- begin
268
- # Perform analysis based on chunk type
269
- case chunk[:strategy]
270
- when "time_based"
271
- results[:data] = analyze_time_chunk(chunk, analysis_type, options)
272
- when "commit_count"
273
- results[:data] = analyze_commit_chunk(chunk, analysis_type, options)
274
- when "size_based"
275
- results[:data] = analyze_size_chunk(chunk, analysis_type, options)
276
- when "feature_based"
277
- results[:data] = analyze_feature_chunk(chunk, analysis_type, options)
278
- end
279
-
280
- results[:status] = "completed"
281
- results[:end_time] = Time.now
282
- results[:duration] = results[:end_time] - results[:start_time]
283
- rescue => e
284
- results[:status] = "failed"
285
- results[:error] = e.message
286
- results[:end_time] = Time.now
287
- results[:duration] = results[:end_time] - results[:start_time]
267
+ # Perform analysis based on chunk type
268
+ case chunk[:strategy]
269
+ when "time_based"
270
+ results[:data] = analyze_time_chunk(chunk, analysis_type, options)
271
+ when "commit_count"
272
+ results[:data] = analyze_commit_chunk(chunk, analysis_type, options)
273
+ when "size_based"
274
+ results[:data] = analyze_size_chunk(chunk, analysis_type, options)
275
+ when "feature_based"
276
+ results[:data] = analyze_feature_chunk(chunk, analysis_type, options)
288
277
  end
289
278
 
279
+ results[:status] = "completed"
280
+ results[:end_time] = Time.now
281
+ results[:duration] = results[:end_time] - results[:start_time]
282
+
290
283
  results
291
284
  end
292
285
 
@@ -171,112 +171,16 @@ module Aidp
171
171
  # Write the output to the specified file
172
172
  File.write(output_file, stdout)
173
173
  else
174
- # Fallback to mock implementation if RubyMaat fails
175
- puts "Warning: RubyMaat analysis failed, using mock data. Error: #{stderr}"
176
- mock_ruby_maat_analysis(analysis_type, input_file, output_file)
174
+ # Raise proper error instead of falling back to fake data
175
+ error_msg = "RubyMaat analysis failed for #{analysis_type}: #{stderr.strip}"
176
+ error_msg += "\n\nTo install ruby-maat, run: gem install ruby-maat"
177
+ error_msg += "\nOr add it to your Gemfile: gem 'ruby-maat'"
178
+ raise error_msg
177
179
  end
178
180
 
179
181
  output_file
180
182
  end
181
183
 
182
- def mock_ruby_maat_analysis(analysis_type, input_file, output_file)
183
- # Parse the Git log to generate mock analysis data
184
- git_log_content = File.read(input_file)
185
-
186
- case analysis_type
187
- when "churn"
188
- generate_mock_churn_data(git_log_content, output_file)
189
- when "coupling"
190
- generate_mock_coupling_data(git_log_content, output_file)
191
- when "authorship"
192
- generate_mock_authorship_data(git_log_content, output_file)
193
- when "summary"
194
- generate_mock_summary_data(git_log_content, output_file)
195
- else
196
- raise "Unknown analysis type: #{analysis_type}"
197
- end
198
-
199
- output_file
200
- end
201
-
202
- def generate_mock_churn_data(git_log_content, output_file)
203
- # Extract file names from Git log and generate mock churn data
204
- files = extract_files_from_git_log(git_log_content)
205
-
206
- csv_content = "entity,n-revs,n-lines-added,n-lines-deleted\n"
207
- files.each_with_index do |file, index|
208
- changes = rand(1..20)
209
- additions = rand(0..changes * 10)
210
- deletions = rand(0..changes * 5)
211
- csv_content += "#{file},#{changes},#{additions},#{deletions}\n"
212
- end
213
-
214
- File.write(output_file, csv_content)
215
- end
216
-
217
- def generate_mock_coupling_data(git_log_content, output_file)
218
- # Generate mock coupling data between files
219
- files = extract_files_from_git_log(git_log_content)
220
-
221
- csv_content = "entity,coupled,degree,average-revs\n"
222
- files.each_slice(2) do |file1, file2|
223
- next unless file2
224
-
225
- shared_changes = rand(1..10)
226
- rand(0.1..1.0).round(2)
227
- avg_revs = rand(1..5)
228
- csv_content += "#{file1},#{file2},#{shared_changes},#{avg_revs}\n"
229
- end
230
-
231
- File.write(output_file, csv_content)
232
- end
233
-
234
- def generate_mock_authorship_data(git_log_content, output_file)
235
- # Generate mock authorship data
236
- files = extract_files_from_git_log(git_log_content)
237
- authors = %w[Alice Bob Charlie Diana Eve]
238
-
239
- csv_content = "entity,n-authors,revs\n"
240
- files.each do |file|
241
- author_count = rand(1..3)
242
- file_authors = authors.sample(author_count)
243
- revs = rand(1..15)
244
- csv_content += "#{file},\"#{file_authors.join(";")}\",#{revs}\n"
245
- end
246
-
247
- File.write(output_file, csv_content)
248
- end
249
-
250
- def generate_mock_summary_data(git_log_content, output_file)
251
- # Generate mock summary data
252
- summary_content = <<~SUMMARY
253
- Number of commits: 42
254
- Number of entities: 15
255
- Number of authors: 5
256
- First commit: 2023-01-01
257
- Last commit: 2024-01-01
258
- Total lines added: 1250
259
- Total lines deleted: 450
260
- SUMMARY
261
-
262
- File.write(output_file, summary_content)
263
- end
264
-
265
- def extract_files_from_git_log(git_log_content)
266
- # Extract file names from Git log content
267
- files = []
268
- git_log_content.lines.each do |line|
269
- # Look for lines that contain file paths (not commit info)
270
- next unless line.match?(/\d+\s+\d+\s+[^\s]+$/)
271
-
272
- parts = line.strip.split(/\s+/)
273
- files << parts[2] if parts.length >= 3 && parts[2] != "-"
274
- end
275
-
276
- # Return unique files, limited to a reasonable number
277
- files.uniq.first(20)
278
- end
279
-
280
184
  # Check if repository is large enough to require chunking
281
185
  def large_repository?(git_log_file)
282
186
  return false unless File.exist?(git_log_file)
@@ -477,7 +381,7 @@ module Aidp
477
381
  report_file = File.join(@project_dir, "code_maat_analysis_report.md")
478
382
 
479
383
  report = <<~REPORT
480
- # Code Maat Analysis Report
384
+ # Ruby-maat Analysis Report
481
385
 
482
386
  Generated on: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}
483
387
  Project: #{File.basename(@project_dir)}