rails-ai-context 5.12.1 → 5.13.0

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: 16b12889884c4694d6e4a0c9e7f13f5eeefadfc2335e3a1706e94f7d50ce0696
4
- data.tar.gz: 7dcef331b58f8fb58d2cc4708bb2081a47ea45807fdb8feca42f31a3fe3185cd
3
+ metadata.gz: 3747460b78ef9b52d5e2d4bbe59e348599499285fb0215a97ce029ad49b27aa0
4
+ data.tar.gz: 2f0fff5d8986ca5ead4d74933dac2c7933d2ca7bd7dd1cab11fc3c4c5c756106
5
5
  SHA512:
6
- metadata.gz: 76ffa75a43e4f58344addab5a5dcd1a305b390bb987b33be27ecab456ba3e8cd07b99791e410d11a91591211131c4cc9304ba79dbc760349fa1dbe18d7d3d7c2
7
- data.tar.gz: 34e7d398f0d6157f36d9f4a606f8f53b6f5d59fdafeb156e4fd654547e1d837254e407e647bda55bfc624909bb3b6dcaa20f00a92f4ccc5630e27997188afd63
6
+ metadata.gz: b343b7456a8f26c0738ebf3ca19b50758f54e859d82b3c40dba97765bde151f429f7da7b6d7a9abba2ac35438faaa858686c306e4f99482e2fc5a0b8692a7055
7
+ data.tar.gz: b18a15e6fcb931bc9ae34c340c8c3c2481092273e7c0d1ca3d63da3b40282f2a18988656a96588c2e80ea271ab2f4ebc224124b7872328663eb5bc63da93ea0f
data/.rubocop.yml CHANGED
@@ -2,7 +2,7 @@ inherit_gem:
2
2
  rubocop-rails-omakase: rubocop.yml
3
3
 
4
4
  AllCops:
5
- TargetRubyVersion: 3.2
5
+ TargetRubyVersion: 3.1
6
6
  NewCops: enable
7
7
  Exclude:
8
8
  - "spec/fixtures/**/*"
data/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ 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
+ ## [5.13.0] - 2026-07-10
9
+
10
+ ### Added
11
+
12
+ - **Ruby 3.1 and Rails 7.0 are now supported.** `required_ruby_version` drops to `>= 3.1.0` and the `railties` floor to `>= 7.0`, widening the test matrix to Ruby 3.1-4.0 x Rails 7.0-8.1. Rails 7.0 is exercised on Ruby 3.1, its only supported pairing (7.0 predates official Ruby 3.2+ support). Reaching parity required:
13
+ - A vendored `Data.define` backport (`lib/rails_ai_context/polyfill/data.rb`) for the value objects `Doctor::Check`, `SchemaHint`, and `HydrationResult`. `Data` is a Ruby 3.2+ class; the shim is inert on 3.2+ (guarded on `Data.respond_to?(:define)`) and activates only on 3.1, preserving keyword construction, the `super`-forwarding custom initialize, frozen instances, and value equality.
14
+ - `rails_search_code` no longer passes the Ruby 3.2+ `timeout:` keyword to `Regexp.new` on Ruby 3.1, where it raised `TypeError: no implicit conversion of Hash into String` and broke every search. The per-pattern ReDoS timeout is applied only when the runtime supports it (`Regexp.respond_to?(:timeout)`); 3.1 builds the pattern without it.
15
+ - Every version-sensitive Rails call in the 40 introspectors was already `respond_to?`/`defined?`-guarded or rescue-wrapped, so no introspector changed to run on Rails 7.0.
16
+ - The dev/test Gemfile pins `sqlite3 ~> 1.4` on Rails 7.0 (whose adapter predates the sqlite3 2.x line) and always loads `logger` (concurrent-ruby 1.3.5 dropped the implicit `require "logger"` that Rails < 7.1 relies on at boot).
17
+
8
18
  ## [5.12.1] - 2026-07-10
9
19
 
10
20
  ### Fixed
