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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aidp/analyze/feature_analyzer.rb +322 -320
  3. data/lib/aidp/auto_update/coordinator.rb +97 -7
  4. data/lib/aidp/auto_update.rb +0 -12
  5. data/lib/aidp/cli/devcontainer_commands.rb +0 -5
  6. data/lib/aidp/cli.rb +2 -1
  7. data/lib/aidp/comment_consolidator.rb +78 -0
  8. data/lib/aidp/concurrency.rb +0 -3
  9. data/lib/aidp/config.rb +0 -1
  10. data/lib/aidp/config_paths.rb +71 -0
  11. data/lib/aidp/execute/work_loop_runner.rb +324 -15
  12. data/lib/aidp/harness/ai_filter_factory.rb +285 -0
  13. data/lib/aidp/harness/config_schema.rb +97 -1
  14. data/lib/aidp/harness/config_validator.rb +1 -1
  15. data/lib/aidp/harness/configuration.rb +61 -5
  16. data/lib/aidp/harness/filter_definition.rb +212 -0
  17. data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
  18. data/lib/aidp/harness/output_filter.rb +50 -25
  19. data/lib/aidp/harness/output_filter_config.rb +129 -0
  20. data/lib/aidp/harness/provider_manager.rb +90 -2
  21. data/lib/aidp/harness/runner.rb +0 -11
  22. data/lib/aidp/harness/test_runner.rb +179 -41
  23. data/lib/aidp/harness/thinking_depth_manager.rb +16 -0
  24. data/lib/aidp/harness/ui/navigation/submenu.rb +0 -2
  25. data/lib/aidp/loader.rb +195 -0
  26. data/lib/aidp/metadata/compiler.rb +29 -17
  27. data/lib/aidp/metadata/query.rb +1 -1
  28. data/lib/aidp/metadata/scanner.rb +8 -1
  29. data/lib/aidp/metadata/tool_metadata.rb +13 -13
  30. data/lib/aidp/metadata/validator.rb +10 -0
  31. data/lib/aidp/metadata.rb +16 -0
  32. data/lib/aidp/pr_worktree_manager.rb +2 -2
  33. data/lib/aidp/provider_manager.rb +1 -7
  34. data/lib/aidp/setup/wizard.rb +279 -9
  35. data/lib/aidp/skills.rb +0 -5
  36. data/lib/aidp/storage/csv_storage.rb +3 -0
  37. data/lib/aidp/style_guide/selector.rb +360 -0
  38. data/lib/aidp/tooling_detector.rb +283 -16
  39. data/lib/aidp/version.rb +1 -1
  40. data/lib/aidp/watch/change_request_processor.rb +152 -14
  41. data/lib/aidp/watch/repository_client.rb +41 -0
  42. data/lib/aidp/watch/runner.rb +29 -18
  43. data/lib/aidp/watch.rb +5 -7
  44. data/lib/aidp/workstream_cleanup.rb +0 -2
  45. data/lib/aidp/workstream_executor.rb +0 -4
  46. data/lib/aidp/worktree.rb +0 -1
  47. data/lib/aidp.rb +21 -106
  48. metadata +72 -36
  49. data/lib/aidp/config/paths.rb +0 -131
