rails-ai-context 2.0.0 → 2.0.2

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.
@@ -256,39 +256,41 @@ module RailsAiContext
256
256
  def render_mcp_tools_rule # rubocop:disable Metrics/MethodLength
257
257
  lines = [
258
258
  "---",
259
- "description: \"Rails MCP tools (25) — use for reference files, read directly if you'll edit\"",
259
+ "description: \"Rails MCP tools (25) — MANDATORY, use before reading any reference files\"",
260
260
  "alwaysApply: true",
261
261
  "---",
262
262
  "",
263
- "# Rails MCP Tools (25) — Use These First",
263
+ "# Rails MCP Tools (25) — MANDATORY, Use Before Read",
264
264
  "",
265
- "Use MCP for reference files (schema, routes, tests). Read files directly if you'll edit them.",
266
- "MCP tools return line numbers for surgical edits.",
265
+ "CRITICAL: This project has live MCP tools. You MUST use them for gathering context.",
266
+ "Read files ONLY when you are about to edit them. Never read reference files directly.",
267
267
  "",
268
- "- `rails_get_schema(detail:\"summary\")` → `rails_get_schema(table:\"name\")`",
269
- "- `rails_get_model_details(detail:\"summary\")``rails_get_model_details(model:\"Name\")`",
270
- "- `rails_get_routes(detail:\"summary\")``rails_get_routes(controller:\"name\")`",
271
- "- `rails_get_controllers(controller:\"Name\", action:\"index\")` one action's source code",
272
- "- `rails_get_view(controller:\"cooks\")` — view list; `rails_get_view(path:\"cooks/index.html.erb\")` — content",
273
- "- `rails_get_stimulus(detail:\"summary\")` → `rails_get_stimulus(controller:\"name\")`",
274
- "- `rails_get_test_info(detail:\"full\")` — fixtures, factories, helpers; `(model:\"Cook\")` — existing tests",
275
- "- `rails_analyze_feature(feature:\"auth\")` — schema + models + controllers + routes for a feature",
276
- "- `rails_get_design_system` — color palette, components, canonical page examples",
277
- "- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_search_code`",
278
- "- `rails_get_edit_context(file:\"path\", near:\"keyword\")` — surgical edit context with line numbers",
279
- "- `rails_validate(files:[\"path\"])` — validate Ruby, ERB, JS syntax in one call",
280
- "- `rails_security_scan` — Brakeman security analysis",
281
- "- `rails_get_concern(name:\"Searchable\")` — concern methods and includers",
282
- "- `rails_get_callbacks(model:\"User\")` — model callbacks in execution order",
283
- "- `rails_get_helper_methods` — app + framework helpers",
284
- "- `rails_get_service_pattern` — service object patterns and interfaces",
285
- "- `rails_get_job_pattern` — background job patterns and schedules",
286
- "- `rails_get_env` — environment variables and credentials keys",
287
- "- `rails_get_partial_interface(path:\"shared/_form\")` — partial locals contract",
288
- "- `rails_get_turbo_map` — Turbo Streams/Frames wiring",
289
- "- `rails_get_context(model:\"User\")` — composite cross-layer context",
268
+ "## Mandatory Workflow",
269
+ "1. Gathering context use MCP tools (NOT file reads, NOT grep)",
270
+ "2. Reading files ONLY files you will edit (Read is required before Edit)",
271
+ "3. After editing → `rails_validate(files:[...])` every time, no exceptions",
290
272
  "",
291
- "After editing: use rails_validate to check syntax. Do NOT re-read files to verify."
273
+ "## Do NOT Bypass Anti-Patterns",
274
+ "| Instead of... | Use this MCP tool |",
275
+ "|---------------|-------------------|",
276
+ "| Reading db/schema.rb | `rails_get_schema(table:\"name\")` |",
277
+ "| Reading config/routes.rb | `rails_get_routes(controller:\"name\")` |",
278
+ "| Reading model files for context | `rails_get_model_details(model:\"Name\")` |",
279
+ "| Grep for code patterns | `rails_search_code(pattern:\"regex\")` |",
280
+ "| Reading test files for patterns | `rails_get_test_info(model:\"Name\")` |",
281
+ "| Reading controller for context | `rails_get_controllers(controller:\"Name\", action:\"x\")` |",
282
+ "| Reading JS for Stimulus API | `rails_get_stimulus(controller:\"name\")` |",
283
+ "| Multiple reads for a feature | `rails_analyze_feature(feature:\"keyword\")` |",
284
+ "| ruby -c / erb / node -c | `rails_validate(files:[...])` |",
285
+ "",
286
+ "## All 25 Tools",
287
+ "- `rails_get_schema` | `rails_get_model_details` | `rails_get_routes` | `rails_get_controllers`",
288
+ "- `rails_get_view` | `rails_get_stimulus` | `rails_get_test_info` | `rails_analyze_feature`",
289
+ "- `rails_get_design_system` | `rails_get_edit_context` | `rails_validate` | `rails_search_code`",
290
+ "- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_security_scan`",
291
+ "- `rails_get_concern` | `rails_get_callbacks` | `rails_get_helper_methods` | `rails_get_service_pattern`",
292
+ "- `rails_get_job_pattern` | `rails_get_env` | `rails_get_partial_interface` | `rails_get_turbo_map`",
293
+ "- `rails_get_context(model:\"X\")` — composite cross-layer context in one call"
292
294
  ]
