aidp 0.32.0 → 0.33.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/lib/aidp/analyze/feature_analyzer.rb +322 -320
- 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.rb +2 -1
- data/lib/aidp/comment_consolidator.rb +78 -0
- data/lib/aidp/concurrency.rb +0 -3
- data/lib/aidp/config.rb +0 -1
- data/lib/aidp/config_paths.rb +71 -0
- data/lib/aidp/execute/work_loop_runner.rb +324 -15
- data/lib/aidp/harness/ai_filter_factory.rb +285 -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/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_manager.rb +90 -2
- data/lib/aidp/harness/runner.rb +0 -11
- data/lib/aidp/harness/test_runner.rb +179 -41
- data/lib/aidp/harness/thinking_depth_manager.rb +16 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +0 -2
- data/lib/aidp/loader.rb +195 -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 +2 -2
- data/lib/aidp/provider_manager.rb +1 -7
- data/lib/aidp/setup/wizard.rb +279 -9
- 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/change_request_processor.rb +152 -14
- data/lib/aidp/watch/repository_client.rb +41 -0
- data/lib/aidp/watch/runner.rb +29 -18
- data/lib/aidp/watch.rb +5 -7
- data/lib/aidp/workstream_cleanup.rb +0 -2
- data/lib/aidp/workstream_executor.rb +0 -4
- data/lib/aidp/worktree.rb +0 -1
- data/lib/aidp.rb +21 -106
- metadata +72 -36
- data/lib/aidp/config/paths.rb +0 -131
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module Harness
|
|
5
|
+
# Value object representing a generated filter definition
|
|
6
|
+
# Created by AI during configuration, applied deterministically at runtime
|
|
7
|
+
#
|
|
8
|
+
# @example Usage
|
|
9
|
+
# definition = FilterDefinition.new(
|
|
10
|
+
# tool_name: "pytest",
|
|
11
|
+
# summary_patterns: ["\\d+ passed", "\\d+ failed"],
|
|
12
|
+
# failure_section_start: "=+ FAILURES =+",
|
|
13
|
+
# failure_section_end: "=+ short test summary",
|
|
14
|
+
# error_patterns: ["AssertionError", "Error:"],
|
|
15
|
+
# location_patterns: ["\\S+\\.py:\\d+"],
|
|
16
|
+
# noise_patterns: ["^\\s*$", "^platform "]
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# @see AIFilterFactory for generation
|
|
20
|
+
# @see GeneratedFilterStrategy for runtime application
|
|
21
|
+
class FilterDefinition
|
|
22
|
+
attr_reader :tool_name, :tool_command, :summary_patterns, :failure_section_start,
|
|
23
|
+
:failure_section_end, :error_section_start, :error_section_end,
|
|
24
|
+
:error_patterns, :location_patterns, :noise_patterns,
|
|
25
|
+
:important_patterns, :context_lines, :created_at
|
|
26
|
+
|
|
27
|
+
# Initialize a filter definition
|
|
28
|
+
#
|
|
29
|
+
# @param tool_name [String] Human-readable tool name (e.g., "pytest", "eslint")
|
|
30
|
+
# @param tool_command [String, nil] The command used to run the tool
|
|
31
|
+
# @param summary_patterns [Array<String>] Regex patterns that match summary lines
|
|
32
|
+
# @param failure_section_start [String, nil] Regex pattern marking start of failures section
|
|
33
|
+
# @param failure_section_end [String, nil] Regex pattern marking end of failures section
|
|
34
|
+
# @param error_section_start [String, nil] Regex pattern marking start of errors section
|
|
35
|
+
# @param error_section_end [String, nil] Regex pattern marking end of errors section
|
|
36
|
+
# @param error_patterns [Array<String>] Regex patterns that match error indicators
|
|
37
|
+
# @param location_patterns [Array<String>] Regex patterns that extract file:line locations
|
|
38
|
+
# @param noise_patterns [Array<String>] Regex patterns for lines to filter out
|
|
39
|
+
# @param important_patterns [Array<String>] Regex patterns for lines to always keep
|
|
40
|
+
# @param context_lines [Integer] Number of context lines around failures
|
|
41
|
+
# @param created_at [Time, nil] When this definition was generated
|
|
42
|
+
def initialize(
|
|
43
|
+
tool_name:,
|
|
44
|
+
tool_command: nil,
|
|
45
|
+
summary_patterns: [],
|
|
46
|
+
failure_section_start: nil,
|
|
47
|
+
failure_section_end: nil,
|
|
48
|
+
error_section_start: nil,
|
|
49
|
+
error_section_end: nil,
|
|
50
|
+
error_patterns: [],
|
|
51
|
+
location_patterns: [],
|
|
52
|
+
noise_patterns: [],
|
|
53
|
+
important_patterns: [],
|
|
54
|
+
context_lines: 3,
|
|
55
|
+
created_at: nil
|
|
56
|
+
)
|
|
57
|
+
@tool_name = tool_name
|
|
58
|
+
@tool_command = tool_command
|
|
59
|
+
@summary_patterns = compile_patterns(summary_patterns)
|
|
60
|
+
@failure_section_start = compile_pattern(failure_section_start)
|
|
61
|
+
@failure_section_end = compile_pattern(failure_section_end)
|
|
62
|
+
@error_section_start = compile_pattern(error_section_start)
|
|
63
|
+
@error_section_end = compile_pattern(error_section_end)
|
|
64
|
+
@error_patterns = compile_patterns(error_patterns)
|
|
65
|
+
@location_patterns = compile_patterns(location_patterns)
|
|
66
|
+
@noise_patterns = compile_patterns(noise_patterns)
|
|
67
|
+
@important_patterns = compile_patterns(important_patterns)
|
|
68
|
+
@context_lines = context_lines
|
|
69
|
+
@created_at = created_at || Time.now
|
|
70
|
+
|
|
71
|
+
freeze
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Create from a hash (e.g., loaded from YAML config)
|
|
75
|
+
#
|
|
76
|
+
# @param hash [Hash] Definition data with string or symbol keys
|
|
77
|
+
# @return [FilterDefinition]
|
|
78
|
+
def self.from_hash(hash)
|
|
79
|
+
hash = hash.transform_keys(&:to_sym)
|
|
80
|
+
|
|
81
|
+
new(
|
|
82
|
+
tool_name: hash[:tool_name] || "unknown",
|
|
83
|
+
tool_command: hash[:tool_command],
|
|
84
|
+
summary_patterns: Array(hash[:summary_patterns]),
|
|
85
|
+
failure_section_start: hash[:failure_section_start],
|
|
86
|
+
failure_section_end: hash[:failure_section_end],
|
|
87
|
+
error_section_start: hash[:error_section_start],
|
|
88
|
+
error_section_end: hash[:error_section_end],
|
|
89
|
+
error_patterns: Array(hash[:error_patterns]),
|
|
90
|
+
location_patterns: Array(hash[:location_patterns]),
|
|
91
|
+
noise_patterns: Array(hash[:noise_patterns]),
|
|
92
|
+
important_patterns: Array(hash[:important_patterns]),
|
|
93
|
+
context_lines: hash[:context_lines] || 3,
|
|
94
|
+
created_at: hash[:created_at] ? Time.parse(hash[:created_at].to_s) : nil
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Convert to hash for serialization (e.g., saving to YAML)
|
|
99
|
+
#
|
|
100
|
+
# @return [Hash] Serializable representation
|
|
101
|
+
def to_h
|
|
102
|
+
{
|
|
103
|
+
tool_name: @tool_name,
|
|
104
|
+
tool_command: @tool_command,
|
|
105
|
+
summary_patterns: patterns_to_strings(@summary_patterns),
|
|
106
|
+
failure_section_start: pattern_to_string(@failure_section_start),
|
|
107
|
+
failure_section_end: pattern_to_string(@failure_section_end),
|
|
108
|
+
error_section_start: pattern_to_string(@error_section_start),
|
|
109
|
+
error_section_end: pattern_to_string(@error_section_end),
|
|
110
|
+
error_patterns: patterns_to_strings(@error_patterns),
|
|
111
|
+
location_patterns: patterns_to_strings(@location_patterns),
|
|
112
|
+
noise_patterns: patterns_to_strings(@noise_patterns),
|
|
113
|
+
important_patterns: patterns_to_strings(@important_patterns),
|
|
114
|
+
context_lines: @context_lines,
|
|
115
|
+
created_at: @created_at&.iso8601
|
|
116
|
+
}.compact
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Check if this definition has failure section markers
|
|
120
|
+
#
|
|
121
|
+
# @return [Boolean]
|
|
122
|
+
def has_failure_section?
|
|
123
|
+
!@failure_section_start.nil?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Check if this definition has error section markers
|
|
127
|
+
#
|
|
128
|
+
# @return [Boolean]
|
|
129
|
+
def has_error_section?
|
|
130
|
+
!@error_section_start.nil?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Check if a line matches any summary pattern
|
|
134
|
+
#
|
|
135
|
+
# @param line [String] Line to check
|
|
136
|
+
# @return [Boolean]
|
|
137
|
+
def summary_line?(line)
|
|
138
|
+
@summary_patterns.any? { |pattern| line.match?(pattern) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Check if a line matches any error pattern
|
|
142
|
+
#
|
|
143
|
+
# @param line [String] Line to check
|
|
144
|
+
# @return [Boolean]
|
|
145
|
+
def error_line?(line)
|
|
146
|
+
@error_patterns.any? { |pattern| line.match?(pattern) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Check if a line should be filtered as noise
|
|
150
|
+
#
|
|
151
|
+
# @param line [String] Line to check
|
|
152
|
+
# @return [Boolean]
|
|
153
|
+
def noise_line?(line)
|
|
154
|
+
@noise_patterns.any? { |pattern| line.match?(pattern) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Check if a line should always be kept
|
|
158
|
+
#
|
|
159
|
+
# @param line [String] Line to check
|
|
160
|
+
# @return [Boolean]
|
|
161
|
+
def important_line?(line)
|
|
162
|
+
@important_patterns.any? { |pattern| line.match?(pattern) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Extract file locations from a line
|
|
166
|
+
#
|
|
167
|
+
# @param line [String] Line to extract locations from
|
|
168
|
+
# @return [Array<String>] Extracted locations
|
|
169
|
+
def extract_locations(line)
|
|
170
|
+
@location_patterns.flat_map do |pattern|
|
|
171
|
+
line.scan(pattern).flatten
|
|
172
|
+
end.compact.uniq
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Equality based on all attributes
|
|
176
|
+
def ==(other)
|
|
177
|
+
return false unless other.is_a?(FilterDefinition)
|
|
178
|
+
|
|
179
|
+
to_h == other.to_h
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
alias_method :eql?, :==
|
|
183
|
+
|
|
184
|
+
def hash
|
|
185
|
+
to_h.hash
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
def compile_patterns(strings)
|
|
191
|
+
Array(strings).map { |s| compile_pattern(s) }.compact
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def compile_pattern(string)
|
|
195
|
+
return nil if string.nil? || string.empty?
|
|
196
|
+
Regexp.new(string, Regexp::IGNORECASE)
|
|
197
|
+
rescue RegexpError => e
|
|
198
|
+
Aidp.log_warn("filter_definition", "Invalid regex pattern",
|
|
199
|
+
pattern: string, error: e.message)
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def patterns_to_strings(patterns)
|
|
204
|
+
patterns.map { |p| p&.source }.compact
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def pattern_to_string(pattern)
|
|
208
|
+
pattern&.source
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "filter_strategy"
|
|
4
|
+
require_relative "filter_definition"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Harness
|
|
8
|
+
# Deterministic filter strategy using AI-generated patterns
|
|
9
|
+
#
|
|
10
|
+
# This strategy applies a FilterDefinition at runtime without any AI calls.
|
|
11
|
+
# The patterns were generated by AIFilterFactory during configuration.
|
|
12
|
+
#
|
|
13
|
+
# @example Usage
|
|
14
|
+
# definition = FilterDefinition.from_hash(config[:filter_definitions][:pytest])
|
|
15
|
+
# strategy = GeneratedFilterStrategy.new(definition)
|
|
16
|
+
# filtered = strategy.filter(raw_output, filter_instance)
|
|
17
|
+
#
|
|
18
|
+
# @see FilterDefinition for the pattern specification
|
|
19
|
+
# @see AIFilterFactory for how definitions are generated
|
|
20
|
+
class GeneratedFilterStrategy < FilterStrategy
|
|
21
|
+
attr_reader :definition
|
|
22
|
+
|
|
23
|
+
# Initialize with a filter definition
|
|
24
|
+
#
|
|
25
|
+
# @param definition [FilterDefinition] The AI-generated filter definition
|
|
26
|
+
def initialize(definition)
|
|
27
|
+
@definition = definition
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Filter output using the definition's patterns
|
|
31
|
+
#
|
|
32
|
+
# @param output [String] Raw output to filter
|
|
33
|
+
# @param filter_instance [OutputFilter] Filter instance with mode and config
|
|
34
|
+
# @return [String] Filtered output
|
|
35
|
+
def filter(output, filter_instance)
|
|
36
|
+
return output if output.nil? || output.empty?
|
|
37
|
+
|
|
38
|
+
case filter_instance.mode
|
|
39
|
+
when :failures_only
|
|
40
|
+
extract_failures_only(output)
|
|
41
|
+
when :minimal
|
|
42
|
+
extract_minimal(output)
|
|
43
|
+
else
|
|
44
|
+
output
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Extract failures and summary for failures_only mode
|
|
51
|
+
def extract_failures_only(output)
|
|
52
|
+
lines = output.lines
|
|
53
|
+
parts = []
|
|
54
|
+
|
|
55
|
+
# Extract summary lines
|
|
56
|
+
summary_lines = extract_summary_lines(lines)
|
|
57
|
+
if summary_lines.any?
|
|
58
|
+
parts << "#{@definition.tool_name} Summary:"
|
|
59
|
+
parts.concat(summary_lines.map(&:strip))
|
|
60
|
+
parts << ""
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Extract failure section if markers defined
|
|
64
|
+
if @definition.has_failure_section?
|
|
65
|
+
failure_content = extract_section(lines,
|
|
66
|
+
@definition.failure_section_start,
|
|
67
|
+
@definition.failure_section_end)
|
|
68
|
+
if failure_content.any?
|
|
69
|
+
parts << "Failures:"
|
|
70
|
+
parts.concat(failure_content)
|
|
71
|
+
parts << ""
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Extract error section if different from failures
|
|
76
|
+
if @definition.has_error_section?
|
|
77
|
+
error_content = extract_section(lines,
|
|
78
|
+
@definition.error_section_start,
|
|
79
|
+
@definition.error_section_end)
|
|
80
|
+
if error_content.any?
|
|
81
|
+
parts << "Errors:"
|
|
82
|
+
parts.concat(error_content)
|
|
83
|
+
parts << ""
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# If no section markers are defined, extract lines with error patterns
|
|
88
|
+
# This handles tools that don't have structured failure sections
|
|
89
|
+
unless @definition.has_failure_section? || @definition.has_error_section?
|
|
90
|
+
error_lines = extract_error_lines(lines)
|
|
91
|
+
if error_lines.any?
|
|
92
|
+
parts << "Issues:"
|
|
93
|
+
parts.concat(error_lines)
|
|
94
|
+
parts << ""
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
result = parts.join("\n")
|
|
99
|
+
result.empty? ? output : result
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Extract minimal information (summary + locations only)
|
|
103
|
+
def extract_minimal(output)
|
|
104
|
+
lines = output.lines
|
|
105
|
+
parts = []
|
|
106
|
+
|
|
107
|
+
# Extract summary
|
|
108
|
+
summary_lines = extract_summary_lines(lines)
|
|
109
|
+
parts.concat(summary_lines.map(&:strip))
|
|
110
|
+
|
|
111
|
+
# Extract all file locations
|
|
112
|
+
locations = extract_all_locations(lines)
|
|
113
|
+
if locations.any?
|
|
114
|
+
parts << ""
|
|
115
|
+
parts << "Locations:"
|
|
116
|
+
parts.concat(locations.uniq.map { |loc| " #{loc}" })
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
parts.join("\n")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Extract lines matching summary patterns
|
|
123
|
+
def extract_summary_lines(lines)
|
|
124
|
+
lines.select { |line| @definition.summary_line?(line) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Extract content between section markers
|
|
128
|
+
def extract_section(lines, start_pattern, end_pattern)
|
|
129
|
+
return [] unless start_pattern
|
|
130
|
+
|
|
131
|
+
content = []
|
|
132
|
+
in_section = false
|
|
133
|
+
|
|
134
|
+
lines.each do |line|
|
|
135
|
+
if !in_section && line.match?(start_pattern)
|
|
136
|
+
in_section = true
|
|
137
|
+
content << line unless @definition.noise_line?(line)
|
|
138
|
+
next
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
if in_section
|
|
142
|
+
# Check for section end
|
|
143
|
+
if end_pattern && line.match?(end_pattern)
|
|
144
|
+
in_section = false
|
|
145
|
+
next
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Skip noise lines but keep important ones
|
|
149
|
+
if @definition.important_line?(line) || !@definition.noise_line?(line)
|
|
150
|
+
content << line
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
content.map(&:rstrip)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Extract lines with error patterns (when no sections defined)
|
|
159
|
+
def extract_error_lines(lines)
|
|
160
|
+
result = []
|
|
161
|
+
context_buffer = []
|
|
162
|
+
context_lines = @definition.context_lines
|
|
163
|
+
|
|
164
|
+
lines.each_with_index do |line, index|
|
|
165
|
+
# Keep track of context
|
|
166
|
+
context_buffer << line
|
|
167
|
+
context_buffer.shift if context_buffer.size > context_lines
|
|
168
|
+
|
|
169
|
+
# Check for error indicators
|
|
170
|
+
if @definition.error_line?(line) || @definition.important_line?(line)
|
|
171
|
+
# Add preceding context
|
|
172
|
+
result.concat(context_buffer[0...-1]) if context_buffer.size > 1
|
|
173
|
+
|
|
174
|
+
# Add the error line
|
|
175
|
+
result << line
|
|
176
|
+
|
|
177
|
+
# Add following context
|
|
178
|
+
following = lines[index + 1, context_lines] || []
|
|
179
|
+
following.each do |following_line|
|
|
180
|
+
break if @definition.summary_line?(following_line)
|
|
181
|
+
result << following_line unless @definition.noise_line?(following_line)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
context_buffer.clear
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
result.uniq.map(&:rstrip)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Extract all file locations from output
|
|
192
|
+
def extract_all_locations(lines)
|
|
193
|
+
lines.flat_map { |line| @definition.extract_locations(line) }
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "output_filter_config"
|
|
4
|
+
|
|
3
5
|
module Aidp
|
|
4
6
|
module Harness
|
|
5
7
|
# Filters test and linter output to reduce token consumption
|
|
6
|
-
#
|
|
8
|
+
#
|
|
9
|
+
# Supports two filtering approaches:
|
|
10
|
+
# 1. AI-generated FilterDefinitions (preferred) - created once during config,
|
|
11
|
+
# applied deterministically at runtime for ANY tool
|
|
12
|
+
# 2. Built-in RSpec strategy (fallback) - for backward compatibility
|
|
13
|
+
#
|
|
14
|
+
# @example Using AI-generated filter
|
|
15
|
+
# definition = FilterDefinition.from_hash(config[:filter_definitions][:pytest])
|
|
16
|
+
# filter = OutputFilter.new(mode: :failures_only, filter_definition: definition)
|
|
17
|
+
# filtered = filter.filter(output)
|
|
18
|
+
#
|
|
19
|
+
# @see AIFilterFactory for generating filter definitions
|
|
20
|
+
# @see FilterDefinition for the definition format
|
|
7
21
|
class OutputFilter
|
|
8
22
|
# Output modes
|
|
9
23
|
MODES = {
|
|
@@ -12,30 +26,32 @@ module Aidp
|
|
|
12
26
|
minimal: :minimal # Minimal failure info + summary
|
|
13
27
|
}.freeze
|
|
14
28
|
|
|
15
|
-
# @param config [Hash] Configuration options
|
|
29
|
+
# @param config [OutputFilterConfig, Hash] Configuration options
|
|
16
30
|
# @option config [Symbol] :mode Output mode (:full, :failures_only, :minimal)
|
|
17
31
|
# @option config [Boolean] :include_context Include surrounding lines
|
|
18
32
|
# @option config [Integer] :context_lines Number of context lines
|
|
19
33
|
# @option config [Integer] :max_lines Maximum output lines
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@
|
|
23
|
-
@
|
|
24
|
-
@
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
# @param filter_definition [FilterDefinition, nil] AI-generated filter patterns
|
|
35
|
+
def initialize(config = {}, filter_definition: nil)
|
|
36
|
+
@config = normalize_config(config)
|
|
37
|
+
@mode = @config.mode
|
|
38
|
+
@include_context = @config.include_context
|
|
39
|
+
@context_lines = @config.context_lines
|
|
40
|
+
@max_lines = @config.max_lines
|
|
41
|
+
@filter_definition = filter_definition
|
|
27
42
|
|
|
28
43
|
Aidp.log_debug("output_filter", "initialized",
|
|
29
44
|
mode: @mode,
|
|
30
45
|
include_context: @include_context,
|
|
31
|
-
max_lines: @max_lines
|
|
46
|
+
max_lines: @max_lines,
|
|
47
|
+
has_definition: !@filter_definition.nil?)
|
|
32
48
|
rescue NameError
|
|
33
49
|
# Logging infrastructure not available in some tests
|
|
34
50
|
end
|
|
35
51
|
|
|
36
52
|
# Filter output based on framework and mode
|
|
37
53
|
# @param output [String] Raw output
|
|
38
|
-
# @param framework [Symbol] Framework identifier
|
|
54
|
+
# @param framework [Symbol] Framework identifier (:rspec, :minitest, :jest, :pytest, :unknown)
|
|
39
55
|
# @return [String] Filtered output
|
|
40
56
|
def filter(output, framework: :unknown)
|
|
41
57
|
return output if @mode == :full
|
|
@@ -43,6 +59,7 @@ module Aidp
|
|
|
43
59
|
|
|
44
60
|
Aidp.log_debug("output_filter", "filtering_start",
|
|
45
61
|
framework: framework,
|
|
62
|
+
mode: @mode,
|
|
46
63
|
input_lines: output.lines.count)
|
|
47
64
|
|
|
48
65
|
strategy = strategy_for_framework(framework)
|
|
@@ -51,6 +68,7 @@ module Aidp
|
|
|
51
68
|
truncated = truncate_if_needed(filtered)
|
|
52
69
|
|
|
53
70
|
Aidp.log_debug("output_filter", "filtering_complete",
|
|
71
|
+
framework: framework,
|
|
54
72
|
output_lines: truncated.lines.count,
|
|
55
73
|
reduction: reduction_stats(output, truncated))
|
|
56
74
|
|
|
@@ -80,31 +98,38 @@ module Aidp
|
|
|
80
98
|
end
|
|
81
99
|
|
|
82
100
|
# Accessors for strategy use
|
|
83
|
-
attr_reader :mode, :include_context, :context_lines, :max_lines
|
|
101
|
+
attr_reader :mode, :include_context, :context_lines, :max_lines, :config, :filter_definition
|
|
84
102
|
|
|
85
103
|
private
|
|
86
104
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
105
|
+
def normalize_config(config)
|
|
106
|
+
case config
|
|
107
|
+
when OutputFilterConfig
|
|
108
|
+
config
|
|
109
|
+
when Hash
|
|
110
|
+
OutputFilterConfig.from_hash(config)
|
|
111
|
+
when nil
|
|
112
|
+
OutputFilterConfig.new
|
|
113
|
+
else
|
|
114
|
+
raise ArgumentError, "Config must be an OutputFilterConfig or Hash, got: #{config.class}"
|
|
90
115
|
end
|
|
91
116
|
end
|
|
92
117
|
|
|
93
118
|
def strategy_for_framework(framework)
|
|
119
|
+
# Use AI-generated filter definition if available (preferred)
|
|
120
|
+
if @filter_definition
|
|
121
|
+
require_relative "generated_filter_strategy"
|
|
122
|
+
return GeneratedFilterStrategy.new(@filter_definition)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Fall back to built-in strategies for backward compatibility
|
|
94
126
|
case framework
|
|
95
127
|
when :rspec
|
|
96
128
|
require_relative "rspec_filter_strategy"
|
|
97
129
|
RSpecFilterStrategy.new
|
|
98
|
-
when :minitest
|
|
99
|
-
require_relative "generic_filter_strategy"
|
|
100
|
-
GenericFilterStrategy.new
|
|
101
|
-
when :jest
|
|
102
|
-
require_relative "generic_filter_strategy"
|
|
103
|
-
GenericFilterStrategy.new
|
|
104
|
-
when :pytest
|
|
105
|
-
require_relative "generic_filter_strategy"
|
|
106
|
-
GenericFilterStrategy.new
|
|
107
130
|
else
|
|
131
|
+
# Use generic strategy for all other frameworks
|
|
132
|
+
# Users should generate a FilterDefinition for better results
|
|
108
133
|
require_relative "generic_filter_strategy"
|
|
109
134
|
GenericFilterStrategy.new
|
|
110
135
|
end
|
|
@@ -123,7 +148,7 @@ module Aidp
|
|
|
123
148
|
def reduction_stats(input, output)
|
|
124
149
|
input_size = input.bytesize
|
|
125
150
|
output_size = output.bytesize
|
|
126
|
-
reduction = ((input_size - output_size).to_f / input_size * 100).round(1)
|
|
151
|
+
reduction = input_size.zero? ? 0 : ((input_size - output_size).to_f / input_size * 100).round(1)
|
|
127
152
|
|
|
128
153
|
{
|
|
129
154
|
input_bytes: input_size,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module Harness
|
|
5
|
+
# Value object for output filter configuration
|
|
6
|
+
# Provides validation and type-safe access to filtering options
|
|
7
|
+
class OutputFilterConfig
|
|
8
|
+
VALID_MODES = %i[full failures_only minimal].freeze
|
|
9
|
+
DEFAULT_MODE = :full
|
|
10
|
+
DEFAULT_INCLUDE_CONTEXT = true
|
|
11
|
+
DEFAULT_CONTEXT_LINES = 3
|
|
12
|
+
DEFAULT_MAX_LINES = 500
|
|
13
|
+
MIN_CONTEXT_LINES = 0
|
|
14
|
+
MAX_CONTEXT_LINES = 20
|
|
15
|
+
MIN_MAX_LINES = 10
|
|
16
|
+
MAX_MAX_LINES = 10_000
|
|
17
|
+
|
|
18
|
+
attr_reader :mode, :include_context, :context_lines, :max_lines
|
|
19
|
+
|
|
20
|
+
# Create a new OutputFilterConfig
|
|
21
|
+
# @param mode [Symbol] Output mode (:full, :failures_only, :minimal)
|
|
22
|
+
# @param include_context [Boolean] Include surrounding lines
|
|
23
|
+
# @param context_lines [Integer] Number of context lines (0-20)
|
|
24
|
+
# @param max_lines [Integer] Maximum output lines (10-10000)
|
|
25
|
+
# @raise [ArgumentError] If any parameter is invalid
|
|
26
|
+
def initialize(mode: DEFAULT_MODE, include_context: DEFAULT_INCLUDE_CONTEXT,
|
|
27
|
+
context_lines: DEFAULT_CONTEXT_LINES, max_lines: DEFAULT_MAX_LINES)
|
|
28
|
+
@mode = validate_mode(mode)
|
|
29
|
+
@include_context = validate_boolean(include_context, "include_context")
|
|
30
|
+
@context_lines = validate_context_lines(context_lines)
|
|
31
|
+
@max_lines = validate_max_lines(max_lines)
|
|
32
|
+
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Create from a hash (useful for configuration loading)
|
|
37
|
+
# @param hash [Hash] Configuration hash
|
|
38
|
+
# @return [OutputFilterConfig] New config instance
|
|
39
|
+
def self.from_hash(hash)
|
|
40
|
+
hash = hash.transform_keys(&:to_sym) if hash.respond_to?(:transform_keys)
|
|
41
|
+
|
|
42
|
+
new(
|
|
43
|
+
mode: hash[:mode] || DEFAULT_MODE,
|
|
44
|
+
include_context: hash.fetch(:include_context, DEFAULT_INCLUDE_CONTEXT),
|
|
45
|
+
context_lines: hash[:context_lines] || DEFAULT_CONTEXT_LINES,
|
|
46
|
+
max_lines: hash[:max_lines] || DEFAULT_MAX_LINES
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Convert to hash (useful for serialization)
|
|
51
|
+
# @return [Hash] Configuration as hash
|
|
52
|
+
def to_h
|
|
53
|
+
{
|
|
54
|
+
mode: @mode,
|
|
55
|
+
include_context: @include_context,
|
|
56
|
+
context_lines: @context_lines,
|
|
57
|
+
max_lines: @max_lines
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if filtering is enabled
|
|
62
|
+
# @return [Boolean] True if mode is not :full
|
|
63
|
+
def filtering_enabled?
|
|
64
|
+
@mode != :full
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Compare with another config
|
|
68
|
+
# @param other [OutputFilterConfig] Other config to compare
|
|
69
|
+
# @return [Boolean] True if equal
|
|
70
|
+
def ==(other)
|
|
71
|
+
return false unless other.is_a?(OutputFilterConfig)
|
|
72
|
+
|
|
73
|
+
@mode == other.mode &&
|
|
74
|
+
@include_context == other.include_context &&
|
|
75
|
+
@context_lines == other.context_lines &&
|
|
76
|
+
@max_lines == other.max_lines
|
|
77
|
+
end
|
|
78
|
+
alias_method :eql?, :==
|
|
79
|
+
|
|
80
|
+
# Hash for use in Hash/Set
|
|
81
|
+
def hash
|
|
82
|
+
[@mode, @include_context, @context_lines, @max_lines].hash
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def validate_mode(mode)
|
|
88
|
+
mode = mode.to_sym if mode.respond_to?(:to_sym)
|
|
89
|
+
|
|
90
|
+
unless VALID_MODES.include?(mode)
|
|
91
|
+
raise ArgumentError,
|
|
92
|
+
"Invalid mode: #{mode.inspect}. Must be one of #{VALID_MODES.join(", ")}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
mode
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def validate_boolean(value, name)
|
|
99
|
+
unless [true, false].include?(value)
|
|
100
|
+
raise ArgumentError, "#{name} must be a boolean, got: #{value.inspect}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
value
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def validate_context_lines(value)
|
|
107
|
+
value = value.to_i if value.respond_to?(:to_i) && !value.is_a?(Integer)
|
|
108
|
+
|
|
109
|
+
unless value.is_a?(Integer) && value >= MIN_CONTEXT_LINES && value <= MAX_CONTEXT_LINES
|
|
110
|
+
raise ArgumentError,
|
|
111
|
+
"context_lines must be an integer between #{MIN_CONTEXT_LINES} and #{MAX_CONTEXT_LINES}, got: #{value.inspect}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
value
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def validate_max_lines(value)
|
|
118
|
+
value = value.to_i if value.respond_to?(:to_i) && !value.is_a?(Integer)
|
|
119
|
+
|
|
120
|
+
unless value.is_a?(Integer) && value >= MIN_MAX_LINES && value <= MAX_MAX_LINES
|
|
121
|
+
raise ArgumentError,
|
|
122
|
+
"max_lines must be an integer between #{MIN_MAX_LINES} and #{MAX_MAX_LINES}, got: #{value.inspect}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
value
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|