aidp 0.3.0 → 0.5.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +59 -4
  3. data/lib/aidp/analyze/agent_personas.rb +1 -1
  4. data/lib/aidp/analyze/database.rb +99 -82
  5. data/lib/aidp/analyze/error_handler.rb +12 -76
  6. data/lib/aidp/analyze/focus_guidance.rb +2 -2
  7. data/lib/aidp/analyze/metrics_storage.rb +336 -0
  8. data/lib/aidp/analyze/prioritizer.rb +2 -2
  9. data/lib/aidp/analyze/ruby_maat_integration.rb +6 -102
  10. data/lib/aidp/analyze/runner.rb +107 -191
  11. data/lib/aidp/analyze/steps.rb +29 -30
  12. data/lib/aidp/analyze/storage.rb +233 -171
  13. data/lib/aidp/cli/jobs_command.rb +489 -0
  14. data/lib/aidp/cli/terminal_io.rb +52 -0
  15. data/lib/aidp/cli.rb +104 -45
  16. data/lib/aidp/core_ext/class_attribute.rb +36 -0
  17. data/lib/aidp/database/pg_adapter.rb +148 -0
  18. data/lib/aidp/database_config.rb +69 -0
  19. data/lib/aidp/database_connection.rb +72 -0
  20. data/lib/aidp/database_migration.rb +158 -0
  21. data/lib/aidp/execute/runner.rb +65 -92
  22. data/lib/aidp/execute/steps.rb +81 -82
  23. data/lib/aidp/job_manager.rb +41 -0
  24. data/lib/aidp/jobs/base_job.rb +47 -0
  25. data/lib/aidp/jobs/provider_execution_job.rb +96 -0
  26. data/lib/aidp/provider_manager.rb +25 -0
  27. data/lib/aidp/providers/agent_supervisor.rb +348 -0
  28. data/lib/aidp/providers/anthropic.rb +166 -3
  29. data/lib/aidp/providers/base.rb +153 -6
  30. data/lib/aidp/providers/cursor.rb +247 -43
  31. data/lib/aidp/providers/gemini.rb +166 -3
  32. data/lib/aidp/providers/supervised_base.rb +317 -0
  33. data/lib/aidp/providers/supervised_cursor.rb +22 -0
  34. data/lib/aidp/version.rb +1 -1
  35. data/lib/aidp.rb +25 -34
  36. data/templates/ANALYZE/01_REPOSITORY_ANALYSIS.md +4 -4
  37. metadata +72 -35
data/lib/aidp/cli.rb CHANGED
@@ -8,7 +8,26 @@ module Aidp
8
8
  desc "execute [STEP]", "Run execute mode step(s)"
9
9
  option :force, type: :boolean, desc: "Force execution even if dependencies are not met"
10
10
  option :rerun, type: :boolean, desc: "Re-run a completed step"
11
+ option :approve, type: :string, desc: "Approve a completed execute gate step"
12
+ option :reset, type: :boolean, desc: "Reset execute mode progress"
11
13
  def execute(project_dir = Dir.pwd, step_name = nil, custom_options = {})
14
+ # Handle reset flag
15
+ if options[:reset] || options["reset"]
16
+ progress = Aidp::Execute::Progress.new(project_dir)
17
+ progress.reset
18
+ puts "🔄 Reset execute mode progress"
19
+ return {status: "success", message: "Progress reset"}
20
+ end
21
+
22
+ # Handle approve flag
23
+ if options[:approve] || options["approve"]
24
+ step_name = options[:approve] || options["approve"]
25
+ progress = Aidp::Execute::Progress.new(project_dir)
26
+ progress.mark_step_completed(step_name)
27
+ puts "✅ Approved execute step: #{step_name}"
28
+ return {status: "success", step: step_name}
29
+ end
30
+
12
31
  if step_name
13
32
  runner = Aidp::Execute::Runner.new(project_dir)
14
33
  # Merge Thor options with custom options
@@ -34,7 +53,71 @@ module Aidp
34
53
  DESC
35
54
  option :force, type: :boolean, desc: "Force execution even if dependencies are not met"
36
55
  option :rerun, type: :boolean, desc: "Re-run a completed step"