293
295
 
294
296
  lines.join("\n")
@@ -165,33 +165,39 @@ module RailsAiContext
165
165
 
166
166
  def render_mcp_guide # rubocop:disable Metrics/MethodLength
167
167
  [
168
- "## MCP Tools (25) — ALWAYS Use These First",
168
+ "## MCP Tools (25) — MANDATORY, Use Before Read",
169
169
  "",
170
- "Use MCP for reference files (schema, routes, tests). Read directly if you'll edit.",
171
- "MCP tools return line numbers. Start with `detail:\"summary\"`.",
170
+ "CRITICAL: This project has live MCP tools. You MUST use them for gathering context.",
171
+ "Read files ONLY when you are about to edit them. Never read reference files directly.",
172
+ "Start with `detail:\"summary\"`, then drill into specifics.",
172
173
  "",
173
- "- `rails_get_schema(detail:\"summary\")` → `rails_get_schema(table:\"name\")`",
174
- "- `rails_get_model_details(detail:\"summary\")` `rails_get_model_details(model:\"Name\")`",
175
- "- `rails_get_routes(detail:\"summary\")` `rails_get_routes(controller:\"name\")`",
176
- "- `rails_get_controllers(controller:\"Name\", action:\"index\")` — one action's source code",
177
- "- `rails_get_view(controller:\"cooks\")` — view list; `(path:\"cooks/index.html.erb\")` content",
178
- "- `rails_get_stimulus(detail:\"summary\")` `(controller:\"name\")` — targets, actions, values",
179
- "- `rails_get_test_info(detail:\"full\")` fixtures, factories, helpers; `(model:\"Cook\")` — tests",
180
- "- `rails_get_edit_context(file:\"path\", near:\"keyword\")` — surgical edit context with line numbers",
181
- "- `rails_analyze_feature(feature:\"auth\")` schema + models + controllers + routes for a feature",
182
- "- `rails_get_design_system` color palette, component patterns, canonical page examples",
183
- "- `rails_validate(files:[...])` — batch syntax check for Ruby, ERB, JS",
184
- "- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_search_code`",
185
- "- `rails_security_scan` Brakeman security analysis",
186
- "- `rails_get_concern(name:\"Searchable\")` — concern methods and includers",
187
- "- `rails_get_callbacks(model:\"User\")` — model callbacks in execution order",
188
- "- `rails_get_helper_methods` app + framework helpers",
189
- "- `rails_get_service_pattern` service object patterns and interfaces",
190
- "- `rails_get_job_pattern` background job patterns and schedules",
191
- "- `rails_get_env` — environment variables and credentials keys",
192
- "- `rails_get_partial_interface(path:\"shared/_form\")` partial locals contract",
193
- "- `rails_get_turbo_map` Turbo Streams/Frames wiring",
194
- "- `rails_get_context(model:\"User\")` composite cross-layer context",
174
+ "### Mandatory Workflow",
175
+ "1. **Before exploring a feature**: `rails_analyze_feature(feature:\"...\")` NOT file reads or grep",
176
+ "2. **Before writing migrations**: `rails_get_schema(table:\"...\")` NOT reading db/schema.rb",
177
+ "3. **Before modifying a model**: `rails_get_model_details(model:\"...\")` — NOT reading the model file",
178
+ "4. **Before adding routes**: `rails_get_routes(controller:\"...\")` — Read only when you will edit",
179
+ "5. **Before creating views**: `rails_get_design_system` — match existing patterns",
180
+ "6. **After editing ANY file**: `rails_validate(files:[...])` — no exceptions",
181
+ "",
182
+ "### Do NOT Bypass",
183
+ "| Instead of... | Use this MCP tool |",
184
+ "|---------------|-------------------|",
185
+ "| Reading db/schema.rb | `rails_get_schema(table:\"x\")` |",
186
+ "| Reading model files | `rails_get_model_details(model:\"X\")` |",
187
+ "| Reading routes.rb | `rails_get_routes(controller:\"x\")` |",
188
+ "| Grep for code | `rails_search_code(pattern:\"x\")` |",
189
+ "| Reading test files | `rails_get_test_info(model:\"X\")` |",
190
+ "| Reading controller | `rails_get_controllers(controller:\"X\", action:\"y\")` |",
191
+ "| ruby -c / erb / node | `rails_validate(files:[...])` |",
192
+ "",
193
+ "### All 25 Tools",
194
+ "- `rails_get_schema` | `rails_get_model_details` | `rails_get_routes` | `rails_get_controllers`",
195
+ "- `rails_get_view` | `rails_get_stimulus` | `rails_get_test_info` | `rails_analyze_feature`",
196
+ "- `rails_get_design_system` | `rails_get_edit_context` | `rails_validate` | `rails_search_code`",
197
+ "- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_security_scan`",
198
+ "- `rails_get_concern` | `rails_get_callbacks` | `rails_get_helper_methods` | `rails_get_service_pattern`",
199
+ "- `rails_get_job_pattern` | `rails_get_env` | `rails_get_partial_interface` | `rails_get_turbo_map`",
200
+ "- `rails_get_context(model:\"X\")` — composite cross-layer context in one call",
195
201
  ""
196
202
  ]
