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
@@ -14,6 +14,7 @@ module RailsAiContext
14
14
  {
15
15
  turbo_frames: extract_turbo_frames,
16
16
  turbo_streams: extract_turbo_stream_templates,
17
+ stream_actions: extract_stream_actions,
17
18
  model_broadcasts: extract_model_broadcasts,
18
19
  morph_meta: detect_morph_meta,
19
20
  permanent_elements: extract_permanent_elements,
@@ -43,13 +44,18 @@ module RailsAiContext
43
44
  content = File.read(path)
44
45
  relative = path.sub("#{views_dir}/", "")
45
46
 
46
- content.scan(/turbo_frame_tag\s+[:"']?(\w+)/).each do |match|
47
- frames << { id: match[0], file: relative }
47
+ content.each_line do |line|
48
+ next unless (match = line.match(/turbo_frame_tag\s+[:"']?(\w+)/))
49
+ entry = { id: match[1], file: relative }
50
+ src_match = line.match(/src:\s*["']?([^"',\s)]+)/)
51
+ entry[:src] = src_match[1] if src_match
52
+ frames << entry
48
53
  end
49
54
  end
50
55
 
51
56
  frames.sort_by { |f| f[:id] }
52
- rescue
57
+ rescue => e
58
+ $stderr.puts "[rails-ai-context] extract_turbo_frames failed: #{e.message}" if ENV["DEBUG"]
53
59
  []
54
60
  end
55
61
 
@@ -61,6 +67,21 @@ module RailsAiContext
61
67
  end.sort
62
68
  end
63
69
 
70
+ def extract_stream_actions
71
+ actions = Hash.new(0)
72
+ return actions unless Dir.exist?(views_dir)
73
+
74
+ Dir.glob(File.join(views_dir, "**", "*.turbo_stream.erb")).each do |path|
75
+ content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
76
+ content.scan(/turbo_stream\.(\w+)/).each { |action| actions[action[0]] += 1 }
77
+ content.scan(/<turbo-stream\s+action=["'](\w+)["']/).each { |action| actions[action[0]] += 1 }
78
+ end
79
+ actions
80
+ rescue => e
81
+ $stderr.puts "[rails-ai-context] extract_stream_actions failed: #{e.message}" if ENV["DEBUG"]
82
+ {}
83
+ end
84
+
64
85
  def extract_model_broadcasts
65
86
  models_dir = File.join(root, "app/models")
66
87
  return [] unless Dir.exist?(models_dir)
@@ -77,7 +98,8 @@ module RailsAiContext
77
98
  end
78
99
 
79
100
  broadcasts.sort_by { |b| b[:model] }
80
- rescue
101
+ rescue => e
102
+ $stderr.puts "[rails-ai-context] extract_model_broadcasts failed: #{e.message}" if ENV["DEBUG"]
81
103
  []
82
104
  end
83
105
 
@@ -89,7 +111,8 @@ module RailsAiContext
89
111
  content = File.read(path) rescue next
90
112
  content.include?('name="turbo-refresh-method"') && content.include?('content="morph"')
91
113
  end
92
- rescue
114
+ rescue => e
115
+ $stderr.puts "[rails-ai-context] detect_morph_meta failed: #{e.message}" if ENV["DEBUG"]
93
116
  false
94
117
  end
95
118
 
@@ -122,7 +145,8 @@ module RailsAiContext
122
145
  end
123
146
 
124
147
  elements.uniq
125
- rescue
148
+ rescue => e
149
+ $stderr.puts "[rails-ai-context] extract_permanent_elements failed: #{e.message}" if ENV["DEBUG"]
126
150
  []
127
151
  end
128
152
 
@@ -146,7 +170,8 @@ module RailsAiContext
146
170
  end
147
171
 
148
172
  counts
149
- rescue
173
+ rescue => e
174
+ $stderr.puts "[rails-ai-context] extract_turbo_drive_settings failed: #{e.message}" if ENV["DEBUG"]
150
175
  { "data-turbo-false": 0, "data-turbo-action": 0, "data-turbo-preload": 0 }
151
176
  end
152
177
 
@@ -159,7 +184,8 @@ module RailsAiContext
159
184
  native_navigation: detect_native_navigation(controllers_dir),
160
185
  native_conditionals: detect_native_conditionals
161
186
  }
162
- rescue
187
+ rescue => e
188
+ $stderr.puts "[rails-ai-context] detect_turbo_native failed: #{e.message}" if ENV["DEBUG"]
163
189
  { detected: false, native_helpers: [], native_navigation: [], native_conditionals: 0 }
164
190
  end
165
191
 
@@ -170,7 +196,8 @@ module RailsAiContext
170
196
  content = File.read(path) rescue next
171
197
  content.match?(/include\s+Turbo::Native::Navigation/)
172
198
  end
173
- rescue
199
+ rescue => e
200
+ $stderr.puts "[rails-ai-context] detect_native_include failed: #{e.message}" if ENV["DEBUG"]
174
201
  false
175
202
  end
176
203
 
@@ -183,7 +210,8 @@ module RailsAiContext
183
210
  path.sub("#{root}/", "")
184
211
  end
185
212
  end.sort
186
- rescue
213
+ rescue => e
214
+ $stderr.puts "[rails-ai-context] detect_native_helpers failed: #{e.message}" if ENV["DEBUG"]
187
215
  []
188
216
  end
189
217
 
@@ -207,7 +235,8 @@ module RailsAiContext
207
235
  end
208
236
 
209
237
  results.sort_by { |r| [ r[:file], r[:method] ] }
210
- rescue
238
+ rescue => e
239
+ $stderr.puts "[rails-ai-context] detect_native_navigation failed: #{e.message}" if ENV["DEBUG"]
211
240
  []
212
241
  end
213
242
 
@@ -221,7 +250,8 @@ module RailsAiContext
221
250
  end
222
251
 
223
252
  count
224
- rescue
253
+ rescue => e
254
+ $stderr.puts "[rails-ai-context] detect_native_conditionals failed: #{e.message}" if ENV["DEBUG"]
225
255
  0
226
256
  end
227
257
 
@@ -248,7 +278,8 @@ module RailsAiContext
248
278
  end
249
279
 
250
280
  responses.uniq.sort_by { |r| [ r[:controller], r[:action] ] }
251
- rescue
281
+ rescue => e
282
+ $stderr.puts "[rails-ai-context] extract_turbo_stream_responses failed: #{e.message}" if ENV["DEBUG"]
252
283
  []
253
284
  end
254
285
  end
@@ -21,7 +21,8 @@ module RailsAiContext
21
21
  template_engines: detect_template_engines,
22
22
  form_builders_detected: detect_form_builders,
23
23
  component_usage: detect_component_usage,
24
- layout_mapping: extract_layout_mapping
24
+ layout_mapping: extract_layout_mapping,
25
+ conditional_layouts: detect_conditional_layouts
25
26
  }
26
27
  rescue => e
27
28
  { error: e.message }
@@ -42,8 +43,16 @@ module RailsAiContext
42
43
  return [] unless Dir.exist?(dir)
43
44
 
44
45
  Dir.glob(File.join(dir, "*")).filter_map do |path|
45
- File.basename(path) if File.file?(path)
46
- end.sort
46
+ next unless File.file?(path)
47
+ content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
48
+ yields = content.scan(/<%=?\s*(?:yield|content_for)\s*[:(]?\s*:?(\w*)/).flatten.reject(&:empty?)
49
+ entry = { name: File.basename(path) }
50
+ entry[:yields] = yields unless yields.empty?
51
+ entry
52
+ rescue => e
53
+ $stderr.puts "[rails-ai-context] extract_layouts read failed: #{e.message}" if ENV["DEBUG"]
54
+ { name: File.basename(path) }
55
+ end.sort_by { |l| l[:name] }
47
56
  end
48
57
 
49
58
  def extract_templates
@@ -98,7 +107,8 @@ module RailsAiContext
98
107
  file: relative,
99
108
  methods: methods
100
109
  }
101
- rescue
110
+ rescue => e
111
+ $stderr.puts "[rails-ai-context] extract_helpers failed: #{e.message}" if ENV["DEBUG"]
102
112
  nil
103
113
  end.sort_by { |h| h[:file] }
104
114
  end
@@ -150,7 +160,8 @@ module RailsAiContext
150
160
  end
151
161
 
152
162
  counts.sort_by { |_, v| -v }.to_h
153
- rescue
163
+ rescue => e
164
+ $stderr.puts "[rails-ai-context] detect_form_builders failed: #{e.message}" if ENV["DEBUG"]
154
165
  {}
155
166
  end
156
167
 
@@ -168,7 +179,8 @@ module RailsAiContext
168
179
  end
169
180
 
170
181
  components.to_a.sort
171
- rescue
182
+ rescue => e
183
+ $stderr.puts "[rails-ai-context] detect_component_usage failed: #{e.message}" if ENV["DEBUG"]
172
184
  []
173
185
  end
174
186
 
@@ -183,7 +195,34 @@ module RailsAiContext
183
195
  name = basename.sub(/\.(html|xml|json)\.(erb|haml|slim)\z/, "").sub(/\.(erb|haml|slim)\z/, "")
184
196
  name
185
197
  end.uniq.sort
186
- rescue
198
+ rescue => e
199
+ $stderr.puts "[rails-ai-context] extract_layout_mapping failed: #{e.message}" if ENV["DEBUG"]
200
+ []
201
+ end
202
+
203
+ def detect_conditional_layouts
204
+ layouts = []
205
+ controllers_dir = File.join(app.root, "app", "controllers")
206
+ return layouts unless Dir.exist?(controllers_dir)
207
+
208
+ Dir.glob(File.join(controllers_dir, "**", "*.rb")).each do |path|
209
+ content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
210
+ content.each_line do |line|
211
+ if (match = line.match(/\A\s*layout\s+["':]*(\w+)["']?(.*)$/))
212
+ entry = { layout: match[1], controller: File.basename(path, ".rb").camelize }
213
+ conditions = match[2].strip
214
+ entry[:only] = conditions.scan(/only:\s*\[?([^\]]+)\]?/).flatten.first&.scan(/:(\w+)/)&.flatten if conditions.include?("only:")
215
+ entry[:except] = conditions.scan(/except:\s*\[?([^\]]+)\]?/).flatten.first&.scan(/:(\w+)/)&.flatten if conditions.include?("except:")
216
+ entry[:condition] = conditions.strip unless conditions.empty?
217
+ layouts << entry
218
+ end
219
+ end
220
+ rescue => e
221
+ $stderr.puts "[rails-ai-context] detect_conditional_layouts failed: #{e.message}" if ENV["DEBUG"]
222
+ end
223
+ layouts
224
+ rescue => e
225
+ $stderr.puts "[rails-ai-context] detect_conditional_layouts failed: #{e.message}" if ENV["DEBUG"]
187
226
  []
188
227
  end
189
228
  end
@@ -41,8 +41,10 @@ module RailsAiContext
41
41
  relative = path.sub("#{views_dir}/", "")
42
42
  content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
43
43
 
44
+ slots = extract_slot_refs(content)
45
+
44
46
  if phlex_view?(path, content)
45
- templates[relative] = {
47
+ entry = {
46
48
  lines: content.lines.count,
47
49
  partials: extract_partial_refs(content),
48
50
  stimulus: extract_stimulus_refs(content),
@@ -50,12 +52,16 @@ module RailsAiContext
50
52
  helpers: extract_phlex_helper_calls(content),
51
53
  phlex: true
52
54
  }
55
+ entry[:slots] = slots unless slots.empty?
56
+ templates[relative] = entry
53
57
  else
54
- templates[relative] = {
58
+ entry = {
55
59
  lines: content.lines.count,
56
60
  partials: extract_partial_refs(content),
57
61
  stimulus: extract_stimulus_refs(content)
58
62
  }
63
+ entry[:slots] = slots unless slots.empty?
64
+ templates[relative] = entry
59
65
  end
60
66
  end
61
67
  templates
@@ -569,7 +575,8 @@ module RailsAiContext
569
575
  builders[:simple_form_for] = content.scan(/\bsimple_form_for\b/).size
570
576
  builders[:formtastic] = content.scan(/\bsemantic_form_for\b/).size
571
577
  builders.reject { |_, v| v == 0 }
572
- rescue
578
+ rescue => e
579
+ $stderr.puts "[rails-ai-context] extract_form_builders failed: #{e.message}" if ENV["DEBUG"]
573
580
  {}
574
581
  end
575
582
 
@@ -580,7 +587,8 @@ module RailsAiContext
580
587
  tags[tag.to_sym] = count if count > 0
581
588
  end
582
589
  tags
583
- rescue
590
+ rescue => e
591
+ $stderr.puts "[rails-ai-context] extract_semantic_html failed: #{e.message}" if ENV["DEBUG"]
584
592
  {}
585
593
  end
586
594
 
@@ -593,7 +601,8 @@ module RailsAiContext
593
601
  sr_only_count = content.scan(/\bsr-only\b/).size
594
602
  patterns[:sr_only] = sr_only_count if sr_only_count > 0
595
603
  patterns
596
- rescue
604
+ rescue => e
605
+ $stderr.puts "[rails-ai-context] extract_accessibility_patterns failed: #{e.message}" if ENV["DEBUG"]
597
606
  {}
598
607
  end
599
608
 
@@ -636,7 +645,8 @@ module RailsAiContext
636
645
  end
637
646
 
638
647
  examples.values.map { |e| e.except(:score) }.first(5)
639
- rescue
648
+ rescue => e
649
+ $stderr.puts "[rails-ai-context] extract_canonical_examples failed: #{e.message}" if ENV["DEBUG"]
640
650
  []
641
651
  end
642
652
 
@@ -697,7 +707,8 @@ module RailsAiContext
697
707
  description = infer_partial_description(name, content)
698
708
  { name: name, lines: content.lines.size, description: description }
699
709
  end
700
- rescue
710
+ rescue => e
711
+ $stderr.puts "[rails-ai-context] discover_shared_partials failed: #{e.message}" if ENV["DEBUG"]
701
712
  []
702
713
  end
703
714
 
@@ -818,6 +829,13 @@ module RailsAiContext
818
829
  end
819
830
  refs.uniq
820
831
  end
832
+
833
+ def extract_slot_refs(content)
834
+ content.scan(/\b(?:renders_one|renders_many)\s+:(\w+)/).flatten
835
+ rescue => e
836
+ $stderr.puts "[rails-ai-context] extract_slot_refs failed: #{e.message}" if ENV["DEBUG"]
837
+ []
838
+ end
821
839
  end
822
840
  end
823
841
  end
@@ -113,7 +113,7 @@ module RailsAiContext
113
113
  content = JSON.pretty_generate(data)
114
114
  [ { uri: uri, mime_type: "application/json", text: content } ]
115
115
  else
116
- raise "Unknown resource: #{uri}"
116
+ raise RailsAiContext::Error, "Unknown resource: #{uri}"
117
117
  end
118
118
  end
119
119
  end
@@ -105,9 +105,10 @@ module RailsAiContext
105
105
 
106
106
  def start_stdio(server)
107
107
  transport = MCP::Server::Transports::StdioTransport.new(server)
108
+ tools = active_tools(RailsAiContext.configuration)
108
109
  # Log to stderr so we don't pollute the JSON-RPC channel on stdout
109
110
  $stderr.puts "[rails-ai-context] MCP server started (stdio transport)"
110
- $stderr.puts "[rails-ai-context] Tools: #{TOOLS.map { |t| t.tool_name }.join(', ')}"
111
+ $stderr.puts "[rails-ai-context] Tools (#{tools.size}): #{tools.map { |t| t.tool_name }.join(', ')}"
111
112
  maybe_start_live_reload(server)
112
113
  transport.open
113
114
  end
@@ -119,13 +120,15 @@ module RailsAiContext
119
120
  # Build a minimal Rack app that delegates to the MCP transport
120
121
  rack_app = build_rack_app(transport, config.http_path)
121
122
 
122
- unless config.http_bind == "127.0.0.1" || config.http_bind == "::1" || config.http_bind == "localhost"
123
+ loopback = %w[127.0.0.1 ::1 localhost].freeze
124
+ unless loopback.include?(config.http_bind)
123
125
  $stderr.puts "[rails-ai-context] WARNING: MCP HTTP transport binding to #{config.http_bind} — " \
124
126
  "this exposes all tools to the network without authentication. " \
125
127
  "Use 127.0.0.1 (default) unless you have external auth in place."
126
128
  end
129
+ tools = active_tools(config)
127
130
  $stderr.puts "[rails-ai-context] MCP server starting on #{config.http_bind}:#{config.http_port}#{config.http_path}"
128
- $stderr.puts "[rails-ai-context] Tools: #{TOOLS.map { |t| t.tool_name }.join(', ')}"
131
+ $stderr.puts "[rails-ai-context] Tools (#{tools.size}): #{tools.map { |t| t.tool_name }.join(', ')}"
129
132
  maybe_start_live_reload(server)
130
133
 
131
134
  begin
@@ -91,7 +91,8 @@ def save_tool_mode_to_initializer(mode)
91
91
  end
92
92
 
93
93
  File.write(init_path, content)
94
- rescue
94
+ rescue => e
95
+ $stderr.puts "[rails-ai-context] save_tool_mode_to_initializer failed: #{e.message}" if ENV["DEBUG"]
95
96
  nil
96
97
  end unless defined?(save_tool_mode_to_initializer)
97
98
 
@@ -113,7 +114,8 @@ def tool_mode_configured?
113
114
  content = File.read(init_path)
114
115
  # Check for uncommented tool_mode line (not just a comment)
115
116
  content.match?(/^\s*config\.tool_mode\s*=/)
116
- rescue
117
+ rescue => e
118
+ $stderr.puts "[rails-ai-context] tool_mode_configured? failed: #{e.message}" if ENV["DEBUG"]
117
119
  false
118
120
  end unless defined?(tool_mode_configured?)
119
121
 
@@ -136,7 +138,8 @@ def save_ai_tools_to_initializer(tools)
136
138
 
137
139
  File.write(init_path, content)
138
140
  puts "💾 Saved to config/initializers/rails_ai_context.rb"
139
- rescue
141
+ rescue => e
142
+ $stderr.puts "[rails-ai-context] save_ai_tools_to_initializer failed: #{e.message}" if ENV["DEBUG"]
140
143
  nil
141
144
  end unless defined?(save_ai_tools_to_initializer)
142
145
 
@@ -165,7 +168,8 @@ def add_ai_tool_to_initializer(format)
165
168
  File.write(init_path, content)
166
169
  puts "💾 Set config.ai_tools = %i[#{format_sym}]"
167
170
  end
168
- rescue
171
+ rescue => e
172
+ $stderr.puts "[rails-ai-context] add_ai_tool_to_initializer failed: #{e.message}" if ENV["DEBUG"]
169
173
  nil
170
174
  end unless defined?(add_ai_tool_to_initializer)
171
175
 
@@ -202,14 +202,20 @@ module RailsAiContext
202
202
  lines << "## Services (#{found.size})"
203
203
  found.each do |path|
204
204
  relative = path.sub("#{root}/", "")
205
- source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
205
+ source = begin
206
+ File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
207
+ rescue => e
208
+ $stderr.puts "[rails-ai-context] discover_services failed: #{e.message}" if ENV["DEBUG"]
209
+ next
210
+ end
206
211
  line_count = source.lines.size
207
212
  methods = source.scan(/\A\s*def (?:self\.)?(\w+)/m).flatten.reject { |m| m == "initialize" }
208
213
  lines << "- `#{relative}` (#{line_count} lines)"
209
214
  lines << " Methods: #{methods.first(20).join(', ')}" if methods.any?
210
215
  end
211
216
  lines << ""
212
- rescue
217
+ rescue => e
218
+ $stderr.puts "[rails-ai-context] discover_services failed: #{e.message}" if ENV["DEBUG"]
213
219
  nil
214
220
  end
215
221
 
@@ -226,13 +232,19 @@ module RailsAiContext
226
232
  lines << "## Jobs (#{found.size})"
227
233
  found.each do |path|
228
234
  relative = path.sub("#{root}/", "")
229
- source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
235
+ source = begin
236
+ File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
237
+ rescue => e
238
+ $stderr.puts "[rails-ai-context] discover_jobs failed: #{e.message}" if ENV["DEBUG"]
239
+ next
240
+ end
230
241
  queue = source.match(/queue_as\s+[:'"](\w+)/)&.captures&.first || "default"
231
242
  retries = source.match(/retry_on.*attempts:\s*(\d+)/)&.captures&.first
232
243
  lines << "- `#{relative}` (queue: #{queue}#{retries ? ", retries: #{retries}" : ""})"
233
244
  end
234
245
  lines << ""
235
- rescue
246
+ rescue => e
247
+ $stderr.puts "[rails-ai-context] discover_jobs failed: #{e.message}" if ENV["DEBUG"]
236
248
  nil
237
249
  end
238
250
 
@@ -249,7 +261,12 @@ module RailsAiContext
249
261
  lines << "## Views (#{found.size})"
250
262
  found.each do |path|
251
263
  relative = path.sub("#{views_dir}/", "")
252
- source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
264
+ source = begin
265
+ File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
266
+ rescue => e
267
+ $stderr.puts "[rails-ai-context] discover_views failed: #{e.message}" if ENV["DEBUG"]
268
+ next
269
+ end
253
270
  line_count = source.lines.size
254
271
  partials = source.scan(/render\s+(?:partial:\s*)?["']([^"']+)["']/).flatten
255
272
  stimulus = source.scan(/data-controller=["']([^"']+)["']/).flat_map { |m| m.first.split }
@@ -259,7 +276,8 @@ module RailsAiContext
259
276
  lines << detail
260
277
  end
261
278
  lines << ""
262
- rescue
279
+ rescue => e
280
+ $stderr.puts "[rails-ai-context] discover_views failed: #{e.message}" if ENV["DEBUG"]
263
281
  nil
264
282
  end
265
283
 
@@ -315,13 +333,19 @@ module RailsAiContext
315
333
  lines << "## Tests (#{found.size})"
316
334
  found.each do |path|
317
335
  relative = path.sub("#{root}/", "")
318
- source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
336
+ source = begin
337
+ File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
338
+ rescue => e
339
+ $stderr.puts "[rails-ai-context] discover_tests failed: #{e.message}" if ENV["DEBUG"]
340
+ next
341
+ end
319
342
  test_count = source.scan(/\b(?:it|test|should)\b/).size
320
343
  lines << "- `#{relative}` (#{test_count} tests)"
321
344
  end
322
345
  lines << ""
323
346
  found
324
- rescue
347
+ rescue => e
348
+ $stderr.puts "[rails-ai-context] discover_tests failed: #{e.message}" if ENV["DEBUG"]
325
349
  []
326
350
  end
327
351
 
@@ -377,7 +401,8 @@ module RailsAiContext
377
401
  lines << "## Test Coverage Gaps"
378
402
  gaps.each { |g| lines << "- #{g}" }
379
403
  lines << ""
380
- rescue
404
+ rescue => e
405
+ $stderr.puts "[rails-ai-context] discover_test_gaps failed: #{e.message}" if ENV["DEBUG"]
381
406
  nil
382
407
  end
383
408
 
@@ -392,14 +417,20 @@ module RailsAiContext
392
417
  # Fallback: read source file
393
418
  path = Rails.root.join("app", "controllers", "#{parent_class.underscore}.rb")
394
419
  return [] unless File.exist?(path)
395
- source = File.read(path, encoding: "UTF-8") rescue nil
420
+ source = begin
421
+ File.read(path, encoding: "UTF-8")
422
+ rescue => e
423
+ $stderr.puts "[rails-ai-context] detect_parent_filters_for_analyze failed: #{e.message}" if ENV["DEBUG"]
424
+ nil
425
+ end
396
426
  return [] unless source
397
427
 
398
428
  source.each_line.filter_map do |line|
399
429
  next if line.include?("only:") || line.include?("except:")
400
430
  { name: $1 } if line.match(/\A\s*before_action\s+:(\w+)/)
401
431
  end
402
- rescue
432
+ rescue => e
433
+ $stderr.puts "[rails-ai-context] detect_parent_filters_for_analyze failed: #{e.message}" if ENV["DEBUG"]
403
434
  []
404
435
  end
405
436
 
@@ -444,7 +475,8 @@ module RailsAiContext
444
475
  lines << "## Concerns"
445
476
  concerns.sort.each { |name, count| lines << "- **#{name}** (used by #{count} model#{'s' if count > 1})" }
446
477
  lines << ""
447
- rescue
478
+ rescue => e
479
+ $stderr.puts "[rails-ai-context] discover_concerns failed: #{e.message}" if ENV["DEBUG"]
448
480
  nil
449
481
  end
450
482
 
@@ -481,7 +513,8 @@ module RailsAiContext
481
513
  lines << "- `#{relative}`"
482
514
  end
483
515
  lines << ""
484
- rescue
516
+ rescue => e
517
+ $stderr.puts "[rails-ai-context] discover_channels failed: #{e.message}" if ENV["DEBUG"]
485
518
  nil
486
519
  end
487
520
 
@@ -496,12 +529,18 @@ module RailsAiContext
496
529
  lines << "## Mailers (#{found.size})"
497
530
  found.each do |path|
498
531
  relative = path.sub("#{root}/", "")
499
- source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
532
+ source = begin
533
+ File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
534
+ rescue => e
535
+ $stderr.puts "[rails-ai-context] discover_mailers failed: #{e.message}" if ENV["DEBUG"]
536
+ next
537
+ end
500
538
  methods = source.scan(/\A\s*def (\w+)/m).flatten.reject { |m| m == "initialize" }
501
539
  lines << "- `#{relative}` — #{methods.join(', ')}" if methods.any?
502
540
  end
503
541
  lines << ""
504
- rescue
542
+ rescue => e
543
+ $stderr.puts "[rails-ai-context] discover_mailers failed: #{e.message}" if ENV["DEBUG"]
505
544
  nil
506
545
  end
507
546
 
@@ -519,7 +558,8 @@ module RailsAiContext
519
558
  lines << "- `#{f[:file]}`: #{f[:issue]}"
520
559
  end
521
560
  lines << ""
522
- rescue
561
+ rescue => e
562
+ $stderr.puts "[rails-ai-context] discover_accessibility failed: #{e.message}" if ENV["DEBUG"]
523
563
  nil
524
564
  end
525
565
 
@@ -543,7 +583,8 @@ module RailsAiContext
543
583
  lines << "- **#{c[:name]}**#{slot_info}#{used_in}"
544
584
  end
545
585
  lines << ""
546
- rescue
586
+ rescue => e
587
+ $stderr.puts "[rails-ai-context] discover_components failed: #{e.message}" if ENV["DEBUG"]
547
588
  nil
548
589
  end
549
590
 
@@ -556,7 +597,12 @@ module RailsAiContext
556
597
  dirs.each do |dir|
557
598
  Dir.glob(File.join(dir, "**", "*.rb")).each do |path|
558
599
  next unless File.basename(path, ".rb").include?(pattern) || path.downcase.include?(pattern)
559
- source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
600
+ source = begin
601
+ File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
602
+ rescue => e
603
+ $stderr.puts "[rails-ai-context] discover_env_dependencies failed: #{e.message}" if ENV["DEBUG"]
604
+ next
605
+ end
560
606
  source.scan(/ENV\[["']([^"']+)["']\]|ENV\.fetch\(["']([^"']+)["']\)/).each do |m|
561
607
  env_vars << (m[0] || m[1])
562
608
  end
@@ -567,7 +613,8 @@ module RailsAiContext
567
613
  lines << "## Environment Dependencies"
568
614
  env_vars.sort.each { |v| lines << "- `#{v}`" }
569
615
  lines << ""
570
- rescue
616
+ rescue => e
617
+ $stderr.puts "[rails-ai-context] discover_env_dependencies failed: #{e.message}" if ENV["DEBUG"]
571
618
  nil
572
619
  end
573
620
  end
@@ -150,7 +150,7 @@ module RailsAiContext
150
150
  # App size classification — tools use this to auto-tune pagination and detail
151
151
  # small: <15 models, medium: 15-50 models, large: 50+ models
152
152
  def app_size
153
- ctx = SHARED_CACHE[:context]
153
+ ctx = SHARED_CACHE[:mutex].synchronize { SHARED_CACHE[:context] }
154
154
  return :medium unless ctx
155
155
 
156
156
  model_count = ctx[:models]&.size || 0
@@ -377,7 +377,8 @@ module RailsAiContext
377
377
  end
378
378
 
379
379
  lines
380
- rescue
380
+ rescue => e
381
+ $stderr.puts "[rails-ai-context] gather_git_context failed: #{e.message}" if ENV["DEBUG"]
381
382
  []
382
383
  end
383
384
 
@@ -390,7 +391,8 @@ module RailsAiContext
390
391
  return [] if text.include?("Log file is empty") || text.include?("not found") || text.include?("No entries")
391
392
 
392
393
  [ "## Recent Error Logs", text, "" ]
393
- rescue
394
+ rescue => e
395
+ $stderr.puts "[rails-ai-context] gather_log_context failed: #{e.message}" if ENV["DEBUG"]
394
396
  []
395
397
  end
396
398
  end
@@ -257,7 +257,8 @@ module RailsAiContext
257
257
  start_line: start_idx + 1,
258
258
  end_line: end_idx + 1
259
259
  }
260
- rescue
260
+ rescue => e
261
+ $stderr.puts "[rails-ai-context] extract_method_source failed: #{e.message}" if ENV["DEBUG"]
261
262
  nil
262
263
  end
263
264
 
@@ -294,7 +295,8 @@ module RailsAiContext
294
295
  end
295
296
 
296
297
  concern_callbacks
297
- rescue
298
+ rescue => e
299
+ $stderr.puts "[rails-ai-context] find_concern_callbacks failed: #{e.message}" if ENV["DEBUG"]
298
300
  {}
299
301
  end
300
302
  end