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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -53
  3. data/CLAUDE.md +3 -1
  4. data/README.md +268 -197
  5. data/demo-trace.gif +0 -0
  6. data/demo-trace.tape +21 -0
  7. data/demo.gif +0 -0
  8. data/demo.tape +33 -0
  9. data/docs/GUIDE.md +9 -9
  10. data/lib/generators/rails_ai_context/install/install_generator.rb +2 -1
  11. data/lib/rails_ai_context/cli/tool_runner.rb +1 -1
  12. data/lib/rails_ai_context/configuration.rb +25 -1
  13. data/lib/rails_ai_context/doctor.rb +4 -2
  14. data/lib/rails_ai_context/fingerprinter.rb +6 -1
  15. data/lib/rails_ai_context/introspectors/accessibility_introspector.rb +52 -1
  16. data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +10 -2
  17. data/lib/rails_ai_context/introspectors/action_text_introspector.rb +22 -2
  18. data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +50 -4
  19. data/lib/rails_ai_context/introspectors/api_introspector.rb +41 -5
  20. data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +10 -4
  21. data/lib/rails_ai_context/introspectors/auth_introspector.rb +62 -7
  22. data/lib/rails_ai_context/introspectors/component_introspector.rb +6 -0
  23. data/lib/rails_ai_context/introspectors/config_introspector.rb +59 -9
  24. data/lib/rails_ai_context/introspectors/controller_introspector.rb +45 -13
  25. data/lib/rails_ai_context/introspectors/convention_detector.rb +25 -2
  26. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +58 -4
  27. data/lib/rails_ai_context/introspectors/design_token_introspector.rb +27 -5
  28. data/lib/rails_ai_context/introspectors/devops_introspector.rb +15 -8
  29. data/lib/rails_ai_context/introspectors/engine_introspector.rb +12 -3
  30. data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +36 -1
  31. data/lib/rails_ai_context/introspectors/gem_introspector.rb +47 -1
  32. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +49 -3
  33. data/lib/rails_ai_context/introspectors/job_introspector.rb +48 -5
  34. data/lib/rails_ai_context/introspectors/middleware_introspector.rb +24 -3
  35. data/lib/rails_ai_context/introspectors/migration_introspector.rb +4 -1
  36. data/lib/rails_ai_context/introspectors/model_introspector.rb +108 -11
  37. data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +57 -12
  38. data/lib/rails_ai_context/introspectors/performance_introspector.rb +34 -9
  39. data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +12 -2
  40. data/lib/rails_ai_context/introspectors/route_introspector.rb +25 -8
  41. data/lib/rails_ai_context/introspectors/schema_introspector.rb +45 -7
  42. data/lib/rails_ai_context/introspectors/seeds_introspector.rb +5 -2
  43. data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +59 -6
  44. data/lib/rails_ai_context/introspectors/test_introspector.rb +50 -5
  45. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +44 -13
  46. data/lib/rails_ai_context/introspectors/view_introspector.rb +46 -7
  47. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +25 -7
  48. data/lib/rails_ai_context/resources.rb +1 -1
  49. data/lib/rails_ai_context/server.rb +6 -3
  50. data/lib/rails_ai_context/tasks/rails_ai_context.rake +8 -4
  51. data/lib/rails_ai_context/tools/analyze_feature.rb +66 -19
  52. data/lib/rails_ai_context/tools/base_tool.rb +1 -1
  53. data/lib/rails_ai_context/tools/diagnose.rb +4 -2
  54. data/lib/rails_ai_context/tools/get_callbacks.rb +4 -2
  55. data/lib/rails_ai_context/tools/get_concern.rb +12 -6
  56. data/lib/rails_ai_context/tools/get_controllers.rb +10 -5
  57. data/lib/rails_ai_context/tools/get_conventions.rb +4 -2
  58. data/lib/rails_ai_context/tools/get_design_system.rb +2 -1
  59. data/lib/rails_ai_context/tools/get_env.rb +8 -4
  60. data/lib/rails_ai_context/tools/get_helper_methods.rb +6 -3
  61. data/lib/rails_ai_context/tools/get_job_pattern.rb +2 -1
  62. data/lib/rails_ai_context/tools/get_model_details.rb +10 -5
  63. data/lib/rails_ai_context/tools/get_partial_interface.rb +14 -7
  64. data/lib/rails_ai_context/tools/get_schema.rb +2 -1
  65. data/lib/rails_ai_context/tools/get_service_pattern.rb +2 -1
  66. data/lib/rails_ai_context/tools/get_stimulus.rb +2 -1
  67. data/lib/rails_ai_context/tools/get_test_info.rb +4 -2
  68. data/lib/rails_ai_context/tools/get_turbo_map.rb +22 -11
  69. data/lib/rails_ai_context/tools/get_view.rb +6 -3
  70. data/lib/rails_ai_context/tools/migration_advisor.rb +2 -1
  71. data/lib/rails_ai_context/tools/onboard.rb +2 -1
  72. data/lib/rails_ai_context/tools/performance_check.rb +2 -1
  73. data/lib/rails_ai_context/tools/query.rb +5 -1
  74. data/lib/rails_ai_context/tools/read_logs.rb +3 -0
  75. data/lib/rails_ai_context/tools/runtime_info.rb +10 -5
  76. data/lib/rails_ai_context/tools/search_code.rb +8 -4
  77. data/lib/rails_ai_context/tools/search_docs.rb +2 -1
  78. data/lib/rails_ai_context/tools/session_context.rb +2 -1
  79. data/lib/rails_ai_context/tools/validate.rb +16 -8
  80. data/lib/rails_ai_context/version.rb +1 -1
  81. 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
- { detected: true, note: "Sharding configuration found in database.yml" } if content.match?(/shard/i)
69
- rescue
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
- if line.match?(/\A\s{2}(\w+):/) && !line.include?("<<")
121
- db_name = line.strip.chomp(":")
122
- databases << { name: db_name } unless skip_keys.include?(db_name)
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].each do |col|
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
- missing << {
200
- table: table_name,
201
- column: col[:name],
202
- suggestion: "add_index :#{table_name}, :#{col[:name]}"
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
- tasks << {
53
+ entry = {
54
54
  name: name,
55
55
  description: last_desc,
56
56
  file: relative
57
- }.compact
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: route.path.spec.to_s.gsub("(.:format)", ""),
43
+ path: route_path,
39
44
  controller: route.defaults[:controller],
40
- action: route.defaults[:action],
45
+ action: action,
41
46
  name: route.name,
42
47
  constraints: extract_constraints(route)
43
- }.compact
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] }.compact
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: extract_outlets(content),
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