37
- def analyze(project_dir = Dir.pwd, step_name = nil, custom_options = {})
56
+ option :background, type: :boolean, desc: "Run analysis in background jobs (requires database setup)"
57
+ option :approve, type: :string, desc: "Approve a completed analyze gate step"
58
+ option :reset, type: :boolean, desc: "Reset analyze mode progress"
59
+ def analyze(*args)
60
+ # Handle reset flag
61
+ if options[:reset] || options["reset"]
62
+ project_dir = Dir.pwd
63
+ progress = Aidp::Analyze::Progress.new(project_dir)
64
+ progress.reset
65
+ puts "🔄 Reset analyze mode progress"
66
+ return {status: "success", message: "Progress reset"}
67
+ end
68
+
69
+ # Handle approve flag
70
+ if options[:approve] || options["approve"]
71
+ project_dir = Dir.pwd
72
+ step_name = options[:approve] || options["approve"]
73
+ progress = Aidp::Analyze::Progress.new(project_dir)
74
+ progress.mark_step_completed(step_name)
75
+ puts "✅ Approved analyze step: #{step_name}"
76
+ return {status: "success", step: step_name}
77
+ end
78
+
79
+ # Handle both old and new calling patterns for backwards compatibility
80
+ case args.length
81
+ when 0
82
+ # analyze() - list steps
83
+ project_dir = Dir.pwd
84
+ step_name = nil
85
+ custom_options = {}
86
+ when 1
87
+ # analyze(step_name) - new CLI pattern
88
+ if args[0].is_a?(String) && Dir.exist?(args[0])
89
+ # analyze(project_dir) - old test pattern
90
+ project_dir = args[0]
91
+ step_name = nil
92
+ else
93
+ # analyze(step_name) - new CLI pattern
94
+ project_dir = Dir.pwd
95
+ step_name = args[0]
96
+ end
97
+ custom_options = {}
98
+ when 2
99
+ # analyze(project_dir, step_name) - old test pattern
100
+ # or analyze(step_name, options) - new CLI pattern
101
+ if Dir.exist?(args[0])
102
+ # analyze(project_dir, step_name)
103
+ project_dir = args[0]
104
+ step_name = args[1]
105
+ custom_options = {}
106
+ else
107
+ # analyze(step_name, options)
108
+ project_dir = Dir.pwd
109
+ step_name = args[0]
110
+ custom_options = args[1] || {}
111
+ end
112
+ when 3
113
+ # analyze(project_dir, step_name, options) - old test pattern
114
+ project_dir = args[0]
115
+ step_name = args[1]
116
+ custom_options = args[2] || {}
117
+ else
118
+ raise ArgumentError, "Wrong number of arguments (given #{args.length}, expected 0..3)"
119
+ end
120
+
38
121
  progress = Aidp::Analyze::Progress.new(project_dir)
39
122
 
40
123
  if step_name
@@ -45,7 +128,19 @@ module Aidp
45
128
  runner = Aidp::Analyze::Runner.new(project_dir)
46
129
  # Merge Thor options with custom options
47
130
  all_options = options.merge(custom_options)
48
- runner.run_step(resolved_step, all_options)
131
+ result = runner.run_step(resolved_step, all_options)
132
+
133
+ # Display the result
134
+ if result[:status] == "completed"
135
+ puts "✅ Step '#{resolved_step}' completed successfully"
136
+ puts " Provider: #{result[:provider]}"
137
+ puts " Message: #{result[:message]}" if result[:message]
138
+ elsif result[:status] == "error"
139
+ puts "❌ Step '#{resolved_step}' failed"
140
+ puts " Error: #{result[:error]}" if result[:error]
141
+ end
142
+
143
+ result
49
144
  else
50
145
  puts "❌ Step '#{step_name}' not found or not available"
51
146
  puts "\nAvailable steps:"
@@ -72,49 +167,6 @@ module Aidp
72
167
  end
73
168
  end
74
169
 
