rails-ai-context 4.2.3 → 4.3.1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/CLAUDE.md +4 -4
  4. data/CONTRIBUTING.md +1 -1
  5. data/README.md +7 -7
  6. data/SECURITY.md +2 -1
  7. data/docs/GUIDE.md +3 -3
  8. data/lib/generators/rails_ai_context/install/install_generator.rb +2 -2
  9. data/lib/rails_ai_context/configuration.rb +4 -2
  10. data/lib/rails_ai_context/doctor.rb +6 -1
  11. data/lib/rails_ai_context/fingerprinter.rb +24 -0
  12. data/lib/rails_ai_context/introspectors/component_introspector.rb +122 -7
  13. data/lib/rails_ai_context/introspectors/performance_introspector.rb +18 -10
  14. data/lib/rails_ai_context/introspectors/schema_introspector.rb +183 -6
  15. data/lib/rails_ai_context/introspectors/view_introspector.rb +2 -2
  16. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +61 -8
  17. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +10 -19
  18. data/lib/rails_ai_context/serializers/claude_serializer.rb +42 -11
  19. data/lib/rails_ai_context/serializers/context_file_serializer.rb +14 -3
  20. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +1 -1
  21. data/lib/rails_ai_context/serializers/design_system_helper.rb +8 -1
  22. data/lib/rails_ai_context/serializers/opencode_serializer.rb +5 -2
  23. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +165 -64
  24. data/lib/rails_ai_context/server.rb +12 -1
  25. data/lib/rails_ai_context/tools/base_tool.rb +63 -1
  26. data/lib/rails_ai_context/tools/diagnose.rb +436 -0
  27. data/lib/rails_ai_context/tools/generate_test.rb +571 -0
  28. data/lib/rails_ai_context/tools/get_callbacks.rb +27 -4
  29. data/lib/rails_ai_context/tools/get_component_catalog.rb +11 -2
  30. data/lib/rails_ai_context/tools/get_context.rb +70 -8
  31. data/lib/rails_ai_context/tools/get_conventions.rb +59 -0
  32. data/lib/rails_ai_context/tools/get_design_system.rb +45 -7
  33. data/lib/rails_ai_context/tools/get_edit_context.rb +3 -2
  34. data/lib/rails_ai_context/tools/get_env.rb +51 -24
  35. data/lib/rails_ai_context/tools/get_frontend_stack.rb +100 -9
  36. data/lib/rails_ai_context/tools/get_model_details.rb +19 -0
  37. data/lib/rails_ai_context/tools/get_partial_interface.rb +1 -1
  38. data/lib/rails_ai_context/tools/get_stimulus.rb +13 -7
  39. data/lib/rails_ai_context/tools/get_turbo_map.rb +35 -2
  40. data/lib/rails_ai_context/tools/get_view.rb +65 -9
  41. data/lib/rails_ai_context/tools/migration_advisor.rb +4 -0
  42. data/lib/rails_ai_context/tools/onboard.rb +755 -0
  43. data/lib/rails_ai_context/tools/query.rb +4 -2
  44. data/lib/rails_ai_context/tools/read_logs.rb +4 -1
  45. data/lib/rails_ai_context/tools/review_changes.rb +299 -0
  46. data/lib/rails_ai_context/tools/runtime_info.rb +289 -0
  47. data/lib/rails_ai_context/tools/search_code.rb +23 -4
  48. data/lib/rails_ai_context/tools/security_scan.rb +7 -1
  49. data/lib/rails_ai_context/tools/session_context.rb +132 -0
  50. data/lib/rails_ai_context/version.rb +1 -1
  51. metadata +10 -4
@@ -105,7 +105,11 @@ module RailsAiContext
105
105
 
106
106
  private_class_method def self.format_summary(model_broadcasts, rb_broadcasts, view_subscriptions, view_frames, warnings, filter_label: nil)
107
107
  total_broadcasts = model_broadcasts.size + rb_broadcasts.size
