aidp 0.5.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26282ecc76062d41d599eb9bc8190303bcef16257fb98fa25250eecaf894270a
4
- data.tar.gz: ee50c920a5aa61d2883f906c7bea5d09c6835725c27c359bd2bce7d698f594e2
3
+ metadata.gz: e32a892070257857ac578c965a7aa1bb240a1a40a58f884bc6740a5aa6a57804
4
+ data.tar.gz: cfca9c65bc0d3b5e784fbab8dcff12545982d5bb00057e575dfdfcd77007accf
5
5
  SHA512:
6
- metadata.gz: 9fa312b72678f5bba517ee9add8c9fcd1bc06237f65781ddf429bb687b7f3a85fb3e031ebe992cb768d53885a09648d74c6f9d57282a6f988eeada7994ff534f
7
- data.tar.gz: e15204813d179e130dea804b89dc0a5225e1410af19fc5420ed2e82a3992d19b7a6e9e2054dce0269dce957f1ebee42e6c881f301e8554643f0eff1167a6fd61
6
+ metadata.gz: 99c4b48fce7af4bc8a043549fcb4ba0119dc84af870fdb8b0aae26045c4626e16cbd035df94e8b5d5c638d10a1a26f3a76b7c260647b701a55ef1f795a373ca0
7
+ data.tar.gz: 818054559441866f659d77d5b0e10721822c3aa5abc74010e90f5cbbb13a3d1fe85355fb6e2f82a867e24ed4b24e71670a72370b23c69526bf0400f7a1bc2a6a
data/README.md CHANGED
@@ -98,6 +98,114 @@ AIDP_PROVIDER=anthropic aidp execute next
98
98
  AIDP_LLM_CMD=/usr/local/bin/claude aidp execute next
