rails-ai-context 4.3.2 → 4.4.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/CHANGELOG.md +127 -53
- data/CLAUDE.md +3 -1
- data/README.md +268 -197
- data/demo-trace.gif +0 -0
- data/demo-trace.tape +21 -0
- data/demo.gif +0 -0
- data/demo.tape +33 -0
- data/docs/GUIDE.md +9 -9
- data/lib/generators/rails_ai_context/install/install_generator.rb +2 -1
- data/lib/rails_ai_context/cli/tool_runner.rb +1 -1
- data/lib/rails_ai_context/configuration.rb +25 -1
- data/lib/rails_ai_context/doctor.rb +4 -2
- data/lib/rails_ai_context/fingerprinter.rb +6 -1
- data/lib/rails_ai_context/introspectors/accessibility_introspector.rb +52 -1
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +10 -2
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +22 -2
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +50 -4
- data/lib/rails_ai_context/introspectors/api_introspector.rb +41 -5
- data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +10 -4
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +62 -7
- data/lib/rails_ai_context/introspectors/component_introspector.rb +6 -0
- data/lib/rails_ai_context/introspectors/config_introspector.rb +59 -9
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +45 -13
- data/lib/rails_ai_context/introspectors/convention_detector.rb +25 -2
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +58 -4
- data/lib/rails_ai_context/introspectors/design_token_introspector.rb +27 -5
- data/lib/rails_ai_context/introspectors/devops_introspector.rb +15 -8
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +12 -3
- data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +36 -1
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +47 -1
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +49 -3
- data/lib/rails_ai_context/introspectors/job_introspector.rb +48 -5
- data/lib/rails_ai_context/introspectors/middleware_introspector.rb +24 -3
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +4 -1
- data/lib/rails_ai_context/introspectors/model_introspector.rb +108 -11
- data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +57 -12
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +34 -9
- data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +12 -2
- data/lib/rails_ai_context/introspectors/route_introspector.rb +25 -8
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +45 -7
- data/lib/rails_ai_context/introspectors/seeds_introspector.rb +5 -2
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +59 -6
- data/lib/rails_ai_context/introspectors/test_introspector.rb +50 -5
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +44 -13
- data/lib/rails_ai_context/introspectors/view_introspector.rb +46 -7
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +25 -7
- data/lib/rails_ai_context/resources.rb +1 -1
- data/lib/rails_ai_context/server.rb +6 -3
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +8 -4
- data/lib/rails_ai_context/tools/analyze_feature.rb +66 -19
- data/lib/rails_ai_context/tools/base_tool.rb +1 -1
- data/lib/rails_ai_context/tools/diagnose.rb +4 -2
- data/lib/rails_ai_context/tools/get_callbacks.rb +4 -2
- data/lib/rails_ai_context/tools/get_concern.rb +12 -6
- data/lib/rails_ai_context/tools/get_controllers.rb +10 -5
- data/lib/rails_ai_context/tools/get_conventions.rb +4 -2
- data/lib/rails_ai_context/tools/get_design_system.rb +2 -1
- data/lib/rails_ai_context/tools/get_env.rb +8 -4
- data/lib/rails_ai_context/tools/get_helper_methods.rb +6 -3
- data/lib/rails_ai_context/tools/get_job_pattern.rb +2 -1
- data/lib/rails_ai_context/tools/get_model_details.rb +10 -5
- data/lib/rails_ai_context/tools/get_partial_interface.rb +14 -7
- data/lib/rails_ai_context/tools/get_schema.rb +2 -1
- data/lib/rails_ai_context/tools/get_service_pattern.rb +2 -1
- data/lib/rails_ai_context/tools/get_stimulus.rb +2 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +4 -2
- data/lib/rails_ai_context/tools/get_turbo_map.rb +22 -11
- data/lib/rails_ai_context/tools/get_view.rb +6 -3
- data/lib/rails_ai_context/tools/migration_advisor.rb +2 -1
- data/lib/rails_ai_context/tools/onboard.rb +2 -1
- data/lib/rails_ai_context/tools/performance_check.rb +2 -1
- data/lib/rails_ai_context/tools/query.rb +5 -1
- data/lib/rails_ai_context/tools/read_logs.rb +3 -0
- data/lib/rails_ai_context/tools/runtime_info.rb +10 -5
- data/lib/rails_ai_context/tools/search_code.rb +8 -4
- data/lib/rails_ai_context/tools/search_docs.rb +2 -1
- data/lib/rails_ai_context/tools/session_context.rb +2 -1
- data/lib/rails_ai_context/tools/validate.rb +16 -8
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +5 -1
|
@@ -43,7 +43,8 @@ module RailsAiContext
|
|
|
43
43
|
else
|
|
44
44
|
parse_database_yml
|
|
45
45
|
end
|
|
46
|
-
rescue
|
|
46
|
+
rescue => e
|
|
47
|
+
$stderr.puts "[rails-ai-context] discover_databases failed: #{e.message}" if ENV["DEBUG"]
|
|
47
48
|
parse_database_yml
|
|
48
49
|
end
|
|
49
50
|
|
|
@@ -56,7 +57,8 @@ module RailsAiContext
|
|
|
56
57
|
else
|
|
57
58
|
[]
|
|
58
59
|
end
|
|
59
|
-
rescue
|
|
60
|
+
rescue => e
|
|
61
|
+
$stderr.puts "[rails-ai-context] discover_replicas failed: #{e.message}" if ENV["DEBUG"]
|
|
60
62
|
[]
|
|
61
63
|
end
|
|
62
64
|
|
|
@@ -65,8 +67,30 @@ module RailsAiContext
|
|
|
65
67
|
return nil unless File.exist?(database_yml)
|
|
66
68
|
|
|
67
69
|
content = File.read(database_yml)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
return nil unless content.match?(/shard/i)
|
|
71
|
+
|
|
72
|
+
result = { detected: true }
|
|
73
|
+
# Extract shard names from database.yml
|
|
74
|
+
shard_names = content.scan(/^\s{4,}(\w*shard\w*):/).flatten.uniq
|
|
75
|
+
result[:shard_names] = shard_names if shard_names.any?
|
|
76
|
+
|
|
77
|
+
# Extract shard config from model source (connects_to shards: { ... })
|
|
78
|
+
models_dir = File.join(root, "app/models")
|
|
79
|
+
if Dir.exist?(models_dir)
|
|
80
|
+
Dir.glob(File.join(models_dir, "**/*.rb")).each do |path|
|
|
81
|
+
src = File.read(path) rescue next
|
|
82
|
+
if (match = src.match(/connects_to\s+.*shards:\s*\{([^}]+)\}/m))
|
|
83
|
+
shard_keys = match[1].scan(/(\w+):/).flatten
|
|
84
|
+
result[:shard_keys] = shard_keys if shard_keys.any?
|
|
85
|
+
result[:shard_count] = shard_keys.size
|
|
86
|
+
break
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
result
|
|
92
|
+
rescue => e
|
|
93
|
+
$stderr.puts "[rails-ai-context] detect_sharding failed: #{e.message}" if ENV["DEBUG"]
|
|
70
94
|
nil
|
|
71
95
|
end
|
|
72
96
|
|
|
@@ -90,7 +114,8 @@ module RailsAiContext
|
|
|
90
114
|
if content.match?(/connected_to\b/)
|
|
91
115
|
connections << { model: model_name, uses_connected_to: true } unless connections.any? { |c| c[:model] == model_name }
|
|
92
116
|
end
|
|
93
|
-
rescue
|
|
117
|
+
rescue => e
|
|
118
|
+
$stderr.puts "[rails-ai-context] detect_model_connections failed: #{e.message}" if ENV["DEBUG"]
|
|
94
119
|
next
|
|
95
120
|
end
|
|
96
121
|
|
|
@@ -105,10 +130,11 @@ module RailsAiContext
|
|
|
105
130
|
databases = []
|
|
106
131
|
current_env = defined?(Rails) ? Rails.env : "development"
|
|
107
132
|
in_env = false
|
|
108
|
-
skip_keys = %w[adapter database host port username password encoding pool timeout socket url]
|
|
133
|
+
skip_keys = %w[adapter database host port username password encoding pool timeout socket url replica]
|
|
134
|
+
current_db = nil
|
|
109
135
|
|
|
110
136
|
content.each_line do |line|
|
|
111
|
-
if line.match?(/\A#{current_env}:/)
|
|
137
|
+
if line.match?(/\A#{Regexp.escape(current_env)}:/)
|
|
112
138
|
in_env = true
|
|
113
139
|
next
|
|
114
140
|
elsif line.match?(/\A\w+:/) && in_env
|
|
@@ -117,14 +143,32 @@ module RailsAiContext
|
|
|
117
143
|
|
|
118
144
|
next unless in_env
|
|
119
145
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
146
|
+
# 2-space indent = database name (primary, cache, etc.) or flat config key
|
|
147
|
+
if (match = line.match(/\A\s{2}(\w+):\s*(.*)/)) && !line.include?("<<")
|
|
148
|
+
key = match[1]
|
|
149
|
+
value = match[2].strip
|
|
150
|
+
if skip_keys.include?(key)
|
|
151
|
+
# Flat config (single-db): extract adapter/database inline
|
|
152
|
+
if key == "adapter" && databases.empty?
|
|
153
|
+
databases << { name: "primary", adapter: value }
|
|
154
|
+
end
|
|
155
|
+
else
|
|
156
|
+
# This is a named database (multi-db config)
|
|
157
|
+
current_db = { name: key }
|
|
158
|
+
databases << current_db
|
|
159
|
+
end
|
|
160
|
+
# 4-space indent = settings under a named database
|
|
161
|
+
elsif current_db && (match = line.match(/\A\s{4}(\w+):\s*(.*)/))
|
|
162
|
+
key = match[1]
|
|
163
|
+
value = match[2].strip
|
|
164
|
+
current_db[:adapter] = value if key == "adapter"
|
|
165
|
+
current_db[:replica] = true if key == "replica" && value == "true"
|
|
123
166
|
end
|
|
124
167
|
end
|
|
125
168
|
|
|
126
169
|
databases
|
|
127
|
-
rescue
|
|
170
|
+
rescue => e
|
|
171
|
+
$stderr.puts "[rails-ai-context] parse_database_yml failed: #{e.message}" if ENV["DEBUG"]
|
|
128
172
|
[]
|
|
129
173
|
end
|
|
130
174
|
|
|
@@ -136,7 +180,8 @@ module RailsAiContext
|
|
|
136
180
|
else
|
|
137
181
|
name
|
|
138
182
|
end
|
|
139
|
-
rescue
|
|
183
|
+
rescue => e
|
|
184
|
+
$stderr.puts "[rails-ai-context] anonymize_db_name failed: #{e.message}" if ENV["DEBUG"]
|
|
140
185
|
"external"
|
|
141
186
|
end
|
|
142
187
|
end
|
|
@@ -91,7 +91,8 @@ module RailsAiContext
|
|
|
91
91
|
scopes_with_includes: content.scan(/scope\s+:\w+.*\.includes\(/).any?,
|
|
92
92
|
content: content
|
|
93
93
|
}
|
|
94
|
-
rescue
|
|
94
|
+
rescue => e
|
|
95
|
+
$stderr.puts "[rails-ai-context] load_model_data failed: #{e.message}" if ENV["DEBUG"]
|
|
95
96
|
nil
|
|
96
97
|
end
|
|
97
98
|
end
|
|
@@ -188,7 +189,9 @@ module RailsAiContext
|
|
|
188
189
|
missing = []
|
|
189
190
|
|
|
190
191
|
schema_data.each do |table_name, table|
|
|
191
|
-
table[:columns]
|
|
192
|
+
columns = table[:columns]
|
|
193
|
+
|
|
194
|
+
columns.each do |col|
|
|
192
195
|
next unless col[:name].end_with?("_id")
|
|
193
196
|
|
|
194
197
|
indexed = table[:indexes].any? { |idx| idx.include?(col[:name]) }
|
|
@@ -196,11 +199,31 @@ module RailsAiContext
|
|
|
196
199
|
next if col[:type] == "references"
|
|
197
200
|
next if indexed
|
|
198
201
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
# Check for polymorphic association (_type column alongside _id)
|
|
203
|
+
base_name = col[:name].sub(/_id\z/, "")
|
|
204
|
+
type_col = columns.find { |c| c[:name] == "#{base_name}_type" }
|
|
205
|
+
|
|
206
|
+
if type_col
|
|
207
|
+
# Polymorphic: need compound index on [type, id]
|
|
208
|
+
compound_indexed = table[:indexes].any? { |idx|
|
|
209
|
+
idx_str = idx.to_s
|
|
210
|
+
idx_str.include?("#{base_name}_type") && idx_str.include?("#{base_name}_id")
|
|
211
|
+
}
|
|
212
|
+
unless compound_indexed
|
|
213
|
+
missing << {
|
|
214
|
+
table: table_name,
|
|
215
|
+
column: "#{base_name}_type, #{base_name}_id",
|
|
216
|
+
polymorphic: true,
|
|
217
|
+
suggestion: "add_index :#{table_name}, [:#{base_name}_type, :#{base_name}_id]"
|
|
218
|
+
}
|
|
219
|
+
end
|
|
220
|
+
else
|
|
221
|
+
missing << {
|
|
222
|
+
table: table_name,
|
|
223
|
+
column: col[:name],
|
|
224
|
+
suggestion: "add_index :#{table_name}, :#{col[:name]}"
|
|
225
|
+
}
|
|
226
|
+
end
|
|
204
227
|
end
|
|
205
228
|
end
|
|
206
229
|
|
|
@@ -218,7 +241,8 @@ module RailsAiContext
|
|
|
218
241
|
content = File.read(path)
|
|
219
242
|
match = content.match(/class\s+(\w+)\s*<\s*ApplicationRecord/)
|
|
220
243
|
match[1] if match
|
|
221
|
-
rescue
|
|
244
|
+
rescue => e
|
|
245
|
+
$stderr.puts "[rails-ai-context] detect_model_all_in_controllers failed: #{e.message}" if ENV["DEBUG"]
|
|
222
246
|
nil
|
|
223
247
|
end
|
|
224
248
|
else
|
|
@@ -270,7 +294,8 @@ module RailsAiContext
|
|
|
270
294
|
associations: has_many_assocs,
|
|
271
295
|
suggestion: "Consider eager loading when rendering #{class_name} with associations: #{has_many_assocs.join(", ")}"
|
|
272
296
|
}
|
|
273
|
-
rescue
|
|
297
|
+
rescue => e
|
|
298
|
+
$stderr.puts "[rails-ai-context] detect_eager_load_candidates failed: #{e.message}" if ENV["DEBUG"]
|
|
274
299
|
next
|
|
275
300
|
end
|
|
276
301
|
|
|
@@ -50,11 +50,21 @@ module RailsAiContext
|
|
|
50
50
|
|
|
51
51
|
if (t_match = line.match(/^\s*task\s+:(\w+)/))
|
|
52
52
|
name = (current_namespace + [ t_match[1] ]).join(":")
|
|
53
|
-
|
|
53
|
+
entry = {
|
|
54
54
|
name: name,
|
|
55
55
|
description: last_desc,
|
|
56
56
|
file: relative
|
|
57
|
-
}
|
|
57
|
+
}
|
|
58
|
+
# Extract task dependencies (=> [:dep1, :dep2] or => :dep)
|
|
59
|
+
if (dep_match = line.match(/=>\s*(?:\[([^\]]+)\]|:(\w+))/))
|
|
60
|
+
deps = dep_match[1] ? dep_match[1].scan(/:(\w+)/).flatten : [ dep_match[2] ]
|
|
61
|
+
entry[:dependencies] = deps if deps.any?
|
|
62
|
+
end
|
|
63
|
+
# Extract task arguments (task :name, [:arg1, :arg2])
|
|
64
|
+
if (args_match = line.match(/task\s+:#{Regexp.escape(t_match[1])}\s*,\s*\[([^\]]+)\]/))
|
|
65
|
+
entry[:args] = args_match[1].scan(/:(\w+)/).flatten
|
|
66
|
+
end
|
|
67
|
+
tasks << entry.compact
|
|
58
68
|
last_desc = nil
|
|
59
69
|
end
|
|
60
70
|
end
|
|
@@ -14,12 +14,14 @@ module RailsAiContext
|
|
|
14
14
|
# @return [Hash] routes grouped by controller
|
|
15
15
|
def call
|
|
16
16
|
routes = extract_routes
|
|
17
|
+
root = routes.find { |r| r[:path] == "/" && r[:verb]&.include?("GET") }
|
|
17
18
|
|
|
18
19
|
{
|
|
19
20
|
total_routes: routes.size,
|
|
20
21
|
by_controller: group_by_controller(routes),
|
|
21
22
|
api_namespaces: detect_api_namespaces(routes),
|
|
22
|
-
mounted_engines: detect_mounted_engines
|
|
23
|
+
mounted_engines: detect_mounted_engines,
|
|
24
|
+
root_route: root ? "#{root[:controller]}##{root[:action]}" : nil
|
|
23
25
|
}
|
|
24
26
|
end
|
|
25
27
|
|
|
@@ -33,28 +35,42 @@ module RailsAiContext
|
|
|
33
35
|
next if route.respond_to?(:internal?) && route.internal?
|
|
34
36
|
next if route.defaults[:controller].blank?
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
route_path = route.path.spec.to_s.gsub("(.:format)", "")
|
|
39
|
+
action = route.defaults[:action]
|
|
40
|
+
|
|
41
|
+
entry = {
|
|
37
42
|
verb: route.verb.presence || "ANY",
|
|
38
|
-
path:
|
|
43
|
+
path: route_path,
|
|
39
44
|
controller: route.defaults[:controller],
|
|
40
|
-
action:
|
|
45
|
+
action: action,
|
|
41
46
|
name: route.name,
|
|
42
47
|
constraints: extract_constraints(route)
|
|
43
|
-
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
params = route_path.scan(/:(\w+)/).flatten
|
|
51
|
+
entry[:params] = params if params.any?
|
|
52
|
+
|
|
53
|
+
entry[:restful] = %w[index show new create edit update destroy].include?(action)
|
|
54
|
+
|
|
55
|
+
entry.compact
|
|
44
56
|
end
|
|
45
57
|
end
|
|
46
58
|
|
|
47
59
|
def extract_constraints(route)
|
|
48
60
|
constraints = route.constraints.to_s
|
|
49
61
|
constraints.empty? ? nil : constraints
|
|
50
|
-
rescue
|
|
62
|
+
rescue => e
|
|
63
|
+
$stderr.puts "[rails-ai-context] extract_constraints failed: #{e.message}" if ENV["DEBUG"]
|
|
51
64
|
nil
|
|
52
65
|
end
|
|
53
66
|
|
|
54
67
|
def group_by_controller(routes)
|
|
55
68
|
routes.group_by { |r| r[:controller] }.transform_values do |controller_routes|
|
|
56
69
|
controller_routes.map do |r|
|
|
57
|
-
{ verb: r[:verb], path: r[:path], action: r[:action], name: r[:name] }
|
|
70
|
+
entry = { verb: r[:verb], path: r[:path], action: r[:action], name: r[:name] }
|
|
71
|
+
entry[:params] = r[:params] if r[:params]
|
|
72
|
+
entry[:restful] = r[:restful] unless r[:restful].nil?
|
|
73
|
+
entry.compact
|
|
58
74
|
end
|
|
59
75
|
end
|
|
60
76
|
end
|
|
@@ -77,7 +93,8 @@ module RailsAiContext
|
|
|
77
93
|
engine: engine_class.name,
|
|
78
94
|
path: r.path.spec.to_s
|
|
79
95
|
}
|
|
80
|
-
rescue
|
|
96
|
+
rescue => e
|
|
97
|
+
$stderr.puts "[rails-ai-context] detect_mounted_engines failed: #{e.message}" if ENV["DEBUG"]
|
|
81
98
|
nil
|
|
82
99
|
end
|
|
83
100
|
end
|
|
@@ -33,13 +33,15 @@ module RailsAiContext
|
|
|
33
33
|
|
|
34
34
|
def active_record_connected?
|
|
35
35
|
defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
|
36
|
-
rescue
|
|
36
|
+
rescue => e
|
|
37
|
+
$stderr.puts "[rails-ai-context] active_record_connected? failed: #{e.message}" if ENV["DEBUG"]
|
|
37
38
|
false
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
def adapter_name
|
|
41
42
|
ActiveRecord::Base.connection.adapter_name
|
|
42
|
-
rescue
|
|
43
|
+
rescue => e
|
|
44
|
+
$stderr.puts "[rails-ai-context] adapter_name failed: #{e.message}" if ENV["DEBUG"]
|
|
43
45
|
"unknown"
|
|
44
46
|
end
|
|
45
47
|
|
|
@@ -106,7 +108,8 @@ module RailsAiContext
|
|
|
106
108
|
on_update: fk.on_update
|
|
107
109
|
}.compact
|
|
108
110
|
end
|
|
109
|
-
rescue
|
|
111
|
+
rescue => e
|
|
112
|
+
$stderr.puts "[rails-ai-context] extract_foreign_keys failed: #{e.message}" if ENV["DEBUG"]
|
|
110
113
|
[] # Some adapters don't support foreign_keys
|
|
111
114
|
end
|
|
112
115
|
|
|
@@ -136,7 +139,8 @@ module RailsAiContext
|
|
|
136
139
|
end
|
|
137
140
|
|
|
138
141
|
defaults
|
|
139
|
-
rescue
|
|
142
|
+
rescue => e
|
|
143
|
+
$stderr.puts "[rails-ai-context] parse_schema_defaults_for_table failed: #{e.message}" if ENV["DEBUG"]
|
|
140
144
|
{}
|
|
141
145
|
end
|
|
142
146
|
|
|
@@ -207,12 +211,20 @@ module RailsAiContext
|
|
|
207
211
|
col[:default] = raw.start_with?('"') ? raw[1..-2] : raw
|
|
208
212
|
end
|
|
209
213
|
col[:array] = true if line.include?("array: true")
|
|
214
|
+
if (comment_match = line.match(/comment:\s*"([^"]+)"/))
|
|
215
|
+
col[:comment] = comment_match[1]
|
|
216
|
+
end
|
|
210
217
|
tables[current_table][:columns] << col
|
|
211
218
|
elsif current_table && (match = line.match(/t\.index\s+\[([^\]]*)\]/))
|
|
212
219
|
cols = match[1].scan(/["'](\w+)["']/).flatten
|
|
213
220
|
unique = line.include?("unique: true")
|
|
214
221
|
idx_name = line.match(/name:\s*["']([^"']+)["']/)&.send(:[], 1)
|
|
215
222
|
tables[current_table][:indexes] << { name: idx_name, columns: cols, unique: unique }.compact if cols.any?
|
|
223
|
+
elsif current_table && (match = line.match(/t\.index\s+"([^"]+)"/))
|
|
224
|
+
expression = match[1]
|
|
225
|
+
idx_name = line.match(/name:\s*["']([^"']+)["']/)&.send(:[], 1)
|
|
226
|
+
unique = line.include?("unique: true")
|
|
227
|
+
tables[current_table][:indexes] << { name: idx_name, columns: [ expression ], unique: unique, expression: true }.compact
|
|
216
228
|
elsif (match = line.match(/add_index\s+"(\w+)",\s+(.+)/))
|
|
217
229
|
table_name = match[1]
|
|
218
230
|
rest = match[2]
|
|
@@ -333,7 +345,8 @@ module RailsAiContext
|
|
|
333
345
|
end
|
|
334
346
|
|
|
335
347
|
constraints
|
|
336
|
-
rescue
|
|
348
|
+
rescue => e
|
|
349
|
+
$stderr.puts "[rails-ai-context] parse_check_constraints failed: #{e.message}" if ENV["DEBUG"]
|
|
337
350
|
[]
|
|
338
351
|
end
|
|
339
352
|
|
|
@@ -352,7 +365,8 @@ module RailsAiContext
|
|
|
352
365
|
end
|
|
353
366
|
|
|
354
367
|
enums
|
|
355
|
-
rescue
|
|
368
|
+
rescue => e
|
|
369
|
+
$stderr.puts "[rails-ai-context] parse_enum_types failed: #{e.message}" if ENV["DEBUG"]
|
|
356
370
|
[]
|
|
357
371
|
end
|
|
358
372
|
|
|
@@ -382,7 +396,8 @@ module RailsAiContext
|
|
|
382
396
|
end
|
|
383
397
|
|
|
384
398
|
columns
|
|
385
|
-
rescue
|
|
399
|
+
rescue => e
|
|
400
|
+
$stderr.puts "[rails-ai-context] parse_generated_columns failed: #{e.message}" if ENV["DEBUG"]
|
|
386
401
|
[]
|
|
387
402
|
end
|
|
388
403
|
|
|
@@ -496,6 +511,29 @@ module RailsAiContext
|
|
|
496
511
|
col[:type] = new_type if col
|
|
497
512
|
end
|
|
498
513
|
|
|
514
|
+
# change_column_default :table, :column, default_value
|
|
515
|
+
elsif (match = stripped.match(/change_column_default\s+[:"'](\w+)['"']?,\s*[:"'](\w+)/))
|
|
516
|
+
table_name, col_name = match[1], match[2]
|
|
517
|
+
if tables[table_name]
|
|
518
|
+
col = tables[table_name][:columns].find { |c| c[:name] == col_name }
|
|
519
|
+
if col
|
|
520
|
+
default_match = stripped.match(/,\s*(?:from:\s*[^,]+,\s*)?to:\s*("[^"]*"|\d+(?:\.\d+)?|true|false|nil)/)
|
|
521
|
+
default_match ||= stripped.match(/,\s*[:"']\w+['"']?,\s*("[^"]*"|\d+(?:\.\d+)?|true|false|nil)\s*\z/)
|
|
522
|
+
if default_match
|
|
523
|
+
raw = default_match[1]
|
|
524
|
+
col[:default] = raw == "nil" ? nil : (raw.start_with?('"') ? raw[1..-2] : raw)
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# change_column_null :table, :column, nullable
|
|
530
|
+
elsif (match = stripped.match(/change_column_null\s+[:"'](\w+)['"']?,\s*[:"'](\w+)['"']?,\s*(true|false)/))
|
|
531
|
+
table_name, col_name, nullable = match[1], match[2], match[3]
|
|
532
|
+
if tables[table_name]
|
|
533
|
+
col = tables[table_name][:columns].find { |c| c[:name] == col_name }
|
|
534
|
+
col[:null] = (nullable == "true") if col
|
|
535
|
+
end
|
|
536
|
+
|
|
499
537
|
# rename_table :old, :new
|
|
500
538
|
elsif (match = stripped.match(/rename_table\s+[:"'](\w+)['"']?,\s*[:"'](\w+)/))
|
|
501
539
|
old_name, new_name = match[1], match[2]
|
|
@@ -42,8 +42,10 @@ module RailsAiContext
|
|
|
42
42
|
uses_insert_all: content.match?(/\.insert_all/),
|
|
43
43
|
uses_faker: content.match?(/Faker::/),
|
|
44
44
|
uses_factory_bot: content.match?(/FactoryBot/),
|
|
45
|
+
uses_csv: content.match?(/CSV\.|require.*csv/i),
|
|
45
46
|
loads_directory: content.match?(/Dir\[|Dir\.glob|load.*seeds/),
|
|
46
|
-
environment_conditional: content.match?(/Rails\.env/)
|
|
47
|
+
environment_conditional: content.match?(/Rails\.env/),
|
|
48
|
+
has_ordering: content.match?(/load.*order|require.*order|seeds.*\d+/i)
|
|
47
49
|
}
|
|
48
50
|
rescue => e
|
|
49
51
|
{ exists: false, error: e.message }
|
|
@@ -78,7 +80,8 @@ module RailsAiContext
|
|
|
78
80
|
model_name = match[0]
|
|
79
81
|
models << model_name unless non_models.include?(model_name)
|
|
80
82
|
end
|
|
81
|
-
rescue
|
|
83
|
+
rescue => e
|
|
84
|
+
$stderr.puts "[rails-ai-context] detect_seeded_models failed: #{e.message}" if ENV["DEBUG"]
|
|
82
85
|
next
|
|
83
86
|
end
|
|
84
87
|
|
|
@@ -19,6 +19,17 @@ module RailsAiContext
|
|
|
19
19
|
parse_controller(path, controllers_dir)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# Merge action bindings from views into each controller's data
|
|
23
|
+
bindings = extract_action_bindings
|
|
24
|
+
if bindings.any?
|
|
25
|
+
controllers.each do |ctrl|
|
|
26
|
+
next unless ctrl[:name]
|
|
27
|
+
if (ctrl_bindings = bindings[ctrl[:name]])
|
|
28
|
+
ctrl[:action_bindings] = ctrl_bindings
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
22
33
|
{
|
|
23
34
|
controllers: controllers,
|
|
24
35
|
cross_controller_composition: extract_cross_controller_composition(root)
|
|
@@ -34,18 +45,22 @@ module RailsAiContext
|
|
|
34
45
|
name = relative.sub(/_controller\.(js|ts)\z/, "").tr("/", "--")
|
|
35
46
|
content = File.read(path)
|
|
36
47
|
|
|
48
|
+
outlets = extract_outlets(content)
|
|
49
|
+
|
|
37
50
|
{
|
|
38
51
|
name: name,
|
|
39
52
|
file: relative,
|
|
40
53
|
targets: extract_targets(content),
|
|
41
54
|
values: extract_values(content),
|
|
42
55
|
actions: extract_actions(content),
|
|
43
|
-
outlets:
|
|
56
|
+
outlets: outlets,
|
|
57
|
+
outlet_controllers: outlets.any? ? outlets.each_with_object({}) { |o, h| h[o] = "#{o}-controller" } : nil,
|
|
44
58
|
classes: extract_classes(content),
|
|
59
|
+
lifecycle: extract_lifecycle(content),
|
|
45
60
|
import_graph: extract_import_graph(content),
|
|
46
61
|
complexity: extract_complexity(content),
|
|
47
62
|
turbo_event_listeners: extract_turbo_event_listeners(content)
|
|
48
|
-
}
|
|
63
|
+
}.compact
|
|
49
64
|
rescue => e
|
|
50
65
|
{ name: File.basename(path), error: e.message }
|
|
51
66
|
end
|
|
@@ -127,7 +142,8 @@ module RailsAiContext
|
|
|
127
142
|
end
|
|
128
143
|
end
|
|
129
144
|
imports
|
|
130
|
-
rescue
|
|
145
|
+
rescue => e
|
|
146
|
+
$stderr.puts "[rails-ai-context] extract_import_graph failed: #{e.message}" if ENV["DEBUG"]
|
|
131
147
|
[]
|
|
132
148
|
end
|
|
133
149
|
|
|
@@ -138,17 +154,53 @@ module RailsAiContext
|
|
|
138
154
|
methods = content.scan(/^\s+(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/).flatten
|
|
139
155
|
method_count = methods.count { |m| !JS_KEYWORDS.include?(m) }
|
|
140
156
|
{ loc: loc, method_count: method_count }
|
|
141
|
-
rescue
|
|
157
|
+
rescue => e
|
|
158
|
+
$stderr.puts "[rails-ai-context] extract_complexity failed: #{e.message}" if ENV["DEBUG"]
|
|
142
159
|
{ loc: 0, method_count: 0 }
|
|
143
160
|
end
|
|
144
161
|
|
|
145
162
|
def extract_turbo_event_listeners(content)
|
|
146
163
|
events = content.scan(/["']turbo:([\w:-]+)["']/).flatten.uniq
|
|
147
164
|
events.map { |e| "turbo:#{e}" }
|
|
148
|
-
rescue
|
|
165
|
+
rescue => e
|
|
166
|
+
$stderr.puts "[rails-ai-context] extract_turbo_event_listeners failed: #{e.message}" if ENV["DEBUG"]
|
|
149
167
|
[]
|
|
150
168
|
end
|
|
151
169
|
|
|
170
|
+
def extract_lifecycle(content)
|
|
171
|
+
hooks = content.scan(/\b(connect|disconnect|initialize)\s*\(\s*\)/).flatten.uniq
|
|
172
|
+
hooks.any? ? hooks : nil
|
|
173
|
+
rescue => e
|
|
174
|
+
$stderr.puts "[rails-ai-context] extract_lifecycle failed: #{e.message}" if ENV["DEBUG"]
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def extract_action_bindings
|
|
179
|
+
bindings = Hash.new { |h, k| h[k] = [] }
|
|
180
|
+
view_dirs = [ File.join(app.root, "app", "views"), File.join(app.root, "app", "components") ]
|
|
181
|
+
view_dirs.each do |dir|
|
|
182
|
+
next unless Dir.exist?(dir)
|
|
183
|
+
Dir.glob(File.join(dir, "**", "*.{erb,haml,slim}")).each do |path|
|
|
184
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
185
|
+
content.scan(/data-action=["']([^"']+)["']/).each do |match|
|
|
186
|
+
match[0].split(/\s+/).each do |binding_str|
|
|
187
|
+
# Format: event->controller#method
|
|
188
|
+
if (m = binding_str.match(/(?:(\w+)->)?(\w[\w-]*)#(\w+)/))
|
|
189
|
+
controller = m[2]
|
|
190
|
+
method = m[3]
|
|
191
|
+
event = m[1]
|
|
192
|
+
bindings[controller] << { event: event, method: method }.compact
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
bindings.transform_values(&:uniq)
|
|
199
|
+
rescue => e
|
|
200
|
+
$stderr.puts "[rails-ai-context] extract_action_bindings failed: #{e.message}" if ENV["DEBUG"]
|
|
201
|
+
{}
|
|
202
|
+
end
|
|
203
|
+
|
|
152
204
|
def extract_cross_controller_composition(root)
|
|
153
205
|
views_dir = File.join(root, "app/views")
|
|
154
206
|
return [] unless Dir.exist?(views_dir)
|
|
@@ -166,7 +218,8 @@ module RailsAiContext
|
|
|
166
218
|
end
|
|
167
219
|
|
|
168
220
|
compositions.uniq
|
|
169
|
-
rescue
|
|
221
|
+
rescue => e
|
|
222
|
+
$stderr.puts "[rails-ai-context] extract_cross_controller_composition failed: #{e.message}" if ENV["DEBUG"]
|
|
170
223
|
[]
|
|
171
224
|
end
|
|
172
225
|
end
|
|
@@ -26,7 +26,9 @@ module RailsAiContext
|
|
|
26
26
|
ci_config: detect_ci,
|
|
27
27
|
coverage: detect_coverage,
|
|
28
28
|
factory_traits: detect_factory_traits,
|
|
29
|
-
test_count_by_category: detect_test_count_by_category
|
|
29
|
+
test_count_by_category: detect_test_count_by_category,
|
|
30
|
+
shared_examples: detect_shared_examples,
|
|
31
|
+
database_cleaner: detect_database_cleaner
|
|
30
32
|
}
|
|
31
33
|
rescue => e
|
|
32
34
|
{ error: e.message }
|
|
@@ -113,7 +115,8 @@ module RailsAiContext
|
|
|
113
115
|
file = path.sub("#{root}/", "")
|
|
114
116
|
factories = File.read(path).scan(/factory\s+:(\w+)/).flatten
|
|
115
117
|
names[file] = factories if factories.any?
|
|
116
|
-
rescue
|
|
118
|
+
rescue => e
|
|
119
|
+
$stderr.puts "[rails-ai-context] detect_factory_names failed: #{e.message}" if ENV["DEBUG"]
|
|
117
120
|
next
|
|
118
121
|
end
|
|
119
122
|
return names if names.any?
|
|
@@ -200,7 +203,8 @@ module RailsAiContext
|
|
|
200
203
|
content = File.read(gemfile_lock)
|
|
201
204
|
return "simplecov" if content.include?("simplecov (")
|
|
202
205
|
nil
|
|
203
|
-
rescue
|
|
206
|
+
rescue => e
|
|
207
|
+
$stderr.puts "[rails-ai-context] detect_coverage failed: #{e.message}" if ENV["DEBUG"]
|
|
204
208
|
nil
|
|
205
209
|
end
|
|
206
210
|
|
|
@@ -219,7 +223,47 @@ module RailsAiContext
|
|
|
219
223
|
return traits if traits.any?
|
|
220
224
|
end
|
|
221
225
|
nil
|
|
222
|
-
rescue
|
|
226
|
+
rescue => e
|
|
227
|
+
$stderr.puts "[rails-ai-context] detect_factory_traits failed: #{e.message}" if ENV["DEBUG"]
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def detect_shared_examples
|
|
232
|
+
shared = []
|
|
233
|
+
%w[spec test].each do |base|
|
|
234
|
+
support_dir = File.join(root, base, "support")
|
|
235
|
+
next unless Dir.exist?(support_dir)
|
|
236
|
+
Dir.glob(File.join(support_dir, "**/*.rb")).each do |path|
|
|
237
|
+
content = File.read(path) rescue next
|
|
238
|
+
content.scan(/(?:shared_examples|shared_context|shared_examples_for)\s+["']([^"']+)["']/).each do |m|
|
|
239
|
+
shared << { name: m[0], file: path.sub("#{root}/", "") }
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
shared.sort_by { |s| s[:name] }
|
|
244
|
+
rescue => e
|
|
245
|
+
$stderr.puts "[rails-ai-context] detect_shared_examples failed: #{e.message}" if ENV["DEBUG"]
|
|
246
|
+
[]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def detect_database_cleaner
|
|
250
|
+
gemfile_lock = File.join(root, "Gemfile.lock")
|
|
251
|
+
return nil unless File.exist?(gemfile_lock)
|
|
252
|
+
content = File.read(gemfile_lock)
|
|
253
|
+
if content.include?("database_cleaner")
|
|
254
|
+
strategy = nil
|
|
255
|
+
%w[spec/rails_helper.rb spec/spec_helper.rb test/test_helper.rb].each do |helper|
|
|
256
|
+
path = File.join(root, helper)
|
|
257
|
+
next unless File.exist?(path)
|
|
258
|
+
helper_content = File.read(path)
|
|
259
|
+
if (match = helper_content.match(/DatabaseCleaner\.strategy\s*=\s*:(\w+)/))
|
|
260
|
+
strategy = match[1]
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
{ detected: true, strategy: strategy }.compact
|
|
264
|
+
end
|
|
265
|
+
rescue => e
|
|
266
|
+
$stderr.puts "[rails-ai-context] detect_database_cleaner failed: #{e.message}" if ENV["DEBUG"]
|
|
223
267
|
nil
|
|
224
268
|
end
|
|
225
269
|
|
|
@@ -234,7 +278,8 @@ module RailsAiContext
|
|
|
234
278
|
end
|
|
235
279
|
end
|
|
236
280
|
counts
|
|
237
|
-
rescue
|
|
281
|
+
rescue => e
|
|
282
|
+
$stderr.puts "[rails-ai-context] detect_test_count_by_category failed: #{e.message}" if ENV["DEBUG"]
|
|
238
283
|
{}
|
|
239
284
|
end
|
|
240
285
|
end
|