aidp 0.32.0 → 0.34.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 +35 -0
- data/lib/aidp/analyze/feature_analyzer.rb +322 -320
- data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
- data/lib/aidp/auto_update/coordinator.rb +97 -7
- data/lib/aidp/auto_update.rb +0 -12
- data/lib/aidp/cli/devcontainer_commands.rb +0 -5
- data/lib/aidp/cli/eval_command.rb +399 -0
- data/lib/aidp/cli/harness_command.rb +1 -1
- data/lib/aidp/cli/security_command.rb +416 -0
- data/lib/aidp/cli/tools_command.rb +6 -4
- data/lib/aidp/cli.rb +172 -4
- data/lib/aidp/comment_consolidator.rb +78 -0
- data/lib/aidp/concurrency/exec.rb +3 -0
- data/lib/aidp/concurrency.rb +0 -3
- data/lib/aidp/config.rb +113 -1
- data/lib/aidp/config_paths.rb +91 -0
- data/lib/aidp/daemon/runner.rb +8 -4
- data/lib/aidp/errors.rb +134 -0
- data/lib/aidp/evaluations/context_capture.rb +205 -0
- data/lib/aidp/evaluations/evaluation_record.rb +114 -0
- data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
- data/lib/aidp/evaluations.rb +23 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
- data/lib/aidp/execute/interactive_repl.rb +6 -2
- data/lib/aidp/execute/prompt_evaluator.rb +359 -0
- data/lib/aidp/execute/repl_macros.rb +100 -1
- data/lib/aidp/execute/work_loop_runner.rb +719 -58
- data/lib/aidp/execute/work_loop_state.rb +4 -1
- data/lib/aidp/execute/workflow_selector.rb +3 -0
- data/lib/aidp/harness/ai_decision_engine.rb +79 -0
- data/lib/aidp/harness/ai_filter_factory.rb +285 -0
- data/lib/aidp/harness/capability_registry.rb +2 -0
- data/lib/aidp/harness/condition_detector.rb +3 -0
- data/lib/aidp/harness/config_loader.rb +3 -0
- data/lib/aidp/harness/config_schema.rb +97 -1
- data/lib/aidp/harness/config_validator.rb +1 -1
- data/lib/aidp/harness/configuration.rb +61 -5
- data/lib/aidp/harness/enhanced_runner.rb +14 -11
- data/lib/aidp/harness/error_handler.rb +3 -0
- data/lib/aidp/harness/filter_definition.rb +212 -0
- data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
- data/lib/aidp/harness/output_filter.rb +50 -25
- data/lib/aidp/harness/output_filter_config.rb +129 -0
- data/lib/aidp/harness/provider_factory.rb +3 -0
- data/lib/aidp/harness/provider_manager.rb +96 -2
- data/lib/aidp/harness/runner.rb +5 -12
- data/lib/aidp/harness/state/persistence.rb +3 -0
- data/lib/aidp/harness/state_manager.rb +3 -0
- data/lib/aidp/harness/status_display.rb +28 -20
- data/lib/aidp/harness/test_runner.rb +179 -41
- data/lib/aidp/harness/thinking_depth_manager.rb +44 -28
- data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
- data/lib/aidp/harness/ui/error_handler.rb +3 -0
- data/lib/aidp/harness/ui/job_monitor.rb +4 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +2 -2
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
- data/lib/aidp/harness/user_interface.rb +3 -0
- data/lib/aidp/loader.rb +195 -0
- data/lib/aidp/logger.rb +3 -0
- data/lib/aidp/message_display.rb +31 -0
- data/lib/aidp/metadata/compiler.rb +29 -17
- data/lib/aidp/metadata/query.rb +1 -1
- data/lib/aidp/metadata/scanner.rb +8 -1
- data/lib/aidp/metadata/tool_metadata.rb +13 -13
- data/lib/aidp/metadata/validator.rb +10 -0
- data/lib/aidp/metadata.rb +16 -0
- data/lib/aidp/pr_worktree_manager.rb +20 -8
- data/lib/aidp/provider_manager.rb +4 -7
- data/lib/aidp/providers/base.rb +2 -0
- data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
- data/lib/aidp/security/secrets_proxy.rb +328 -0
- data/lib/aidp/security/secrets_registry.rb +227 -0
- data/lib/aidp/security/trifecta_state.rb +220 -0
- data/lib/aidp/security/watch_mode_handler.rb +306 -0
- data/lib/aidp/security/work_loop_adapter.rb +277 -0
- data/lib/aidp/security.rb +56 -0
- data/lib/aidp/setup/wizard.rb +283 -11
- data/lib/aidp/skills.rb +0 -5
- data/lib/aidp/storage/csv_storage.rb +3 -0
- data/lib/aidp/style_guide/selector.rb +360 -0
- data/lib/aidp/tooling_detector.rb +283 -16
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/auto_merger.rb +274 -0
- data/lib/aidp/watch/auto_pr_processor.rb +125 -7
- data/lib/aidp/watch/build_processor.rb +16 -1
- data/lib/aidp/watch/change_request_processor.rb +682 -150
- data/lib/aidp/watch/ci_fix_processor.rb +262 -4
- data/lib/aidp/watch/feedback_collector.rb +191 -0
- data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
- data/lib/aidp/watch/implementation_verifier.rb +142 -1
- data/lib/aidp/watch/plan_generator.rb +70 -13
- data/lib/aidp/watch/plan_processor.rb +12 -5
- data/lib/aidp/watch/projects_processor.rb +286 -0
- data/lib/aidp/watch/repository_client.rb +871 -22
- data/lib/aidp/watch/review_processor.rb +33 -6
- data/lib/aidp/watch/runner.rb +80 -29
- data/lib/aidp/watch/state_store.rb +233 -0
- data/lib/aidp/watch/sub_issue_creator.rb +221 -0
- data/lib/aidp/watch.rb +5 -7
- data/lib/aidp/workflows/guided_agent.rb +4 -0
- data/lib/aidp/workstream_cleanup.rb +0 -2
- data/lib/aidp/workstream_executor.rb +3 -4
- data/lib/aidp/worktree.rb +61 -12
- data/lib/aidp/worktree_branch_manager.rb +347 -101
- data/lib/aidp.rb +21 -106
- data/templates/implementation/iterative_implementation.md +46 -3
- metadata +91 -36
- data/lib/aidp/config/paths.rb +0 -131
data/lib/aidp/errors.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "time"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
3
6
|
module Aidp
|
|
4
7
|
# Error classes for AIDP
|
|
5
8
|
module Errors
|
|
@@ -8,5 +11,136 @@ module Aidp
|
|
|
8
11
|
class ValidationError < StandardError; end
|
|
9
12
|
class StateError < StandardError; end
|
|
10
13
|
class UserError < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Exception raised when a Rule of Two policy violation occurs
|
|
16
|
+
# Contains detailed context about the violation for logging and debugging
|
|
17
|
+
class PolicyViolation < StandardError
|
|
18
|
+
attr_reader :flag, :source, :current_state, :suggested_mitigations
|
|
19
|
+
|
|
20
|
+
def initialize(flag:, source:, current_state:, message: nil)
|
|
21
|
+
@flag = flag
|
|
22
|
+
@source = source
|
|
23
|
+
@current_state = current_state
|
|
24
|
+
@suggested_mitigations = build_suggested_mitigations(flag, current_state)
|
|
25
|
+
|
|
26
|
+
super(message || default_message)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Export violation details for logging
|
|
30
|
+
def to_h
|
|
31
|
+
{
|
|
32
|
+
type: "rule_of_two_violation",
|
|
33
|
+
flag_attempted: @flag,
|
|
34
|
+
source: @source,
|
|
35
|
+
current_state: @current_state,
|
|
36
|
+
suggested_mitigations: @suggested_mitigations,
|
|
37
|
+
timestamp: Time.now.iso8601
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# JSON representation for structured logging
|
|
42
|
+
def to_json(*args)
|
|
43
|
+
to_h.to_json(*args)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def default_message
|
|
49
|
+
"Rule of Two violation: Cannot enable '#{@flag}' - would create lethal trifecta"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Build contextual mitigation suggestions based on which flags are enabled
|
|
53
|
+
def build_suggested_mitigations(flag, state)
|
|
54
|
+
mitigations = []
|
|
55
|
+
|
|
56
|
+
# Suggest based on which flags would form the trifecta
|
|
57
|
+
if state[:untrusted_input]
|
|
58
|
+
mitigations << {
|
|
59
|
+
action: "sanitize_input",
|
|
60
|
+
description: "Sanitize the untrusted input before processing to remove the untrusted_input flag",
|
|
61
|
+
command: "aidp security sanitize-input --source <source>"
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if state[:private_data]
|
|
66
|
+
mitigations << {
|
|
67
|
+
action: "use_secrets_proxy",
|
|
68
|
+
description: "Route credential access through the Secrets Proxy to get short-lived tokens",
|
|
69
|
+
command: "aidp security proxy-request --secret <name>"
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if state[:egress]
|
|
74
|
+
mitigations << {
|
|
75
|
+
action: "disable_egress",
|
|
76
|
+
description: "Disable external communication for this operation",
|
|
77
|
+
command: "Use deterministic unit or sandbox the operation"
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Add flag-specific suggestion for the attempted flag
|
|
82
|
+
case flag
|
|
83
|
+
when :untrusted_input
|
|
84
|
+
mitigations << {
|
|
85
|
+
action: "validate_source",
|
|
86
|
+
description: "Validate and trust the input source (e.g., from trusted author allowlist)",
|
|
87
|
+
command: "Add author to watch.safety.author_allowlist in aidp.yml"
|
|
88
|
+
}
|
|
89
|
+
when :private_data
|
|
90
|
+
mitigations << {
|
|
91
|
+
action: "scope_credentials",
|
|
92
|
+
description: "Use capability-scoped tokens instead of full credentials",
|
|
93
|
+
command: "aidp security register-secret --scoped"
|
|
94
|
+
}
|
|
95
|
+
when :egress
|
|
96
|
+
mitigations << {
|
|
97
|
+
action: "queue_for_approval",
|
|
98
|
+
description: "Queue the operation for manual execution outside the agent context",
|
|
99
|
+
command: "Operation will be logged for manual review"
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
mitigations.uniq { |m| m[:action] }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Exception raised when secrets proxy cannot fulfill a request
|
|
108
|
+
class SecretsProxyError < StandardError
|
|
109
|
+
attr_reader :secret_name, :reason
|
|
110
|
+
|
|
111
|
+
def initialize(secret_name:, reason:)
|
|
112
|
+
@secret_name = secret_name
|
|
113
|
+
@reason = reason
|
|
114
|
+
super("Secrets proxy error for '#{secret_name}': #{reason}")
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Exception raised when attempting to access an unregistered secret
|
|
119
|
+
class UnregisteredSecretError < SecretsProxyError
|
|
120
|
+
def initialize(secret_name:)
|
|
121
|
+
super(
|
|
122
|
+
secret_name: secret_name,
|
|
123
|
+
reason: "Secret not registered. Use 'aidp security register-secret #{secret_name}' to register."
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Exception raised when a token has expired
|
|
129
|
+
class TokenExpiredError < SecretsProxyError
|
|
130
|
+
def initialize(secret_name:, expired_at:)
|
|
131
|
+
super(
|
|
132
|
+
secret_name: secret_name,
|
|
133
|
+
reason: "Token expired at #{expired_at}. Request a new token via the secrets proxy."
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Convenience aliases in Security module for backward compatibility
|
|
140
|
+
module Security
|
|
141
|
+
PolicyViolation = Aidp::Errors::PolicyViolation
|
|
142
|
+
SecretsProxyError = Aidp::Errors::SecretsProxyError
|
|
143
|
+
UnregisteredSecretError = Aidp::Errors::UnregisteredSecretError
|
|
144
|
+
TokenExpiredError = Aidp::Errors::TokenExpiredError
|
|
11
145
|
end
|
|
12
146
|
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../config_paths"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Evaluations
|
|
7
|
+
# Captures rich context for evaluation records
|
|
8
|
+
#
|
|
9
|
+
# Gathers information about:
|
|
10
|
+
# - Prompt metadata (template, persona, skills, provider, model, tokens, settings)
|
|
11
|
+
# - Work-loop data (unit count, checkpoints, retries, file modifications)
|
|
12
|
+
# - Environment details (devcontainer status, Ruby version, branch info)
|
|
13
|
+
#
|
|
14
|
+
# @example Capturing context
|
|
15
|
+
# context = ContextCapture.new(project_dir: Dir.pwd)
|
|
16
|
+
# data = context.capture(step_name: "01_INIT", iteration: 3)
|
|
17
|
+
class ContextCapture
|
|
18
|
+
def initialize(project_dir: Dir.pwd, config: nil)
|
|
19
|
+
@project_dir = project_dir
|
|
20
|
+
@config = config
|
|
21
|
+
|
|
22
|
+
Aidp.log_debug("context_capture", "initialize", project_dir: project_dir)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Capture full context for an evaluation
|
|
26
|
+
#
|
|
27
|
+
# @param step_name [String, nil] Current work loop step
|
|
28
|
+
# @param iteration [Integer, nil] Current iteration number
|
|
29
|
+
# @param provider [String, nil] AI provider being used
|
|
30
|
+
# @param model [String, nil] AI model being used
|
|
31
|
+
# @param additional [Hash] Additional context to include
|
|
32
|
+
# @return [Hash] Captured context
|
|
33
|
+
def capture(step_name: nil, iteration: nil, provider: nil, model: nil, additional: {})
|
|
34
|
+
Aidp.log_debug("context_capture", "capture",
|
|
35
|
+
step_name: step_name, iteration: iteration, provider: provider)
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
prompt: capture_prompt_context(step_name),
|
|
39
|
+
work_loop: capture_work_loop_context(step_name, iteration),
|
|
40
|
+
environment: capture_environment_context,
|
|
41
|
+
provider: {
|
|
42
|
+
name: provider,
|
|
43
|
+
model: model
|
|
44
|
+
},
|
|
45
|
+
timestamp: Time.now.iso8601
|
|
46
|
+
}.merge(additional)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Capture minimal context (for quick evaluations)
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash] Minimal context with timestamp and environment basics
|
|
52
|
+
def capture_minimal
|
|
53
|
+
Aidp.log_debug("context_capture", "capture_minimal")
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
environment: {
|
|
57
|
+
ruby_version: RUBY_VERSION,
|
|
58
|
+
branch: current_git_branch,
|
|
59
|
+
aidp_version: aidp_version
|
|
60
|
+
},
|
|
61
|
+
timestamp: Time.now.iso8601
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Capture watch mode context for evaluating watch outputs
|
|
66
|
+
#
|
|
67
|
+
# @param repo [String] Repository in owner/repo format
|
|
68
|
+
# @param number [Integer] Issue or PR number
|
|
69
|
+
# @param processor_type [String] Type of processor (plan, review, build, ci_fix, change_request)
|
|
70
|
+
# @return [Hash] Watch mode context
|
|
71
|
+
def capture_watch(repo:, number:, processor_type:)
|
|
72
|
+
Aidp.log_debug("context_capture", "capture_watch",
|
|
73
|
+
repo: repo, number: number, processor_type: processor_type)
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
watch: {
|
|
77
|
+
repo: repo,
|
|
78
|
+
number: number,
|
|
79
|
+
processor_type: processor_type,
|
|
80
|
+
state: load_watch_state(repo, number, processor_type)
|
|
81
|
+
},
|
|
82
|
+
environment: capture_environment_context,
|
|
83
|
+
timestamp: Time.now.iso8601
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def load_watch_state(repo, number, processor_type)
|
|
90
|
+
# Try to load state from the watch state store
|
|
91
|
+
state_file = find_watch_state_file(repo)
|
|
92
|
+
return nil unless state_file && File.exist?(state_file)
|
|
93
|
+
|
|
94
|
+
require "yaml"
|
|
95
|
+
state = YAML.safe_load_file(state_file, permitted_classes: [Time, Date, Symbol])
|
|
96
|
+
return nil unless state
|
|
97
|
+
|
|
98
|
+
# Extract relevant state based on processor type
|
|
99
|
+
case processor_type
|
|
100
|
+
when "plan"
|
|
101
|
+
state.dig("issues", number.to_s, "plan") ||
|
|
102
|
+
state.dig(:issues, number, :plan)
|
|
103
|
+
when "review"
|
|
104
|
+
state.dig("pull_requests", number.to_s, "review") ||
|
|
105
|
+
state.dig(:pull_requests, number, :review)
|
|
106
|
+
when "build"
|
|
107
|
+
state.dig("issues", number.to_s, "build") ||
|
|
108
|
+
state.dig(:issues, number, :build)
|
|
109
|
+
when "ci_fix"
|
|
110
|
+
state.dig("pull_requests", number.to_s, "ci_fix") ||
|
|
111
|
+
state.dig(:pull_requests, number, :ci_fix)
|
|
112
|
+
when "change_request"
|
|
113
|
+
state.dig("pull_requests", number.to_s, "change_request") ||
|
|
114
|
+
state.dig(:pull_requests, number, :change_request)
|
|
115
|
+
end
|
|
116
|
+
rescue => e
|
|
117
|
+
Aidp.log_error("context_capture", "load_watch_state failed", error: e.message)
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def find_watch_state_file(repo)
|
|
122
|
+
watch_dir = File.join(@project_dir, ".aidp", "watch")
|
|
123
|
+
return nil unless Dir.exist?(watch_dir)
|
|
124
|
+
|
|
125
|
+
# Sanitize repo name the same way StateStore does
|
|
126
|
+
sanitized = repo.tr("/", "_").gsub(/[^a-zA-Z0-9_-]/, "")
|
|
127
|
+
state_file = File.join(watch_dir, "#{sanitized}.yml")
|
|
128
|
+
|
|
129
|
+
File.exist?(state_file) ? state_file : nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def capture_prompt_context(step_name)
|
|
133
|
+
prompt_file = File.join(@project_dir, ".aidp", "PROMPT.md")
|
|
134
|
+
return {} unless File.exist?(prompt_file)
|
|
135
|
+
|
|
136
|
+
content = File.read(prompt_file)
|
|
137
|
+
{
|
|
138
|
+
step_name: step_name,
|
|
139
|
+
prompt_length: content.length,
|
|
140
|
+
has_prompt: true
|
|
141
|
+
}
|
|
142
|
+
rescue => e
|
|
143
|
+
Aidp.log_error("context_capture", "capture_prompt_context failed", error: e.message)
|
|
144
|
+
{}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def capture_work_loop_context(step_name, iteration)
|
|
148
|
+
checkpoint_file = ConfigPaths.checkpoint_file(@project_dir)
|
|
149
|
+
checkpoint_data = if File.exist?(checkpoint_file)
|
|
150
|
+
require "yaml"
|
|
151
|
+
YAML.safe_load_file(checkpoint_file, permitted_classes: [Time, Date, Symbol])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
step_name: step_name,
|
|
156
|
+
iteration: iteration,
|
|
157
|
+
checkpoint: checkpoint_data ? {
|
|
158
|
+
status: checkpoint_data["status"] || checkpoint_data[:status],
|
|
159
|
+
metrics: checkpoint_data["metrics"] || checkpoint_data[:metrics]
|
|
160
|
+
} : nil
|
|
161
|
+
}
|
|
162
|
+
rescue => e
|
|
163
|
+
Aidp.log_error("context_capture", "capture_work_loop_context failed", error: e.message)
|
|
164
|
+
{step_name: step_name, iteration: iteration}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def capture_environment_context
|
|
168
|
+
{
|
|
169
|
+
ruby_version: RUBY_VERSION,
|
|
170
|
+
platform: RUBY_PLATFORM,
|
|
171
|
+
branch: current_git_branch,
|
|
172
|
+
commit: current_git_commit,
|
|
173
|
+
devcontainer: in_devcontainer?,
|
|
174
|
+
aidp_version: aidp_version
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def current_git_branch
|
|
179
|
+
Dir.chdir(@project_dir) do
|
|
180
|
+
`git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
181
|
+
end
|
|
182
|
+
rescue
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def current_git_commit
|
|
187
|
+
Dir.chdir(@project_dir) do
|
|
188
|
+
`git rev-parse --short HEAD 2>/dev/null`.strip
|
|
189
|
+
end
|
|
190
|
+
rescue
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def in_devcontainer?
|
|
195
|
+
File.exist?("/.dockerenv") || ENV["REMOTE_CONTAINERS"] == "true"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def aidp_version
|
|
199
|
+
Aidp::VERSION if defined?(Aidp::VERSION)
|
|
200
|
+
rescue
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Evaluations
|
|
7
|
+
# Represents a single evaluation record
|
|
8
|
+
#
|
|
9
|
+
# An evaluation captures user feedback (good/neutral/bad) for AIDP outputs
|
|
10
|
+
# such as prompts, work units, or full work loops, along with rich context.
|
|
11
|
+
#
|
|
12
|
+
# @example Creating an evaluation
|
|
13
|
+
# record = EvaluationRecord.new(
|
|
14
|
+
# rating: "good",
|
|
15
|
+
# comment: "Generated code was clean and well-structured",
|
|
16
|
+
# target_type: "work_unit",
|
|
17
|
+
# target_id: "01_INIT"
|
|
18
|
+
# )
|
|
19
|
+
class EvaluationRecord
|
|
20
|
+
VALID_RATINGS = %w[good neutral bad].freeze
|
|
21
|
+
VALID_TARGET_TYPES = %w[prompt work_unit work_loop step plan review build ci_fix change_request].freeze
|
|
22
|
+
|
|
23
|
+
attr_reader :id, :rating, :comment, :target_type, :target_id,
|
|
24
|
+
:context, :created_at
|
|
25
|
+
|
|
26
|
+
def initialize(rating:, comment: nil, target_type: nil, target_id: nil, context: {}, id: nil, created_at: nil)
|
|
27
|
+
@id = id || generate_id
|
|
28
|
+
@rating = validate_rating(rating)
|
|
29
|
+
@comment = comment
|
|
30
|
+
@target_type = validate_target_type(target_type)
|
|
31
|
+
@target_id = target_id
|
|
32
|
+
@context = context || {}
|
|
33
|
+
@created_at = created_at || Time.now.iso8601
|
|
34
|
+
|
|
35
|
+
Aidp.log_debug("evaluation_record", "create",
|
|
36
|
+
id: @id, rating: @rating, target_type: @target_type, target_id: @target_id)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Convert to hash for storage
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
id: @id,
|
|
43
|
+
rating: @rating,
|
|
44
|
+
comment: @comment,
|
|
45
|
+
target_type: @target_type,
|
|
46
|
+
target_id: @target_id,
|
|
47
|
+
context: @context,
|
|
48
|
+
created_at: @created_at
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Create record from stored hash
|
|
53
|
+
def self.from_h(hash)
|
|
54
|
+
hash = symbolize_keys(hash)
|
|
55
|
+
new(
|
|
56
|
+
id: hash[:id],
|
|
57
|
+
rating: hash[:rating],
|
|
58
|
+
comment: hash[:comment],
|
|
59
|
+
target_type: hash[:target_type],
|
|
60
|
+
target_id: hash[:target_id],
|
|
61
|
+
context: hash[:context] || {},
|
|
62
|
+
created_at: hash[:created_at]
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check if rating is positive
|
|
67
|
+
def good?
|
|
68
|
+
@rating == "good"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Check if rating is negative
|
|
72
|
+
def bad?
|
|
73
|
+
@rating == "bad"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check if rating is neutral
|
|
77
|
+
def neutral?
|
|
78
|
+
@rating == "neutral"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def generate_id
|
|
84
|
+
"eval_#{Time.now.strftime("%Y%m%d_%H%M%S")}_#{SecureRandom.hex(4)}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_rating(rating)
|
|
88
|
+
rating_str = rating.to_s.downcase
|
|
89
|
+
unless VALID_RATINGS.include?(rating_str)
|
|
90
|
+
raise ArgumentError, "Invalid rating '#{rating}'. Must be one of: #{VALID_RATINGS.join(", ")}"
|
|
91
|
+
end
|
|
92
|
+
rating_str
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def validate_target_type(target_type)
|
|
96
|
+
return nil if target_type.nil?
|
|
97
|
+
type_str = target_type.to_s.downcase
|
|
98
|
+
unless VALID_TARGET_TYPES.include?(type_str)
|
|
99
|
+
raise ArgumentError, "Invalid target_type '#{target_type}'. Must be one of: #{VALID_TARGET_TYPES.join(", ")}"
|
|
100
|
+
end
|
|
101
|
+
type_str
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class << self
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def symbolize_keys(hash)
|
|
108
|
+
return hash unless hash.is_a?(Hash)
|
|
109
|
+
hash.transform_keys { |k| k.is_a?(String) ? k.to_sym : k }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|