197
203
  end
@@ -67,34 +67,35 @@ module RailsAiContext
67
67
 
68
68
  def render_mcp_tools_rule # rubocop:disable Metrics/MethodLength
69
69
  lines = [
70
- "# Rails MCP Tools (25) — Use These First",
70
+ "# Rails MCP Tools (25) — MANDATORY, Use Before Read",
71
71
  "",
72
- "Use MCP for reference files (schema, routes, tests). Read directly if you'll edit.",
72
+ "CRITICAL: This project has live MCP tools. Use them for ALL context gathering.",
73
+ "Read files ONLY when you are about to edit them.",
73
74
  "",
74
- "- rails_get_schema(detail:\"summary\") → rails_get_schema(table:\"name\")",
75
- "- rails_get_model_details(detail:\"summary\")rails_get_model_details(model:\"Name\")",
76
- "- rails_get_routes(detail:\"summary\")rails_get_routes(controller:\"name\")",
77
- "- rails_get_controllers(controller:\"Name\", action:\"index\") one action's source",
78
- "- rails_get_view(controller:\"cooks\") — views; rails_get_view(path:\"file\") — content",
79
- "- rails_get_stimulus(detail:\"summary\") → rails_get_stimulus(controller:\"name\")",
80
- "- rails_get_test_info(detail:\"full\") — fixtures, helpers; (model:\"Cook\") — tests",
81
- "- rails_analyze_feature(feature:\"auth\") — schema + models + controllers + routes for a feature",
82
- "- rails_get_design_system — color palette, components, page examples",
83
- "- rails_get_config | rails_get_gems | rails_get_conventions | rails_search_code",
84
- "- rails_get_edit_context(file:\"path\", near:\"keyword\") — surgical edit context with line numbers",
85
- "- rails_validate(files:[\"path\"]) — validate Ruby, ERB, JS syntax in one call",
86
- "- rails_security_scan — Brakeman security analysis",
87
- "- rails_get_concern(name:\"Searchable\") — concern methods and includers",
88
- "- rails_get_callbacks(model:\"User\") — model callbacks in execution order",
89
- "- rails_get_helper_methods — app + framework helpers",
90
- "- rails_get_service_pattern — service object patterns and interfaces",
91
- "- rails_get_job_pattern — background job patterns and schedules",
92
- "- rails_get_env — environment variables and credentials keys",
93
- "- rails_get_partial_interface(path:\"shared/_form\") — partial locals contract",
94
- "- rails_get_turbo_map — Turbo Streams/Frames wiring",
95
- "- rails_get_context(model:\"User\") — composite cross-layer context",
75
+ "Mandatory Workflow:",
76
+ "1. Gathering context use MCP tools (NOT file reads)",
77
+ "2. Reading files ONLY files you will edit",
78
+ "3. After editing rails_validate(files:[...]) every time",
96
79
  "",
97
- "After editing: use rails_validate to check syntax. Do NOT re-read files to verify."
80
+ "Do NOT Bypass:",
81
+ "- Reading db/schema.rb → rails_get_schema(table:\"name\")",
82
+ "- Reading config/routes.rb → rails_get_routes(controller:\"name\")",
83
+ "- Reading model files → rails_get_model_details(model:\"Name\")",
84
+ "- Grep for code → rails_search_code(pattern:\"regex\")",
85
+ "- Reading test files → rails_get_test_info(model:\"Name\")",
86
+ "- Reading controller → rails_get_controllers(controller:\"Name\", action:\"x\")",
87
+ "- Reading JS for Stimulus → rails_get_stimulus(controller:\"name\")",
88
+ "- Multiple reads for feature → rails_analyze_feature(feature:\"keyword\")",
89
+ "- ruby -c / erb / node -c → rails_validate(files:[...])",
90
+ "",
91
+ "All 25 Tools:",
92
+ "- rails_get_schema | rails_get_model_details | rails_get_routes | rails_get_controllers",
93
+ "- rails_get_view | rails_get_stimulus | rails_get_test_info | rails_analyze_feature",
94
+ "- rails_get_design_system | rails_get_edit_context | rails_validate | rails_search_code",
95
+ "- rails_get_config | rails_get_gems | rails_get_conventions | rails_security_scan",
96
+ "- rails_get_concern | rails_get_callbacks | rails_get_helper_methods | rails_get_service_pattern",
97
+ "- rails_get_job_pattern | rails_get_env | rails_get_partial_interface | rails_get_turbo_map",
98
+ "- rails_get_context(model:\"X\") — composite cross-layer context in one call"
98
99
  ]
