Class: RailsAiContext::Tools::Diagnose

Inherits:
BaseTool
  • Object
show all
Defined in:
lib/rails_ai_context/tools/diagnose.rb

Constant Summary collapse

MAX_TOTAL_OUTPUT =

Section size limits for output truncation

20_000
MAX_SECTION_CHARS =
{
  controller_context: 3_000,
  model_context: 3_000,
  code_context: 3_000,
  schema_context: 3_000,
  method_trace: 3_000,
  git_changes: 2_000,
  logs: 2_000
}.freeze
ERROR_CLASSIFICATIONS =
{
  /NameError.*uninitialized constant/ => {
    type: :name_error,
    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.",
    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"
  },
  /NoMethodError/ => {
    type: :nil_reference,
    likely: "A method was called on nil or an undefined name was referenced. Check for: missing association, unfetched record, typo in method name.",
    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"
  },
  /NameError/ => {
    type: :name_error,
    likely: "An undefined name was referenced. This could be a typo in a variable, method, or constant name, or a missing require/autoload.",
    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"
  },
  /RecordNotFound/ => {
    type: :record_not_found,
    likely: "A `.find()` or `.find_by!()` call failed because the record doesn't exist. Common causes: stale URL, deleted record, wrong ID parameter.",
    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"
  },
  /RecordInvalid|RecordNotSaved/ => {
    type: :validation_failure,
    likely: "A `.save!`, `.create!`, or `.update!` call failed validation. The model has validations that the submitted data doesn't satisfy.",
    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"
  },
  /RoutingError/ => {
    type: :routing,
    likely: "No route matches the request. The URL or HTTP verb doesn't match any defined route.",
    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"
  },
  /ParameterMissing/ => {
    type: :strong_params,
    likely: "A required parameter is missing from the request. The `params.require(:key)` call didn't find the expected key.",
    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"
  },
  /StatementInvalid|UndefinedColumn|UndefinedTable/ => {
    type: :schema_mismatch,
    likely: "A database query references a column or table that doesn't exist. Common after migrations or in stale schema.",
    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"
  },
  /Template::Error|ActionView/ => {
    type: :view_error,
    likely: "An error occurred while rendering a view template. The underlying error is usually a NoMethodError or missing local variable.",
    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"
  },
  /ArgumentError/ => {
    type: :argument_error,
    likely: "A method received wrong number or type of arguments.",
    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"
  }
}.freeze

Constants inherited from BaseTool

BaseTool::SESSION_CONTEXT, BaseTool::SHARED_CACHE

Class Method Summary collapse

Methods inherited from BaseTool

abstract!, abstract?, cache_key, cached_context, config, extract_method_source_from_file, extract_method_source_from_string, find_closest_match, fuzzy_find_key, inherited, not_found_response, paginate, rails_app, registered_tools, reset_all_caches!, reset_cache!, session_queries, session_record, session_reset!, set_call_params, text_response

Class Method Details

.call(error:, file: nil, line: nil, action: nil, server_context: nil) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/rails_ai_context/tools/diagnose.rb', line 105

def self.call(error:, file: nil, line: nil, action: nil, server_context: nil)
  return text_response("The `error` parameter is required.") if error.nil? || error.strip.empty?

  parsed = parse_error(error)
  classification = classify_error(parsed)

  lines = [ "# Error Diagnosis", "" ]
  lines << "**Error:** `#{parsed[:exception_class] || 'Unknown'}`"
  lines << "**Message:** #{parsed[:message]}"
  lines << "**Classification:** #{classification[:type]}"
  lines << ""

  # Likely cause — enriched with specific inference when possible
  lines << "## Likely Cause"
  lines << classification[:likely]
  specific = infer_specific_cause(parsed, classification, file, action)
  lines << "" << "**Specific:** #{specific}" if specific
  lines << ""

  # Suggested fix
  lines << "## Suggested Fix"
  lines << classification[:fix]
  lines << ""

  # Gather context based on parameters and error type
  context_sections = gather_context(parsed, classification, file, line, action)

  # Recent git changes
  git_section = gather_git_context(file, parsed[:file_refs])

  # Recent error logs
  log_section = gather_log_context(parsed[:exception_class])

  # Truncate large sections before assembling final output
  context_sections = truncate_section(context_sections, "Controller Context", MAX_SECTION_CHARS[:controller_context])
  context_sections = truncate_section(context_sections, "Code Context", MAX_SECTION_CHARS[:code_context])
  context_sections = truncate_section(context_sections, "Schema Context", MAX_SECTION_CHARS[:schema_context])
  context_sections = truncate_section(context_sections, "Model Context", MAX_SECTION_CHARS[:model_context])
  context_sections = truncate_section(context_sections, "Method Trace", MAX_SECTION_CHARS[:method_trace])
  git_section = truncate_section(git_section, "Recent Git Changes", MAX_SECTION_CHARS[:git_changes])
  log_section = truncate_section(log_section, "Recent Error Logs", MAX_SECTION_CHARS[:logs])

  lines.concat(context_sections)
  lines.concat(git_section) if git_section.any?
  lines.concat(log_section) if log_section.any?

  # Next steps
  lines << "## Next Steps"
  if file
    lines << "_Use `rails_get_edit_context(file:\"#{file}\", near:\"#{parsed[:method_name] || line || parsed[:exception_class]}\")` to see the code._"
  end
  if parsed[:method_name]
    lines << "_Use `rails_search_code(pattern:\"#{parsed[:method_name]}\", match_type:\"trace\")` to trace the method._"
  end

  output = lines.join("\n")

  # Final safety cap: if total output still exceeds limit, hard-truncate
  if output.length > MAX_TOTAL_OUTPUT
    output = output[0, MAX_TOTAL_OUTPUT] + "\n\n_... output truncated (#{output.length} chars exceeded #{MAX_TOTAL_OUTPUT} limit)._"
  end

  text_response(output)
rescue => e
  text_response("Diagnosis error: #{e.message}")
end