rails-ai-context 5.12.0 → 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 +17 -0
- data/CONTRIBUTING.md +2 -1
- data/README.md +3 -3
- data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +2 -2
- data/lib/rails_ai_context/polyfill/data.rb +112 -0
- data/lib/rails_ai_context/tools/query.rb +8 -1
- 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,23 @@ 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
|
+
|
|
18
|
+
## [5.12.1] - 2026-07-10
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `rails_query` no longer fails on MySQL with `Transaction characteristics can't be changed while a transaction is in progress`. `execute_mysql` issued `SET TRANSACTION READ ONLY` inside `conn.transaction`, but Rails materializes the lazy `BEGIN` before the first in-block statement, so MySQL (error 1568) rejected the SET on every call -- a 100% failure rate for plain queries and `explain: true`. The SET now runs before the transaction opens; without `GLOBAL`/`SESSION` scope it applies only to the next transaction, which the query transaction immediately consumes, so the read-only guard still holds and nothing leaks onto the pooled connection. PostgreSQL (where `SET TRANSACTION` applies to the current transaction) and SQLite are unaffected. (#89)
|
|
23
|
+
- The static `schema.rb` parser now recognizes the PostgreSQL `timestamptz` and `tsvector` column types. `t.timestamptz` and `t.tsvector` columns were dropped from the parsed schema because the DSL listener's column-type set omitted them, so schema-backed tools under-reported columns on Postgres apps using `timestamp with time zone` or full-text search vectors. (#88)
|
|
24
|
+
|
|
8
25
|
## [5.12.0] - 2026-06-29
|
|
9
26
|
|
|
10
27
|
### 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>
|
|
@@ -8,10 +8,10 @@ module RailsAiContext
|
|
|
8
8
|
class SchemaDslListener < BaseListener
|
|
9
9
|
COLUMN_TYPES = %w[
|
|
10
10
|
string integer text boolean datetime date decimal float binary
|
|
11
|
-
references belongs_to jsonb json uuid bigint timestamp time
|
|
11
|
+
references belongs_to jsonb json uuid bigint timestamp timestamptz time
|
|
12
12
|
inet cidr macaddr hstore ltree numrange tsrange daterange
|
|
13
13
|
bit bit_varying money oid xml point line lseg box path
|
|
14
|
-
polygon circle interval serial virtual primary_key
|
|
14
|
+
polygon circle interval serial tsvector virtual primary_key
|
|
15
15
|
].to_set.freeze
|
|
16
16
|
|
|
17
17
|
def on_call_node_enter(node)
|
|
@@ -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
|
|
@@ -307,9 +307,16 @@ module RailsAiContext
|
|
|
307
307
|
sql
|
|
308
308
|
end
|
|
309
309
|
|
|
310
|
+
# The SET must run BEFORE the transaction opens: Rails materializes
|
|
311
|
+
# the lazy BEGIN ahead of the first in-block statement, and MySQL
|
|
312
|
+
# rejects changing transaction characteristics mid-transaction
|
|
313
|
+
# (error 1568). Without GLOBAL/SESSION scope the SET applies only to
|
|
314
|
+
# the next transaction, which the block below immediately consumes,
|
|
315
|
+
# so the read-only characteristic cannot leak onto the pooled
|
|
316
|
+
# connection. (#89)
|
|
310
317
|
result = nil
|
|
318
|
+
conn.execute("SET TRANSACTION READ ONLY")
|
|
311
319
|
conn.transaction do
|
|
312
|
-
conn.execute("SET TRANSACTION READ ONLY")
|
|
313
320
|
result = conn.select_all(hinted_sql)
|
|
314
321
|
raise ActiveRecord::Rollback
|
|
315
322
|
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
|
- - ">="
|