aidp 0.34.1 → 0.39.3
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 +14 -14
- data/lib/aidp/agent_harness_adapter.rb +309 -0
- data/lib/aidp/cli/issue_importer.rb +20 -6
- data/lib/aidp/cli/jobs_command.rb +1 -1
- data/lib/aidp/cli/models_command.rb +9 -16
- data/lib/aidp/cli/prompts_command.rb +253 -0
- data/lib/aidp/cli/security_command.rb +4 -10
- data/lib/aidp/cli/storage_command.rb +277 -0
- data/lib/aidp/cli/temporal_command.rb +460 -0
- data/lib/aidp/cli.rb +24 -2
- data/lib/aidp/config.rb +15 -0
- data/lib/aidp/config_paths.rb +12 -2
- data/lib/aidp/database/migrations.rb +114 -0
- data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
- data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
- data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
- data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
- data/lib/aidp/database/repositories/job_repository.rb +278 -0
- data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
- data/lib/aidp/database/repositories/progress_repository.rb +210 -0
- data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
- data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
- data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
- data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
- data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
- data/lib/aidp/database/repositories/task_repository.rb +231 -0
- data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
- data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
- data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
- data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
- data/lib/aidp/database/repository.rb +166 -0
- data/lib/aidp/database/schema.rb +347 -0
- data/lib/aidp/database/storage_migrator.rb +693 -0
- data/lib/aidp/database.rb +139 -0
- data/lib/aidp/debug_mixin.rb +2 -2
- data/lib/aidp/execute/checkpoint.rb +26 -0
- data/lib/aidp/execute/checkpoint_display.rb +16 -0
- data/lib/aidp/execute/prompt_evaluator.rb +30 -57
- data/lib/aidp/execute/work_loop_runner.rb +294 -46
- data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
- data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
- data/lib/aidp/harness/ai_decision_engine.rb +78 -296
- data/lib/aidp/harness/config_schema.rb +59 -0
- data/lib/aidp/harness/configuration.rb +229 -10
- data/lib/aidp/harness/deprecation_cache.rb +53 -11
- data/lib/aidp/harness/enhanced_runner.rb +1 -1
- data/lib/aidp/harness/error_handler.rb +11 -5
- data/lib/aidp/harness/provider_config.rb +36 -0
- data/lib/aidp/harness/provider_factory.rb +26 -24
- data/lib/aidp/harness/provider_info.rb +9 -5
- data/lib/aidp/harness/provider_manager.rb +2 -2
- data/lib/aidp/harness/provider_metrics.rb +103 -1
- data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
- data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
- data/lib/aidp/harness/runner.rb +7 -2
- data/lib/aidp/harness/state_manager.rb +1 -1
- data/lib/aidp/harness/test_runner.rb +167 -11
- data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
- data/lib/aidp/harness/usage_limit.rb +179 -0
- data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
- data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
- data/lib/aidp/harness/usage_period.rb +234 -0
- data/lib/aidp/init/agent_instructions_generator.rb +317 -0
- data/lib/aidp/init/runner.rb +25 -4
- data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
- data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
- data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
- data/lib/aidp/interfaces/logger_interface.rb +150 -0
- data/lib/aidp/interfaces/ui_interface.rb +254 -0
- data/lib/aidp/logger.rb +25 -7
- data/lib/aidp/orchestration_adapter.rb +279 -0
- data/lib/aidp/pr_worktree_manager.rb +4 -15
- data/lib/aidp/prompts/feedback_collector.rb +198 -0
- data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
- data/lib/aidp/prompts/template_evolver.rb +305 -0
- data/lib/aidp/prompts/template_version_manager.rb +325 -0
- data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
- data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
- data/lib/aidp/setup/wizard.rb +616 -176
- data/lib/aidp/shell_executor.rb +58 -0
- data/lib/aidp/style_guide/selector.rb +56 -1
- data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
- data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
- data/lib/aidp/temporal/activities/base_activity.rb +78 -0
- data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
- data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
- data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
- data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
- data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
- data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
- data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
- data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
- data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
- data/lib/aidp/temporal/configuration.rb +169 -0
- data/lib/aidp/temporal/connection.rb +93 -0
- data/lib/aidp/temporal/worker.rb +119 -0
- data/lib/aidp/temporal/workflow_client.rb +171 -0
- data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
- data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
- data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
- data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
- data/lib/aidp/temporal.rb +120 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/base_processor.rb +35 -0
- data/lib/aidp/watch/build_processor.rb +132 -0
- data/lib/aidp/watch/github_state_extractor.rb +0 -8
- data/lib/aidp/watch/rebase_processor.rb +245 -0
- data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
- data/lib/aidp/watch/runner.rb +366 -285
- data/lib/aidp/watch/state_store.rb +80 -0
- data/lib/aidp/watch/work_item.rb +96 -0
- data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
- data/lib/aidp/watch/worktree_reconciler.rb +643 -0
- data/lib/aidp/workflows/guided_agent.rb +1 -1
- data/lib/aidp/worktree.rb +62 -0
- data/templates/aidp.yml.example +70 -0
- metadata +117 -16
- data/lib/aidp/analyze/json_file_storage.rb +0 -292
- data/lib/aidp/execute/future_work_backlog.rb +0 -411
- data/lib/aidp/providers/adapter.rb +0 -239
- data/lib/aidp/providers/aider.rb +0 -264
- data/lib/aidp/providers/anthropic.rb +0 -614
- data/lib/aidp/providers/base.rb +0 -617
- data/lib/aidp/providers/capability_registry.rb +0 -204
- data/lib/aidp/providers/codex.rb +0 -267
- data/lib/aidp/providers/cursor.rb +0 -304
- data/lib/aidp/providers/error_taxonomy.rb +0 -195
- data/lib/aidp/providers/gemini.rb +0 -110
- data/lib/aidp/providers/github_copilot.rb +0 -227
- data/lib/aidp/providers/kilocode.rb +0 -157
- data/lib/aidp/providers/opencode.rb +0 -139
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../repository"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Database
|
|
8
|
+
module Repositories
|
|
9
|
+
# Repository for tasks table
|
|
10
|
+
# Replaces tasklist.jsonl
|
|
11
|
+
class TaskRepository < Repository
|
|
12
|
+
VALID_STATUSES = %w[pending in_progress done abandoned].freeze
|
|
13
|
+
VALID_PRIORITIES = %w[high medium low].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(project_dir: Dir.pwd)
|
|
16
|
+
super(project_dir: project_dir, table_name: "tasks")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Create a new task
|
|
20
|
+
#
|
|
21
|
+
# @param description [String] Task description
|
|
22
|
+
# @param priority [String, Symbol] Task priority (high, medium, low)
|
|
23
|
+
# @param session [String, nil] Session identifier
|
|
24
|
+
# @param discovered_during [String, nil] When task was discovered
|
|
25
|
+
# @param tags [Array<String>] Task tags
|
|
26
|
+
# @return [Hash] Created task
|
|
27
|
+
def create(description:, priority: "medium", session: nil, discovered_during: nil, tags: [])
|
|
28
|
+
now = current_timestamp
|
|
29
|
+
id = generate_id
|
|
30
|
+
|
|
31
|
+
execute(
|
|
32
|
+
insert_sql([
|
|
33
|
+
:id, :project_dir, :description, :priority, :status,
|
|
34
|
+
:tags, :source, :created_at, :updated_at
|
|
35
|
+
]),
|
|
36
|
+
[
|
|
37
|
+
id,
|
|
38
|
+
project_dir,
|
|
39
|
+
description.strip,
|
|
40
|
+
priority.to_s,
|
|
41
|
+
"pending",
|
|
42
|
+
serialize_json(Array(tags)),
|
|
43
|
+
serialize_json({session: session, discovered_during: discovered_during}),
|
|
44
|
+
now,
|
|
45
|
+
now
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
Aidp.log_debug("task_repository", "created", id: id, description: description)
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
id: id,
|
|
53
|
+
description: description.strip,
|
|
54
|
+
status: :pending,
|
|
55
|
+
priority: priority.to_sym,
|
|
56
|
+
tags: Array(tags),
|
|
57
|
+
session: session,
|
|
58
|
+
discovered_during: discovered_during,
|
|
59
|
+
created_at: now,
|
|
60
|
+
updated_at: now
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Update task status
|
|
65
|
+
#
|
|
66
|
+
# @param task_id [String] Task ID
|
|
67
|
+
# @param new_status [String, Symbol] New status
|
|
68
|
+
# @param reason [String, nil] Reason for status change (for abandoned)
|
|
69
|
+
# @return [Hash, nil] Updated task or nil if not found
|
|
70
|
+
def update_status(task_id, new_status, reason: nil)
|
|
71
|
+
task = find(task_id)
|
|
72
|
+
return nil unless task
|
|
73
|
+
|
|
74
|
+
now = current_timestamp
|
|
75
|
+
status_str = new_status.to_s
|
|
76
|
+
|
|
77
|
+
updates = {status: status_str, updated_at: now}
|
|
78
|
+
|
|
79
|
+
case status_str
|
|
80
|
+
when "in_progress"
|
|
81
|
+
updates[:started_at] = now unless task[:started_at]
|
|
82
|
+
when "done"
|
|
83
|
+
updates[:completed_at] = now
|
|
84
|
+
when "abandoned"
|
|
85
|
+
# Store reason in source JSON
|
|
86
|
+
source = task[:source] || {}
|
|
87
|
+
source[:abandoned_reason] = reason
|
|
88
|
+
updates[:source] = serialize_json(source)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
set_clauses = updates.keys.map { |k| "#{k} = ?" }.join(", ")
|
|
92
|
+
values = updates.values + [task_id, project_dir]
|
|
93
|
+
|
|
94
|
+
execute(
|
|
95
|
+
"UPDATE tasks SET #{set_clauses} WHERE id = ? AND project_dir = ?",
|
|
96
|
+
values
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
Aidp.log_debug("task_repository", "status_updated", id: task_id, status: status_str)
|
|
100
|
+
|
|
101
|
+
find(task_id)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Find task by ID
|
|
105
|
+
#
|
|
106
|
+
# @param task_id [String] Task ID
|
|
107
|
+
# @return [Hash, nil] Task or nil
|
|
108
|
+
def find(task_id)
|
|
109
|
+
row = query_one(
|
|
110
|
+
"SELECT * FROM tasks WHERE id = ? AND project_dir = ?",
|
|
111
|
+
[task_id, project_dir]
|
|
112
|
+
)
|
|
113
|
+
return nil unless row
|
|
114
|
+
|
|
115
|
+
deserialize_task(row)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get all tasks with optional filters
|
|
119
|
+
#
|
|
120
|
+
# @param status [Symbol, String, nil] Filter by status
|
|
121
|
+
# @param priority [Symbol, String, nil] Filter by priority
|
|
122
|
+
# @param since [Time, nil] Filter by created_at
|
|
123
|
+
# @param tags [Array<String>, nil] Filter by tags (any match)
|
|
124
|
+
# @return [Array<Hash>] Matching tasks
|
|
125
|
+
def all(status: nil, priority: nil, since: nil, tags: nil)
|
|
126
|
+
conditions = ["project_dir = ?"]
|
|
127
|
+
params = [project_dir]
|
|
128
|
+
|
|
129
|
+
if status
|
|
130
|
+
conditions << "status = ?"
|
|
131
|
+
params << status.to_s
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if priority
|
|
135
|
+
conditions << "priority = ?"
|
|
136
|
+
params << priority.to_s
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
if since
|
|
140
|
+
conditions << "created_at >= ?"
|
|
141
|
+
params << since.strftime("%Y-%m-%d %H:%M:%S")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
sql = <<~SQL
|
|
145
|
+
SELECT * FROM tasks
|
|
146
|
+
WHERE #{conditions.join(" AND ")}
|
|
147
|
+
ORDER BY created_at DESC
|
|
148
|
+
SQL
|
|
149
|
+
|
|
150
|
+
rows = query(sql, params)
|
|
151
|
+
tasks = rows.map { |row| deserialize_task(row) }
|
|
152
|
+
|
|
153
|
+
# Filter by tags in Ruby (JSON array matching is complex in SQLite)
|
|
154
|
+
if tags && !tags.empty?
|
|
155
|
+
tag_set = tags.map(&:to_s)
|
|
156
|
+
tasks = tasks.select do |task|
|
|
157
|
+
(Array(task[:tags]) & tag_set).any?
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
tasks
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Get pending tasks
|
|
165
|
+
#
|
|
166
|
+
# @return [Array<Hash>] Pending tasks
|
|
167
|
+
def pending
|
|
168
|
+
all(status: :pending)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Get in-progress tasks
|
|
172
|
+
#
|
|
173
|
+
# @return [Array<Hash>] In-progress tasks
|
|
174
|
+
def in_progress
|
|
175
|
+
all(status: :in_progress)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Get task counts by status
|
|
179
|
+
#
|
|
180
|
+
# @return [Hash] Counts by status
|
|
181
|
+
def counts
|
|
182
|
+
sql = <<~SQL
|
|
183
|
+
SELECT status, COUNT(*) as count
|
|
184
|
+
FROM tasks
|
|
185
|
+
WHERE project_dir = ?
|
|
186
|
+
GROUP BY status
|
|
187
|
+
SQL
|
|
188
|
+
|
|
189
|
+
rows = query(sql, [project_dir])
|
|
190
|
+
counts_by_status = rows.each_with_object({}) do |row, h|
|
|
191
|
+
h[row["status"].to_sym] = row["count"]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
{
|
|
195
|
+
total: counts_by_status.values.sum,
|
|
196
|
+
pending: counts_by_status[:pending] || 0,
|
|
197
|
+
in_progress: counts_by_status[:in_progress] || 0,
|
|
198
|
+
done: counts_by_status[:done] || 0,
|
|
199
|
+
abandoned: counts_by_status[:abandoned] || 0
|
|
200
|
+
}
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
def generate_id
|
|
206
|
+
"task_#{Time.now.to_i}_#{SecureRandom.hex(4)}"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def deserialize_task(row)
|
|
210
|
+
source = deserialize_json(row["source"]) || {}
|
|
211
|
+
|
|
212
|
+
{
|
|
213
|
+
id: row["id"],
|
|
214
|
+
description: row["description"],
|
|
215
|
+
status: row["status"]&.to_sym,
|
|
216
|
+
priority: row["priority"]&.to_sym,
|
|
217
|
+
tags: deserialize_json(row["tags"]) || [],
|
|
218
|
+
session: source[:session],
|
|
219
|
+
discovered_during: source[:discovered_during],
|
|
220
|
+
abandoned_reason: source[:abandoned_reason],
|
|
221
|
+
created_at: row["created_at"],
|
|
222
|
+
updated_at: row["updated_at"],
|
|
223
|
+
started_at: row["started_at"],
|
|
224
|
+
completed_at: row["completed_at"],
|
|
225
|
+
source: source
|
|
226
|
+
}
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../repository"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Database
|
|
7
|
+
module Repositories
|
|
8
|
+
# Repository for template_versions table
|
|
9
|
+
# Manages versioned prompt templates with feedback-based selection
|
|
10
|
+
#
|
|
11
|
+
# Per issue #402:
|
|
12
|
+
# - Store template versions in SQLite
|
|
13
|
+
# - Track positive/negative votes per version
|
|
14
|
+
# - Retain last 5 versions, ensuring at least 2 positive-feedback versions
|
|
15
|
+
# - Select best version based on positive vote count
|
|
16
|
+
#
|
|
17
|
+
class TemplateVersionRepository < Repository
|
|
18
|
+
# Minimum versions to retain per template
|
|
19
|
+
MIN_VERSIONS = 5
|
|
20
|
+
|
|
21
|
+
# Minimum positive-feedback versions to retain
|
|
22
|
+
MIN_POSITIVE_VERSIONS = 2
|
|
23
|
+
|
|
24
|
+
def initialize(project_dir: Dir.pwd)
|
|
25
|
+
super(project_dir: project_dir, table_name: "template_versions")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Create a new template version
|
|
29
|
+
#
|
|
30
|
+
# @param template_id [String] Template identifier (e.g., "work_loop/decide_whats_next")
|
|
31
|
+
# @param content [String] Full YAML content of the template
|
|
32
|
+
# @param parent_version_id [Integer, nil] ID of parent version (for AGD-generated variants)
|
|
33
|
+
# @param metadata [Hash, nil] Additional metadata
|
|
34
|
+
# @return [Hash] Result with :success, :id, :version_number
|
|
35
|
+
def create(template_id:, content:, parent_version_id: nil, metadata: nil)
|
|
36
|
+
# Validate content is present
|
|
37
|
+
if content.nil? || content.to_s.strip.empty?
|
|
38
|
+
Aidp.log_error("template_version_repository", "invalid_content",
|
|
39
|
+
template_id: template_id,
|
|
40
|
+
error: "Content cannot be nil or empty")
|
|
41
|
+
return {success: false, error: "Content cannot be nil or empty"}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Aidp.log_debug("template_version_repository", "creating_version",
|
|
45
|
+
template_id: template_id,
|
|
46
|
+
parent_version_id: parent_version_id)
|
|
47
|
+
|
|
48
|
+
transaction do
|
|
49
|
+
# Get next version number for this template
|
|
50
|
+
next_version = next_version_number(template_id)
|
|
51
|
+
|
|
52
|
+
# Deactivate all existing versions for this template
|
|
53
|
+
execute(
|
|
54
|
+
"UPDATE template_versions SET is_active = 0 WHERE project_dir = ? AND template_id = ?",
|
|
55
|
+
[project_dir, template_id]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Insert new version as active
|
|
59
|
+
execute(
|
|
60
|
+
insert_sql([
|
|
61
|
+
:project_dir, :template_id, :version_number, :content,
|
|
62
|
+
:parent_version_id, :is_active, :positive_votes, :negative_votes, :metadata
|
|
63
|
+
]),
|
|
64
|
+
[
|
|
65
|
+
project_dir,
|
|
66
|
+
template_id,
|
|
67
|
+
next_version,
|
|
68
|
+
content,
|
|
69
|
+
parent_version_id,
|
|
70
|
+
1, # is_active
|
|
71
|
+
0, # positive_votes
|
|
72
|
+
0, # negative_votes
|
|
73
|
+
serialize_json(metadata)
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
id = last_insert_row_id
|
|
78
|
+
|
|
79
|
+
Aidp.log_info("template_version_repository", "version_created",
|
|
80
|
+
template_id: template_id,
|
|
81
|
+
version_number: next_version,
|
|
82
|
+
id: id)
|
|
83
|
+
|
|
84
|
+
{success: true, id: id, version_number: next_version}
|
|
85
|
+
end
|
|
86
|
+
rescue => e
|
|
87
|
+
Aidp.log_error("template_version_repository", "create_failed",
|
|
88
|
+
template_id: template_id,
|
|
89
|
+
error: e.message)
|
|
90
|
+
{success: false, error: e.message}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get the active version for a template
|
|
94
|
+
#
|
|
95
|
+
# @param template_id [String] Template identifier
|
|
96
|
+
# @return [Hash, nil] Active version or nil
|
|
97
|
+
def active_version(template_id:)
|
|
98
|
+
row = query_one(
|
|
99
|
+
<<~SQL,
|
|
100
|
+
SELECT * FROM template_versions
|
|
101
|
+
WHERE project_dir = ? AND template_id = ? AND is_active = 1
|
|
102
|
+
LIMIT 1
|
|
103
|
+
SQL
|
|
104
|
+
[project_dir, template_id]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
deserialize_version(row)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Get a specific version by ID
|
|
111
|
+
#
|
|
112
|
+
# @param id [Integer] Version ID
|
|
113
|
+
# @return [Hash, nil] Version or nil
|
|
114
|
+
def find(id:)
|
|
115
|
+
row = query_one(
|
|
116
|
+
"SELECT * FROM template_versions WHERE id = ? AND project_dir = ?",
|
|
117
|
+
[id, project_dir]
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
deserialize_version(row)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Get all versions for a template
|
|
124
|
+
#
|
|
125
|
+
# @param template_id [String] Template identifier
|
|
126
|
+
# @param limit [Integer] Maximum versions to return
|
|
127
|
+
# @return [Array<Hash>] Versions sorted by version_number descending
|
|
128
|
+
def list(template_id:, limit: 20)
|
|
129
|
+
rows = query(
|
|
130
|
+
<<~SQL,
|
|
131
|
+
SELECT * FROM template_versions
|
|
132
|
+
WHERE project_dir = ? AND template_id = ?
|
|
133
|
+
ORDER BY version_number DESC
|
|
134
|
+
LIMIT ?
|
|
135
|
+
SQL
|
|
136
|
+
[project_dir, template_id, limit]
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
rows.map { |row| deserialize_version(row) }.compact
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Record a positive vote for a version
|
|
143
|
+
#
|
|
144
|
+
# @param id [Integer] Version ID
|
|
145
|
+
# @return [Hash] Result with :success
|
|
146
|
+
def record_positive_vote(id:)
|
|
147
|
+
Aidp.log_debug("template_version_repository", "recording_positive_vote", id: id)
|
|
148
|
+
|
|
149
|
+
execute(
|
|
150
|
+
"UPDATE template_versions SET positive_votes = positive_votes + 1 WHERE id = ? AND project_dir = ?",
|
|
151
|
+
[id, project_dir]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
{success: true}
|
|
155
|
+
rescue => e
|
|
156
|
+
Aidp.log_error("template_version_repository", "positive_vote_failed",
|
|
157
|
+
id: id, error: e.message)
|
|
158
|
+
{success: false, error: e.message}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Record a negative vote for a version
|
|
162
|
+
#
|
|
163
|
+
# @param id [Integer] Version ID
|
|
164
|
+
# @return [Hash] Result with :success
|
|
165
|
+
def record_negative_vote(id:)
|
|
166
|
+
Aidp.log_debug("template_version_repository", "recording_negative_vote", id: id)
|
|
167
|
+
|
|
168
|
+
execute(
|
|
169
|
+
"UPDATE template_versions SET negative_votes = negative_votes + 1 WHERE id = ? AND project_dir = ?",
|
|
170
|
+
[id, project_dir]
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
{success: true}
|
|
174
|
+
rescue => e
|
|
175
|
+
Aidp.log_error("template_version_repository", "negative_vote_failed",
|
|
176
|
+
id: id, error: e.message)
|
|
177
|
+
{success: false, error: e.message}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Activate a specific version
|
|
181
|
+
#
|
|
182
|
+
# @param id [Integer] Version ID to activate
|
|
183
|
+
# @return [Hash] Result with :success
|
|
184
|
+
def activate(id:)
|
|
185
|
+
version = find(id: id)
|
|
186
|
+
return {success: false, error: "Version not found"} unless version
|
|
187
|
+
|
|
188
|
+
Aidp.log_debug("template_version_repository", "activating_version",
|
|
189
|
+
id: id,
|
|
190
|
+
template_id: version[:template_id])
|
|
191
|
+
|
|
192
|
+
transaction do
|
|
193
|
+
# Deactivate all versions for this template
|
|
194
|
+
execute(
|
|
195
|
+
"UPDATE template_versions SET is_active = 0 WHERE project_dir = ? AND template_id = ?",
|
|
196
|
+
[project_dir, version[:template_id]]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Activate the specified version
|
|
200
|
+
execute(
|
|
201
|
+
"UPDATE template_versions SET is_active = 1 WHERE id = ? AND project_dir = ?",
|
|
202
|
+
[id, project_dir]
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
Aidp.log_info("template_version_repository", "version_activated",
|
|
207
|
+
id: id,
|
|
208
|
+
template_id: version[:template_id])
|
|
209
|
+
|
|
210
|
+
{success: true}
|
|
211
|
+
rescue => e
|
|
212
|
+
Aidp.log_error("template_version_repository", "activation_failed",
|
|
213
|
+
id: id, error: e.message)
|
|
214
|
+
{success: false, error: e.message}
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Get the best version for a template based on positive votes
|
|
218
|
+
# Prioritizes higher positive vote counts
|
|
219
|
+
#
|
|
220
|
+
# @param template_id [String] Template identifier
|
|
221
|
+
# @return [Hash, nil] Best version or nil
|
|
222
|
+
def best_version(template_id:)
|
|
223
|
+
row = query_one(
|
|
224
|
+
<<~SQL,
|
|
225
|
+
SELECT * FROM template_versions
|
|
226
|
+
WHERE project_dir = ? AND template_id = ?
|
|
227
|
+
ORDER BY positive_votes DESC, version_number DESC
|
|
228
|
+
LIMIT 1
|
|
229
|
+
SQL
|
|
230
|
+
[project_dir, template_id]
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
deserialize_version(row)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Get versions needing evolution (have negative votes)
|
|
237
|
+
#
|
|
238
|
+
# @param template_id [String, nil] Filter by template (nil for all)
|
|
239
|
+
# @return [Array<Hash>] Versions with negative votes
|
|
240
|
+
def versions_needing_evolution(template_id: nil)
|
|
241
|
+
rows = if template_id
|
|
242
|
+
query(
|
|
243
|
+
<<~SQL,
|
|
244
|
+
SELECT * FROM template_versions
|
|
245
|
+
WHERE project_dir = ? AND template_id = ? AND negative_votes > 0
|
|
246
|
+
ORDER BY negative_votes DESC
|
|
247
|
+
SQL
|
|
248
|
+
[project_dir, template_id]
|
|
249
|
+
)
|
|
250
|
+
else
|
|
251
|
+
query(
|
|
252
|
+
<<~SQL,
|
|
253
|
+
SELECT * FROM template_versions
|
|
254
|
+
WHERE project_dir = ? AND negative_votes > 0
|
|
255
|
+
ORDER BY negative_votes DESC
|
|
256
|
+
SQL
|
|
257
|
+
[project_dir]
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
rows.map { |row| deserialize_version(row) }.compact
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Prune old versions, keeping:
|
|
265
|
+
# - At least MIN_VERSIONS (5) versions
|
|
266
|
+
# - At least MIN_POSITIVE_VERSIONS (2) positive-feedback versions
|
|
267
|
+
#
|
|
268
|
+
# @param template_id [String] Template identifier
|
|
269
|
+
# @return [Hash] Result with :success, :pruned_count
|
|
270
|
+
def prune_old_versions(template_id:)
|
|
271
|
+
Aidp.log_debug("template_version_repository", "pruning_versions",
|
|
272
|
+
template_id: template_id)
|
|
273
|
+
|
|
274
|
+
# Capture active version reference before list/delete operations.
|
|
275
|
+
# This ensures we never prune the currently active version, even if
|
|
276
|
+
# activation changes during pruning (defensive programming).
|
|
277
|
+
active = active_version(template_id: template_id)
|
|
278
|
+
|
|
279
|
+
versions = list(template_id: template_id, limit: 100)
|
|
280
|
+
return {success: true, pruned_count: 0} if versions.size <= MIN_VERSIONS
|
|
281
|
+
|
|
282
|
+
# Identify versions to keep
|
|
283
|
+
versions_to_keep = identify_versions_to_keep(versions)
|
|
284
|
+
versions_to_prune = versions.reject { |v| versions_to_keep.include?(v[:id]) }
|
|
285
|
+
|
|
286
|
+
# Never prune the active version
|
|
287
|
+
versions_to_prune.reject! { |v| v[:id] == active&.dig(:id) }
|
|
288
|
+
|
|
289
|
+
pruned_count = 0
|
|
290
|
+
versions_to_prune.each do |version|
|
|
291
|
+
execute("DELETE FROM template_versions WHERE id = ?", [version[:id]])
|
|
292
|
+
pruned_count += 1
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
Aidp.log_info("template_version_repository", "versions_pruned",
|
|
296
|
+
template_id: template_id,
|
|
297
|
+
pruned_count: pruned_count,
|
|
298
|
+
remaining_count: versions.size - pruned_count)
|
|
299
|
+
|
|
300
|
+
{success: true, pruned_count: pruned_count}
|
|
301
|
+
rescue => e
|
|
302
|
+
Aidp.log_error("template_version_repository", "prune_failed",
|
|
303
|
+
template_id: template_id, error: e.message)
|
|
304
|
+
{success: false, error: e.message}
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Count versions for a template
|
|
308
|
+
#
|
|
309
|
+
# @param template_id [String] Template identifier
|
|
310
|
+
# @return [Integer] Version count
|
|
311
|
+
def count(template_id:)
|
|
312
|
+
query_value(
|
|
313
|
+
"SELECT COUNT(*) FROM template_versions WHERE project_dir = ? AND template_id = ?",
|
|
314
|
+
[project_dir, template_id]
|
|
315
|
+
) || 0
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Check if any versions exist for a template
|
|
319
|
+
#
|
|
320
|
+
# @param template_id [String] Template identifier
|
|
321
|
+
# @return [Boolean]
|
|
322
|
+
def any?(template_id:)
|
|
323
|
+
count(template_id: template_id).positive?
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Get all unique template IDs with versions
|
|
327
|
+
#
|
|
328
|
+
# @return [Array<String>] Template IDs
|
|
329
|
+
def template_ids
|
|
330
|
+
rows = query(
|
|
331
|
+
"SELECT DISTINCT template_id FROM template_versions WHERE project_dir = ? ORDER BY template_id",
|
|
332
|
+
[project_dir]
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
rows.map { |r| r["template_id"] }
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
private
|
|
339
|
+
|
|
340
|
+
def next_version_number(template_id)
|
|
341
|
+
current_max = query_value(
|
|
342
|
+
"SELECT MAX(version_number) FROM template_versions WHERE project_dir = ? AND template_id = ?",
|
|
343
|
+
[project_dir, template_id]
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
(current_max || 0) + 1
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def identify_versions_to_keep(versions)
|
|
350
|
+
keep_ids = Set.new
|
|
351
|
+
|
|
352
|
+
# Always keep the most recent MIN_VERSIONS
|
|
353
|
+
versions.take(MIN_VERSIONS).each { |v| keep_ids << v[:id] }
|
|
354
|
+
|
|
355
|
+
# Ensure we keep at least MIN_POSITIVE_VERSIONS with positive votes
|
|
356
|
+
positive_versions = versions.select { |v| v[:positive_votes].positive? }
|
|
357
|
+
.sort_by { |v| -v[:positive_votes] }
|
|
358
|
+
|
|
359
|
+
positive_versions.take(MIN_POSITIVE_VERSIONS).each { |v| keep_ids << v[:id] }
|
|
360
|
+
|
|
361
|
+
keep_ids
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def deserialize_version(row)
|
|
365
|
+
return nil unless row
|
|
366
|
+
|
|
367
|
+
{
|
|
368
|
+
id: row["id"],
|
|
369
|
+
template_id: row["template_id"],
|
|
370
|
+
version_number: row["version_number"],
|
|
371
|
+
content: row["content"],
|
|
372
|
+
parent_version_id: row["parent_version_id"],
|
|
373
|
+
is_active: row["is_active"] == 1,
|
|
374
|
+
positive_votes: row["positive_votes"] || 0,
|
|
375
|
+
negative_votes: row["negative_votes"] || 0,
|
|
376
|
+
created_at: row["created_at"],
|
|
377
|
+
metadata: deserialize_json(row["metadata"])
|
|
378
|
+
}
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|