108
+ turbo_data = cached_context[:turbo]
109
+ turbo_stream_response_count = turbo_data.is_a?(Hash) && !turbo_data[:error] ? turbo_data[:turbo_stream_responses]&.size.to_i : 0
110
+
108
111
  lines = [ "# Turbo Map", "" ]
112
+ lines << "- **Turbo Stream responses:** #{turbo_stream_response_count} (controller `.turbo_stream.erb` templates)" if turbo_stream_response_count > 0
109
113
  lines << "- **Model broadcasts:** #{model_broadcasts.size} (via `broadcasts`, `broadcasts_to`, etc.)"
110
114
  lines << "- **Explicit broadcasts:** #{rb_broadcasts.size} (via `broadcast_*_to` calls in .rb files)"
111
115
  lines << "- **Stream subscriptions:** #{view_subscriptions.size} (`turbo_stream_from` in views)"
@@ -195,7 +199,9 @@ module RailsAiContext
195
199
  lines << ""
196
200
  end
197
201
 
198
- if model_broadcasts.empty? && rb_broadcasts.empty? && view_subscriptions.empty? && view_frames.empty?
202
+ has_turbo_stream_responses = turbo_data.is_a?(Hash) && turbo_data[:turbo_stream_responses]&.any?
203
+
204
+ if model_broadcasts.empty? && rb_broadcasts.empty? && view_subscriptions.empty? && view_frames.empty? && !has_turbo_stream_responses
199
205
  if filter_label
200
206
  lines << "_No Turbo usage matching #{filter_label}. Try without filter to see all Turbo Streams and Frames._"
201
207
  else
@@ -211,6 +217,31 @@ module RailsAiContext
211
217
  private_class_method def self.format_full(model_broadcasts, rb_broadcasts, view_subscriptions, view_frames, warnings, filter_label: nil)
212
218
  lines = [ "# Turbo Map (Full Detail)", "" ]
213
219
 
220
+ # Turbo Drive Configuration & Stream Responses
221
+ turbo_data = cached_context[:turbo]
222
+ if turbo_data.is_a?(Hash) && !turbo_data[:error]
223
+ drive_parts = []
224
+ drive_parts << "morph: #{turbo_data[:morph_meta] ? 'yes' : 'no'}" unless turbo_data[:morph_meta].nil?
225
+ drive_parts << "permanent elements: #{turbo_data[:permanent_elements].size}" if turbo_data[:permanent_elements]&.any?
226
+ if turbo_data[:turbo_drive_settings].is_a?(Hash) && turbo_data[:turbo_drive_settings].any?
227
+ turbo_data[:turbo_drive_settings].each { |k, v| drive_parts << "#{k}: #{v}" }
228
+ end
229
+ if drive_parts.any?
230
+ lines << "## Turbo Drive Configuration"
231
+ drive_parts.each { |p| lines << "- #{p}" }
232
+ lines << ""
233
+ end
234
+
235
+ # Turbo Stream responses
236
+ if turbo_data[:turbo_stream_responses]&.any?
237
+ lines << "## Turbo Stream Responses (#{turbo_data[:turbo_stream_responses].size})"
238
+ turbo_data[:turbo_stream_responses].each do |resp|
239
+ lines << "- `#{resp}`"
240
+ end
241
+ lines << ""
242
+ end
243
+ end
244
+
214
245
  # Model broadcasts with full context
215
246
  if model_broadcasts.any?
216
247
  lines << "## Model Broadcasts (#{model_broadcasts.size})"
@@ -289,7 +320,9 @@ module RailsAiContext
289
320
  lines << ""
290
321
  end
291
322
 
292
- if model_broadcasts.empty? && rb_broadcasts.empty? && view_subscriptions.empty? && view_frames.empty?
323
+ has_turbo_stream_responses = turbo_data.is_a?(Hash) && turbo_data[:turbo_stream_responses]&.any?
324
+
325
+ if model_broadcasts.empty? && rb_broadcasts.empty? && view_subscriptions.empty? && view_frames.empty? && !has_turbo_stream_responses
293
326
  if filter_label