99
100
 
100
101
  lines.join("\n")
@@ -24,24 +24,95 @@ def apply_context_mode_override
24
24
  end
25
25
  end unless defined?(apply_context_mode_override)
26
26
 
27
+ AI_TOOL_OPTIONS = {
28
+ "1" => { key: :claude, name: "Claude Code" },
29
+ "2" => { key: :cursor, name: "Cursor" },
30
+ "3" => { key: :copilot, name: "GitHub Copilot" },
31
+ "4" => { key: :windsurf, name: "Windsurf" },
32
+ "5" => { key: :opencode, name: "OpenCode" }
33
+ }.freeze unless defined?(AI_TOOL_OPTIONS)
34
+
35
+ def prompt_ai_tools
36
+ puts ""
37
+ puts "Which AI tools do you use? (select all that apply)"
38
+ puts ""
39
+ AI_TOOL_OPTIONS.each { |num, info| puts " #{num}. #{info[:name]}" }
40
+ puts " a. All of the above"
41
+ puts ""
42
+ print "Enter numbers separated by commas (e.g. 1,2) or 'a' for all: "
43
+ input = $stdin.gets&.strip&.downcase || "a"
44
+
45
+ selected = if input == "a" || input == "all" || input.empty?
46
+ AI_TOOL_OPTIONS.values.map { |t| t[:key] }
47
+ else
48
+ input.split(/[\s,]+/).filter_map { |n| AI_TOOL_OPTIONS[n]&.dig(:key) }
49
+ end
50
+
51
+ if selected.empty?
52
+ puts "No tools selected — using all."
53
+ selected = AI_TOOL_OPTIONS.values.map { |t| t[:key] }
54
+ end
55
+
56
+ names = AI_TOOL_OPTIONS.values.select { |t| selected.include?(t[:key]) }.map { |t| t[:name] }
57
+ puts "Selected: #{names.join(', ')}"
58
+ selected
59
+ end unless defined?(prompt_ai_tools)
60
+
61
+ def save_ai_tools_to_initializer(tools)
62
+ init_path = Rails.root.join("config/initializers/rails_ai_context.rb")
63
+ return unless File.exist?(init_path)
64
+
65
+ content = File.read(init_path)
66
+ tools_line = " config.ai_tools = %i[#{tools.join(' ')}]"
67
+
68
+ if content.include?("config.ai_tools")
69
+ # Replace existing ai_tools line
70
+ content.sub!(/^.*config\.ai_tools.*$/, tools_line)
71
+ elsif content.include?("RailsAiContext.configure")
72
+ # Insert after configure block opening
73
+ content.sub!(/RailsAiContext\.configure do \|config\|\n/, "RailsAiContext.configure do |config|\n#{tools_line}\n")
74
+ else
75
+ return
76
+ end
77
+
78
+ File.write(init_path, content)
79
+ puts "💾 Saved to config/initializers/rails_ai_context.rb"
80
+ rescue
81
+ nil
82
+ end unless defined?(save_ai_tools_to_initializer)
83
+
27
84
  namespace :ai do