75
- desc "analyze-approve STEP", "Approve a completed analyze gate step"
76
- def analyze_approve(project_dir = Dir.pwd, step_name = nil)
77
- progress = Aidp::Analyze::Progress.new(project_dir)
78
- progress.mark_step_completed(step_name)
79
- puts "✅ Approved analyze step: #{step_name}"
80
- {status: "success", step: step_name}
81
- end
82
-
83
- desc "analyze-reset", "Reset analyze mode progress"
84
- def analyze_reset(project_dir = Dir.pwd)
85
- progress = Aidp::Analyze::Progress.new(project_dir)
86
- progress.reset
87
- puts "🔄 Reset analyze mode progress"
88
- {status: "success", message: "Progress reset"}
89
- end
90
-
91
- desc "execute-approve STEP", "Approve a completed execute gate step"
92
- def execute_approve(project_dir = Dir.pwd, step_name = nil)
93
- progress = Aidp::Execute::Progress.new(project_dir)
94
- progress.mark_step_completed(step_name)
95
- puts "✅ Approved execute step: #{step_name}"
96
- {status: "success", step: step_name}
97
- end
98
-
99
- desc "execute-reset", "Reset execute mode progress"
100
- def execute_reset(project_dir = Dir.pwd)
101
- progress = Aidp::Execute::Progress.new(project_dir)
102
- progress.reset
103
- puts "🔄 Reset execute mode progress"
104
- {status: "success", message: "Progress reset"}
105
- end
106
-
107
- # Backward compatibility aliases
108
- desc "approve STEP", "Approve a completed execute gate step (alias for execute-approve)"
109
- def approve(project_dir = Dir.pwd, step_name = nil)
110
- execute_approve(project_dir, step_name)
111
- end
112
-
113
- desc "reset", "Reset execute mode progress (alias for execute-reset)"
114
- def reset(project_dir = Dir.pwd)
115
- execute_reset(project_dir)
116
- end
117
-
118
170
  desc "status", "Show current progress for both modes"
119
171
  def status
120
172
  puts "\n📊 AI Dev Pipeline Status"
@@ -137,6 +189,13 @@ module Aidp
137
189
  end
138
190
  end
139
191
 
192
+ desc "jobs", "Show and manage background jobs"
193
+ def jobs
194
+ require_relative "cli/jobs_command"
195
+ command = Aidp::CLI::JobsCommand.new
196
+ command.run
197
+ end
198
+
140
199
  desc "version", "Show version information"
141
200
  def version
