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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -0
- data/CLAUDE.md +4 -4
- data/CONTRIBUTING.md +1 -1
- data/README.md +7 -7
- data/SECURITY.md +2 -1
- data/docs/GUIDE.md +3 -3
- data/lib/generators/rails_ai_context/install/install_generator.rb +2 -2
- data/lib/rails_ai_context/configuration.rb +4 -2
- data/lib/rails_ai_context/doctor.rb +6 -1
- data/lib/rails_ai_context/fingerprinter.rb +24 -0
- data/lib/rails_ai_context/introspectors/component_introspector.rb +122 -7
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +18 -10
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +183 -6
- data/lib/rails_ai_context/introspectors/view_introspector.rb +2 -2
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +61 -8
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +10 -19
- data/lib/rails_ai_context/serializers/claude_serializer.rb +42 -11
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +14 -3
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/design_system_helper.rb +8 -1
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +5 -2
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +165 -64
- data/lib/rails_ai_context/server.rb +12 -1
- data/lib/rails_ai_context/tools/base_tool.rb +63 -1
- data/lib/rails_ai_context/tools/diagnose.rb +436 -0
- data/lib/rails_ai_context/tools/generate_test.rb +571 -0
- data/lib/rails_ai_context/tools/get_callbacks.rb +27 -4
- data/lib/rails_ai_context/tools/get_component_catalog.rb +11 -2
- data/lib/rails_ai_context/tools/get_context.rb +70 -8
- data/lib/rails_ai_context/tools/get_conventions.rb +59 -0
- data/lib/rails_ai_context/tools/get_design_system.rb +45 -7
- data/lib/rails_ai_context/tools/get_edit_context.rb +3 -2
- data/lib/rails_ai_context/tools/get_env.rb +51 -24
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +100 -9
- data/lib/rails_ai_context/tools/get_model_details.rb +19 -0
- data/lib/rails_ai_context/tools/get_partial_interface.rb +1 -1
- data/lib/rails_ai_context/tools/get_stimulus.rb +13 -7
- data/lib/rails_ai_context/tools/get_turbo_map.rb +35 -2
- data/lib/rails_ai_context/tools/get_view.rb +65 -9
- data/lib/rails_ai_context/tools/migration_advisor.rb +4 -0
- data/lib/rails_ai_context/tools/onboard.rb +755 -0
- data/lib/rails_ai_context/tools/query.rb +4 -2
- data/lib/rails_ai_context/tools/read_logs.rb +4 -1
- data/lib/rails_ai_context/tools/review_changes.rb +299 -0
- data/lib/rails_ai_context/tools/runtime_info.rb +289 -0
- data/lib/rails_ai_context/tools/search_code.rb +23 -4
- data/lib/rails_ai_context/tools/security_scan.rb +7 -1
- data/lib/rails_ai_context/tools/session_context.rb +132 -0
- data/lib/rails_ai_context/version.rb +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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"
|