28
- desc "Generate AI context files (CLAUDE.md, .cursor/rules/, .windsurfrules, .github/copilot-instructions.md)"
85
+ desc "Generate AI context files for configured AI tools (prompts on first run)"
29
86
  task context: :environment do
30
87
  require "rails_ai_context"
31
88
 
32
89
  apply_context_mode_override
33
90
 
91
+ ai_tools = RailsAiContext.configuration.ai_tools
92
+
93
+ # First time — no tools configured, ask the user
94
+ if ai_tools.nil?
95
+ ai_tools = prompt_ai_tools
96
+ save_ai_tools_to_initializer(ai_tools) if ai_tools
97
+ end
98
+
34
99
  puts "🔍 Introspecting #{Rails.application.class.module_parent_name}..."
35
100
 
36
- puts "📝 Writing context files..."
37
- result = RailsAiContext.generate_context(format: :all)
101
+ if ai_tools.nil? || ai_tools.empty?
102
+ puts "📝 Writing context files for all AI tools..."
103
+ result = RailsAiContext.generate_context(format: :all)
104
+ print_result(result)
105
+ else
106
+ puts "📝 Writing context files for: #{ai_tools.map(&:to_s).join(', ')}..."
107
+ ai_tools.each do |fmt|
108
+ result = RailsAiContext.generate_context(format: fmt)
109
+ print_result(result)
110
+ end
111
+ end
38
112
 
39
- print_result(result)
40
113
  puts ""
41
- puts "Done! Your AI assistants now understand your Rails app."
42
- puts "Commit these files so your whole team benefits."
43
- puts ""
44
- puts ASSISTANT_TABLE
114
+ puts "Done! Commit these files so your team benefits."
115
+ puts "Change AI tools: config/initializers/rails_ai_context.rb (config.ai_tools)"
45
116
  end
46
117
 
47
118
  desc "Generate AI context in a specific format (claude, cursor, windsurf, copilot, json)"