294
327
  lines << "_No Turbo usage matching #{filter_label}. Try without filter to see all Turbo Streams and Frames._"
295
328
  else
@@ -87,7 +87,9 @@ module RailsAiContext
87
87
  ctrl_templates.sort.each do |name, meta|
88
88
  parts = meta[:partials]&.any? ? " renders: #{meta[:partials].join(', ')}" : ""
89
89
  stim = meta[:stimulus]&.any? ? " stimulus: #{meta[:stimulus].join(', ')}" : ""
90
- lines << "- #{name} (#{meta[:lines]} lines)#{parts}#{stim}"
90
+ comps = meta[:components]&.any? ? " components: #{meta[:components].join(', ')}" : ""
91
+ phlex_tag = meta[:phlex] ? " [phlex]" : ""
92
+ lines << "- #{name} (#{meta[:lines]} lines#{phlex_tag})#{parts}#{comps}#{stim}"
91
93
  end
92
94
  ctrl_partials.sort.each do |name, meta|
93
95
  lines << "- #{name} (#{meta[:lines]} lines)"
@@ -118,13 +120,29 @@ module RailsAiContext
118
120
  lines << "## #{ctrl}/" unless controller && all_dirs.size == 1
119
121
  ctrl_templates.sort.each do |name, meta|
120
122
  detail_parts = []
121
- detail_parts << "renders: #{meta[:partials].join(', ')}" if meta[:partials]&.any?
122
- detail_parts << "stimulus: #{meta[:stimulus].join(', ')}" if meta[:stimulus]&.any?
123
123
  extra = extract_view_metadata(name)
124
- detail_parts << "ivars: #{extra[:ivars].join(', ')}" if extra[:ivars]&.any?
125
- detail_parts << "turbo: #{extra[:turbo].join(', ')}" if extra[:turbo]&.any?
124
+
125
+ if meta[:phlex]
126
+ # Phlex views: show components, helpers, stimulus, ivars
127
+ # Prefer introspector-level data, fall back to extract_view_metadata
128
+ components = meta[:components]&.any? ? meta[:components] : extra[:components]
129
+ helpers = meta[:helpers]&.any? ? meta[:helpers] : extra[:helpers]
130
+ detail_parts << "ivars: #{extra[:ivars].join(', ')}" if extra[:ivars]&.any?
131
+ detail_parts << "components: #{components.join(', ')}" if components&.any?
132
+ detail_parts << "helpers: #{helpers.join(', ')}" if helpers&.any?
133
+ detail_parts << "stimulus: #{meta[:stimulus].join(', ')}" if meta[:stimulus]&.any?
134
+ detail_parts << "turbo: #{extra[:turbo].join(', ')}" if extra[:turbo]&.any?
135
+ else
136
+ # ERB/Haml/Slim views: existing behavior
137
+ detail_parts << "renders: #{meta[:partials].join(', ')}" if meta[:partials]&.any?
138
+ detail_parts << "stimulus: #{meta[:stimulus].join(', ')}" if meta[:stimulus]&.any?
139
+ detail_parts << "ivars: #{extra[:ivars].join(', ')}" if extra[:ivars]&.any?
140
+ detail_parts << "turbo: #{extra[:turbo].join(', ')}" if extra[:turbo]&.any?
141
+ end
142
+
143
+ phlex_tag = meta[:phlex] ? " [phlex]" : ""
126
144
  details = detail_parts.any? ? " — #{detail_parts.join(' | ')}" : ""
127
- lines << "- **#{name}** (#{meta[:lines]} lines)#{details}"
145
+ lines << "- **#{name}** (#{meta[:lines]} lines#{phlex_tag})#{details}"
128
146
  end
129
147
  ctrl_partials.sort.each do |name, meta|
130
148
  fields = meta[:fields]&.any? ? " fields: #{meta[:fields].join(', ')}" : ""
