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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +3 -2
- data/docs/GUIDE.md +22 -14
- data/lib/generators/rails_ai_context/install/install_generator.rb +79 -48
- data/lib/rails_ai_context/configuration.rb +5 -0
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +45 -23
- data/lib/rails_ai_context/serializers/claude_serializer.rb +40 -24
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +29 -27
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +29 -27
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +31 -25
- data/lib/rails_ai_context/serializers/windsurf_rules_serializer.rb +26 -25
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +79 -8
- data/lib/rails_ai_context/tools/get_routes.rb +13 -1
- data/lib/rails_ai_context/tools/get_schema.rb +13 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +88 -0
- data/lib/rails_ai_context/tools/get_view.rb +7 -5
- data/lib/rails_ai_context/tools/search_code.rb +220 -40
- data/lib/rails_ai_context/tools/validate.rb +48 -0
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +2 -2
- metadata +1 -1
|
@@ -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
|
|
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
|
|
263
|
+
"# Rails MCP Tools (25) — MANDATORY, Use Before Read",
|
|
264
264
|
"",
|
|
265
|
-
"
|
|
266
|
-
"
|
|
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
|
-
"
|
|
269
|
-
"
|
|
270
|
-
"
|
|
271
|
-
"
|
|
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
|
-
"
|
|
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) —
|
|
168
|
+
"## MCP Tools (25) — MANDATORY, Use Before Read",
|
|
169
169
|
"",
|
|
170
|
-
"
|
|
171
|
-
"
|
|
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
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
"
|
|
177
|
-
"
|
|
178
|
-
"
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"
|
|
184
|
-
"
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"-
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"- `
|
|
194
|
-
"- `
|
|
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
|
|
70
|
+
"# Rails MCP Tools (25) — MANDATORY, Use Before Read",
|
|
71
71
|
"",
|
|
72
|
-
"
|
|
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
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
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
|
-
"
|
|
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 (
|
|
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
|
-
|
|
37
|
-
|
|
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!
|
|
42
|
-
puts "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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(', ')}" : ""
|