@@ -168,7 +168,19 @@ module RailsAiContext
168
168
  ctrl_lines << "- `#{r[:verb]}` `#{r[:path]}` → #{r[:action]}#{helper_part}#{params_part}"
169
169
  end
170
170
  if ctrl_lines.any?
171
- lines << "## #{ctrl}"
171
+ # Inline controller summary so AI doesn't need a separate get_controllers call
172
+ ctrl_class = "#{ctrl.camelize}Controller"
173
+ ctrl_data = cached_context.dig(:controllers, :controllers, ctrl_class)
174
+ ctrl_summary = ""
175
+ if ctrl_data
176
+ filters = (ctrl_data[:filters] || []).map { |f| f[:name] }.first(3)
177
+ formats = ctrl_data[:respond_to_formats]
178
+ parts = []
179
+ parts << "filters: #{filters.join(', ')}" if filters.any?
180
+ parts << "formats: #{formats.join(', ')}" if formats&.any?
181
+ ctrl_summary = " (#{parts.join(' | ')})" if parts.any?
182
+ end
183
+ lines << "## #{ctrl}#{ctrl_summary}"
172
184
  lines.concat(ctrl_lines)
173
185
  lines << ""
174
186
  end
@@ -144,7 +144,19 @@ module RailsAiContext
144
144
  hint_str = hints.any? ? " [#{hints.join(', ')}]" : ""
145
145
  "#{c[:name]}:#{c[:type]}#{hint_str}"
146
146
  end.join(", ")
147
- lines << "### #{name}"
147
+ # Inline model info so AI doesn't need a separate get_model_details call
148
+ model_info = ""
149
+ if model_refs.any?
150
+ model_refs.each do |mname|
151
+ md = models_data[mname]
152
+ next unless md.is_a?(Hash) && !md[:error]
153
+ assoc_count = md[:associations]&.size || 0
154
+ val_count = md[:validations]&.size || 0
155
+ model_info = " → **#{mname}** (#{assoc_count} assoc, #{val_count} val)"
156
+ break
157
+ end
158
+ end
159
+ lines << "### #{name}#{model_info}"
148
160
  lines << cols
149
161
  lines << ""
150
162
  end
@@ -76,6 +76,11 @@ module RailsAiContext
76
76
  lines << "" << "## Test Helpers"
77
77
  data[:test_helpers].each { |h| lines << "- `#{h}`" }
78
78
  end
79
+
80
+ # Generate a test template based on app patterns
81
+ template = generate_test_template(data)
82
+ lines.concat(template) if template.any?
83
+
79
84
  text_response(lines.join("\n"))
80
85
 
81
86
  when "full"
@@ -210,6 +215,89 @@ module RailsAiContext
210
215
  "No test file found for #{name}. Searched: #{candidates.join(', ')}#{hint}"
211
216
  end
212
217
 
