rails-ai-context 4.2.2 → 4.2.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9930b0d98fb5e6eb1203d30ed2aa29e1582d8d7dce45b8b6046dbf319b6848c6
4
- data.tar.gz: c8c16b4b5d5d7f2f677079b6b32162f4d1c808cf4766e5ff864386c86deaf5fc
3
+ metadata.gz: b5a6ff4a0b32e6be5d9109b7c603f3b7ae097cf97439f8b7a219ee374a62469d
4
+ data.tar.gz: f80ea797a13825703b45ced9c9f2b94544f1c00774f00cec683194a19af8d3b9
5
5
  SHA512:
6
- metadata.gz: 8fd433d22ebcc80e37e3b42e2b81a194cea749bc3a58a5c9cc6a062d263e8c38b4d8e2566197c931d26fd192120f7a327d4fe997141d928ecede7b671bf7bdae
7
- data.tar.gz: ee8e50f02af410230d37e703eaf048f540f7c8ac7ea745686349c4a16ba5c9e74f0e429c77948f87930089c97cc79ccf5e3510f7f939d0bb0a91bf1f781fb371
6
+ metadata.gz: 370160ebbbcd9bca03eb55171474b017c23235a7211f46ba5df52e64a33fdf3fa470acd38952a422ec7ccca770377d8415e8f0011a101622a15b3db1e5a98ee4
7
+ data.tar.gz: 07df46d61e556d7290ad8768d3ecb6fc7ef3d9498c6fe646791d12c05794bfec258f15085571ae3c09aa8b10ff517b7aa5b372964f5708ad2190675c21bacfc4
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.2.3] — 2026-04-01
9
+
10
+ ### Fixed
11
+ - **Unicode output** — `rails_get_context` ivar cross-check now renders actual Unicode symbols (✓✗⚠) instead of literal `\u2713` escape sequences
12
+ - **Scope name rendering** — all 6 serializers (claude, cursor, copilot, opencode, claude_rules, copilot_instructions) now extract scope names from hash-style scope data instead of dumping raw `{:name=>"active", :body=>"..."}` into output
13
+ - **Scope exclusion** — `ModelIntrospector#extract_public_class_methods` now correctly extracts scope names from hash-style scope data so scopes are properly excluded from the class methods listing
14
+ - **Pending migrations check** — `Doctor#check_pending_migrations` now uses `MigrationContext#pending_migrations` on Rails 7.1+ instead of the deprecated `ActiveRecord::Migrator.new` API (silently returned nil on modern Rails)
15
+ - **SQLite query timeout** — `rails_query` now uses `set_progress_handler` for real statement timeout enforcement on SQLite instead of `busy_timeout` (which only controls lock-wait, not query execution time)
16
+ - **ripgrep caching** — `SearchCode.ripgrep_available?` now caches `false` results, avoiding repeated `which rg` system calls on every search when ripgrep is not installed
17
+ - **Controller action extraction** — `SearchCode#extract_controller_actions_from_matches` now correctly captures RESTful action names instead of always appending `nil` (was using `match?` which doesn't set `$1`, plus overly broad `[a-z_]+` regex)
18
+
19
+ ### Changed
20
+ - Test count: 1003 → 1016
21
+
8
22
  ## [4.2.2] — 2026-04-01
9
23
 
10
24
  ### Fixed
data/CLAUDE.md CHANGED
@@ -60,7 +60,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
60
60
  ## Testing
61
61
 
62
62
  ```bash
63
- bundle exec rspec # Run specs (1003 examples)
63
+ bundle exec rspec # Run specs (1016 examples)
64
64
  bundle exec rubocop # Lint
65
65
  ```
66
66
 
data/README.md CHANGED
@@ -8,12 +8,12 @@
8
8
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-green)](https://registry.modelcontextprotocol.io)
9
9
  [![Ruby](https://img.shields.io/badge/Ruby-3.2%20%7C%203.3%20%7C%203.4-red)](https://github.com/crisnahine/rails-ai-context)
10
10
  [![Rails](https://img.shields.io/badge/Rails-7.1%20%7C%207.2%20%7C%208.0-red)](https://github.com/crisnahine/rails-ai-context)
11
- [![Tests](https://img.shields.io/badge/Tests-1003%20passing-brightgreen)](https://github.com/crisnahine/rails-ai-context/actions)
11
+ [![Tests](https://img.shields.io/badge/Tests-1016%20passing-brightgreen)](https://github.com/crisnahine/rails-ai-context/actions)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
13
13
 
14
14
  **Works with:** Claude Code • Cursor • GitHub Copilot • OpenCode • Any terminal
15
15
 
16
- > Built by a Rails developer with 10+ years of production experience. AI assisted — the same way it assists me shipping features at work. I designed the architecture, made every decision, reviewed every line, and wrote 1003 tests. This gem exists because I understand Rails deeply enough to know exactly what AI agents get wrong and what context they need to get it right.
16
+ > Built by a Rails developer with 10+ years of production experience. AI assisted — the same way it assists me shipping features at work. I designed the architecture, made every decision, reviewed every line, and wrote 1016 tests. This gem exists because I understand Rails deeply enough to know exactly what AI agents get wrong and what context they need to get it right.
17
17
 
18
18
  ```bash
19
19
  gem "rails-ai-context", group: :development
@@ -64,7 +64,14 @@ module RailsAiContext
64
64
  def check_pending_migrations
65
65
  return nil unless defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
66
66
 
67
- pending = ActiveRecord::Migrator.new(:up, ActiveRecord::MigrationContext.new(File.join(app.root, "db/migrate")).migrations).pending_migrations
67
+ context = ActiveRecord::MigrationContext.new(File.join(app.root, "db/migrate"))
68
+ pending = if context.respond_to?(:pending_migrations)
69
+ # Rails 7.1+
70
+ context.pending_migrations
71
+ else
72
+ # Rails 7.0 and earlier
73
+ ActiveRecord::Migrator.new(:up, context.migrations).pending_migrations
74
+ end
68
75
  if pending.empty?
69
76
  Check.new(name: "Pending migrations", status: :pass, message: "No pending migrations", fix: nil)
70
77
  else
@@ -236,7 +236,7 @@ module RailsAiContext
236
236
  ].to_set.freeze
237
237
 
238
238
  def extract_public_class_methods(model)
239
- scope_names = extract_scopes(model).map(&:to_s)
239
+ scope_names = extract_scopes(model).map { |s| s.is_a?(Hash) ? s[:name].to_s : s.to_s }
240
240
 
241
241
  # Prioritize methods defined in the model's own source file
242
242
  source_methods = extract_source_class_methods(model)
@@ -220,7 +220,8 @@ module RailsAiContext
220
220
 
221
221
  # Include scopes so agents know available query methods
222
222
  scopes = data[:scopes] || []
223
- lines << " scopes: #{scopes.join(', ')}" if scopes.any?
223
+ scope_names = scopes.map { |s| s.is_a?(Hash) ? s[:name] : s }
224
+ lines << " scopes: #{scope_names.join(', ')}" if scopes.any?
224
225
 
225
226
  # Instance methods — introspector already prioritizes source-defined and filters Devise
226
227
  methods = (data[:instance_methods] || []).reject { |m| m.end_with?("=") }.first(20)
@@ -125,7 +125,8 @@ module RailsAiContext
125
125
  constants = (data[:constants] || [])
126
126
  if scopes.any? || constants.any?
127
127
  extras = []
128
- extras << "scopes: #{scopes.join(', ')}" if scopes.any?
128
+ scope_names = scopes.map { |s| s.is_a?(Hash) ? s[:name] : s }
129
+ extras << "scopes: #{scope_names.join(', ')}" if scopes.any?
129
130
  constants.each { |c| extras << "#{c[:name]}: #{c[:values].join(', ')}" }
130
131
  lines << " #{extras.join(' | ')}"
131
132
  end
@@ -149,7 +149,8 @@ module RailsAiContext
149
149
  constants = (data[:constants] || [])
150
150
  if scopes.any? || constants.any?
151
151
  extras = []
152
- extras << "scopes: #{scopes.join(', ')}" if scopes.any?
152
+ scope_names = scopes.map { |s| s.is_a?(Hash) ? s[:name] : s }
153
+ extras << "scopes: #{scope_names.join(', ')}" if scopes.any?
153
154
  constants.each { |c| extras << "#{c[:name]}: #{c[:values].join(', ')}" }
154
155
  lines << " #{extras.join(' | ')}"
155
156
  end
@@ -162,7 +162,8 @@ module RailsAiContext
162
162
  constants = (data[:constants] || [])
163
163
  if scopes.any? || constants.any?
164
164
  extras = []
165
- extras << "scopes: #{scopes.join(', ')}" if scopes.any?
165
+ scope_names = scopes.map { |s| s.is_a?(Hash) ? s[:name] : s }
166
+ extras << "scopes: #{scope_names.join(', ')}" if scopes.any?
166
167
  constants.each { |c| extras << "#{c[:name]}: #{c[:values].join(', ')}" }
167
168
  lines << " #{extras.join(' | ')}"
168
169
  end
@@ -72,7 +72,8 @@ module RailsAiContext
72
72
  constants = (data[:constants] || [])
73
73
  if scopes.any? || constants.any?
74
74
  extras = []
75
- extras << "scopes: #{scopes.join(', ')}" if scopes.any?
75
+ scope_names = scopes.map { |s| s.is_a?(Hash) ? s[:name] : s }
76
+ extras << "scopes: #{scope_names.join(', ')}" if scopes.any?
76
77
  constants.each { |c| extras << "#{c[:name]}: #{c[:values].join(', ')}" }
77
78
  lines << " #{extras.join(' | ')}"
78
79
  end
@@ -151,12 +151,12 @@ module RailsAiContext
151
151
  in_ctrl = ctrl_ivars.include?(ivar)
152
152
  in_view = view_ivars.include?(ivar)
153
153
  if in_ctrl && in_view
154
- lines << "- \\u2713 @#{ivar} — set in controller, used in view"
154
+ lines << "- \u2713 @#{ivar} — set in controller, used in view"
155
155
  elsif in_view && !in_ctrl
156
- lines << "- \\u2717 @#{ivar} — used in view but NOT set in controller"
156
+ lines << "- \u2717 @#{ivar} — used in view but NOT set in controller"
157
157
  mismatches = true
158
158
  elsif in_ctrl && !in_view
159
- lines << "- \\u26A0 @#{ivar} — set in controller but not used in view"
159
+ lines << "- \u26A0 @#{ivar} — set in controller but not used in view"
160
160
  end
161
161
  end
162
162
 
@@ -175,12 +175,24 @@ module RailsAiContext
175
175
 
176
176
  private_class_method def self.execute_sqlite(conn, sql, timeout)
177
177
  raw = conn.raw_connection
178
- raw.busy_timeout = (timeout * 1000).to_i
179
178
  result = nil
180
179
  begin
181
180
  conn.execute("PRAGMA query_only = ON")
181
+ # SQLite has no native statement timeout. Use a progress handler
182
+ # to abort queries that run too long (checked every 1000 VM steps).
183
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
184
+ if raw.respond_to?(:set_progress_handler)
185
+ raw.set_progress_handler(1000) do
186
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
187
+ 1 # non-zero = abort
188
+ else
189
+ 0
190
+ end
191
+ end
192
+ end
182
193
  result = conn.select_all(sql)
183
194
  ensure
195
+ raw.set_progress_handler(0, nil) if raw.respond_to?(:set_progress_handler)
184
196
  conn.execute("PRAGMA query_only = OFF")
185
197
  end
186
198
  result
@@ -180,7 +180,8 @@ module RailsAiContext
180
180
  end
181
181
 
182
182
  private_class_method def self.ripgrep_available?
183
- @rg_available ||= system("which rg > /dev/null 2>&1")
183
+ return @rg_available unless @rg_available.nil?
184
+ @rg_available = system("which rg > /dev/null 2>&1")
184
185
  end
185
186
 
186
187
  private_class_method def self.search_with_ripgrep(pattern, search_path, file_type, max_results, root, ctx_lines = 0, exclude_tests: false)
@@ -462,8 +463,10 @@ module RailsAiContext
462
463
  private_class_method def self.extract_controller_actions_from_matches(matches)
463
464
  actions = []
464
465
  matches.each do |m|
465
- # Look for the method name from indentation context
466
- actions << $1 if m[:content].match?(/\b(create|index|show|new|edit|update|destroy|[a-z_]+)\b/)
466
+ # Match standard RESTful action names from the content
467
+ if (match = m[:content].match(/\b(index|show|new|create|edit|update|destroy)\b/))
468
+ actions << match[1]
469
+ end
467
470
  end
468
471
  actions.uniq.first(3)
469
472
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAiContext
4
- VERSION = "4.2.2"
4
+ VERSION = "4.2.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-ai-context
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.2
4
+ version: 4.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - crisnahine