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 +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +10 -0
- data/CONTRIBUTING.md +2 -1
- data/README.md +3 -3
- data/lib/rails_ai_context/polyfill/data.rb +112 -0
- data/lib/rails_ai_context/tools/search_code.rb +17 -2
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context.rb +5 -0
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3747460b78ef9b52d5e2d4bbe59e348599499285fb0215a97ce029ad49b27aa0
|
|
4
|
+
data.tar.gz: 2f0fff5d8986ca5ead4d74933dac2c7933d2ca7bd7dd1cab11fc3c4c5c756106
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b343b7456a8f26c0738ebf3ca19b50758f54e859d82b3c40dba97765bde151f429f7da7b6d7a9abba2ac35438faaa858686c306e4f99482e2fc5a0b8692a7055
|
|
7
|
+
data.tar.gz: b18a15e6fcb931bc9ae34c340c8c3c2481092273e7c0d1ca3d63da3b40282f2a18988656a96588c2e80ea271ab2f4ebc224124b7872328663eb5bc63da93ea0f
|
data/.rubocop.yml
CHANGED
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
|
|
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
|
[](https://github.com/crisnahine/rails-ai-context/actions)
|
|
20
20
|
[](https://registry.modelcontextprotocol.io)
|
|
21
21
|
<br>
|
|
22
|
-
[](https://github.com/crisnahine/rails-ai-context)
|
|
23
|
-
[](https://github.com/crisnahine/rails-ai-context)
|
|
22
|
+
[](https://github.com/crisnahine/rails-ai-context)
|
|
23
|
+
[](https://github.com/crisnahine/rails-ai-context)
|
|
24
24
|
[](https://github.com/crisnahine/rails-ai-context/actions)
|
|
25
25
|
[](LICENSE)
|
|
26
26
|
|
|
@@ -531,7 +531,7 @@ end
|
|
|
531
531
|
|
|
532
532
|
## Requirements
|
|
533
533
|
|
|
534
|
-
- **Ruby** >= 3.
|
|
534
|
+
- **Ruby** >= 3.1 **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
|
-
|
|
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 =
|
|
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
|
data/lib/rails_ai_context.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
427
|
+
version: 3.1.0
|
|
427
428
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
428
429
|
requirements:
|
|
429
430
|
- - ">="
|