99
99
  ```
100
100
 
101
+ ## Tree-sitter Static Analysis
102
+
103
+ AIDP includes powerful Tree-sitter-based static analysis capabilities for code.
104
+
105
+ ### Tree-sitter Dependencies
106
+
107
+ The Tree-sitter analysis requires the Tree-sitter system library and pre-compiled language parsers:
108
+
109
+ ```bash
110
+ # Install Tree-sitter system library
111
+ # macOS
112
+ brew install tree-sitter
113
+
114
+ # Ubuntu/Debian
115
+ sudo apt-get install tree-sitter
116
+
117
+ # Or follow the ruby_tree_sitter README for other platforms
118
+ # https://github.com/Faveod/ruby-tree-sitter#installation
119
+
120
+ # Install Tree-sitter parsers
121
+ ./install_tree_sitter_parsers.sh
122
+ ```
123
+
124
+ ### Parser Installation Script
125
+
126
+ The `install_tree_sitter_parsers.sh` script automatically downloads and installs pre-built Tree-sitter parsers:
127
+
128
+ ```bash
129
+ # Make the script executable
130
+ chmod +x install_tree_sitter_parsers.sh
131
+
132
+ # Run the installation script
133
+ ./install_tree_sitter_parsers.sh
134
+ ```
135
+
136
+ The script will:
137
+
138
+ - Detect your OS and architecture (macOS ARM64, Linux x64, etc.)
139
+ - Download the appropriate parser bundle from [Faveod/tree-sitter-parsers](https://github.com/Faveod/tree-sitter-parsers/releases/tag/v4.9)
140
+ - Extract parsers to `.aidp/parsers/` directory
141
+ - Set up the `TREE_SITTER_PARSERS` environment variable
142
+
143
+ ### Environment Setup
144
+
145
+ After running the installation script, make the environment variable permanent:
146
+
147
+ ```bash
148
+ # Add to your shell profile (e.g., ~/.zshrc, ~/.bashrc)
149
+ echo 'export TREE_SITTER_PARSERS="$(pwd)/.aidp/parsers"' >> ~/.zshrc
150
+
151
+ # Reload your shell
152
+ source ~/.zshrc
153
+ ```
154
+
155
+ ### Tree-sitter Analysis Commands
156
+
157
+ ```bash
158
+ # Run Tree-sitter static analysis
159
+ aidp analyze code
160
+
161
+ # Analyze specific languages
162
+ aidp analyze code --langs ruby,javascript,typescript
163
+
164
+ # Use multiple threads for faster analysis
165
+ aidp analyze code --threads 8
166
+
167
+ # Rebuild knowledge base from scratch
168
+ aidp analyze code --rebuild
169
+
170
+ # Specify custom KB directory
171
+ aidp analyze code --kb-dir .aidp/custom-kb
172
+
173
+ # Inspect generated knowledge base
174
+ aidp kb show
175
+
176
+ # Show specific KB data
177
+ aidp kb show symbols
178
+ aidp kb show imports
179
+ aidp kb show seams
180
+
181
+ # Generate dependency graphs
182
+ aidp kb graph imports
183
+ aidp kb graph calls
184
+ ```
185
+
186
+ ### Knowledge Base Structure
187
+
188
+ The Tree-sitter analysis generates structured JSON files in `.aidp/kb/`:
189
+
190
+ - **`symbols.json`** - Classes, modules, methods, and their metadata
191
+ - **`imports.json`** - Require statements and dependencies
192
+ - **`calls.json`** - Method calls and invocation patterns
193
+ - **`metrics.json`** - Code complexity and size metrics
194
+ - **`seams.json`** - Integration points and dependency injection opportunities
195
+ - **`hotspots.json`** - Frequently changed code areas (based on git history)
196
+ - **`tests.json`** - Test coverage analysis
197
+ - **`cycles.json`** - Circular dependency detection
198
+
199
+ ### Legacy Code Analysis Features
200
+
201
+ The Tree-sitter analysis specifically supports:
202
+
203
+ - **Seam Detection**: Identifies I/O operations, global state access, and constructor dependencies
204
+ - **Change Hotspots**: Uses git history to identify frequently modified code
205
+ - **Dependency Analysis**: Maps import relationships and call graphs
206
+ - **Test Coverage**: Identifies untested public APIs
207
+ - **Refactoring Opportunities**: Suggests dependency injection points and seam locations
208
+
101
209
  ## Background Jobs
102
210
 
103
211
  AIDP uses background jobs to handle all AI provider executions, providing better reliability and real-time monitoring capabilities.
@@ -217,9 +325,19 @@ aidp execute next
217
325
  # Install dependencies
218
326
  bundle install
219
327
 
328
+ # Install Tree-sitter parsers for development
329
+ ./install_tree_sitter_parsers.sh
330
+
331
+ # Set up environment variables
332
+ export TREE_SITTER_PARSERS="$(pwd)/.aidp/parsers"
333
+
220
334
  # Run tests
221
335
  bundle exec rspec
222
336
 
337
+ # Run Tree-sitter analysis tests specifically
338
+ bundle exec rspec spec/aidp/analysis/
339
+ bundle exec rspec spec/integration/tree_sitter_analysis_workflow_spec.rb
340
+
223
341
  # Run linter
224
342
  bundle exec standardrb
225
343
 
@@ -230,6 +348,19 @@ bundle exec standardrb --fix
230
348
  bundle exec rake build
231
349
  ```
232
350
 
351
+ ### Development Dependencies
352
+
353
+ The following system dependencies are required for development:
354
+
355
+ - **Tree-sitter** - System library for parsing (install via `brew install tree-sitter` or package manager)
356
+ - **PostgreSQL** - Database for job management
357
+ - **Ruby gems** - All required gems are specified in `aidp.gemspec` and installed via `bundle install`
358
+
359
+ Optional gems with fallbacks:
360
+
361
+ - **`concurrent-ruby`** - Parallel processing (fallback to basic threading if not available)
362
+ - **`tty-table`** - Table rendering (fallback to basic ASCII tables if not available)
363
+
233
364
  ## Contributing
234
365
 
235
366
  See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and conventional commit guidelines.
@@ -254,7 +385,7 @@ The gem automates a complete 15-step development pipeline:
254
385
  - **Threat Model** → Security analysis (`docs/ThreatModel.md`)
