aidp 0.5.0 → 0.8.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 +128 -151
- data/bin/aidp +1 -1
- data/lib/aidp/analysis/kb_inspector.rb +471 -0
- data/lib/aidp/analysis/seams.rb +159 -0
- data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +480 -0
- data/lib/aidp/analysis/tree_sitter_scan.rb +686 -0
- data/lib/aidp/analyze/error_handler.rb +2 -78
- data/lib/aidp/analyze/json_file_storage.rb +292 -0
- data/lib/aidp/analyze/progress.rb +12 -0
- data/lib/aidp/analyze/progress_visualizer.rb +12 -17
- data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
- data/lib/aidp/analyze/runner.rb +256 -87
- data/lib/aidp/analyze/steps.rb +6 -0
- data/lib/aidp/cli/jobs_command.rb +103 -435
- data/lib/aidp/cli.rb +317 -191
- data/lib/aidp/config.rb +298 -10
- data/lib/aidp/debug_logger.rb +195 -0
- data/lib/aidp/debug_mixin.rb +187 -0
- data/lib/aidp/execute/progress.rb +9 -0
- data/lib/aidp/execute/runner.rb +221 -40
- data/lib/aidp/execute/steps.rb +17 -7
- data/lib/aidp/execute/workflow_selector.rb +211 -0
- data/lib/aidp/harness/completion_checker.rb +268 -0
- data/lib/aidp/harness/condition_detector.rb +1526 -0
- data/lib/aidp/harness/config_loader.rb +373 -0
- data/lib/aidp/harness/config_manager.rb +382 -0
- data/lib/aidp/harness/config_schema.rb +1006 -0
- data/lib/aidp/harness/config_validator.rb +355 -0
- data/lib/aidp/harness/configuration.rb +477 -0
- data/lib/aidp/harness/enhanced_runner.rb +494 -0
- data/lib/aidp/harness/error_handler.rb +616 -0
- data/lib/aidp/harness/provider_config.rb +423 -0
- data/lib/aidp/harness/provider_factory.rb +306 -0
- data/lib/aidp/harness/provider_manager.rb +1269 -0
- data/lib/aidp/harness/provider_type_checker.rb +88 -0
- data/lib/aidp/harness/runner.rb +411 -0
- data/lib/aidp/harness/state/errors.rb +28 -0
- data/lib/aidp/harness/state/metrics.rb +219 -0
- data/lib/aidp/harness/state/persistence.rb +128 -0
- data/lib/aidp/harness/state/provider_state.rb +132 -0
- data/lib/aidp/harness/state/ui_state.rb +68 -0
- data/lib/aidp/harness/state/workflow_state.rb +123 -0
- data/lib/aidp/harness/state_manager.rb +586 -0
- data/lib/aidp/harness/status_display.rb +888 -0
- data/lib/aidp/harness/ui/base.rb +16 -0
- data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
- data/lib/aidp/harness/ui/error_handler.rb +132 -0
- data/lib/aidp/harness/ui/frame_manager.rb +361 -0
- data/lib/aidp/harness/ui/job_monitor.rb +500 -0
- data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
- data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
- data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
- data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
- data/lib/aidp/harness/ui/progress_display.rb +280 -0
- data/lib/aidp/harness/ui/question_collector.rb +141 -0
- data/lib/aidp/harness/ui/spinner_group.rb +184 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
- data/lib/aidp/harness/ui/status_manager.rb +312 -0
- data/lib/aidp/harness/ui/status_widget.rb +280 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
- data/lib/aidp/harness/user_interface.rb +2381 -0
- data/lib/aidp/provider_manager.rb +131 -7
- data/lib/aidp/providers/anthropic.rb +28 -109
- data/lib/aidp/providers/base.rb +170 -0
- data/lib/aidp/providers/cursor.rb +52 -183
- data/lib/aidp/providers/gemini.rb +24 -109
- data/lib/aidp/providers/macos_ui.rb +99 -5
- data/lib/aidp/providers/opencode.rb +194 -0
- data/lib/aidp/storage/csv_storage.rb +172 -0
- data/lib/aidp/storage/file_manager.rb +214 -0
- data/lib/aidp/storage/json_storage.rb +140 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +56 -35
- data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
- data/templates/COMMON/AGENT_BASE.md +11 -0
- data/templates/EXECUTE/00_PRD.md +4 -4
- data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
- data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
- data/templates/EXECUTE/08_TASKS.md +4 -4
- data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
- data/templates/README.md +279 -0
- data/templates/aidp-development.yml.example +373 -0
- data/templates/aidp-minimal.yml.example +48 -0
- data/templates/aidp-production.yml.example +475 -0
- data/templates/aidp.yml.example +598 -0
- metadata +106 -64
- data/lib/aidp/analyze/agent_personas.rb +0 -71
- data/lib/aidp/analyze/agent_tool_executor.rb +0 -445
- data/lib/aidp/analyze/data_retention_manager.rb +0 -426
- data/lib/aidp/analyze/database.rb +0 -260
- data/lib/aidp/analyze/dependencies.rb +0 -335
- data/lib/aidp/analyze/export_manager.rb +0 -425
- data/lib/aidp/analyze/focus_guidance.rb +0 -517
- data/lib/aidp/analyze/incremental_analyzer.rb +0 -543
- data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
- data/lib/aidp/analyze/large_analysis_progress.rb +0 -504
- data/lib/aidp/analyze/memory_manager.rb +0 -365
- data/lib/aidp/analyze/metrics_storage.rb +0 -336
- data/lib/aidp/analyze/parallel_processor.rb +0 -460
- data/lib/aidp/analyze/performance_optimizer.rb +0 -694
- data/lib/aidp/analyze/repository_chunker.rb +0 -704
- data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
- data/lib/aidp/analyze/storage.rb +0 -662
- data/lib/aidp/analyze/tool_configuration.rb +0 -456
- data/lib/aidp/analyze/tool_modernization.rb +0 -750
- data/lib/aidp/database/pg_adapter.rb +0 -148
- data/lib/aidp/database_config.rb +0 -69
- data/lib/aidp/database_connection.rb +0 -72
- data/lib/aidp/database_migration.rb +0 -158
- data/lib/aidp/job_manager.rb +0 -41
- data/lib/aidp/jobs/base_job.rb +0 -47
- data/lib/aidp/jobs/provider_execution_job.rb +0 -96
- data/lib/aidp/project_detector.rb +0 -117
- data/lib/aidp/providers/agent_supervisor.rb +0 -348
- data/lib/aidp/providers/supervised_base.rb +0 -317
- data/lib/aidp/providers/supervised_cursor.rb +0 -22
- data/lib/aidp/sync.rb +0 -13
- data/lib/aidp/workspace.rb +0 -19
@@ -0,0 +1,686 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "fileutils"
|
5
|
+
require "digest"
|
6
|
+
require "etc"
|
7
|
+
|
8
|
+
require_relative "tree_sitter_grammar_loader"
|
9
|
+
require_relative "seams"
|
10
|
+
|
11
|
+
module Aidp
|
12
|
+
module Analysis
|
13
|
+
class TreeSitterScan
|
14
|
+
def initialize(root: Dir.pwd, kb_dir: ".aidp/kb", langs: %w[ruby], threads: Etc.nprocessors)
|
15
|
+
@root = File.expand_path(root)
|
16
|
+
@kb_dir = File.expand_path(kb_dir, @root)
|
17
|
+
@langs = Array(langs)
|
18
|
+
@threads = threads
|
19
|
+
@grammar_loader = TreeSitterGrammarLoader.new(@root)
|
20
|
+
|
21
|
+
# Data structures to accumulate analysis results
|
22
|
+
@symbols = []
|
23
|
+
@imports = []
|
24
|
+
@calls = []
|
25
|
+
@metrics = []
|
26
|
+
@seams = []
|
27
|
+
@hotspots = []
|
28
|
+
@tests = []
|
29
|
+
@cycles = []
|
30
|
+
|
31
|
+
# Cache for parsed files
|
32
|
+
@cache = {}
|
33
|
+
@cache_file = File.join(@kb_dir, ".cache")
|
34
|
+
end
|
35
|
+
|
36
|
+
def run
|
37
|
+
puts "🔍 Starting Tree-sitter static analysis..."
|
38
|
+
puts "📁 Root: #{@root}"
|
39
|
+
puts "🗂️ KB Directory: #{@kb_dir}"
|
40
|
+
puts "🌐 Languages: #{@langs.join(", ")}"
|
41
|
+
puts "🧵 Threads: #{@threads}"
|
42
|
+
|
43
|
+
files = discover_files
|
44
|
+
puts "📄 Found #{files.length} files to analyze"
|
45
|
+
|
46
|
+
prepare_kb_dir
|
47
|
+
load_cache
|
48
|
+
|
49
|
+
parallel_parse(files)
|
50
|
+
write_kb_files
|
51
|
+
|
52
|
+
puts "✅ Tree-sitter analysis complete!"
|
53
|
+
puts "📊 Generated KB files in #{@kb_dir}"
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def discover_files
|
59
|
+
files = []
|
60
|
+
|
61
|
+
@langs.each do |lang|
|
62
|
+
patterns = @grammar_loader.file_patterns_for_language(lang)
|
63
|
+
patterns.each do |pattern|
|
64
|
+
files.concat(Dir.glob(File.join(@root, pattern)))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Filter out files that should be ignored
|
69
|
+
files = filter_ignored_files(files)
|
70
|
+
|
71
|
+
# Sort for consistent processing
|
72
|
+
files.sort
|
73
|
+
end
|
74
|
+
|
75
|
+
def filter_ignored_files(files)
|
76
|
+
# Respect .gitignore
|
77
|
+
gitignore_path = File.join(@root, ".gitignore")
|
78
|
+
ignored_patterns = []
|
79
|
+
|
80
|
+
if File.exist?(gitignore_path)
|
81
|
+
File.readlines(gitignore_path).each do |line|
|
82
|
+
line = line.strip
|
83
|
+
next if line.empty? || line.start_with?("#")
|
84
|
+
|
85
|
+
# Convert gitignore patterns to glob patterns
|
86
|
+
pattern = convert_gitignore_to_glob(line)
|
87
|
+
ignored_patterns << pattern
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Add common ignore patterns
|
92
|
+
ignored_patterns.concat([
|
93
|
+
"**/.git/**", "**/node_modules/**", "**/vendor/**",
|
94
|
+
"**/tmp/**", "tmp/**", "**/log/**", "log/**", "**/.aidp/**"
|
95
|
+
])
|
96
|
+
|
97
|
+
files.reject do |file|
|
98
|
+
relative_path = file.sub(/^#{Regexp.escape(@root)}\/?/, "")
|
99
|
+
ignored_patterns.any? { |pattern| File.fnmatch?(pattern, relative_path) }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Convert gitignore patterns to Ruby glob patterns
|
104
|
+
def convert_gitignore_to_glob(gitignore_pattern)
|
105
|
+
# Handle different gitignore pattern types
|
106
|
+
case gitignore_pattern
|
107
|
+
when /^\//
|
108
|
+
# Absolute path from root: /foo -> foo
|
109
|
+
gitignore_pattern[1..]
|
110
|
+
when /\/$/
|
111
|
+
# Directory only: foo/ -> **/foo/**
|
112
|
+
"**/" + gitignore_pattern.chomp("/") + "/**"
|
113
|
+
when /\*\*/
|
114
|
+
# Already contains **: leave as-is for glob
|
115
|
+
gitignore_pattern
|
116
|
+
else
|
117
|
+
# Regular pattern: foo -> **/foo
|
118
|
+
if gitignore_pattern.include?("/")
|
119
|
+
# Contains path separator: keep relative structure
|
120
|
+
gitignore_pattern
|
121
|
+
else
|
122
|
+
# Simple filename: match anywhere
|
123
|
+
"**/" + gitignore_pattern
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def prepare_kb_dir
|
129
|
+
FileUtils.mkdir_p(@kb_dir)
|
130
|
+
end
|
131
|
+
|
132
|
+
def load_cache
|
133
|
+
return unless File.exist?(@cache_file)
|
134
|
+
|
135
|
+
begin
|
136
|
+
@cache = JSON.parse(File.read(@cache_file), symbolize_names: true)
|
137
|
+
rescue JSON::ParserError
|
138
|
+
@cache = {}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def save_cache
|
143
|
+
File.write(@cache_file, JSON.pretty_generate(@cache))
|
144
|
+
end
|
145
|
+
|
146
|
+
def parallel_parse(files)
|
147
|
+
puts "🔄 Parsing files in parallel..."
|
148
|
+
|
149
|
+
# Group files by language for efficient processing
|
150
|
+
files_by_lang = files.group_by { |file| detect_language(file) }
|
151
|
+
|
152
|
+
# Process each language group
|
153
|
+
files_by_lang.each do |lang, lang_files|
|
154
|
+
puts "📝 Processing #{lang_files.length} #{lang} files..."
|
155
|
+
|
156
|
+
# Load grammar for this language
|
157
|
+
grammar = @grammar_loader.load_grammar(lang)
|
158
|
+
|
159
|
+
# Process files synchronously (simplified)
|
160
|
+
lang_files.each do |file|
|
161
|
+
parse_file(file, grammar)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
save_cache
|
166
|
+
end
|
167
|
+
|
168
|
+
def detect_language(file_path)
|
169
|
+
case File.extname(file_path)
|
170
|
+
when ".rb"
|
171
|
+
"ruby"
|
172
|
+
when ".js", ".jsx"
|
173
|
+
"javascript"
|
174
|
+
when ".ts", ".tsx"
|
175
|
+
"typescript"
|
176
|
+
when ".py"
|
177
|
+
"python"
|
178
|
+
else
|
179
|
+
"unknown"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def parse_file(file_path, grammar)
|
184
|
+
relative_path = file_path.sub(@root + File::SEPARATOR, "")
|
185
|
+
|
186
|
+
# Check cache first
|
187
|
+
cache_key = relative_path
|
188
|
+
file_mtime = File.mtime(file_path).to_i
|
189
|
+
|
190
|
+
if @cache[cache_key] && @cache[cache_key][:mtime] == file_mtime
|
191
|
+
# Use cached results
|
192
|
+
cached_data = @cache[cache_key][:data]
|
193
|
+
merge_cached_data(cached_data)
|
194
|
+
return
|
195
|
+
end
|
196
|
+
|
197
|
+
# Parse the file
|
198
|
+
# Set current file for context in helper methods
|
199
|
+
@current_file_path = file_path
|
200
|
+
|
201
|
+
source_code = File.read(file_path)
|
202
|
+
ast = grammar[:parser][:parse].call(source_code)
|
203
|
+
|
204
|
+
# Extract data from AST
|
205
|
+
file_data = extract_file_data(file_path, ast, source_code)
|
206
|
+
|
207
|
+
# Cache the results
|
208
|
+
@cache[cache_key] = {
|
209
|
+
mtime: file_mtime,
|
210
|
+
data: file_data
|
211
|
+
}
|
212
|
+
|
213
|
+
# Merge into global data structures
|
214
|
+
merge_file_data(file_data)
|
215
|
+
end
|
216
|
+
|
217
|
+
def extract_file_data(file_path, ast, source_code)
|
218
|
+
relative_path = file_path.sub(@root + File::SEPARATOR, "")
|
219
|
+
|
220
|
+
{
|
221
|
+
symbols: extract_symbols(ast, relative_path),
|
222
|
+
imports: extract_imports(ast, relative_path),
|
223
|
+
calls: extract_calls(ast, relative_path),
|
224
|
+
metrics: calculate_metrics(ast, source_code, relative_path),
|
225
|
+
seams: extract_seams(ast, relative_path)
|
226
|
+
}
|
227
|
+
end
|
228
|
+
|
229
|
+
def extract_symbols(ast, file_path)
|
230
|
+
symbols = []
|
231
|
+
|
232
|
+
children = ast[:children] || []
|
233
|
+
children = children.is_a?(Array) ? children : []
|
234
|
+
|
235
|
+
children.each do |node|
|
236
|
+
case node[:type].to_s
|
237
|
+
when "class"
|
238
|
+
symbols << {
|
239
|
+
id: "#{file_path}:#{node[:line]}:#{node[:name]}",
|
240
|
+
file: file_path,
|
241
|
+
line: node[:line],
|
242
|
+
kind: "class",
|
243
|
+
name: node[:name],
|
244
|
+
visibility: "public",
|
245
|
+
arity: 0,
|
246
|
+
loc: {
|
247
|
+
start_line: node[:line],
|
248
|
+
end_line: node[:line],
|
249
|
+
start_column: node[:start_column],
|
250
|
+
end_column: node[:end_column]
|
251
|
+
},
|
252
|
+
nesting_depth: calculate_nesting_depth(node)
|
253
|
+
}
|
254
|
+
when "module"
|
255
|
+
symbols << {
|
256
|
+
id: "#{file_path}:#{node[:line]}:#{node[:name]}",
|
257
|
+
file: file_path,
|
258
|
+
line: node[:line],
|
259
|
+
kind: "module",
|
260
|
+
name: node[:name],
|
261
|
+
visibility: "public",
|
262
|
+
arity: 0,
|
263
|
+
loc: {
|
264
|
+
start_line: node[:line],
|
265
|
+
end_line: node[:line],
|
266
|
+
start_column: node[:start_column],
|
267
|
+
end_column: node[:end_column]
|
268
|
+
},
|
269
|
+
nesting_depth: calculate_nesting_depth(node)
|
270
|
+
}
|
271
|
+
when "method"
|
272
|
+
symbols << {
|
273
|
+
id: "#{file_path}:#{node[:line]}:#{node[:name]}",
|
274
|
+
file: file_path,
|
275
|
+
line: node[:line],
|
276
|
+
kind: "method",
|
277
|
+
name: node[:name],
|
278
|
+
visibility: determine_method_visibility(node),
|
279
|
+
arity: calculate_method_arity(node),
|
280
|
+
loc: {
|
281
|
+
start_line: node[:line],
|
282
|
+
end_line: node[:line],
|
283
|
+
start_column: node[:start_column],
|
284
|
+
end_column: node[:end_column]
|
285
|
+
},
|
286
|
+
nesting_depth: calculate_nesting_depth(node)
|
287
|
+
}
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
symbols
|
292
|
+
end
|
293
|
+
|
294
|
+
def extract_imports(ast, file_path)
|
295
|
+
imports = []
|
296
|
+
|
297
|
+
children = ast[:children] || []
|
298
|
+
children = children.is_a?(Array) ? children : []
|
299
|
+
|
300
|
+
children.each do |node|
|
301
|
+
case node[:type].to_s
|
302
|
+
when "require"
|
303
|
+
imports << {
|
304
|
+
file: file_path,
|
305
|
+
kind: "require",
|
306
|
+
target: node[:target],
|
307
|
+
line: node[:line]
|
308
|
+
}
|
309
|
+
when "require_relative"
|
310
|
+
imports << {
|
311
|
+
file: file_path,
|
312
|
+
kind: "require_relative",
|
313
|
+
target: node[:target],
|
314
|
+
line: node[:line]
|
315
|
+
}
|
316
|
+
when "call"
|
317
|
+
# Handle require statements that are parsed as call nodes
|
318
|
+
# Check if this is a require call by looking at the first child (identifier)
|
319
|
+
# The children are nested in the structure
|
320
|
+
actual_children = (node[:children] && node[:children][:children]) ? node[:children][:children] : node[:children]
|
321
|
+
if actual_children&.is_a?(Array) && actual_children.first
|
322
|
+
first_child = actual_children.first
|
323
|
+
# Extract the actual identifier name from the source code
|
324
|
+
identifier_name = extract_identifier_name(first_child, file_path)
|
325
|
+
if identifier_name == "require"
|
326
|
+
# Extract the target from the argument list
|
327
|
+
target = extract_require_target(node)
|
328
|
+
if target
|
329
|
+
imports << {
|
330
|
+
file: file_path,
|
331
|
+
kind: "require",
|
332
|
+
target: target,
|
333
|
+
line: node[:line]
|
334
|
+
}
|
335
|
+
end
|
336
|
+
elsif identifier_name == "require_relative"
|
337
|
+
# Extract the target from the argument list
|
338
|
+
target = extract_require_target(node)
|
339
|
+
if target
|
340
|
+
imports << {
|
341
|
+
file: file_path,
|
342
|
+
kind: "require_relative",
|
343
|
+
target: target,
|
344
|
+
line: node[:line]
|
345
|
+
}
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
imports
|
353
|
+
end
|
354
|
+
|
355
|
+
def extract_require_target(node)
|
356
|
+
# Recursively search for string literals in the require call
|
357
|
+
find_string_in_node(node)
|
358
|
+
end
|
359
|
+
|
360
|
+
# Recursively find string content in a Tree-sitter node
|
361
|
+
def find_string_in_node(node)
|
362
|
+
return nil unless node.is_a?(Hash)
|
363
|
+
|
364
|
+
case node[:type].to_s
|
365
|
+
when "string"
|
366
|
+
# Found a string node - extract its content
|
367
|
+
return extract_string_literal_content(node)
|
368
|
+
when "string_content"
|
369
|
+
# Direct string content - extract text
|
370
|
+
return extract_node_text_from_source(node)
|
371
|
+
end
|
372
|
+
|
373
|
+
# Recursively search in children
|
374
|
+
children = get_node_children(node)
|
375
|
+
if children&.is_a?(Array)
|
376
|
+
children.each do |child|
|
377
|
+
result = find_string_in_node(child)
|
378
|
+
return result if result
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
nil
|
383
|
+
end
|
384
|
+
|
385
|
+
# Extract content from a string literal, handling quotes properly
|
386
|
+
def extract_string_literal_content(string_node)
|
387
|
+
# Get the full text of the string node including quotes
|
388
|
+
full_text = extract_node_text_from_source(string_node)
|
389
|
+
return nil unless full_text
|
390
|
+
|
391
|
+
# Remove quotes and handle escape sequences
|
392
|
+
case full_text[0]
|
393
|
+
when '"'
|
394
|
+
# Double-quoted string - handle escape sequences
|
395
|
+
content = full_text[1..-2] # Remove surrounding quotes
|
396
|
+
unescape_string(content)
|
397
|
+
when "'"
|
398
|
+
# Single-quoted string - minimal escaping
|
399
|
+
content = full_text[1..-2] # Remove surrounding quotes
|
400
|
+
content.gsub("\\'", "'").gsub("\\\\", "\\")
|
401
|
+
when "%"
|
402
|
+
# Percent notation strings (%q, %Q, %w, etc.)
|
403
|
+
handle_percent_string(full_text)
|
404
|
+
else
|
405
|
+
# Fallback: try to extract content between quotes
|
406
|
+
if (match = full_text.match(/^["'](.*)["']$/))
|
407
|
+
match[1]
|
408
|
+
else
|
409
|
+
full_text
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
# Handle Ruby's percent notation strings
|
415
|
+
def handle_percent_string(text)
|
416
|
+
return text unless text.start_with?("%")
|
417
|
+
|
418
|
+
case text[1]
|
419
|
+
when "q", "Q"
|
420
|
+
# %q{...} or %Q{...}
|
421
|
+
delimiter = text[2]
|
422
|
+
closing_delimiter = get_closing_delimiter(delimiter)
|
423
|
+
content = text[3..-(closing_delimiter.length + 1)]
|
424
|
+
(text[1] == "Q") ? unescape_string(content) : content
|
425
|
+
else
|
426
|
+
text
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
# Get closing delimiter for percent strings
|
431
|
+
def get_closing_delimiter(opening)
|
432
|
+
case opening
|
433
|
+
when "(" then ")"
|
434
|
+
when "[" then "]"
|
435
|
+
when "{" then "}"
|
436
|
+
when "<" then ">"
|
437
|
+
else; opening
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
# Unescape common Ruby escape sequences
|
442
|
+
def unescape_string(str)
|
443
|
+
str.gsub("\\n", "\n")
|
444
|
+
.gsub("\\t", "\t")
|
445
|
+
.gsub("\\r", "\r")
|
446
|
+
.gsub('\"', '"')
|
447
|
+
.gsub("\\\\", "\\")
|
448
|
+
end
|
449
|
+
|
450
|
+
# Safely extract children from a node, handling nested structures
|
451
|
+
def get_node_children(node)
|
452
|
+
if node[:children].is_a?(Hash) && node[:children][:children]
|
453
|
+
node[:children][:children]
|
454
|
+
else
|
455
|
+
node[:children]
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def extract_identifier_name(identifier_node, file_path)
|
460
|
+
# Extract the actual identifier name from the source code using node position
|
461
|
+
extract_node_text_from_source(identifier_node, file_path)
|
462
|
+
end
|
463
|
+
|
464
|
+
# Generalized method to extract text from any Tree-sitter node using source position
|
465
|
+
def extract_node_text_from_source(node, file_path = nil)
|
466
|
+
return nil unless node&.dig(:start_line) && node.dig(:end_line)
|
467
|
+
return nil unless node&.dig(:start_column) && node.dig(:end_column)
|
468
|
+
|
469
|
+
# Get source file path - either from parameter or from current parsing context
|
470
|
+
source_file = file_path ? File.join(@root, file_path) : @current_file_path
|
471
|
+
return nil unless source_file && File.exist?(source_file)
|
472
|
+
|
473
|
+
# Read source lines
|
474
|
+
source_lines = File.readlines(source_file)
|
475
|
+
|
476
|
+
start_line = node[:start_line]
|
477
|
+
end_line = node[:end_line]
|
478
|
+
start_col = node[:start_column]
|
479
|
+
end_col = node[:end_column]
|
480
|
+
|
481
|
+
# Handle single line nodes
|
482
|
+
if start_line == end_line
|
483
|
+
line_content = source_lines[start_line - 1] || ""
|
484
|
+
return line_content[start_col...end_col]
|
485
|
+
end
|
486
|
+
|
487
|
+
# Handle multi-line nodes
|
488
|
+
result = ""
|
489
|
+
(start_line..end_line).each do |line_num|
|
490
|
+
line_content = source_lines[line_num - 1] || ""
|
491
|
+
|
492
|
+
result += if line_num == start_line
|
493
|
+
# First line: from start_col to end
|
494
|
+
line_content[start_col..] || ""
|
495
|
+
elsif line_num == end_line
|
496
|
+
# Last line: from start to end_col
|
497
|
+
line_content[0...end_col] || ""
|
498
|
+
else
|
499
|
+
# Middle lines: entire line
|
500
|
+
line_content
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
result
|
505
|
+
end
|
506
|
+
|
507
|
+
def extract_calls(_ast, _file_path)
|
508
|
+
[]
|
509
|
+
|
510
|
+
# This would extract method calls from the AST
|
511
|
+
# For now, return empty array
|
512
|
+
end
|
513
|
+
|
514
|
+
def calculate_metrics(ast, source_code, file_path)
|
515
|
+
metrics = []
|
516
|
+
|
517
|
+
ast[:children]&.each do |node|
|
518
|
+
if node[:type] == "method"
|
519
|
+
method_metrics = {
|
520
|
+
symbol_id: "#{file_path}:#{node[:line]}:#{node[:name]}",
|
521
|
+
file: file_path,
|
522
|
+
method: node[:name],
|
523
|
+
cyclomatic_proxy: calculate_cyclomatic_complexity(node),
|
524
|
+
branch_count: count_branches(node),
|
525
|
+
max_nesting: calculate_max_nesting(node),
|
526
|
+
fan_out: calculate_fan_out(node),
|
527
|
+
lines: calculate_method_lines(node)
|
528
|
+
}
|
529
|
+
metrics << method_metrics
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
# Add file-level metrics
|
534
|
+
children = ast[:children] || []
|
535
|
+
children = children.is_a?(Array) ? children : []
|
536
|
+
|
537
|
+
file_metrics = {
|
538
|
+
file: file_path,
|
539
|
+
total_lines: source_code.lines.count,
|
540
|
+
total_methods: children.count { |n| n[:type].to_s == "method" },
|
541
|
+
total_classes: children.count { |n| n[:type].to_s == "class" },
|
542
|
+
total_modules: children.count { |n| n[:type].to_s == "module" }
|
543
|
+
}
|
544
|
+
metrics << file_metrics
|
545
|
+
|
546
|
+
metrics
|
547
|
+
end
|
548
|
+
|
549
|
+
def extract_seams(ast, file_path)
|
550
|
+
children = ast[:children] || []
|
551
|
+
children = children.is_a?(Array) ? children : []
|
552
|
+
Seams.detect_seams_in_ast(children, file_path)
|
553
|
+
end
|
554
|
+
|
555
|
+
def calculate_nesting_depth(_node)
|
556
|
+
# Simple nesting depth calculation
|
557
|
+
# In practice, this would analyze the actual AST structure
|
558
|
+
0
|
559
|
+
end
|
560
|
+
|
561
|
+
def determine_method_visibility(_node)
|
562
|
+
# Determine method visibility based on context
|
563
|
+
# In practice, this would analyze the AST structure
|
564
|
+
"public"
|
565
|
+
end
|
566
|
+
|
567
|
+
def calculate_method_arity(_node)
|
568
|
+
# Calculate method arity from parameters
|
569
|
+
# In practice, this would analyze the method's parameter list
|
570
|
+
0
|
571
|
+
end
|
572
|
+
|
573
|
+
def calculate_cyclomatic_complexity(node)
|
574
|
+
# Calculate cyclomatic complexity proxy
|
575
|
+
# Count control flow statements
|
576
|
+
count_branches(node) + 1
|
577
|
+
end
|
578
|
+
|
579
|
+
def count_branches(_node)
|
580
|
+
# Count branching statements in the method
|
581
|
+
# This would analyze the method's AST for if/elsif/else/case/when/while/until/rescue
|
582
|
+
0
|
583
|
+
end
|
584
|
+
|
585
|
+
def calculate_max_nesting(_node)
|
586
|
+
# Calculate maximum nesting depth in the method
|
587
|
+
0
|
588
|
+
end
|
589
|
+
|
590
|
+
def calculate_fan_out(_node)
|
591
|
+
# Calculate fan-out (number of distinct method calls)
|
592
|
+
0
|
593
|
+
end
|
594
|
+
|
595
|
+
def calculate_method_lines(_node)
|
596
|
+
# Calculate lines of code in the method
|
597
|
+
1
|
598
|
+
end
|
599
|
+
|
600
|
+
def merge_cached_data(cached_data)
|
601
|
+
@symbols.concat(cached_data[:symbols] || [])
|
602
|
+
@imports.concat(cached_data[:imports] || [])
|
603
|
+
@calls.concat(cached_data[:calls] || [])
|
604
|
+
@metrics.concat(cached_data[:metrics] || [])
|
605
|
+
@seams.concat(cached_data[:seams] || [])
|
606
|
+
end
|
607
|
+
|
608
|
+
def merge_file_data(file_data)
|
609
|
+
@symbols.concat(file_data[:symbols])
|
610
|
+
@imports.concat(file_data[:imports])
|
611
|
+
@calls.concat(file_data[:calls])
|
612
|
+
@metrics.concat(file_data[:metrics])
|
613
|
+
@seams.concat(file_data[:seams])
|
614
|
+
end
|
615
|
+
|
616
|
+
def write_kb_files
|
617
|
+
puts "💾 Writing knowledge base files..."
|
618
|
+
|
619
|
+
prepare_kb_dir
|
620
|
+
|
621
|
+
write_json_file("symbols.json", @symbols)
|
622
|
+
write_json_file("imports.json", @imports)
|
623
|
+
write_json_file("calls.json", @calls)
|
624
|
+
write_json_file("metrics.json", @metrics)
|
625
|
+
write_json_file("seams.json", @seams)
|
626
|
+
|
627
|
+
# Generate derived data
|
628
|
+
generate_hotspots
|
629
|
+
generate_tests
|
630
|
+
generate_cycles
|
631
|
+
|
632
|
+
write_json_file("hotspots.json", @hotspots)
|
633
|
+
write_json_file("tests.json", @tests)
|
634
|
+
write_json_file("cycles.json", @cycles)
|
635
|
+
end
|
636
|
+
|
637
|
+
def write_json_file(filename, data)
|
638
|
+
file_path = File.join(@kb_dir, filename)
|
639
|
+
File.write(file_path, JSON.pretty_generate(data))
|
640
|
+
puts "📄 Written #{filename} (#{data.length} entries)"
|
641
|
+
end
|
642
|
+
|
643
|
+
def generate_hotspots
|
644
|
+
# Merge structural metrics with git churn data
|
645
|
+
# Generate hotspots based on complexity and change frequency
|
646
|
+
@hotspots = @metrics.select { |m| m[:symbol_id] }
|
647
|
+
.map do |metric|
|
648
|
+
{
|
649
|
+
symbol_id: metric[:symbol_id],
|
650
|
+
score: (metric[:cyclomatic_proxy] || 1) * (metric[:fan_out] || 1),
|
651
|
+
complexity: metric[:cyclomatic_proxy] || 1,
|
652
|
+
touches: 1, # This would come from git log analysis
|
653
|
+
file: metric[:file],
|
654
|
+
method: metric[:method]
|
655
|
+
}
|
656
|
+
end
|
657
|
+
.sort_by { |h| -h[:score] }
|
658
|
+
.first(20)
|
659
|
+
end
|
660
|
+
|
661
|
+
def generate_tests
|
662
|
+
# Map public APIs to tests based on naming conventions
|
663
|
+
public_methods = @symbols.select { |s| s[:kind] == "method" && s[:visibility] == "public" }
|
664
|
+
|
665
|
+
@tests = public_methods.map do |method|
|
666
|
+
{
|
667
|
+
symbol_id: method[:id],
|
668
|
+
tests: find_tests_for_method(method)
|
669
|
+
}
|
670
|
+
end
|
671
|
+
end
|
672
|
+
|
673
|
+
def generate_cycles
|
674
|
+
# Detect import cycles
|
675
|
+
# For now, return empty array
|
676
|
+
@cycles = []
|
677
|
+
end
|
678
|
+
|
679
|
+
def find_tests_for_method(_method)
|
680
|
+
# Find test files that might test this method
|
681
|
+
# This would analyze test file naming and content
|
682
|
+
[]
|
683
|
+
end
|
684
|
+
end
|
685
|
+
end
|
686
|
+
end
|