@@ -269,7 +287,7 @@ module RailsAiContext
269
287
  # Extract instance variables and Turbo wiring from a view template
270
288
  private_class_method def self.extract_view_metadata(relative_path)
271
289
  content = read_view_content(relative_path)
272
- return { ivars: [], turbo: [] } if content.nil? || content.include?("(file not found)")
290
+ return { ivars: [], turbo: [], components: [], helpers: [] } if content.nil? || content.include?("(file not found)")
273
291
 
274
292
  # Instance variables used in template
275
293
  ivars = content.scan(/@(\w+)/).flatten.uniq.reject { |v| %w[output_buffer virtual_path _request].include?(v) }.sort
@@ -284,9 +302,47 @@ module RailsAiContext
284
302
  turbo << "stream:#{val}" unless val.start_with?('"') || val.start_with?("'") || turbo.any? { |t| t.include?(val) }
285
303
  end
286
304
 
287
- { ivars: ivars, turbo: turbo.uniq }
305
+ result = { ivars: ivars, turbo: turbo.uniq }
306
+
307
+ # For Phlex views (.rb), extract component renders and helper calls
308
+ if relative_path.end_with?(".rb") && phlex_view_content?(content)
309
+ result[:components] = extract_phlex_components(content)
310
+ result[:helpers] = extract_phlex_helpers(content)
311
+ end
312
+
313
+ result
288
314
  rescue
289
- { ivars: [], turbo: [] }
315
+ { ivars: [], turbo: [], components: [], helpers: [] }
316
+ end
317
+
318
+ # Detect if content is a Phlex view class
319
+ private_class_method def self.phlex_view_content?(content)
320
+ content.match?(/class\s+\S+\s*<\s*\S+/) && content.match?(/def\s+view_template\b/)
321
+ end
322
+
323
+ # Extract component render calls from Phlex Ruby DSL
324
+ private_class_method def self.extract_phlex_components(content)
325
+ components = Set.new
326
+ content.scan(/render[\s(]+([A-Z]\w+(?:::\w+)*)\.new/).each do |match|
327
+ components << match[0]
328
+ end
329
+ components.to_a.sort
330
+ end
331
+
332
+ # Extract helper method calls from Phlex views
333
+ PHLEX_HELPERS = %w[
334
+ link_to image_tag content_for button_to form_with form_for
335
+ content_tag tag number_to_currency number_to_human
336
+ time_ago_in_words distance_of_time_in_words
337
+ truncate pluralize raw sanitize dom_id
338
+ ].freeze
339
+
340
+ private_class_method def self.extract_phlex_helpers(content)
341
+ helpers = []
342
+ PHLEX_HELPERS.each do |method|
343
+ helpers << method if content.match?(/\b#{method}\b/)
344
+ end
345
+ helpers
290
346
  end
291
347
 
292
348
  # Scan templates that render a partial to extract locals keys
@@ -130,6 +130,8 @@ module RailsAiContext
130
130
  opts = options ? ", #{options}" : ""
131
131
  class_name = migration_class_name("add", table, column)
132
132
 
133
+ lines << "**Run:** `bin/rails generate migration #{class_name} #{column}:#{type}`"
134
+ lines << ""
133
135
  lines << "```ruby"
134
136
  lines << "# rails generate migration #{class_name} #{column}:#{type}"
135
137
  lines << "class #{class_name} < ActiveRecord::Migration[#{rails_version}]"
@@ -165,6 +167,8 @@ module RailsAiContext
165
167
  # Check if column is referenced
166
168
  col_type = find_column_type(table, column, schema) || type || "string"
167
169
 
170
+ lines << "**Run:** `bin/rails generate migration #{class_name} #{column}:#{col_type}`"
171
+ lines << ""
168
172
  lines << "**Warning:** `remove_column` is irreversible without specifying the column type."
169
173
  lines << ""
170
174
  lines << "```ruby"