255
386
  - **Test Plan** → Testing strategy (`docs/TestPlan.md`)
256
387
  - **Scaffolding** → Project structure guidance (`docs/ScaffoldingGuide.md`)
257
- - **Static Analysis** → Code quality tools (`docs/StaticAnalysis.md`)
388
+ - **Static Analysis** → Code quality tools and Tree-sitter analysis (`docs/StaticAnalysis.md`, `.aidp/kb/`)
258
389
  - **Observability** → Monitoring and SLOs (`docs/Observability.md`)
259
390
  - **Delivery** → Deployment strategy (`docs/DeliveryPlan.md`)
260
391
  - **Docs Portal** → Documentation portal (`docs/DocsPortalPlan.md`)
@@ -0,0 +1,456 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "tty-table"
5
+
6
+ module Aidp
7
+ module Analysis
8
+ class KBInspector
9
+ def initialize(kb_dir = ".aidp/kb")
10
+ @kb_dir = File.expand_path(kb_dir)
11
+ @data = load_kb_data
12
+ end
13
+
14
+ def show(type, format: "summary")
15
+ case type
16
+ when "seams"
17
+ show_seams(format)
18
+ when "hotspots"
19
+ show_hotspots(format)
20
+ when "cycles"
21
+ show_cycles(format)
22
+ when "apis"
23
+ show_apis(format)
24
+ when "symbols"
25
+ show_symbols(format)
26
+ when "imports"
27
+ show_imports(format)
28
+ when "summary"
29
+ show_summary(format)
30
+ else
31
+ puts "Unknown KB type: #{type}"
32
+ puts "Available types: seams, hotspots, cycles, apis, symbols, imports, summary"
33
+ end
34
+ end
35
+
36
+ def generate_graph(type, format: "dot", output: nil)
37
+ case type
38
+ when "imports"
39
+ generate_import_graph(format, output)
40
+ when "calls"
41
+ generate_call_graph(format, output)
42
+ when "cycles"
43
+ generate_cycle_graph(format, output)
44
+ else
45
+ puts "Unknown graph type: #{type}"
46
+ puts "Available types: imports, calls, cycles"
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def truncate_text(text, max_length = 50)
53
+ return nil unless text
54
+ return text if text.length <= max_length
55
+
56
+ text[0..max_length - 4] + "..."
57
+ end
58
+
59
+ def create_table(header, rows)
60
+ TTY::Table.new(header: header, rows: rows)
61
+ end
62
+
63
+ def load_kb_data
64
+ data = {}
65
+
66
+ %w[symbols imports calls metrics seams hotspots tests cycles].each do |type|
67
+ file_path = File.join(@kb_dir, "#{type}.json")
68
+ if File.exist?(file_path)
69
+ begin
70
+ data[type.to_sym] = JSON.parse(File.read(file_path), symbolize_names: true)
71
+ rescue JSON::ParserError => e
72
+ puts "Warning: Could not parse #{file_path}: #{e.message}"
73
+ data[type.to_sym] = []
74
+ end
75
+ else
76
+ data[type.to_sym] = []
77
+ end
78
+ end
79
+
80
+ data
81
+ end
82
+
83
+ def show_summary(_format)
84
+ puts "\nšŸ“Š Knowledge Base Summary"
85
+ puts "=" * 50
86
+
87
+ puts "šŸ“ KB Directory: #{@kb_dir}"
88
+ puts "šŸ“„ Files analyzed: #{count_files}"
89
+ puts "šŸ—ļø Symbols: #{@data[:symbols]&.length || 0}"
90
+ puts "šŸ“¦ Imports: #{@data[:imports]&.length || 0}"
91
+ puts "šŸ”— Calls: #{@data[:calls]&.length || 0}"
92
+ puts "šŸ“ Metrics: #{@data[:metrics]&.length || 0}"
93
+ puts "šŸ”§ Seams: #{@data[:seams]&.length || 0}"
94
+ puts "šŸ”„ Hotspots: #{@data[:hotspots]&.length || 0}"
95
+ puts "🧪 Tests: #{@data[:tests]&.length || 0}"
96
+ puts "šŸ”„ Cycles: #{@data[:cycles]&.length || 0}"
97
+
98
+ if @data[:seams]&.any?
99
+ puts "\nšŸ”§ Seam Types:"
100
+ seam_types = @data[:seams].group_by { |s| s[:kind] }
101
+ seam_types.each do |type, seams|
102
+ puts " #{type}: #{seams.length}"
103
+ end
104
+ end
105
+
106
+ if @data[:hotspots]&.any?
107
+ puts "\nšŸ”„ Top 5 Hotspots:"
108
+ @data[:hotspots].first(5).each_with_index do |hotspot, i|
109
+ puts " #{i + 1}. #{hotspot[:file]}:#{hotspot[:method]} (score: #{hotspot[:score]})"
110
+ end
111
+ end
112
+ end
113
+
114
+ def show_seams(format)
115
+ return puts "No seams data available" unless @data[:seams]&.any?
116
+
117
+ case format
118
+ when "json"
119
+ puts JSON.pretty_generate(@data[:seams])
120
+ when "table"
121
+ show_seams_table
122
+ else
123
+ show_seams_summary
124
+ end
125
+ end
126
+
127
+ def show_seams_table
128
+ table = create_table(
129
+ ["Type", "File", "Line", "Symbol", "Suggestion"],
130
+ @data[:seams].map do |seam|
131
+ [
132
+ seam[:kind],
133
+ seam[:file],
134
+ seam[:line],
135
+ seam[:symbol_id]&.split(":")&.last || "N/A",
136
+ truncate_text(seam[:suggestion], 50) || "N/A"
137
+ ]
138
+ end
139
+ )
140
+
141
+ puts "\nšŸ”§ Seams Analysis"
142
+ puts "=" * 80
143
+ puts table.render
144
+ end
145
+
146
+ def show_seams_summary
147
+ puts "\nšŸ”§ Seams Analysis"
148
+ puts "=" * 50
149
+
150
+ seam_types = @data[:seams].group_by { |s| s[:kind] }
151
+
152
+ seam_types.each do |type, seams|
153
+ puts "\nšŸ“Œ #{type.upcase} (#{seams.length} found)"
154
+ puts "-" * 30
155
+
156
+ seams.first(10).each do |seam|
157
+ puts " #{seam[:file]}:#{seam[:line]}"
158
+ puts " Symbol: #{seam[:symbol_id]&.split(":")&.last}"
159
+ puts " Suggestion: #{seam[:suggestion]}"
160
+ puts
161
+ end
162
+
163
+ if seams.length > 10
164
+ puts " ... and #{seams.length - 10} more"
165
+ end
166
+ end
167
+ end
168
+
169
+ def show_hotspots(format)
170
+ return puts "No hotspots data available" unless @data[:hotspots]&.any?
171
+
172
+ case format
173
+ when "json"
174
+ puts JSON.pretty_generate(@data[:hotspots])
175
+ when "table"
176
+ show_hotspots_table
177
+ else
178
+ show_hotspots_summary
179
+ end
180
+ end
181
+
182
+ def show_hotspots_table
183
+ table = create_table(
184
+ ["Rank", "File", "Method", "Score", "Complexity", "Touches"],
185
+ @data[:hotspots].map.with_index do |hotspot, i|
186
+ [
187
+ i + 1,
188
+ hotspot[:file],
189
+ hotspot[:method],
190
+ hotspot[:score],
191
+ hotspot[:complexity],
192
+ hotspot[:touches]
193
+ ]
194
+ end
195
+ )
196
+
197
+ puts "\nšŸ”„ Code Hotspots"
198
+ puts "=" * 80
199
+ puts table.render
200
+ end
201
+
202
+ def show_hotspots_summary
203
+ puts "\nšŸ”„ Code Hotspots (Top 20)"
204
+ puts "=" * 50
205
+
206
+ @data[:hotspots].each_with_index do |hotspot, i|
207
+ puts "#{i + 1}. #{hotspot[:file]}:#{hotspot[:method]}"
208
+ puts " Score: #{hotspot[:score]} (Complexity: #{hotspot[:complexity]}, Touches: #{hotspot[:touches]})"
209
+ puts
210
+ end
211
+ end
212
+
213
+ def show_cycles(format)
214
+ return puts "No cycles data available" unless @data[:cycles]&.any?
215
+
216
+ case format
217
+ when "json"
218
+ puts JSON.pretty_generate(@data[:cycles])
219
+ else
220
+ show_cycles_summary
221
+ end
222
+ end
223
+
224
+ def show_cycles_summary
225
+ puts "\nšŸ”„ Import Cycles"
226
+ puts "=" * 50
227
+
228
+ @data[:cycles].each_with_index do |cycle, i|
229
+ puts "Cycle #{i + 1}:"
230
+ cycle[:members].each do |member|
231
+ puts " - #{member}"
232
+ end
233
+ puts " Weight: #{cycle[:weight]}" if cycle[:weight]
234
+ puts
235
+ end
236
+ end
237
+
238
+ def show_apis(format)
239
+ return puts "No APIs data available" unless @data[:tests]&.any?
240
+
241
+ untested_apis = @data[:tests].select { |t| t[:tests].empty? }
242
+
243
+ case format
244
+ when "json"
245
+ puts JSON.pretty_generate(untested_apis)
246
+ else
247
+ show_apis_summary(untested_apis)
248
+ end
249
+ end
250
+
251
+ def show_apis_summary(untested_apis)
252
+ puts "\n🧪 Untested Public APIs"
253
+ puts "=" * 50
254
+
255
+ if untested_apis.empty?
256
+ puts "āœ… All public APIs have associated tests!"
257
+ else
258
+ puts "Found #{untested_apis.length} untested public APIs:"
259
+ puts
260
+
261
+ untested_apis.each do |api|
262
+ symbol = @data[:symbols]&.find { |s| s[:id] == api[:symbol_id] }
263
+ if symbol
264
+ puts " #{symbol[:file]}:#{symbol[:line]} - #{symbol[:name]}"
265
+ puts " Suggestion: Create characterization tests"
266
+ puts
267
+ end
268
+ end
269
+ end
270
+ end
271
+
272
+ def show_symbols(format)
273
+ return puts "No symbols data available" unless @data[:symbols]&.any?
274
+
275
+ case format
276
+ when "json"
277
+ puts JSON.pretty_generate(@data[:symbols])
278
+ when "table"
279
+ show_symbols_table
280
+ else
281
+ show_symbols_summary
282
+ end
283
+ end
284
+
285
+ def show_symbols_table
286
+ table = create_table(
287
+ ["Type", "Name", "File", "Line", "Visibility"],
288
+ @data[:symbols].map do |symbol|
289
+ [
290
+ symbol[:kind],
291
+ symbol[:name],
292
+ symbol[:file],
293
+ symbol[:line],
294
+ symbol[:visibility]
295
+ ]
296
+ end
297
+ )
298
+
299
+ puts "\nšŸ—ļø Symbols"
300
+ puts "=" * 80
301
+ puts table.render
302
+ end
303
+
304
+ def show_symbols_summary
305
+ puts "\nšŸ—ļø Symbols Summary"
306
+ puts "=" * 50
307
+
308
+ symbol_types = @data[:symbols].group_by { |s| s[:kind] }
309
+
310
+ symbol_types.each do |type, symbols|
311
+ puts "#{type.capitalize}: #{symbols.length}"
312
+ end
313
+ end
314
+
315
+ def show_imports(format)
316
+ return puts "No imports data available" unless @data[:imports]&.any?
317
+
318
+ case format
319
+ when "json"
320
+ puts JSON.pretty_generate(@data[:imports])
321
+ when "table"
322
+ show_imports_table
323
+ else
324
+ show_imports_summary
325
+ end
326
+ end
327
+
328
+ def show_imports_table
329
+ table = create_table(
330
+ ["Type", "Target", "File", "Line"],
331
+ @data[:imports].map do |import|
332
+ [
333
+ import[:kind],
334
+ import[:target],
335
+ import[:file],
336
+ import[:line]
337
+ ]
338
+ end
339
+ )
340
+
341
+ puts "\nšŸ“¦ Imports"
342
+ puts "=" * 80
343
+ puts table.render
344
+ end
345
+
346
+ def show_imports_summary
347
+ puts "\nšŸ“¦ Imports Summary"
348
+ puts "=" * 50
349
+
350
+ import_types = @data[:imports].group_by { |i| i[:kind] }
351
+
352
+ import_types.each do |type, imports|
353
+ puts "#{type.capitalize}: #{imports.length}"
354
+ end
355
+ end
356
+
357
+ def generate_import_graph(format, output)
358
+ puts "Generating import graph in #{format} format..."
359
+
360
+ case format
361
+ when "dot"
362
+ generate_dot_graph(output)
363
+ when "mermaid"
364
+ generate_mermaid_graph(output)
365
+ when "json"
366
+ generate_json_graph(output)
367
+ else
368
+ puts "Unsupported graph format: #{format}"
369
+ end
370
+ end
371
+
372
+ def generate_dot_graph(output)
373
+ content = ["digraph ImportGraph {"]
374
+ content << " rankdir=LR;"
375
+ content << " node [shape=box];"
376
+
377
+ @data[:imports]&.each do |import|
378
+ from = import[:file].gsub(/[^a-zA-Z0-9]/, "_")
379
+ to = import[:target].gsub(/[^a-zA-Z0-9]/, "_")
380
+ content << " \"#{from}\" -> \"#{to}\" [label=\"#{import[:kind]}\"];"
381
+ end
382
+
383
+ content << "}"
384
+
385
+ if output
386
+ File.write(output, content.join("\n"))
387
+ puts "Graph written to #{output}"
388
+ else
389
+ puts content.join("\n")
390
+ end
391
+ end
392
+
393
+ def generate_mermaid_graph(output)
394
+ content = ["graph LR"]
395
+
396
+ @data[:imports]&.each do |import|
397
+ from = import[:file].gsub(/[^a-zA-Z0-9]/, "_")
398
+ to = import[:target].gsub(/[^a-zA-Z0-9]/, "_")
399
+ content << " #{from} --> #{to}"
400
+ end
401
+
402
+ if output
403
+ File.write(output, content.join("\n"))
404
+ puts "Graph written to #{output}"
405
+ else
406
+ puts content.join("\n")
407
+ end
408
+ end
409
+
410
+ def generate_json_graph(output)
411
+ graph_data = {
412
+ nodes: [],
413
+ edges: []
414
+ }
415
+
416
+ # Add nodes
417
+ files = (@data[:imports]&.map { |i| i[:file] } || []).uniq
418
+ targets = (@data[:imports]&.map { |i| i[:target] } || []).uniq
419
+
420
+ (files + targets).uniq.each do |node|
421
+ graph_data[:nodes] << {id: node, label: node}
422
+ end
423
+
424
+ # Add edges
425
+ @data[:imports]&.each do |import|
426
+ graph_data[:edges] << {
427
+ from: import[:file],
428
+ to: import[:target],
429
+ label: import[:kind]
430
+ }
431
+ end
432
+
433
+ if output
434
+ File.write(output, JSON.pretty_generate(graph_data))
435
+ puts "Graph written to #{output}"
436
+ else
437
+ puts JSON.pretty_generate(graph_data)
438
+ end
439
+ end
440
+
441
+ def generate_call_graph(_format, _output)
442
+ # Similar to import graph but for method calls
443
+ puts "Call graph generation not yet implemented"
444
+ end
445
+
446
+ def generate_cycle_graph(_format, _output)
447
+ # Generate graph showing only the cycles
448
+ puts "Cycle graph generation not yet implemented"
449
+ end
450
+
451
+ def count_files
452
+ @data[:symbols]&.map { |s| s[:file] }&.uniq&.length || 0
453
+ end
454
+ end
455
+ end
456
+ end