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
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module RailsAiContext
|
|
6
|
+
module Tools
|
|
7
|
+
class Diagnose < BaseTool
|
|
8
|
+
tool_name "rails_diagnose"
|
|
9
|
+
description "One-call error diagnosis: parses the error, classifies it, gathers controller/model/schema context, " \
|
|
10
|
+
"shows recent git changes, pulls relevant logs, and suggests a fix. " \
|
|
11
|
+
"Use when: you hit an error and need to understand why. " \
|
|
12
|
+
"Key params: error (required — paste the error message), file, line, action (controller#action)."
|
|
13
|
+
|
|
14
|
+
input_schema(
|
|
15
|
+
properties: {
|
|
16
|
+
error: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Error message or exception (e.g. 'NoMethodError: undefined method `foo` for nil:NilClass')."
|
|
19
|
+
},
|
|
20
|
+
file: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "File where error occurs, relative to Rails root (e.g. 'app/controllers/cooks_controller.rb')."
|
|
23
|
+
},
|
|
24
|
+
line: {
|
|
25
|
+
type: "integer",
|
|
26
|
+
description: "Line number where the error occurs."
|
|
27
|
+
},
|
|
28
|
+
action: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Controller#action format (e.g. 'cooks#create'). Pulls full action context."
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
required: %w[error]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: false, open_world_hint: true)
|
|
37
|
+
|
|
38
|
+
# ── Error classification ──────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
# Section size limits for output truncation
|
|
41
|
+
MAX_TOTAL_OUTPUT = 20_000
|
|
42
|
+
MAX_SECTION_CHARS = {
|
|
43
|
+
controller_context: 3_000,
|
|
44
|
+
model_context: 3_000,
|
|
45
|
+
code_context: 3_000,
|
|
46
|
+
schema_context: 3_000,
|
|
47
|
+
method_trace: 3_000,
|
|
48
|
+
git_changes: 2_000,
|
|
49
|
+
logs: 2_000
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
ERROR_CLASSIFICATIONS = {
|
|
53
|
+
/NameError.*uninitialized constant/ => {
|
|
54
|
+
type: :name_error,
|
|
55
|
+
likely: "A class or module constant could not be found. This usually means a typo in the class/module name, a missing require, or an autoload issue.",
|
|
56
|
+
fix: "1. Check for typo in class/module name, missing require, or autoload issue\n2. Verify the file is in the correct autoload path (e.g. app/models, app/services)\n3. Run `rails_search_code(pattern:\"ClassName\", match_type:\"trace\")` to find where the constant is defined"
|
|
57
|
+
},
|
|
58
|
+
/NoMethodError/ => {
|
|
59
|
+
type: :nil_reference,
|
|
60
|
+
likely: "A method was called on nil or an undefined name was referenced. Check for: missing association, unfetched record, typo in method name.",
|
|
61
|
+
fix: "1. Check the variable/object is not nil before calling the method\n2. Verify the association or attribute exists on the model\n3. Use `&.` safe navigation if the value can legitimately be nil"
|
|
62
|
+
},
|
|
63
|
+
/NameError/ => {
|
|
64
|
+
type: :name_error,
|
|
65
|
+
likely: "An undefined name was referenced. This could be a typo in a variable, method, or constant name, or a missing require/autoload.",
|
|
66
|
+
fix: "1. Check for typo in class/module name, missing require, or autoload issue\n2. Verify the name is defined and accessible in the current scope\n3. Use `rails_search_code(pattern:\"name\", match_type:\"trace\")` to find where it is defined"
|
|
67
|
+
},
|
|
68
|
+
/RecordNotFound/ => {
|
|
69
|
+
type: :record_not_found,
|
|
70
|
+
likely: "A `.find()` or `.find_by!()` call failed because the record doesn't exist. Common causes: stale URL, deleted record, wrong ID parameter.",
|
|
71
|
+
fix: "1. Use `.find_by` (returns nil) instead of `.find` (raises)\n2. Add a rescue handler or `before_action :set_record` with proper error handling\n3. Check the route parameter name matches what the controller expects"
|
|
72
|
+
},
|
|
73
|
+
/RecordInvalid|RecordNotSaved/ => {
|
|
74
|
+
type: :validation_failure,
|
|
75
|
+
likely: "A `.save!`, `.create!`, or `.update!` call failed validation. The model has validations that the submitted data doesn't satisfy.",
|
|
76
|
+
fix: "1. Use `.save` (returns false) instead of `.save!` (raises) for user-facing flows\n2. Check model validations against the params being submitted\n3. Verify required associations exist before saving"
|
|
77
|
+
},
|
|
78
|
+
/RoutingError/ => {
|
|
79
|
+
type: :routing,
|
|
80
|
+
likely: "No route matches the request. The URL or HTTP verb doesn't match any defined route.",
|
|
81
|
+
fix: "1. Run `rails_get_routes` to see all defined routes\n2. Check the HTTP verb (GET vs POST vs PATCH) matches\n3. Verify the route is not restricted by constraints"
|
|
82
|
+
},
|
|
83
|
+
/ParameterMissing/ => {
|
|
84
|
+
type: :strong_params,
|
|
85
|
+
likely: "A required parameter is missing from the request. The `params.require(:key)` call didn't find the expected key.",
|
|
86
|
+
fix: "1. Check the form field names match what strong params expects\n2. Verify the parameter nesting (e.g., `params[:user][:name]` vs `params[:name]`)\n3. Check the controller's `*_params` method"
|
|
87
|
+
},
|
|
88
|
+
/StatementInvalid|UndefinedColumn|UndefinedTable/ => {
|
|
89
|
+
type: :schema_mismatch,
|
|
90
|
+
likely: "A database query references a column or table that doesn't exist. Common after migrations or in stale schema.",
|
|
91
|
+
fix: "1. Run `rails db:migrate` to apply pending migrations\n2. Check `rails_get_schema(table:\"...\")` for actual column names\n3. Verify the migration was generated correctly"
|
|
92
|
+
},
|
|
93
|
+
/Template::Error|ActionView/ => {
|
|
94
|
+
type: :view_error,
|
|
95
|
+
likely: "An error occurred while rendering a view template. The underlying error is usually a NoMethodError or missing local variable.",
|
|
96
|
+
fix: "1. Check the instance variables set in the controller action\n2. Verify partial locals are passed correctly\n3. Use `rails_get_view(controller:\"...\")` to see template structure"
|
|
97
|
+
},
|
|
98
|
+
/ArgumentError/ => {
|
|
99
|
+
type: :argument_error,
|
|
100
|
+
likely: "A method received wrong number or type of arguments.",
|
|
101
|
+
fix: "1. Check the method signature matches the call site\n2. Use `rails_search_code(pattern:\"method_name\", match_type:\"trace\")` to see definition and callers"
|
|
102
|
+
}
|
|
103
|
+
}.freeze
|
|
104
|
+
|
|
105
|
+
def self.call(error:, file: nil, line: nil, action: nil, server_context: nil)
|
|
106
|
+
return text_response("The `error` parameter is required.") if error.nil? || error.strip.empty?
|
|
107
|
+
|
|
108
|
+
parsed = parse_error(error)
|
|
109
|
+
classification = classify_error(parsed)
|
|
110
|
+
|
|
111
|
+
lines = [ "# Error Diagnosis", "" ]
|
|
112
|
+
lines << "**Error:** `#{parsed[:exception_class] || 'Unknown'}`"
|
|
113
|
+
lines << "**Message:** #{parsed[:message]}"
|
|
114
|
+
lines << "**Classification:** #{classification[:type]}"
|
|
115
|
+
lines << ""
|
|
116
|
+
|
|
117
|
+
# Likely cause — enriched with specific inference when possible
|
|
118
|
+
lines << "## Likely Cause"
|
|
119
|
+
lines << classification[:likely]
|
|
120
|
+
specific = infer_specific_cause(parsed, classification, file, action)
|
|
121
|
+
lines << "" << "**Specific:** #{specific}" if specific
|
|
122
|
+
lines << ""
|
|
123
|
+
|
|
124
|
+
# Suggested fix
|
|
125
|
+
lines << "## Suggested Fix"
|
|
126
|
+
lines << classification[:fix]
|
|
127
|
+
lines << ""
|
|
128
|
+
|
|
129
|
+
# Gather context based on parameters and error type
|
|
130
|
+
context_sections = gather_context(parsed, classification, file, line, action)
|
|
131
|
+
|
|
132
|
+
# Recent git changes
|
|
133
|
+
git_section = gather_git_context(file, parsed[:file_refs])
|
|
134
|
+
|
|
135
|
+
# Recent error logs
|
|
136
|
+
log_section = gather_log_context(parsed[:exception_class])
|
|
137
|
+
|
|
138
|
+
# Truncate large sections before assembling final output
|
|
139
|
+
context_sections = truncate_section(context_sections, "Controller Context", MAX_SECTION_CHARS[:controller_context])
|
|
140
|
+
context_sections = truncate_section(context_sections, "Code Context", MAX_SECTION_CHARS[:code_context])
|
|
141
|
+
context_sections = truncate_section(context_sections, "Schema Context", MAX_SECTION_CHARS[:schema_context])
|
|
142
|
+
context_sections = truncate_section(context_sections, "Model Context", MAX_SECTION_CHARS[:model_context])
|
|
143
|
+
context_sections = truncate_section(context_sections, "Method Trace", MAX_SECTION_CHARS[:method_trace])
|
|
144
|
+
git_section = truncate_section(git_section, "Recent Git Changes", MAX_SECTION_CHARS[:git_changes])
|
|
145
|
+
log_section = truncate_section(log_section, "Recent Error Logs", MAX_SECTION_CHARS[:logs])
|
|
146
|
+
|
|
147
|
+
lines.concat(context_sections)
|
|
148
|
+
lines.concat(git_section) if git_section.any?
|
|
149
|
+
lines.concat(log_section) if log_section.any?
|
|
150
|
+
|
|
151
|
+
# Next steps
|
|
152
|
+
lines << "## Next Steps"
|
|
153
|
+
if file
|
|
154
|
+
lines << "_Use `rails_get_edit_context(file:\"#{file}\", near:\"#{parsed[:method_name] || line || parsed[:exception_class]}\")` to see the code._"
|
|
155
|
+
end
|
|
156
|
+
if parsed[:method_name]
|
|
157
|
+
lines << "_Use `rails_search_code(pattern:\"#{parsed[:method_name]}\", match_type:\"trace\")` to trace the method._"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
output = lines.join("\n")
|
|
161
|
+
|
|
162
|
+
# Final safety cap: if total output still exceeds limit, hard-truncate
|
|
163
|
+
if output.length > MAX_TOTAL_OUTPUT
|
|
164
|
+
output = output[0, MAX_TOTAL_OUTPUT] + "\n\n_... output truncated (#{output.length} chars exceeded #{MAX_TOTAL_OUTPUT} limit)._"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
text_response(output)
|
|
168
|
+
rescue => e
|
|
169
|
+
text_response("Diagnosis error: #{e.message}")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
class << self
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def parse_error(error_string)
|
|
176
|
+
result = { exception_class: nil, message: error_string.strip, file_refs: [], method_name: nil }
|
|
177
|
+
|
|
178
|
+
# Extract exception class: "NoMethodError: ..." or "ActiveRecord::RecordNotFound ..."
|
|
179
|
+
if (m = error_string.match(/\A([\w:]+(?:Error|Exception|Invalid|NotFound|NotSaved|Missing))\s*[:—]\s*(.*)/m))
|
|
180
|
+
result[:exception_class] = m[1]
|
|
181
|
+
result[:message] = m[2].strip
|
|
182
|
+
elsif (m = error_string.match(/\A([\w:]+::\w+)\s*[:—]\s*(.*)/m))
|
|
183
|
+
result[:exception_class] = m[1]
|
|
184
|
+
result[:message] = m[2].strip
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Extract file:line references
|
|
188
|
+
error_string.scan(%r{(app/\S+\.rb):(\d+)}).each do |file, line|
|
|
189
|
+
result[:file_refs] << { file: file, line: line.to_i }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Extract method name from "undefined method `foo`" or "undefined method 'foo'"
|
|
193
|
+
if (m = result[:message].match(/undefined method [`'](\w+[?!]?)[`']/))
|
|
194
|
+
result[:method_name] = m[1]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
result
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def classify_error(parsed)
|
|
201
|
+
error_str = "#{parsed[:exception_class]} #{parsed[:message]}"
|
|
202
|
+
|
|
203
|
+
ERROR_CLASSIFICATIONS.each do |pattern, info|
|
|
204
|
+
return info if error_str.match?(pattern)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
{
|
|
208
|
+
type: :unknown,
|
|
209
|
+
likely: "Unable to automatically classify this error. Review the full error message and stack trace.",
|
|
210
|
+
fix: "1. Check the error message for clues about what went wrong\n2. Use `rails_search_code` to find the failing code\n3. Use `rails_read_logs(level:\"ERROR\")` for more context"
|
|
211
|
+
}
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def gather_context(parsed, classification, file, line, action) # rubocop:disable Metrics
|
|
215
|
+
lines = []
|
|
216
|
+
|
|
217
|
+
# Controller context from action: parameter
|
|
218
|
+
if action
|
|
219
|
+
ctrl, act = action.split("#", 2)
|
|
220
|
+
if ctrl && act
|
|
221
|
+
begin
|
|
222
|
+
ctrl_class = ctrl.end_with?("Controller") ? ctrl : "#{ctrl.camelize}Controller"
|
|
223
|
+
result = GetControllers.call(controller: ctrl_class, action: act)
|
|
224
|
+
text = result.content.first[:text]
|
|
225
|
+
unless text.include?("not found")
|
|
226
|
+
lines << "## Controller Context"
|
|
227
|
+
lines << text
|
|
228
|
+
lines << ""
|
|
229
|
+
end
|
|
230
|
+
rescue => e
|
|
231
|
+
lines << "## Controller Context"
|
|
232
|
+
lines << "_Could not load: #{e.message}_"
|
|
233
|
+
lines << ""
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# File context
|
|
239
|
+
if file && line
|
|
240
|
+
begin
|
|
241
|
+
result = GetEditContext.call(file: file, near: parsed[:method_name] || line.to_s)
|
|
242
|
+
text = result.content.first[:text]
|
|
243
|
+
unless text.include?("not found") || text.include?("not allowed")
|
|
244
|
+
lines << "## Code Context"
|
|
245
|
+
lines << text
|
|
246
|
+
lines << ""
|
|
247
|
+
end
|
|
248
|
+
rescue => e
|
|
249
|
+
lines << "## Code Context"
|
|
250
|
+
lines << "_Could not load: #{e.message}_"
|
|
251
|
+
lines << ""
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Schema context for schema_mismatch errors
|
|
256
|
+
if classification[:type] == :schema_mismatch
|
|
257
|
+
# Try to extract table name from error
|
|
258
|
+
table = parsed[:message].match(/(?:table|relation)\s+["']?(\w+)["']?/i)&.[](1)
|
|
259
|
+
if table
|
|
260
|
+
begin
|
|
261
|
+
result = GetSchema.call(table: table)
|
|
262
|
+
text = result.content.first[:text]
|
|
263
|
+
unless text.include?("not found")
|
|
264
|
+
lines << "## Schema Context"
|
|
265
|
+
lines << text
|
|
266
|
+
lines << ""
|
|
267
|
+
end
|
|
268
|
+
rescue; end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Model context for validation errors
|
|
273
|
+
if classification[:type] == :validation_failure
|
|
274
|
+
# Try to extract model from file path or error
|
|
275
|
+
model_name = if file&.match?(%r{app/models/(.+)\.rb})
|
|
276
|
+
file.match(%r{app/models/(.+)\.rb})[1].camelize
|
|
277
|
+
end
|
|
278
|
+
if model_name
|
|
279
|
+
begin
|
|
280
|
+
result = GetModelDetails.call(model: model_name)
|
|
281
|
+
text = result.content.first[:text]
|
|
282
|
+
unless text.include?("not found")
|
|
283
|
+
lines << "## Model Context"
|
|
284
|
+
lines << text
|
|
285
|
+
lines << ""
|
|
286
|
+
end
|
|
287
|
+
rescue; end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Trace method if we know the method name
|
|
292
|
+
if parsed[:method_name] && lines.none? { |l| l.include?("Code Context") }
|
|
293
|
+
begin
|
|
294
|
+
result = SearchCode.call(pattern: parsed[:method_name], match_type: "trace")
|
|
295
|
+
text = result.content.first[:text]
|
|
296
|
+
unless text.include?("No results") || text.include?("No definition")
|
|
297
|
+
lines << "## Method Trace"
|
|
298
|
+
lines << text
|
|
299
|
+
lines << ""
|
|
300
|
+
end
|
|
301
|
+
rescue; end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
lines
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Infer a specific diagnosis from the error + context
|
|
308
|
+
def infer_specific_cause(parsed, classification, file, action) # rubocop:disable Metrics
|
|
309
|
+
msg = parsed[:message].to_s
|
|
310
|
+
method = parsed[:method_name]
|
|
311
|
+
|
|
312
|
+
# "undefined method X for nil" — identify WHAT is nil
|
|
313
|
+
if classification[:type] == :nil_reference && msg.include?("for nil")
|
|
314
|
+
# Check if calling on current_user (common: auth not running)
|
|
315
|
+
if file&.include?("controller") && msg.match?(/current_user/)
|
|
316
|
+
return "`current_user` is nil — the `authenticate_user!` before_action may not be running for this route. " \
|
|
317
|
+
"Check if this action is excluded via `unless:` or `skip_before_action`."
|
|
318
|
+
end
|
|
319
|
+
# Check if calling on an association
|
|
320
|
+
if method && file
|
|
321
|
+
begin
|
|
322
|
+
ctx = GetEditContext.call(file: file, near: method)
|
|
323
|
+
code = ctx.content.first[:text]
|
|
324
|
+
# Find the receiver: something.method_name
|
|
325
|
+
receiver_match = code.match(/(\w+)\.#{Regexp.escape(method)}/)
|
|
326
|
+
if receiver_match
|
|
327
|
+
receiver = receiver_match[1]
|
|
328
|
+
return "`#{receiver}` is nil when `.#{method}` is called. " \
|
|
329
|
+
"This variable may not be set in all code paths — check if it's assigned before use, " \
|
|
330
|
+
"or use `#{receiver}&.#{method}` for safe navigation."
|
|
331
|
+
end
|
|
332
|
+
rescue; end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# RecordNotFound — check if there's a set_* before_action
|
|
337
|
+
if classification[:type] == :record_not_found && action
|
|
338
|
+
ctrl, act = action.split("#", 2)
|
|
339
|
+
if ctrl && act
|
|
340
|
+
begin
|
|
341
|
+
ctrl_class = ctrl.end_with?("Controller") ? ctrl : "#{ctrl.camelize}Controller"
|
|
342
|
+
result = GetControllers.call(controller: ctrl_class, action: act)
|
|
343
|
+
text = result.content.first[:text]
|
|
344
|
+
if text.include?("set_") && text.include?("find")
|
|
345
|
+
return "The `set_*` before_action uses `.find` which raises RecordNotFound. " \
|
|
346
|
+
"The record with the given ID doesn't exist or doesn't belong to the current user. " \
|
|
347
|
+
"Check if the record was deleted or if the user is authorized to access it."
|
|
348
|
+
end
|
|
349
|
+
rescue; end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
nil
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def gather_git_context(file, file_refs)
|
|
357
|
+
lines = []
|
|
358
|
+
root = Rails.root.to_s
|
|
359
|
+
|
|
360
|
+
files_to_check = [ file, *file_refs.map { |r| r[:file] } ].compact.uniq.first(3)
|
|
361
|
+
return lines if files_to_check.empty?
|
|
362
|
+
|
|
363
|
+
git_output = []
|
|
364
|
+
files_to_check.each do |f|
|
|
365
|
+
full = File.join(root, f)
|
|
366
|
+
next unless File.exist?(full)
|
|
367
|
+
output, status = Open3.capture2("git", "log", "--oneline", "-5", "--", f, chdir: root)
|
|
368
|
+
if status.success? && !output.strip.empty?
|
|
369
|
+
git_output << "**#{f}:**\n#{output.strip}"
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
if git_output.any?
|
|
374
|
+
lines << "## Recent Git Changes"
|
|
375
|
+
lines.concat(git_output)
|
|
376
|
+
lines << ""
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
lines
|
|
380
|
+
rescue
|
|
381
|
+
[]
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def gather_log_context(exception_class)
|
|
385
|
+
return [] unless exception_class
|
|
386
|
+
|
|
387
|
+
begin
|
|
388
|
+
result = ReadLogs.call(level: "ERROR", lines: 15, search: exception_class)
|
|
389
|
+
text = result.content.first[:text]
|
|
390
|
+
return [] if text.include?("Log file is empty") || text.include?("not found") || text.include?("No entries")
|
|
391
|
+
|
|
392
|
+
[ "## Recent Error Logs", text, "" ]
|
|
393
|
+
rescue
|
|
394
|
+
[]
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Truncate the content of a named section (identified by "## heading") within a lines array.
|
|
399
|
+
# Returns a new array with the section's content lines truncated if they exceed max_chars.
|
|
400
|
+
def truncate_section(lines, heading, max_chars)
|
|
401
|
+
return lines if lines.empty? || max_chars.nil?
|
|
402
|
+
|
|
403
|
+
header_marker = "## #{heading}"
|
|
404
|
+
header_idx = lines.index(header_marker)
|
|
405
|
+
return lines unless header_idx
|
|
406
|
+
|
|
407
|
+
# Find the end of this section: next "## " header or end of array
|
|
408
|
+
section_end = nil
|
|
409
|
+
(header_idx + 1...lines.length).each do |i|
|
|
410
|
+
if lines[i].is_a?(String) && lines[i].start_with?("## ")
|
|
411
|
+
section_end = i
|
|
412
|
+
break
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
section_end ||= lines.length
|
|
416
|
+
|
|
417
|
+
# Measure content between header and section_end
|
|
418
|
+
content_lines = lines[(header_idx + 1)...section_end]
|
|
419
|
+
content = content_lines.join("\n")
|
|
420
|
+
|
|
421
|
+
return lines if content.length <= max_chars
|
|
422
|
+
|
|
423
|
+
# Truncate and rebuild
|
|
424
|
+
truncated_content = content[0, max_chars]
|
|
425
|
+
truncated_content += "\n\n_... section truncated (#{content.length} chars → #{max_chars} max)._"
|
|
426
|
+
|
|
427
|
+
result = lines[0...header_idx + 1]
|
|
428
|
+
result << truncated_content
|
|
429
|
+
result << ""
|
|
430
|
+
result.concat(lines[section_end..])
|
|
431
|
+
result
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|