data/CONTRIBUTING.md CHANGED
@@ -64,7 +64,8 @@ The `ToolRunner` (`lib/rails_ai_context/cli/tool_runner.rb`) handles CLI executi
64
64
  ## Code Style
65
65
 
66
66
  - Follow `rubocop-rails-omakase` style (run `bundle exec rubocop`)
67
- - Ruby 3.2+ features welcome (pattern matching, etc.)
67
+ - Target Ruby 3.1 (the gem's floor). Ruby 3.2+ features (`Data.define`, `Regexp` `timeout:`, etc.) need a runtime-guarded fallback so 3.1 keeps working
68
+ - Support Rails 7.0 through 8.1. Guard Rails 7.1+ APIs with `respond_to?`/`defined?` so introspectors degrade cleanly on 7.0
68
69
  - Every introspector must return a Hash and never raise - wrap errors in `{ error: msg }`
69
70
  - MCP tools return `MCP::Tool::Response` objects
70
71
  - All tools must be prefixed with `rails_` and annotated as read-only
data/README.md CHANGED
@@ -19,8 +19,8 @@
19
19
  [![CI](https://github.com/crisnahine/rails-ai-context/actions/workflows/ci.yml/badge.svg)](https://github.com/crisnahine/rails-ai-context/actions)
20
20
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-green)](https://registry.modelcontextprotocol.io)
21
21
  <br>
22
- [![Ruby](https://img.shields.io/badge/Ruby-3.2%20%7C%203.3%20%7C%203.4-CC342D)](https://github.com/crisnahine/rails-ai-context)
23
- [![Rails](https://img.shields.io/badge/Rails-7.1%20%7C%207.2%20%7C%208.0-CC0000)](https://github.com/crisnahine/rails-ai-context)
22
+ [![Ruby](https://img.shields.io/badge/Ruby-3.1%20%7C%203.2%20%7C%203.3%20%7C%203.4-CC342D)](https://github.com/crisnahine/rails-ai-context)
23
+ [![Rails](https://img.shields.io/badge/Rails-7.0%20%7C%207.1%20%7C%207.2%20%7C%208.0-CC0000)](https://github.com/crisnahine/rails-ai-context)
24
24
  [![Tests](https://img.shields.io/badge/Tests-2257%20passing-brightgreen)](https://github.com/crisnahine/rails-ai-context/actions)
25
25
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
26
26
 
@@ -531,7 +531,7 @@ end
531
531
 
532
532
  ## Requirements
533
533
 
534
- - **Ruby** >= 3.2 &nbsp;&nbsp; **Rails** >= 7.1
534
+ - **Ruby** >= 3.1 &nbsp;&nbsp; **Rails** >= 7.0
535
535
  - **Optional:** `brakeman` for security scanning, `listen` for watch mode, `ripgrep` for fast search
536
536
 
537
537
  <br>
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Backport of the Ruby 3.2+ immutable value class Data for Ruby 3.1, which
4
+ # ships no Data at all. Inert on Ruby >= 3.2: the guard sees the built-in
5
+ # Data.define and defines nothing, so every runtime from 3.2 up keeps the real
6
+ # implementation and only 3.1 gets this shim.
7
+ #
8
+ # The value-setting initialize lives on this base class so a block-defined
9
+ # initialize on a subclass can call super into it (the HydrationResult idiom).
10
+ # Members are keyword- or positionally-constructable, instances are frozen, and
11
+ # equality is by value, matching how Doctor::Check, SchemaHint, and
12
+ # HydrationResult are built and compared.
13
+ unless defined?(Data) && Data.respond_to?(:define)
14
+ class Data
15
+ class << self
16
+ def define(*members, &block)
17
+ members = members.map(&:to_sym)
18
+ raise ArgumentError, "duplicate member" if members.uniq.length != members.length
19
+
20
+ subclass = ::Class.new(self)
21
+ subclass.instance_variable_set(:@members, members.freeze)
22
+ members.each do |name|
23
+ subclass.define_method(name) { instance_variable_get(:"@#{name}") }
24
+ end
25
+ subclass.class_eval(&block) if block
26
+ subclass
27
+ end
28
+
29
+ def members
30
+ @members || []
31
+ end
32
+ end
33
+
34
+ def initialize(*args, **kwargs)
35
+ members = self.class.members
36
+ if kwargs.empty?
37
+ unless args.length == members.length
38
+ raise ArgumentError, "wrong number of arguments (given #{args.length}, expected #{members.length})"
39
+ end
40
+
41
+ members.each_with_index { |m, i| instance_variable_set(:"@#{m}", args[i]) }
42
+ else
43
+ unless args.empty?
44
+ raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 0)"
45
+ end
46
+
47
+ extra = kwargs.keys - members
48
+ unless extra.empty?
49
+ raise ArgumentError, "unknown keyword#{extra.length > 1 ? 's' : ''}: #{extra.map(&:inspect).join(', ')}"
50
+ end
51
+
52
+ missing = members - kwargs.keys
53
+ unless missing.empty?
54
+ raise ArgumentError, "missing keyword#{missing.length > 1 ? 's' : ''}: #{missing.map(&:inspect).join(', ')}"
55
+ end
56
+
57
+ members.each { |m| instance_variable_set(:"@#{m}", kwargs[m]) }
58
+ end
59
+ freeze
60
+ end
61
+
62
+ def members
63
+ self.class.members
64
+ end
65
+
66
+ def to_h(&block)
67
+ pairs = self.class.members.map { |m| [ m, instance_variable_get(:"@#{m}") ] }
68
+ return pairs.to_h unless block
69
+
70
+ pairs.each_with_object({}) do |(k, v), acc|
71
+ nk, nv = block.call(k, v)
72
+ acc[nk] = nv
73
+ end
74
+ end
75
+
76
+ def with(**kwargs)
77
+ extra = kwargs.keys - self.class.members
78
+ unless extra.empty?
79
+ raise ArgumentError, "unknown keyword#{extra.length > 1 ? 's' : ''}: #{extra.map(&:inspect).join(', ')}"
80
+ end
81
+
82
+ self.class.new(**to_h.merge(kwargs))
83
+ end
84
+
85
+ def deconstruct
86
+ self.class.members.map { |m| instance_variable_get(:"@#{m}") }
87
+ end
88
+
89
+ def deconstruct_keys(keys)
90
+ return to_h if keys.nil?
91
+
92
+ keys.each_with_object({}) do |k, acc|
93
+ acc[k] = instance_variable_get(:"@#{k}") if self.class.members.include?(k)
94
+ end
95
+ end
96
+
97
+ def ==(other)
98
+ other.class == self.class && other.to_h == to_h
99
+ end
100
+ alias_method :eql?, :==
101
+
102
+ def hash
103
+ [ self.class, to_h ].hash
104
+ end
105
+
106
+ def inspect
107
+ pairs = self.class.members.map { |m| "#{m}=#{instance_variable_get(:"@#{m}").inspect}" }
108
+ "#<data #{self.class.name}#{pairs.empty? ? '' : " #{pairs.join(', ')}"}>"
109
+ end
110
+ alias_method :to_s, :inspect
111
+ end
112
+ end
@@ -5,6 +5,10 @@ require "open3"
5
5
  module RailsAiContext
6
6
  module Tools
7
7
  class SearchCode < BaseTool
8
+ # Per-pattern Regexp timeout (a ReDoS guard) is Ruby 3.2+. Ruby 3.1 has
9
+ # no per-match timeout, so the pattern is built without one there.
10
+ REGEXP_TIMEOUT_SUPPORTED = Regexp.respond_to?(:timeout)
11
+
8
12
  tool_name "rails_search_code"
9
13
  description "Search the Rails codebase with smart modes. " \
10
14
  "Use match_type:\"trace\" to see where a method is defined, who calls it, and what it calls - in one call. " \
@@ -102,7 +106,7 @@ module RailsAiContext
102
106
 
103
107
  # Validate regex syntax early
104
108
  begin
105
- Regexp.new(search_pattern, timeout: 1)
109
+ build_regexp(search_pattern, timeout: 1)
106
110
  rescue RegexpError => e
107
111
  return text_response("Invalid regex pattern: #{e.message}")
108
112
  end
@@ -174,6 +178,17 @@ module RailsAiContext
174
178
  end
175
179
  end
176
180
 
181
+ # Build a Regexp, applying the ReDoS timeout only on runtimes that
182
+ # support it (Ruby 3.2+). Keeps a single construction path for both the
183
+ # syntax-validation probe and the actual Ruby-fallback search.
184
+ private_class_method def self.build_regexp(pattern, options = nil, timeout:)
185
+ if REGEXP_TIMEOUT_SUPPORTED
186
+ Regexp.new(pattern, options, timeout: timeout)
187
+ else
188
+ Regexp.new(pattern, options)
189
+ end
190
+ end
191
+
177
192
  private_class_method def self.ripgrep_available?
178
193
  return @rg_available unless @rg_available.nil?
179
194
  @rg_available = system("which rg > /dev/null 2>&1")
@@ -244,7 +259,7 @@ module RailsAiContext
244
259
  private_class_method def self.search_with_ruby(pattern, search_path, file_type, max_results, root, exclude_tests: false)
245
260
  results = []
246
261
  begin
247
- regex = Regexp.new(pattern, Regexp::IGNORECASE, timeout: 2)
262
+ regex = build_regexp(pattern, Regexp::IGNORECASE, timeout: 2)
248
263
  rescue RegexpError => e
249
264
  return [ { file: "error", line_number: 0, content: "Invalid regex: #{e.message}" } ]
250
265
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAiContext
4
- VERSION = "5.12.1"
4
+ VERSION = "5.13.0"
5
5
  end
@@ -2,10 +2,15 @@
2
2
 
3
3
  require "zeitwerk"
4
4
 
5
+ # Provide Data.define on Ruby 3.1 (no-op on 3.2+) before any value object is
6
+ # autoloaded. Defines a top-level constant, so it stays outside Zeitwerk.
7
+ require_relative "rails_ai_context/polyfill/data"
8
+
5
9
  loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
6
10
  loader.inflector.inflect("devops_introspector" => "DevOpsIntrospector", "cli" => "CLI", "vfs" => "VFS")
7
11
  loader.ignore("#{__dir__}/generators")
8
12
  loader.ignore("#{__dir__}/rails-ai-context.rb")
13
+ loader.ignore("#{__dir__}/rails_ai_context/polyfill")
9
14
  loader.setup
10
15
 
11
16
  module RailsAiContext
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: 5.12.1
4
+ version: 5.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - crisnahine
@@ -35,7 +35,7 @@ dependencies:
35
35
  requirements:
36
36
  - - ">="
37
37
  - !ruby/object:Gem::Version
38
- version: '7.1'
38
+ version: '7.0'
39
39
  - - "<"
40
40
  - !ruby/object:Gem::Version
41
41
  version: '9.0'
@@ -45,7 +45,7 @@ dependencies:
45
45
  requirements:
46
46
  - - ">="
47
47
  - !ruby/object:Gem::Version
48
- version: '7.1'
48
+ version: '7.0'
49
49
  - - "<"
50
50
  - !ruby/object:Gem::Version
51
51
  version: '9.0'
@@ -329,6 +329,7 @@ files:
329
329
  - lib/rails_ai_context/live_reload.rb
330
330
  - lib/rails_ai_context/mcp_config_generator.rb
331
331
  - lib/rails_ai_context/middleware.rb
332
+ - lib/rails_ai_context/polyfill/data.rb
332
333
  - lib/rails_ai_context/resources.rb
333
334
  - lib/rails_ai_context/safe_file.rb
334
335
  - lib/rails_ai_context/schema_hint.rb
@@ -423,7 +424,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
423
424
  requirements:
424
425
  - - ">="
425
426
  - !ruby/object:Gem::Version
426
- version: 3.2.0
427
+ version: 3.1.0
427
428
  required_rubygems_version: !ruby/object:Gem::Requirement
428
429
  requirements:
429
430
  - - ">="