142
201
  puts "Aidp version #{Aidp::VERSION}"
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module CoreExt
5
+ module ClassAttribute
6
+ def class_attribute(*attrs)
7
+ attrs.each do |name|
8
+ # Define class instance variable
9
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
10
+ @#{name} = nil
11
+
12
+ def self.#{name}
13
+ return @#{name} if defined?(@#{name})
14
+ return superclass.#{name} if superclass.respond_to?(:#{name})
15
+ nil
16
+ end
17
+
18
+ def self.#{name}=(val)
19
+ @#{name} = val
20
+ end
21
+
22
+ def #{name}
23
+ self.class.#{name}
24
+ end
25
+
26
+ def #{name}=(val)
27
+ raise "#{name} is a class attribute, cannot be set on instance"
28
+ end
29
+ RUBY
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ Class.include Aidp::CoreExt::ClassAttribute
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Database
5
+ class PgAdapter
6
+ def initialize(connection)
7
+ @connection = connection
8
+ end
9
+
10
+ def execute(sql, params = [])
11
+ result = @connection.exec_params(sql, params)
12
+ result.to_a.map { |row| row.transform_keys(&:to_sym) }
13
+ end
14
+
15
+ def in_transaction?
16
+ @connection.transaction_status != PG::PQTRANS_IDLE
17
+ end
18
+
19
+ def checkout
20
+ yield self
21
+ end
22
+
23
+ def after_commit
24
+ yield
25
+ end
26
+
27
+ def server_version
28
+ @connection.server_version
29
+ end
30
+
31
+ def transaction_status
32
+ @connection.transaction_status
33
+ end
34
+
35
+ def transaction
36
+ @connection.transaction do
37
+ yield
38
+ end
39
+ end
40
+
41
+ def quote_table_name(name)
42
+ "\"#{name}\""
43
+ end
44
+
45
+ def quote_identifier(name)
46
+ "\"#{name}\""
47
+ end
48
+
49
+ def quote_string(string)
50
+ "'#{string.gsub("'", "''")}'"
51
+ end
52
+
53
+ def quote_date(date)
54
+ date.strftime("%Y-%m-%d")
55
+ end
56
+
57
+ def quote_time(time)
58
+ time.strftime("%Y-%m-%d %H:%M:%S.%6N %z")
59
+ end
60
+
61
+ # Additional methods required by Que
62
+ def async_connection
63
+ self
64
+ end
65
+
66
+ def wait_for_notify(timeout = nil)
67
+ @connection.wait_for_notify(timeout)
68
+ end
69
+
70
+ def listen(channel)
71
+ @connection.exec("LISTEN #{quote_identifier(channel)}")
72
+ end
73
+
74
+ def unlisten(channel)
75
+ @connection.exec("UNLISTEN #{quote_identifier(channel)}")
76
+ end
77
+
78
+ def unlisten_all
79
+ @connection.exec("UNLISTEN *")
80
+ end
81
+
82
+ def notifications
83
+ @connection.notifications
84
+ end
85
+
86
+ def reset
87
+ @connection.reset
88
+ end
89
+
90
+ def type_map_for_queries
91
+ @connection.type_map_for_queries
92
+ end
93
+
94
+ def type_map_for_results
95
+ @connection.type_map_for_results
96
+ end
97
+
98
+ # Additional methods for Que compatibility
99
+ def adapter_name
100
+ "pg"
101
+ end
102
+
103
+ def active?
104
+ @connection.status == PG::CONNECTION_OK
105
+ end
106
+
107
+ def disconnect!
108
+ @connection.close
109
+ end
110
+
111
+ def reconnect!
112
+ @connection.reset
113
+ end
114
+
115
+ def raw_connection
116
+ @connection
117
+ end
118
+
119
+ def schema_search_path
120
+ execute("SHOW search_path")[0][:search_path]
121
+ end
122
+
123
+ def schema_search_path=(path)
124
+ execute("SET search_path TO #{path}")
125
+ end
126
+
127
+ def table_exists?(name)
128
+ execute(<<~SQL, [name]).any?
129
+ SELECT 1
130
+ FROM pg_tables
131
+ WHERE tablename = $1
132
+ SQL
133
+ end
134
+
135
+ def advisory_lock(id)
136
+ execute("SELECT pg_advisory_lock($1)", [id])
137
+ end
138
+
139
+ def advisory_unlock(id)
140
+ execute("SELECT pg_advisory_unlock($1)", [id])
141
+ end
142
+
143
+ def advisory_unlock_all
144
+ execute("SELECT pg_advisory_unlock_all()")
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+
6
+ module Aidp
7
+ class DatabaseConfig
8
+ DEFAULT_CONFIG = {
9
+ "database" => {
10
+ "adapter" => "postgresql",
11
+ "host" => "localhost",
12
+ "port" => 5432,
13
+ "database" => "aidp",
14
+ "username" => ENV["USER"],
15
+ "password" => nil,
16
+ "pool" => 5,
17
+ "timeout" => 5000
18
+ }
19
+ }.freeze
20
+
21
+ def self.load(project_dir = Dir.pwd)
22
+ new(project_dir).load
23
+ end
24
+
25
+ def initialize(project_dir)
26
+ @project_dir = project_dir
27
+ @config_file = File.join(project_dir, ".aidp-config.yml")
28
+ end
29
+
30
+ def load
31
+ ensure_config_exists
32
+ config = YAML.load_file(@config_file)
33
+ validate_config(config)
34
+ config["database"]
35
+ end
36
+
37
+ private
38
+
39
+ def ensure_config_exists
40
+ return if File.exist?(@config_file)
41
+
42
+ # Create config directory if it doesn't exist
43
+ FileUtils.mkdir_p(File.dirname(@config_file))
44
+
45
+ # Write default config
46
+ File.write(@config_file, YAML.dump(DEFAULT_CONFIG))
47
+
48
+ puts "Created default database configuration at #{@config_file}"
49
+ puts "Please update the configuration with your database settings"
50
+ end
51
+
52
+ def validate_config(config)
53
+ unless config.is_a?(Hash) && config["database"].is_a?(Hash)
54
+ raise "Invalid configuration format in #{@config_file}"
55
+ end
56
+
57
+ required_keys = %w[adapter host port database username]
58
+ missing_keys = required_keys - config["database"].keys
59
+
60
+ unless missing_keys.empty?
61
+ raise "Missing required configuration keys: #{missing_keys.join(", ")}"
62
+ end
63
+
64
+ unless config["database"]["adapter"] == "postgresql"
65
+ raise "Only PostgreSQL is supported as a database adapter"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg"
4
+ require "que"
5
+ require "sequel"
6
+
7
+ module Aidp
8
+ class DatabaseConnection
9
+ class << self
10
+ def initialize_mutex
11
+ @mutex ||= Mutex.new
12
+ end
13
+
14
+ def establish_connection
15
+ initialize_mutex
16
+ @mutex.synchronize do
17
+ # Return existing connection if already established
18
+ return @connection if @connection && !@connection.finished?
19
+ @connection = PG.connect(connection_params)
20
+ @sequel_db = Sequel.connect(
21
+ adapter: "postgres",
22
+ host: ENV["AIDP_DB_HOST"] || "localhost",
23
+ port: (ENV["AIDP_DB_PORT"] || 5432).to_i,
24
+ database: ENV["AIDP_DB_NAME"] || "aidp",
25
+ user: ENV["AIDP_DB_USER"] || ENV["USER"],
26
+ password: ENV["AIDP_DB_PASSWORD"]
27
+ )
28
+ Que.connection = @sequel_db
29
+ Que.migrate!(version: Que::Migrations::CURRENT_VERSION)
30
+ @connection
31
+ end
32
+ end
33
+
34
+ def connection
35
+ return @connection if @connection && !@connection.finished?
36
+ establish_connection
37
+ end
38
+
39
+ def disconnect
40
+ initialize_mutex
41
+ @mutex.synchronize do
42
+ return unless @connection
43
+
44
+ # Safely disconnect in reverse order
45
+ begin
46
+ Que.connection = nil
47
+ rescue => e
48
+ # Log but don't fail on Que disconnection issues
49
+ puts "Warning: Error setting Que.connection to nil: #{e.message}" if ENV["AIDP_DEBUG"]
50
+ end
51
+
52
+ @sequel_db&.disconnect
53
+ @connection&.close
54
+ @connection = nil
55
+ @sequel_db = nil
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def connection_params
62
+ {
63
+ host: ENV["AIDP_DB_HOST"] || "localhost",
64
+ port: (ENV["AIDP_DB_PORT"] || 5432).to_i,
65
+ dbname: ENV["AIDP_DB_NAME"] || "aidp",
66
+ user: ENV["AIDP_DB_USER"] || ENV["USER"],
67
+ password: ENV["AIDP_DB_PASSWORD"]
68
+ }
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sqlite3"
4
+ require "fileutils"
5
+
6
+ module Aidp
7
+ # Handles database migrations for AIDP
8
+ class DatabaseMigration
9
+ def initialize(project_dir = Dir.pwd)
10
+ @project_dir = project_dir
11
+ @old_db_path = File.join(project_dir, ".aidp-analysis.db")
12
+ @new_db_path = File.join(project_dir, ".aidp.db")
13
+ end
14
+
15
+ # Migrate database from old to new format
16
+ def migrate
17
+ # If neither database exists, create new one directly
18
+ if !File.exist?(@old_db_path) && !File.exist?(@new_db_path)
19
+ create_new_database
20
+ return true
21
+ end
22
+
23
+ # If new database already exists, skip migration
24
+ if File.exist?(@new_db_path)
25
+ puts "Database .aidp.db already exists, skipping migration"
26
+ return false
27
+ end
28
+
29
+ # Rename old database to new name
30
+ FileUtils.mv(@old_db_path, @new_db_path)
31
+
32
+ # Open database connection
33
+ db = SQLite3::Database.new(@new_db_path)
34
+
35
+ # Create new tables for job management
36
+ create_job_tables(db)
37
+
38
+ # Close connection
39
+ db.close
40
+
41
+ true
42
+ rescue => e
43
+ puts "Error during database migration: #{e.message}"
44
+ # Try to restore old database if something went wrong
45
+ if File.exist?(@new_db_path) && !File.exist?(@old_db_path)
46
+ FileUtils.mv(@new_db_path, @old_db_path)
47
+ end
48
+ false
49
+ end
50
+
51
+ private
52
+
53
+ def create_new_database
54
+ db = SQLite3::Database.new(@new_db_path)
55
+
56
+ # Create original tables
57
+ create_original_tables(db)
58
+
59
+ # Create new job tables
60
+ create_job_tables(db)
61
+
62
+ db.close
63
+ end
64
+
65
+ def create_original_tables(db)
66
+ # Create analysis_results table
67
+ db.execute(<<~SQL)
68
+ CREATE TABLE analysis_results (
69
+ step_name TEXT PRIMARY KEY,
70
+ data TEXT NOT NULL,
71
+ metadata TEXT,
72
+ created_at TEXT NOT NULL,
73
+ updated_at TEXT NOT NULL
74
+ )
75
+ SQL
76
+
77
+ # Create analysis_metrics table
78
+ db.execute(<<~SQL)
79
+ CREATE TABLE analysis_metrics (
80
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
81
+ step_name TEXT NOT NULL,
82
+ metric_name TEXT NOT NULL,
83
+ value TEXT NOT NULL,
84
+ recorded_at TEXT NOT NULL,
85
+ UNIQUE(step_name, metric_name, recorded_at)
86
+ )
87
+ SQL
88
+
89
+ # Create embeddings table
90
+ db.execute(<<~SQL)
91
+ CREATE TABLE embeddings (
92
+ step_name TEXT PRIMARY KEY,
93
+ embeddings_data TEXT NOT NULL,
94
+ created_at TEXT NOT NULL
95
+ )
96
+ SQL
97
+
98
+ # Create indexes
99
+ db.execute("CREATE INDEX idx_analysis_metrics_step_name ON analysis_metrics(step_name)")
100
+ db.execute("CREATE INDEX idx_analysis_metrics_recorded_at ON analysis_metrics(recorded_at)")
101
+ db.execute("CREATE INDEX idx_analysis_results_updated_at ON analysis_results(updated_at)")
102
+ end
103
+
104
+ def create_job_tables(db)
105
+ # Create jobs table
106
+ db.execute(<<~SQL)
107
+ CREATE TABLE jobs (
108
+ id INTEGER PRIMARY KEY,
109
+ job_type TEXT NOT NULL,
110
+ provider TEXT NOT NULL,
111
+ status TEXT NOT NULL,
112
+ created_at INTEGER NOT NULL,
113
+ started_at INTEGER,
114
+ completed_at INTEGER,
115
+ error TEXT,
116
+ metadata TEXT
117
+ )
118
+ SQL
119
+
120
+ # Create job_executions table
121
+ db.execute(<<~SQL)
122
+ CREATE TABLE job_executions (
123
+ id INTEGER PRIMARY KEY,
124
+ job_id INTEGER NOT NULL,
125
+ attempt INTEGER NOT NULL,
126
+ status TEXT NOT NULL,
127
+ started_at INTEGER NOT NULL,
128
+ completed_at INTEGER,
129
+ error TEXT,
130
+ FOREIGN KEY (job_id) REFERENCES jobs(id)
131
+ )
132
+ SQL
133
+
134
+ # Create job_logs table
135
+ db.execute(<<~SQL)
136
+ CREATE TABLE job_logs (
137
+ id INTEGER PRIMARY KEY,
138
+ job_id INTEGER NOT NULL,
139
+ execution_id INTEGER NOT NULL,
140
+ timestamp INTEGER NOT NULL,
141
+ message TEXT NOT NULL,
142
+ level TEXT NOT NULL,
143
+ metadata TEXT,
144
+ FOREIGN KEY (job_id) REFERENCES jobs(id),
145
+ FOREIGN KEY (execution_id) REFERENCES job_executions(id)
146
+ )
147
+ SQL
148
+
149
+ # Create indexes for job tables
150
+ db.execute("CREATE INDEX idx_jobs_status ON jobs(status)")
151
+ db.execute("CREATE INDEX idx_jobs_provider ON jobs(provider)")
152
+ db.execute("CREATE INDEX idx_job_executions_job_id ON job_executions(job_id)")
153
+ db.execute("CREATE INDEX idx_job_logs_job_id ON job_logs(job_id)")
154
+ db.execute("CREATE INDEX idx_job_logs_execution_id ON job_logs(execution_id)")
155
+ db.execute("CREATE INDEX idx_job_logs_timestamp ON job_logs(timestamp)")
156
+ end
157
+ end
158
+ end