@@ -0,0 +1,360 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module StyleGuide
5
+ # Deterministic selector for STYLE_GUIDE sections based on task context
6
+ #
7
+ # This class provides intelligent section selection from STYLE_GUIDE.md
8
+ # based on keywords in the task context. For providers with their own
9
+ # instruction files (Claude, GitHub Copilot), it skips style guide injection.
10
+ #
11
+ # @example Basic usage
12
+ # selector = Selector.new(project_dir: "/path/to/project")
13
+ # content = selector.select_sections(keywords: ["testing", "error"])
14
+ #
15
+ # @example Check if provider needs style guide
16
+ # selector.provider_needs_style_guide?("cursor") # => true
17
+ # selector.provider_needs_style_guide?("claude") # => false
18
+ class Selector
19
+ # Providers that have their own instruction files and don't need
20
+ # the style guide injected into prompts
21
+ PROVIDERS_WITH_INSTRUCTION_FILES = %w[
22
+ claude
23
+ anthropic
24
+ github_copilot
25
+ ].freeze
26
+
27
+ # Mapping of keywords/topics to STYLE_GUIDE.md line ranges
28
+ # Each entry is [start_line, end_line, description]
29
+ SECTION_MAPPING = {
30
+ # Core Engineering
31
+ "code_organization" => [[25, 117, "Code Organization"]],
32
+ "class" => [[25, 117, "Code Organization"], [217, 236, "Sandi Metz Rules"]],
33
+ "method" => [[224, 229, "Method Design"], [217, 236, "Sandi Metz Rules"]],
34
+ "composition" => [[29, 35, "Composition over Inheritance"]],
35
+ "inheritance" => [[29, 35, "Composition over Inheritance"]],
36
+ "small_objects" => [[25, 117, "Code Organization"]],
37
+ "single_responsibility" => [[25, 117, "Code Organization"]],
38
+
39
+ # Sandi Metz
40
+ "sandi_metz" => [[217, 236, "Sandi Metz Rules"]],
41
+ "parameter" => [[231, 236, "Parameter Limits"]],
42
+
43
+ # Ruby Conventions
44
+ "ruby" => [[259, 278, "Ruby Conventions"], [446, 498, "Ruby Version Management"]],
45
+ "naming" => [[51, 56, "Naming Conventions"]],
46
+ "convention" => [[259, 278, "Ruby Conventions"]],
47
+ "style" => [[259, 278, "Ruby Conventions"]],
48
+ "require" => [[263, 266, "Require Practices"]],
49
+ "mise" => [[446, 498, "Ruby Version Management"]],
50
+ "version" => [[446, 498, "Ruby Version Management"]],
51
+
52
+ # Feature Organization
53
+ "feature" => [[58, 116, "Feature Organization by Purpose"]],
54
+ "workflow" => [[58, 116, "Feature Organization by Purpose"]],
55
+ "template" => [[118, 210, "Template/Skill Separation"]],
56
+ "skill" => [[118, 210, "Template/Skill Separation"]],
57
+
58
+ # Logging
59
+ "logging" => [[287, 430, "Logging Practices"]],
60
+ "log" => [[287, 430, "Logging Practices"]],
61
+ "debug" => [[287, 430, "Logging Practices"]],
62
+
63
+ # ZFC - Zero Framework Cognition
64
+ "zfc" => [[500, 797, "Zero Framework Cognition (ZFC)"]],
65
+ "zero_framework" => [[500, 797, "Zero Framework Cognition (ZFC)"]],
66
+ "ai_decision" => [[500, 797, "Zero Framework Cognition (ZFC)"]],
67
+ "decision_engine" => [[500, 797, "Zero Framework Cognition (ZFC)"]],
68
+
69
+ # AGD - AI-Generated Determinism
70
+ "agd" => [[798, 855, "AI-Generated Determinism (AGD)"]],
71
+ "determinism" => [[798, 855, "AI-Generated Determinism (AGD)"]],
72
+ "config_time" => [[798, 855, "AI-Generated Determinism (AGD)"]],
73
+
74
+ # TTY / TUI
75
+ "tty" => [[856, 1105, "TTY Toolkit Guidelines"]],
76
+ "tui" => [[856, 1105, "TTY Toolkit Guidelines"]],
77
+ "ui" => [[856, 1105, "TTY Toolkit Guidelines"]],
78
+ "prompt" => [[856, 1105, "TTY Toolkit Guidelines"], [936, 972, "TTY Output Practices"]],
79
+ "progress" => [[856, 1105, "TTY Toolkit Guidelines"]],
80
+ "spinner" => [[856, 1105, "TTY Toolkit Guidelines"]],
81
+ "table" => [[856, 1105, "TTY Toolkit Guidelines"]],
82
+ "select" => [[856, 1105, "TTY Toolkit Guidelines"]],
83
+
84
+ # Testing
85
+ "testing" => [[1873, 2112, "Testing Guidelines"]],
86
+ "test" => [[1873, 2112, "Testing Guidelines"], [1550, 1800, "Test Coverage Philosophy"]],
87
+ "spec" => [[1873, 2112, "Testing Guidelines"]],
88
+ "rspec" => [[1873, 2112, "Testing Guidelines"]],
89
+ "mock" => [[1873, 2112, "Testing Guidelines"], [1930, 2012, "Dependency Injection"]],
90
+ "stub" => [[1873, 2112, "Testing Guidelines"]],
91
+ "coverage" => [[1550, 1800, "Test Coverage Philosophy"]],
92
+ "pending" => [[1816, 1870, "Pending Specs Policy"]],
93
+ "expect_script" => [[1173, 1234, "expect Scripts for TUI"]],
94
+ "tmux" => [[1203, 1295, "tmux Testing"]],
95
+ "time_test" => [[1656, 1690, "Time-based Testing"]],
96
+ "fork" => [[1718, 1778, "Forked Process Testing"]],
97
+ "encoding" => [[1780, 1813, "String Encoding"]],
98
+ "dependency_injection" => [[1930, 2012, "Dependency Injection"]],
99
+
100
+ # Error Handling
101
+ "error" => [[2113, 2168, "Error Handling"], [280, 286, "Error Handling Basics"]],
102
+ "exception" => [[2113, 2168, "Error Handling"], [2130, 2140, "Error Class Pattern"]],
103
+ "rescue" => [[280, 286, "Error Handling Basics"], [2113, 2168, "Error Handling"]],
104
+
105
+ # Concurrency
106
+ "concurrency" => [[2170, 2185, "Concurrency & Threads"]],
107
+ "thread" => [[2170, 2185, "Concurrency & Threads"]],
108
+ "async" => [[2170, 2185, "Concurrency & Threads"]],
109
+
110
+ # Performance
111
+ "performance" => [[2206, 2232, "Performance"]],
112
+ "optimization" => [[2206, 2232, "Performance"]],
113
+ "cache" => [[2206, 2232, "Performance"]],
114
+
115
+ # Security
116
+ "security" => [[2233, 2272, "Security & Safety"]],
117
+ "safety" => [[2233, 2272, "Security & Safety"]],
118
+ "validation" => [[2233, 2272, "Security & Safety"]],
119
+
120
+ # Backward Compatibility / Pre-release
121
+ "backward_compatibility" => [[2273, 2472, "Backward Compatibility"]],
122
+ "deprecation" => [[2273, 2472, "Backward Compatibility"]],
123
+ "legacy" => [[2273, 2472, "Backward Compatibility"]],
124
+
125
+ # Commit Hygiene
126
+ "commit" => [[2476, 2482, "Commit Hygiene"]],
127
+ "git" => [[2476, 2482, "Commit Hygiene"]],
128
+
129
+ # Prompt Optimization
130
+ "prompt_optimization" => [[2523, 2854, "Prompt Optimization"]],
131
+ "fragment" => [[2523, 2854, "Prompt Optimization"]],
132
+
133
+ # Task Filing
134
+ "task" => [[2856, 2890, "Task Filing"]],
135
+ "tasklist" => [[2856, 2890, "Task Filing"]]
136
+ }.freeze
137
+
138
+ # Default sections to always include (core rules)
139
+ CORE_SECTIONS = [
140
+ [25, 117, "Code Organization"],
141
+ [217, 236, "Sandi Metz Rules"],
142
+ [287, 430, "Logging Practices"]
143
+ ].freeze
144
+
145
+ attr_reader :project_dir
146
+
147
+ def initialize(project_dir:)
148
+ @project_dir = project_dir
149
+ @style_guide_content = nil
150
+ @style_guide_lines = nil
151
+ end
152
+
153
+ # Check if a provider needs style guide injection
154
+ #
155
+ # @param provider_name [String] Name of the provider
156
+ # @return [Boolean] true if style guide should be injected
157
+ def provider_needs_style_guide?(provider_name)
158
+ return true if provider_name.nil?
159
+
160
+ normalized = provider_name.to_s.downcase.strip
161
+ !PROVIDERS_WITH_INSTRUCTION_FILES.include?(normalized)
162
+ end
163
+
164
+ # Select relevant sections from STYLE_GUIDE.md based on keywords
165
+ #
166
+ # @param keywords [Array<String>] Keywords to match against
167
+ # @param include_core [Boolean] Whether to include core sections
168
+ # @param max_lines [Integer, nil] Maximum lines to return (nil for unlimited)
169
+ # @return [String] Combined content of selected sections
170
+ def select_sections(keywords: [], include_core: true, max_lines: nil)
171
+ Aidp.log_debug("style_guide_selector", "selecting_sections",
172
+ keywords: keywords, include_core: include_core, max_lines: max_lines)
173
+
174
+ return "" unless style_guide_exists?
175
+
176
+ # Gather all matching sections
177
+ sections = gather_sections(keywords, include_core)
178
+
179
+ # Merge overlapping ranges and sort
180
+ merged_ranges = merge_and_sort_ranges(sections)
181
+
182
+ # Extract content from ranges
183
+ content = extract_content(merged_ranges, max_lines)
184
+
185
+ Aidp.log_debug("style_guide_selector", "sections_selected",
186
+ section_count: merged_ranges.size, content_lines: content.lines.count)
187
+
188
+ content
189
+ end
190
+
191
+ # Extract keywords from task context
192
+ #
193
+ # @param context [Hash, String] Task context (description, affected files, etc.)
194
+ # @return [Array<String>] Extracted keywords
195
+ def extract_keywords(context)
196
+ text = context.is_a?(Hash) ? context.values.join(" ") : context.to_s
197
+ text_lower = text.downcase
198
+
199
+ keywords = []
200
+
201
+ SECTION_MAPPING.keys.each do |keyword|
202
+ # Convert keyword format (snake_case to spaces/variations)
203
+ patterns = build_patterns(keyword)
204
+ keywords << keyword if patterns.any? { |p| text_lower.include?(p) }
205
+ end
206
+
207
+ Aidp.log_debug("style_guide_selector", "keywords_extracted",
208
+ input_length: text.length, keywords_found: keywords.size)
209
+
210
+ keywords.uniq
211
+ end
212
+
213
+ # Get all available section names
214
+ #
215
+ # @return [Array<String>] List of all section mapping keys
216
+ def available_keywords
217
+ SECTION_MAPPING.keys.sort
218
+ end
219
+
220
+ # Check if style guide file exists
221
+ #
222
+ # @return [Boolean]
223
+ def style_guide_exists?
224
+ File.exist?(style_guide_path)
225
+ end
226
+
227
+ # Get information about what sections would be selected for given keywords
228
+ #
229
+ # @param keywords [Array<String>] Keywords to check
230
+ # @return [Array<Hash>] Section info with line ranges and descriptions
231
+ def preview_selection(keywords)
232
+ sections = gather_sections(keywords, false)
233
+ merged = merge_and_sort_ranges(sections)
234
+
235
+ merged.map do |start_line, end_line, description|
236
+ {
237
+ start_line: start_line,
238
+ end_line: end_line,
239
+ description: description,
240
+ estimated_lines: end_line - start_line + 1
241
+ }
242
+ end
243
+ end
244
+
245
+ private
246
+
247
+ def style_guide_path
248
+ File.join(@project_dir, "docs", "STYLE_GUIDE.md")
249
+ end
250
+
251
+ def style_guide_lines
252
+ @style_guide_lines ||= begin
253
+ return [] unless style_guide_exists?
254
+
255
+ content = File.read(style_guide_path, encoding: "UTF-8")
256
+ content = content.encode("UTF-8", invalid: :replace, undef: :replace)
257
+ content.lines
258
+ end
259
+ end
260
+
261
+ def gather_sections(keywords, include_core)
262
+ sections = []
263
+
264
+ # Add core sections if requested
265
+ sections.concat(CORE_SECTIONS.dup) if include_core
266
+
267
+ # Add sections matching keywords
268
+ keywords.each do |keyword|
269
+ normalized = keyword.to_s.downcase.gsub(/[^a-z0-9]/, "_")
270
+ if SECTION_MAPPING.key?(normalized)
271
+ sections.concat(SECTION_MAPPING[normalized])
272
+ end
273
+ end
274
+
275
+ sections
276
+ end
277
+
278
+ def merge_and_sort_ranges(sections)
279
+ return [] if sections.empty?
280
+
281
+ # Convert to sortable format and sort by start line
282
+ ranges = sections.map { |start_l, end_l, desc| [start_l.to_i, end_l.to_i, desc] }
283
+ ranges.sort_by!(&:first)
284
+
285
+ # Merge overlapping/adjacent ranges
286
+ merged = []
287
+ current_start, current_end, current_desc = ranges.first
288
+
289
+ ranges[1..].each do |start_l, end_l, desc|
290
+ if start_l <= current_end + 5 # Allow small gaps (5 lines)
291
+ # Extend current range
292
+ current_end = [current_end, end_l].max
293
+ current_desc = "#{current_desc}, #{desc}" unless current_desc.include?(desc)
294
+ else
295
+ # Save current range and start new one
296
+ merged << [current_start, current_end, current_desc]
297
+ current_start = start_l
298
+ current_end = end_l
299
+ current_desc = desc
300
+ end
301
+ end
302
+
303
+ # Don't forget the last range
304
+ merged << [current_start, current_end, current_desc]
305
+
306
+ merged
307
+ end
308
+
309
+ def extract_content(ranges, max_lines)
310
+ return "" if ranges.empty?
311
+
312
+ lines = style_guide_lines
313
+ return "" if lines.empty?
314
+
315
+ parts = []
316
+ total_lines = 0
317
+
318
+ ranges.each do |start_line, end_line, description|
319
+ break if max_lines && total_lines >= max_lines
320
+
321
+ # Adjust for 0-based indexing
322
+ start_idx = [start_line - 1, 0].max
323
+ end_idx = [end_line - 1, lines.length - 1].min
324
+
325
+ section_lines = lines[start_idx..end_idx]
326
+ next if section_lines.nil? || section_lines.empty?
327
+
328
+ # Add section header comment
329
+ parts << "<!-- Section: #{description} (lines #{start_line}-#{end_line}) -->"
330
+ parts << section_lines.join
331
+
332
+ total_lines += section_lines.length
333
+ end
334
+
335
+ content = parts.join("\n")
336
+
337
+ # Trim to max_lines if specified
338
+ if max_lines && content.lines.count > max_lines
339
+ content = content.lines.first(max_lines).join
340
+ end
341
+
342
+ content
343
+ end
344
+
345
+ def build_patterns(keyword)
346
+ patterns = [keyword]
347
+
348
+ # Add variations
349
+ patterns << keyword.tr("_", " ")
350
+ patterns << keyword.tr("_", "-")
351
+
352
+ # Add singular/plural variations for common terms
353
+ patterns << "#{keyword}s" unless keyword.end_with?("s")
354
+ patterns << keyword.chomp("s") if keyword.end_with?("s")
355
+
356
+ patterns.uniq
357
+ end
358
+ end
359
+ end
360
+ end