218
+ # Generate a test template based on the app's actual test patterns
219
+ private_class_method def self.generate_test_template(data)
220
+ lines = []
221
+ framework = data[:framework]
222
+
223
+ if framework&.include?("RSpec") || framework&.include?("rspec")
224
+ lines << "" << "## Test Template (follow this pattern for new tests)"
225
+ lines << "```ruby"
226
+ lines << "require \"rails_helper\""
227
+ lines << ""
228
+ lines << "RSpec.describe ModelName, type: :model do"
229
+ lines << " describe \"validations\" do"
230
+ lines << " it { is_expected.to validate_presence_of(:field) }"
231
+ lines << " end"
232
+ lines << ""
233
+ lines << " describe \"#method_name\" do"
234
+ lines << " it \"does something\" do"
235
+ lines << " record = create(:model_name)"
236
+ lines << " expect(record.method_name).to eq(expected)"
237
+ lines << " end"
238
+ lines << " end"
239
+ lines << "end"
240
+ lines << "```"
241
+ else
242
+ # Detect Devise + sign_in pattern from existing tests
243
+ has_devise = false
244
+ has_sign_in = false
245
+ test_dir = Rails.root.join("test")
246
+ if Dir.exist?(test_dir)
247
+ Dir.glob(File.join(test_dir, "**/*_test.rb")).first(5).each do |path|
248
+ content = File.read(path, encoding: "UTF-8") rescue next
249
+ has_devise = true if content.include?("Devise::Test")
250
+ has_sign_in = true if content.include?("sign_in")
251
+ end
252
+ end
253
+
254
+ lines << "" << "## Test Template (follow this pattern for new tests)"
255
+ lines << "```ruby"
256
+ lines << "require \"test_helper\""
257
+ lines << ""
258
+ lines << "class ModelNameTest < ActiveSupport::TestCase"
259
+ lines << " test \"should be valid with required attributes\" do"
260
+ lines << " record = model_names(:fixture_name)"
261
+ lines << " assert record.valid?"
262
+ lines << " end"
263
+ lines << ""
264
+ lines << " test \"should require field\" do"
265
+ lines << " record = ModelName.new"
266
+ lines << " assert_not record.valid?"
267
+ lines << " assert_includes record.errors[:field], \"can't be blank\""
268
+ lines << " end"
269
+ lines << "end"
270
+ lines << "```"
271
+
272
+ if has_devise
273
+ lines << ""
274
+ lines << "```ruby"
275
+ lines << "# Controller test pattern"
276
+ lines << "require \"test_helper\""
277
+ lines << ""
278
+ lines << "class FeatureControllerTest < ActionDispatch::IntegrationTest"
279
+ lines << " include Devise::Test::IntegrationHelpers" if has_devise
280
+ lines << ""
281
+ lines << " test \"requires authentication\" do"
282
+ lines << " get feature_path"
283
+ lines << " assert_response :redirect"
284
+ lines << " end"
285
+ lines << ""
286
+ lines << " test \"shows page for signed in user\" do"
287
+ lines << " sign_in users(:chef_one)" if has_sign_in
288
+ lines << " get feature_path"
289
+ lines << " assert_response :success"
290
+ lines << " end"
291
+ lines << "end"
292
+ lines << "```"
293
+ end
294
+ end
295
+
296
+ lines
297
+ rescue
298
+ []
299
+ end
300
+
213
301
  # Parse factory file to extract attributes and traits
214
302
  private_class_method def self.parse_factory_details(relative_path)
215
303
  # Try common factory locations
@@ -96,12 +96,14 @@ module RailsAiContext
96
96
 
97
97
  lines << "## #{ctrl}/" unless controller && all_dirs.size == 1
98
98
  ctrl_templates.sort.each do |name, meta|
99
- parts = meta[:partials]&.any? ? " renders: #{meta[:partials].join(', ')}" : ""
100
- stim = meta[:stimulus]&.any? ? " stimulus: #{meta[:stimulus].join(', ')}" : ""
99
+ detail_parts = []
100
+ detail_parts << "renders: #{meta[:partials].join(', ')}" if meta[:partials]&.any?
101
+ detail_parts << "stimulus: #{meta[:stimulus].join(', ')}" if meta[:stimulus]&.any?
101
102
  extra = extract_view_metadata(name)
102
- ivars = extra[:ivars]&.any? ? " ivars: #{extra[:ivars].join(', ')}" : ""
103
- turbo = extra[:turbo]&.any? ? " turbo: #{extra[:turbo].join(', ')}" : ""
104
- lines << "- #{name} (#{meta[:lines]} lines)#{parts}#{stim}#{ivars}#{turbo}"
103
+ detail_parts << "ivars: #{extra[:ivars].join(', ')}" if extra[:ivars]&.any?
104
+ detail_parts << "turbo: #{extra[:turbo].join(', ')}" if extra[:turbo]&.any?
105
+ details = detail_parts.any? ? " #{detail_parts.join(' | ')}" : ""
106
+ lines << "- **#{name}** (#{meta[:lines]} lines)#{details}"
105
107
  end
106
108
  ctrl_partials.sort.each do |name, meta|
107
109
  fields = meta[:fields]&.any? ? " fields: #{meta[:fields].join(', ')}" : ""