rails-ai-context 5.20.1 → 5.20.2
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/CHANGELOG.md +62 -0
- data/lib/rails_ai_context/app_kind.rb +23 -0
- data/lib/rails_ai_context/cli/tool_runner.rb +44 -1
- data/lib/rails_ai_context/code_reloader.rb +79 -0
- data/lib/rails_ai_context/introspectors/api_introspector.rb +15 -1
- data/lib/rails_ai_context/introspectors/job_introspector.rb +12 -3
- data/lib/rails_ai_context/introspectors/model_introspector.rb +25 -6
- data/lib/rails_ai_context/live_reload.rb +7 -2
- data/lib/rails_ai_context/resources.rb +7 -0
- data/lib/rails_ai_context/schema_adapter.rb +85 -0
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +1 -1
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +7 -0
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +1 -1
- data/lib/rails_ai_context/tools/base_tool.rb +9 -1
- data/lib/rails_ai_context/tools/get_job_pattern.rb +4 -4
- data/lib/rails_ai_context/tools/get_schema.rb +9 -2
- data/lib/rails_ai_context/tools/onboard.rb +5 -25
- data/lib/rails_ai_context/tools/safe_call.rb +24 -9
- data/lib/rails_ai_context/tools/validate.rb +10 -1
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/watcher.rb +3 -0
- data/server.json +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 81eedfa398603e64a27f7555b4288e39a11e41093c3a06450354c2f5cb1d17c5
|
|
4
|
+
data.tar.gz: 34972bf4259e8e12695f008744111bff94ed7caa93155bd3f8352010e3134c15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c88339dc6c73a5fdb38209d69e61437a6494382c069ae943478e51a265e57b2b847d4cb89f22ef0449c024bdb43185feef7e58a5e92f199c0a0389f4822898ca
|
|
7
|
+
data.tar.gz: 2f0a283b5691f00d4f49aeed839da56af13e248f566c9a526e3926cbc0c71185cf2e99a9b809d5e316d7e1144826456bb65ea36148493b4102f44b3bf0857584
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,68 @@ 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.20.2] - 2026-08-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **A long-lived process no longer answers about the app as it was at boot.**
|
|
13
|
+
The MCP server and `watch` invalidated their own caches on a file change but
|
|
14
|
+
never reloaded Rails, and `Zeitwerk#eager_load_dir` does not re-scan a
|
|
15
|
+
directory it has already loaded - so a model written after the server started
|
|
16
|
+
stayed invisible for the life of the process, and the server told callers it
|
|
17
|
+
did not exist. Routes were unaffected because `RouteIntrospector` already
|
|
18
|
+
asked `routes_reloader.execute_if_updated`. Both change handlers now run
|
|
19
|
+
Rails' own reloader first, gated on `config.enable_reloading` (the flag that
|
|
20
|
+
decides whether Rails unloads anything - `eager_load` does not), and every
|
|
21
|
+
tool call and every resource read now runs inside the executor so a reload
|
|
22
|
+
cannot unload constants while one is reading them. A failing reload leaves
|
|
23
|
+
the process alive, and a tool failure is not reported to the host app's
|
|
24
|
+
error reporter - it is handled here and returned as an isError result.
|
|
25
|
+
|
|
26
|
+
- **A `validates_with` validator no longer replaces the whole model answer.**
|
|
27
|
+
`ActiveModel::Validator` has no `#attributes` - only `EachValidator` does -
|
|
28
|
+
so one of them anywhere on a model turned `rails_get_model_details`,
|
|
29
|
+
`rails_get_callbacks` and `rails://models/X` into a single
|
|
30
|
+
"Error inspecting X" line. (#121)
|
|
31
|
+
- **Static-tier callbacks are reported.** The two tiers handed back different
|
|
32
|
+
types for one key: a Hash keyed by callback type when booted, a flat Array
|
|
33
|
+
from the listener otherwise. Every consumer filters on `is_a?(Hash)`, so
|
|
34
|
+
`--no-boot` answered "No models with callbacks found" for an app full of
|
|
35
|
+
them, and `--model X --no-boot` raised `TypeError` on a Hash lookup against
|
|
36
|
+
an Array. (#122)
|
|
37
|
+
- **`get_mailers` reports the actions Rails calls actions.** It listed
|
|
38
|
+
`instance_methods(false)`, which includes ActiveSupport's generated
|
|
39
|
+
`_run_*_callbacks` and any public helper, so an abstract `ApplicationMailer`
|
|
40
|
+
was reported as having deliverable actions. It now asks
|
|
41
|
+
`action_methods`. (#126)
|
|
42
|
+
- **One answer to which database the app runs on.** Four surfaces each carried
|
|
43
|
+
their own substitute for the internal `static_parse` marker and disagreed:
|
|
44
|
+
the generated `CLAUDE.md`, `rails_get_schema`, `rails_onboard` and
|
|
45
|
+
`rails ai:inspect` could name three different databases for one app, and
|
|
46
|
+
`onboard`'s gem loop let the last match win where the serializer's let the
|
|
47
|
+
first. They now share `RailsAiContext::SchemaAdapter`, which resolves from
|
|
48
|
+
the app's own database configuration first, then the `structure.sql` dialect
|
|
49
|
+
already parsed, then the Gemfile - order-independent. (#125)
|
|
50
|
+
- **Generated context files no longer print `Database: static_parse`.** That is
|
|
51
|
+
the internal marker for "read from db/schema.rb, not the connection";
|
|
52
|
+
`onboard` already substituted the live adapter and the serializers did
|
|
53
|
+
not. (#125)
|
|
54
|
+
- **`get_job_pattern` says what it checked.** "This app may not use background
|
|
55
|
+
jobs" was a claim about the whole app drawn from one directory, and apps
|
|
56
|
+
keeping Sidekiq workers in `app/workers/` were told they have none. (#120)
|
|
57
|
+
- **`validate`'s empty-files message advises a syntax the CLI accepts.** It
|
|
58
|
+
recommended `files:["..."]`, which the CLI reads as a literal filename. (#124)
|
|
59
|
+
- **`validate --files a.rb b.rb` validates every file.** Array parameters
|
|
60
|
+
consumed only the first token, so the rest were dropped silently and the
|
|
61
|
+
summary read "1/1 files passed" for a set containing a broken file. (#124)
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- **The static tier reads `config.api_only`.** Without a booted app the view
|
|
66
|
+
tools fell back to "no Stimulus controllers found" on an API-only app, where a
|
|
67
|
+
booted run says "Not applicable". Both were true, but only one of them stops
|
|
68
|
+
an agent adding a view layer to an app that has none by design.
|
|
69
|
+
|
|
8
70
|
## [5.20.1] - 2026-08-11
|
|
9
71
|
|
|
10
72
|
### Fixed
|
|
@@ -27,5 +27,28 @@ module RailsAiContext
|
|
|
27
27
|
content = RailsAiContext::SafeFile.read(gemfile)
|
|
28
28
|
!!content&.match?(/^\s*gem\s+["']mongoid["']/)
|
|
29
29
|
end
|
|
30
|
+
|
|
31
|
+
# An API-only app has no view layer, and saying "no Stimulus controllers
|
|
32
|
+
# found" about one invites an agent to add some. The flag is written in
|
|
33
|
+
# config/application.rb, so this answer needs no booted app.
|
|
34
|
+
#
|
|
35
|
+
# Read from the AST, not a regex: `config.api_only = true` is an
|
|
36
|
+
# assignment, which docs/INTROSPECTORS.md puts squarely in AST territory,
|
|
37
|
+
# and ConfigAssignmentListener already reports exactly this shape. A regex
|
|
38
|
+
# also has to hand-roll what the parser knows for free - comments, strings
|
|
39
|
+
# and heredocs that merely contain the text.
|
|
40
|
+
def api_only?(root)
|
|
41
|
+
path = File.join(root.to_s, "config", "application.rb")
|
|
42
|
+
return false unless File.exist?(path)
|
|
43
|
+
return false unless RailsAiContext::SafeFile.read(path)
|
|
44
|
+
|
|
45
|
+
walked = Introspectors::SourceIntrospector.walk(
|
|
46
|
+
path, { config: Introspectors::Listeners::ConfigAssignmentListener }
|
|
47
|
+
)
|
|
48
|
+
hit = Array(walked[:config]).find { |entry| entry[:path] == [ :api_only ] }
|
|
49
|
+
!hit.nil? && hit[:value] == true
|
|
50
|
+
rescue StandardError, ScriptError
|
|
51
|
+
false
|
|
52
|
+
end
|
|
30
53
|
end
|
|
31
54
|
end
|
|
@@ -211,7 +211,17 @@ module RailsAiContext
|
|
|
211
211
|
if arg.include?("=")
|
|
212
212
|
key, value = arg.sub("--", "").split("=", 2)
|
|
213
213
|
key = key.tr("-", "_").to_sym
|
|
214
|
-
|
|
214
|
+
prop = properties[key] || {}
|
|
215
|
+
if prop[:type] == "array"
|
|
216
|
+
# `--files=a.rb b.rb` is the same call as `--files a.rb b.rb`;
|
|
217
|
+
# honouring only one of the two spellings docs/CLI.md teaches
|
|
218
|
+
# left the other still dropping every file after the first.
|
|
219
|
+
trailing = collect_array_values(args, i + 1)
|
|
220
|
+
result[key] = Array(coerce_value(value, prop)) + trailing
|
|
221
|
+
i += 1 + values_consumed(args, i + 1)
|
|
222
|
+
next
|
|
223
|
+
end
|
|
224
|
+
result[key] = coerce_value(value, prop)
|
|
215
225
|
else
|
|
216
226
|
key = arg.sub("--", "").tr("-", "_").to_sym
|
|
217
227
|
prop = properties[key] || {}
|
|
@@ -232,6 +242,24 @@ module RailsAiContext
|
|
|
232
242
|
next
|
|
233
243
|
end
|
|
234
244
|
|
|
245
|
+
if prop[:type] == "array"
|
|
246
|
+
values = collect_array_values(args, i + 1)
|
|
247
|
+
unless values.empty?
|
|
248
|
+
result[key] = values
|
|
249
|
+
i += 1 + values_consumed(args, i + 1)
|
|
250
|
+
next
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# A bare `--files` with nothing after it is a mistake, not a
|
|
255
|
+
# request for `files: true` - an array param holding a Boolean
|
|
256
|
+
# reaches the tool as a type it never accepts.
|
|
257
|
+
if prop[:type] == "array"
|
|
258
|
+
result[key] = []
|
|
259
|
+
i += 1
|
|
260
|
+
next
|
|
261
|
+
end
|
|
262
|
+
|
|
235
263
|
value = (i + 1 < args.size) ? args[i + 1] : nil
|
|
236
264
|
if value && !value.start_with?("--")
|
|
237
265
|
result[key] = coerce_value(value, prop)
|
|
@@ -258,6 +286,21 @@ module RailsAiContext
|
|
|
258
286
|
result
|
|
259
287
|
end
|
|
260
288
|
|
|
289
|
+
# Tokens belonging to an array flag: everything up to the next flag,
|
|
290
|
+
# stopping at a rake-style `key=value` token so `--include a model=Post`
|
|
291
|
+
# does not swallow the second parameter. Each token is still
|
|
292
|
+
# comma-split, so the documented `a.rb,b.rb` form keeps working and
|
|
293
|
+
# mixing the two spellings does not fabricate a path.
|
|
294
|
+
def collect_array_values(args, from)
|
|
295
|
+
args[from..].to_a
|
|
296
|
+
.take_while { |a| !a.start_with?("--") && !a.include?("=") }
|
|
297
|
+
.flat_map { |a| Array(coerce_value(a, { type: "array" })) }
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def values_consumed(args, from)
|
|
301
|
+
args[from..].to_a.take_while { |a| !a.start_with?("--") && !a.include?("=") }.size
|
|
302
|
+
end
|
|
303
|
+
|
|
261
304
|
# Coerce a string value to the type specified in the JSON Schema property.
|
|
262
305
|
def coerce_value(raw, property_schema)
|
|
263
306
|
case property_schema[:type]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
# Re-runs Rails' own code reloader so a long-lived process sees files written
|
|
5
|
+
# after it booted.
|
|
6
|
+
#
|
|
7
|
+
# Clearing the gem's caches is not enough on its own. Introspectors reach the
|
|
8
|
+
# app through constants, and the eager loading they trigger
|
|
9
|
+
# (`Zeitwerk::Loader#eager_load_dir`) is idempotent per process - a directory
|
|
10
|
+
# already loaded is never re-scanned, so a model added after boot stays
|
|
11
|
+
# invisible for the life of the server. Routes never had the problem because
|
|
12
|
+
# RouteIntrospector asks `routes_reloader.execute_if_updated` every call.
|
|
13
|
+
module CodeReloader
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Reload the app's autoloaded code. Returns whether a reload actually ran,
|
|
17
|
+
# so callers can say what they did instead of guessing.
|
|
18
|
+
def reload!
|
|
19
|
+
return false unless reloadable?
|
|
20
|
+
|
|
21
|
+
Rails.application.reloader.reload!
|
|
22
|
+
true
|
|
23
|
+
rescue StandardError, ScriptError => e
|
|
24
|
+
# A broken file mid-edit is the common case. Zeitwerk reloads by
|
|
25
|
+
# unload-then-setup, so a raise can leave constants already unloaded -
|
|
26
|
+
# they re-autoload lazily on next reference. Either way a running server
|
|
27
|
+
# must survive it.
|
|
28
|
+
$stderr.puts "[rails-ai-context] code reload failed: #{e.class}: #{e.message}" if ENV["DEBUG"]
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Run a block with the sharing lock held, so a reload cannot unload
|
|
33
|
+
# constants underneath it.
|
|
34
|
+
#
|
|
35
|
+
# `Reloader#class_unload!` takes the unload lock through
|
|
36
|
+
# ActiveSupport::Dependencies.interlock, but that only blocks threads
|
|
37
|
+
# holding the sharing lock - which is acquired inside `executor.wrap`.
|
|
38
|
+
# Nothing here wrapped anything, so the Listen thread could clear
|
|
39
|
+
# DescendantsTracker while a tool call was midway through reading
|
|
40
|
+
# `ActiveRecord::Base.descendants`, returning a short list with no
|
|
41
|
+
# exception for the per-section rescue to notice.
|
|
42
|
+
#
|
|
43
|
+
# Only taken when a reload could actually happen; everywhere else this is
|
|
44
|
+
# a plain yield.
|
|
45
|
+
def with_app_code
|
|
46
|
+
return yield unless reloadable?
|
|
47
|
+
|
|
48
|
+
app = Rails.application
|
|
49
|
+
return yield unless app.respond_to?(:executor)
|
|
50
|
+
|
|
51
|
+
app.executor.wrap { yield }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# `enable_reloading` is the flag that decides whether Rails unloads
|
|
55
|
+
# anything: with it off, the finisher registers no class_unload callback,
|
|
56
|
+
# so `reload!` runs the prepare callbacks and returns having reloaded
|
|
57
|
+
# nothing. Gating on `eager_load` instead reported success for every
|
|
58
|
+
# eager_load=false + cache_classes=true environment (stock `test`, a
|
|
59
|
+
# cache-classes staging container) - a reload that never happened,
|
|
60
|
+
# announced as one.
|
|
61
|
+
def reloadable?
|
|
62
|
+
return false if RailsAiContext.static_tier?
|
|
63
|
+
return false unless defined?(Rails) && Rails.respond_to?(:application)
|
|
64
|
+
|
|
65
|
+
app = Rails.application
|
|
66
|
+
return false unless app.respond_to?(:reloader) && app.respond_to?(:config)
|
|
67
|
+
|
|
68
|
+
config = app.config
|
|
69
|
+
# Rails 7.1+ names it enable_reloading; older releases only have
|
|
70
|
+
# cache_classes, which is its inverse.
|
|
71
|
+
return config.enable_reloading if config.respond_to?(:enable_reloading)
|
|
72
|
+
return !config.cache_classes if config.respond_to?(:cache_classes)
|
|
73
|
+
|
|
74
|
+
false
|
|
75
|
+
rescue StandardError
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -6,7 +6,7 @@ module RailsAiContext
|
|
|
6
6
|
# versioning patterns, rate limiting.
|
|
7
7
|
class ApiIntrospector
|
|
8
8
|
extend StaticTier
|
|
9
|
-
static_tier :
|
|
9
|
+
static_tier :alternate_source
|
|
10
10
|
|
|
11
11
|
attr_reader :app
|
|
12
12
|
|
|
@@ -14,6 +14,20 @@ module RailsAiContext
|
|
|
14
14
|
@app = app
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# Everything but api_only needs a booted app, but api_only itself is a
|
|
18
|
+
# plain assignment in config/application.rb - and the view tools already
|
|
19
|
+
# read it there. Leaving this section wholly unavailable meant one
|
|
20
|
+
# process answering "this is an API-only app" from get_stimulus and
|
|
21
|
+
# "cannot say" from get_api.
|
|
22
|
+
def static_call
|
|
23
|
+
{
|
|
24
|
+
api_only: AppKind.api_only?(app.root),
|
|
25
|
+
unavailable_sections: StaticTier.unavailable_reason
|
|
26
|
+
}
|
|
27
|
+
rescue StandardError
|
|
28
|
+
{ unavailable: StaticTier.unavailable_reason }
|
|
29
|
+
end
|
|
30
|
+
|
|
17
31
|
def call
|
|
18
32
|
{
|
|
19
33
|
api_only: app.config.api_only,
|
|
@@ -234,7 +234,14 @@ module RailsAiContext
|
|
|
234
234
|
ActionMailer::Base.descendants.filter_map do |mailer|
|
|
235
235
|
next if mailer.name.nil?
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
# Rails' own answer: it subtracts internal and inherited methods, so
|
|
238
|
+
# an abstract ApplicationMailer reports none and ActiveSupport's
|
|
239
|
+
# generated `_run_*_callbacks` never appears. `instance_methods(false)`
|
|
240
|
+
# listed both as deliverable actions. Called bare, like
|
|
241
|
+
# ControllerIntrospector does - AbstractController::Base has defined
|
|
242
|
+
# it across the whole supported range, so a fallback here would only
|
|
243
|
+
# be unreachable code holding the old bug.
|
|
244
|
+
actions = mailer.action_methods.to_a.map(&:to_s).sort
|
|
238
245
|
next if actions.empty?
|
|
239
246
|
|
|
240
247
|
{
|
|
@@ -248,8 +255,10 @@ module RailsAiContext
|
|
|
248
255
|
[]
|
|
249
256
|
end
|
|
250
257
|
|
|
251
|
-
# A mailer's actions are its public instance methods
|
|
252
|
-
#
|
|
258
|
+
# A mailer's actions are its public instance methods. The booted side asks
|
|
259
|
+
# `action_methods`, which also subtracts inherited and internal ones - the
|
|
260
|
+
# AST cannot see that, so a public helper on a concrete mailer is still
|
|
261
|
+
# listed here and not there.
|
|
253
262
|
def extract_mailers_from_source
|
|
254
263
|
source_classes(File.join(app.root, "app", "mailers")).filter_map do |name, methods|
|
|
255
264
|
next if name == "ApplicationMailer"
|
|
@@ -222,7 +222,10 @@ module RailsAiContext
|
|
|
222
222
|
model.validators.map do |validator|
|
|
223
223
|
{
|
|
224
224
|
kind: validator.kind.to_s,
|
|
225
|
-
|
|
225
|
+
# `validates_with` registers a bare ActiveModel::Validator, which
|
|
226
|
+
# has no #attributes - only EachValidator does. Calling it blind
|
|
227
|
+
# replaced the whole model's answer with one error line.
|
|
228
|
+
attributes: validator.respond_to?(:attributes) ? Array(validator.attributes).map(&:to_s) : [],
|
|
226
229
|
options: sanitize_options(validator.options)
|
|
227
230
|
}
|
|
228
231
|
end
|
|
@@ -332,9 +335,15 @@ module RailsAiContext
|
|
|
332
335
|
end
|
|
333
336
|
|
|
334
337
|
def extract_callbacks_from_ast(source_data)
|
|
335
|
-
source_data[:callbacks]
|
|
336
|
-
|
|
337
|
-
|
|
338
|
+
group_callbacks_by_type(source_data[:callbacks])
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# One shape for both tiers: { "before_validation" => ["normalize"] }.
|
|
342
|
+
def group_callbacks_by_type(callbacks)
|
|
343
|
+
Array(callbacks).each_with_object({}) do |cb, hash|
|
|
344
|
+
next unless cb.is_a?(Hash) && cb[:type]
|
|
345
|
+
|
|
346
|
+
(hash[cb[:type].to_s] ||= []) << cb[:method]
|
|
338
347
|
end
|
|
339
348
|
end
|
|
340
349
|
|
|
@@ -576,7 +585,12 @@ module RailsAiContext
|
|
|
576
585
|
validations: data[:validations],
|
|
577
586
|
scopes: data[:scopes],
|
|
578
587
|
enums: data[:enums],
|
|
579
|
-
|
|
588
|
+
# Same shape as the booted tier: a Hash keyed by callback type. The
|
|
589
|
+
# listener hands back a flat Array, and every consumer filters on
|
|
590
|
+
# `callbacks.is_a?(Hash)` - so passing it through rendered "No models
|
|
591
|
+
# with callbacks found" and then raised a TypeError on a Hash lookup
|
|
592
|
+
# against an Array.
|
|
593
|
+
callbacks: group_callbacks_by_type(data[:callbacks]),
|
|
580
594
|
macros: data[:macros],
|
|
581
595
|
methods: data[:methods]
|
|
582
596
|
}
|
|
@@ -641,7 +655,12 @@ module RailsAiContext
|
|
|
641
655
|
associations: data[:associations],
|
|
642
656
|
validations: data[:validations],
|
|
643
657
|
scopes: data[:scopes],
|
|
644
|
-
|
|
658
|
+
# Same shape as the booted tier: a Hash keyed by callback type. The
|
|
659
|
+
# listener hands back a flat Array, and every consumer filters on
|
|
660
|
+
# `callbacks.is_a?(Hash)` - so passing it through rendered "No models
|
|
661
|
+
# with callbacks found" and then raised a TypeError on a Hash lookup
|
|
662
|
+
# against an Array.
|
|
663
|
+
callbacks: group_callbacks_by_type(data[:callbacks]),
|
|
645
664
|
methods: data[:methods]
|
|
646
665
|
}
|
|
647
666
|
collection = macros.find { |m| m[:macro] == :store_in }&.dig(:options, :collection)
|
|
@@ -55,6 +55,11 @@ module RailsAiContext
|
|
|
55
55
|
|
|
56
56
|
@last_fingerprint = Fingerprinter.compute(app)
|
|
57
57
|
|
|
58
|
+
# Order matters: reload the app's code first, then drop the caches built
|
|
59
|
+
# from the old constants. Clearing caches alone left the server answering
|
|
60
|
+
# from whatever Rails autoloaded at boot.
|
|
61
|
+
reloaded = CodeReloader.reload!
|
|
62
|
+
|
|
58
63
|
# Invalidate all tool caches (includes AstCache.clear)
|
|
59
64
|
Tools::BaseTool.reset_all_caches!
|
|
60
65
|
|
|
@@ -64,12 +69,12 @@ module RailsAiContext
|
|
|
64
69
|
# Notify connected MCP clients
|
|
65
70
|
mcp_server.notify_resources_list_changed
|
|
66
71
|
mcp_server.notify_log_message(
|
|
67
|
-
data: "#{message} Tool caches invalidated.",
|
|
72
|
+
data: "#{message} Tool caches invalidated#{reloaded ? " and app code reloaded" : ""}.",
|
|
68
73
|
level: "info",
|
|
69
74
|
logger: "rails-ai-context"
|
|
70
75
|
)
|
|
71
76
|
|
|
72
|
-
$stderr.puts "[rails-ai-context] #{message} Tool caches invalidated."
|
|
77
|
+
$stderr.puts "[rails-ai-context] #{message} Tool caches invalidated#{reloaded ? " and app code reloaded" : ""}."
|
|
73
78
|
rescue => e
|
|
74
79
|
$stderr.puts "[rails-ai-context] Live reload error: #{e.message}"
|
|
75
80
|
end
|
|
@@ -143,6 +143,13 @@ module RailsAiContext
|
|
|
143
143
|
# the handler's return value into the JSON-RPC response without renaming
|
|
144
144
|
# keys, so snake_case would leak to clients as-is.
|
|
145
145
|
def handle_read(params)
|
|
146
|
+
# Resource reads introspect just like tool calls do, and they bypassed
|
|
147
|
+
# SafeCall entirely - so a live reload could unload constants while one
|
|
148
|
+
# was reading them and hand back a short list with nothing raised.
|
|
149
|
+
RailsAiContext::CodeReloader.with_app_code { read_resource(params) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def read_resource(params)
|
|
146
153
|
uri = params[:uri]
|
|
147
154
|
|
|
148
155
|
# The two schemes are historical; accept either one for every
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
# One answer to "which database is this app on", for every surface that asks.
|
|
5
|
+
#
|
|
6
|
+
# `SchemaIntrospector` writes `static_parse` when it read `db/schema.rb`
|
|
7
|
+
# instead of the connection. That is an internal detail: printed raw it names
|
|
8
|
+
# a database that does not exist. Four call sites each grew their own
|
|
9
|
+
# substitute and gave three different answers for one app - the generated
|
|
10
|
+
# CLAUDE.md said PostgreSQL, `rails_get_schema` said unknown, `rails ai:inspect`
|
|
11
|
+
# said static_parse. This is the seam they share.
|
|
12
|
+
module SchemaAdapter
|
|
13
|
+
# Values that mean "no adapter was observed", not "the adapter is X".
|
|
14
|
+
PLACEHOLDERS = %w[static_parse unknown].freeze
|
|
15
|
+
|
|
16
|
+
# Gemfile display names, only as the last resort.
|
|
17
|
+
GEM_ADAPTERS = {
|
|
18
|
+
"pg" => "PostgreSQL",
|
|
19
|
+
"mysql2" => "MySQL",
|
|
20
|
+
"trilogy" => "MySQL",
|
|
21
|
+
"sqlite3" => "SQLite"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# structure.sql dialects, as SchemaIntrospector records them.
|
|
25
|
+
DIALECTS = {
|
|
26
|
+
"postgresql" => "PostgreSQL",
|
|
27
|
+
"mysql" => "MySQL",
|
|
28
|
+
"sqlite" => "SQLite"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# @param context [Hash] a full introspection context
|
|
34
|
+
# @return [String] the adapter to show, never an internal placeholder
|
|
35
|
+
def label(context)
|
|
36
|
+
context = {} unless context.is_a?(Hash)
|
|
37
|
+
schema = context[:schema]
|
|
38
|
+
observed = schema.is_a?(Hash) ? schema[:adapter] : nil
|
|
39
|
+
return observed unless placeholder?(observed)
|
|
40
|
+
|
|
41
|
+
# Best first: the app's own database config, which needs no connection.
|
|
42
|
+
# Then the dialect parsed out of structure.sql. Only then the Gemfile,
|
|
43
|
+
# which is a guess - an app can carry two adapter gems.
|
|
44
|
+
from_configurations(context) || from_dialect(schema) || from_gems(context) || "unknown"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def placeholder?(adapter)
|
|
48
|
+
adapter.nil? || PLACEHOLDERS.include?(adapter.to_s)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def from_configurations(context)
|
|
52
|
+
multi = context[:multi_database]
|
|
53
|
+
return nil unless Serializers::SectionGuard.usable?(multi)
|
|
54
|
+
|
|
55
|
+
primary = Array(multi[:databases]).find { |db| db[:name].to_s == "primary" } ||
|
|
56
|
+
Array(multi[:databases]).first
|
|
57
|
+
adapter = primary&.dig(:adapter)
|
|
58
|
+
return nil if placeholder?(adapter)
|
|
59
|
+
|
|
60
|
+
# These are ActiveRecord adapter names (`postgresql`, `sqlite3`), not gem
|
|
61
|
+
# names. The two overlap by accident at sqlite3/mysql2/trilogy, so
|
|
62
|
+
# reading only GEM_ADAPTERS let `postgresql` fall through raw - a Postgres
|
|
63
|
+
# app rendered "Database: postgresql" beside a SQLite app's "SQLite".
|
|
64
|
+
DIALECTS[adapter.to_s] || GEM_ADAPTERS[adapter.to_s] || adapter
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def from_dialect(schema)
|
|
68
|
+
return nil unless schema.is_a?(Hash)
|
|
69
|
+
|
|
70
|
+
DIALECTS[schema[:dialect].to_s]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Last, and order-independent: an app with both `pg` and `sqlite3` gets the
|
|
74
|
+
# same answer here as anywhere else, rather than depending on which match
|
|
75
|
+
# a loop happened to see last.
|
|
76
|
+
def from_gems(context)
|
|
77
|
+
gems = context[:gems]
|
|
78
|
+
return nil unless Serializers::SectionGuard.usable?(gems)
|
|
79
|
+
|
|
80
|
+
names = Array(gems[:notable_gems]).map { |g| g[:name].to_s }
|
|
81
|
+
GEM_ADAPTERS.each_key { |gem_name| return GEM_ADAPTERS[gem_name] if names.include?(gem_name) }
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -43,7 +43,7 @@ module RailsAiContext
|
|
|
43
43
|
# Compact counts - gems and architecture are already in the root file (CLAUDE.md/AGENTS.md)
|
|
44
44
|
schema = context[:schema]
|
|
45
45
|
if SectionGuard.usable?(schema)
|
|
46
|
-
lines << "- Database: #{schema
|
|
46
|
+
lines << "- Database: #{database_adapter_label(schema)} - #{count_phrase(schema[:total_tables], "table")}"
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
models = context[:models]
|
|
@@ -26,7 +26,7 @@ module RailsAiContext
|
|
|
26
26
|
schema = context[:schema]
|
|
27
27
|
if SectionGuard.usable?(schema)
|
|
28
28
|
tables = schema[:total_tables]
|
|
29
|
-
lines << "- Database: #{schema
|
|
29
|
+
lines << "- Database: #{database_adapter_label(schema)} - #{count_phrase(tables, "table")}"
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
models = context[:models]
|
|
@@ -45,7 +45,7 @@ module RailsAiContext
|
|
|
45
45
|
|
|
46
46
|
schema = context[:schema]
|
|
47
47
|
if SectionGuard.usable?(schema)
|
|
48
|
-
lines << "- Database: #{schema
|
|
48
|
+
lines << "- Database: #{database_adapter_label(schema)} - #{count_phrase(schema[:total_tables], "table")}"
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
models = context[:models]
|
|
@@ -29,7 +29,7 @@ module RailsAiContext
|
|
|
29
29
|
schema = context[:schema]
|
|
30
30
|
if SectionGuard.usable?(schema)
|
|
31
31
|
tables = schema[:total_tables]
|
|
32
|
-
lines << "- Database: #{schema
|
|
32
|
+
lines << "- Database: #{database_adapter_label(schema)} - #{count_phrase(tables, "table")}"
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
models = context[:models]
|
|
@@ -74,7 +74,7 @@ module RailsAiContext
|
|
|
74
74
|
|
|
75
75
|
schema = context[:schema]
|
|
76
76
|
if SectionGuard.usable?(schema)
|
|
77
|
-
lines << "- Database: #{schema
|
|
77
|
+
lines << "- Database: #{database_adapter_label(schema)} - #{count_phrase(schema[:total_tables], "table")}"
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
models = context[:models]
|
|
@@ -213,6 +213,13 @@ module RailsAiContext
|
|
|
213
213
|
$stderr.puts "[rails-ai-context] Before actions scan skipped: #{e.message}" if ENV["DEBUG"]
|
|
214
214
|
[]
|
|
215
215
|
end
|
|
216
|
+
|
|
217
|
+
# One seam for every surface that names the database, so the generated
|
|
218
|
+
# files, the tools and the rake task cannot answer differently for one
|
|
219
|
+
# app. See RailsAiContext::SchemaAdapter.
|
|
220
|
+
def database_adapter_label(_schema = nil)
|
|
221
|
+
SchemaAdapter.label(context)
|
|
222
|
+
end
|
|
216
223
|
end
|
|
217
224
|
end
|
|
218
225
|
end
|
|
@@ -438,7 +438,7 @@ namespace :ai do
|
|
|
438
438
|
puts ""
|
|
439
439
|
|
|
440
440
|
if context[:schema] && !context[:schema][:error]
|
|
441
|
-
puts "📦 Database: #{RailsAiContext::CountPhrase.call(context[:schema][:total_tables], "table")} (#{context
|
|
441
|
+
puts "📦 Database: #{RailsAiContext::CountPhrase.call(context[:schema][:total_tables], "table")} (#{RailsAiContext::SchemaAdapter.label(context)})"
|
|
442
442
|
end
|
|
443
443
|
|
|
444
444
|
if context[:models] && !context[:models].is_a?(Hash)
|
|
@@ -355,7 +355,15 @@ module RailsAiContext
|
|
|
355
355
|
return api[:api_only] == true if api.is_a?(Hash) && api.key?(:api_only)
|
|
356
356
|
|
|
357
357
|
app = rails_app
|
|
358
|
-
app.respond_to?(:config) && app.config.respond_to?(:api_only)
|
|
358
|
+
if app.respond_to?(:config) && app.config.respond_to?(:api_only)
|
|
359
|
+
return app.config.api_only == true
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Static tier: no booted config to ask, but the flag is declared in
|
|
363
|
+
# config/application.rb. Without this the view tools fall back to
|
|
364
|
+
# "none found", which reads as "not built yet" for an app that has
|
|
365
|
+
# no view layer by design.
|
|
366
|
+
AppKind.api_only?(app.root)
|
|
359
367
|
rescue StandardError
|
|
360
368
|
false
|
|
361
369
|
end
|
|
@@ -86,9 +86,9 @@ module RailsAiContext
|
|
|
86
86
|
# directory is there (e.g. freshly generated by `rails new`).
|
|
87
87
|
private_class_method def self.no_job_files_message(jobs_dir_exists)
|
|
88
88
|
if jobs_dir_exists
|
|
89
|
-
"app/jobs/
|
|
89
|
+
"No job classes in app/jobs/ beyond ApplicationJob. Workers outside that directory (Sidekiq::Worker in app/workers/, for example) are not covered by this tool."
|
|
90
90
|
else
|
|
91
|
-
"No app/jobs/ directory
|
|
91
|
+
"No app/jobs/ directory. Workers outside it (Sidekiq::Worker in app/workers/, for example) are not covered by this tool."
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
|
|
@@ -101,9 +101,9 @@ module RailsAiContext
|
|
|
101
101
|
private_class_method def self.no_jobs_or_channels_message(jobs_dir_exists, channels_note)
|
|
102
102
|
channels_clause = channels_note || "no Action Cable channels detected"
|
|
103
103
|
if jobs_dir_exists
|
|
104
|
-
"app/jobs/
|
|
104
|
+
"No job classes in app/jobs/ beyond ApplicationJob, and #{channels_clause}. Workers outside that directory are not covered by this tool."
|
|
105
105
|
else
|
|
106
|
-
"No app/jobs/ directory
|
|
106
|
+
"No app/jobs/ directory and #{channels_clause}. Workers outside it are not covered by this tool."
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
109
|
|
|
@@ -95,7 +95,7 @@ module RailsAiContext
|
|
|
95
95
|
return text_response("No tables at offset #{offset}. Total: #{total}. Use `offset:0` to start over.")
|
|
96
96
|
end
|
|
97
97
|
lines = [ "# Schema Summary (#{count_phrase(total, "table")})", "" ]
|
|
98
|
-
lines << "**Adapter:** #{schema
|
|
98
|
+
lines << "**Adapter:** #{adapter_label(schema)}" if schema[:adapter]
|
|
99
99
|
lines.concat(static_source_lines(schema))
|
|
100
100
|
paginated.each do |name|
|
|
101
101
|
data = tables[name]
|
|
@@ -209,6 +209,13 @@ module RailsAiContext
|
|
|
209
209
|
end
|
|
210
210
|
end
|
|
211
211
|
|
|
212
|
+
# The same seam the generated context files use. Answering this question
|
|
213
|
+
# locally is how one app came to be told it runs on PostgreSQL by
|
|
214
|
+
# CLAUDE.md and on "unknown" by this tool, in the same session.
|
|
215
|
+
private_class_method def self.adapter_label(_schema = nil)
|
|
216
|
+
RailsAiContext::SchemaAdapter.label(cached_context)
|
|
217
|
+
end
|
|
218
|
+
|
|
212
219
|
private_class_method def self.models_for_table(table_name)
|
|
213
220
|
models = cached_context[:models]
|
|
214
221
|
return [] unless models.is_a?(Hash)
|
|
@@ -333,7 +340,7 @@ module RailsAiContext
|
|
|
333
340
|
lines = [
|
|
334
341
|
"# Database Schema",
|
|
335
342
|
"",
|
|
336
|
-
"- Adapter: #{schema
|
|
343
|
+
"- Adapter: #{adapter_label(schema)}",
|
|
337
344
|
"- Tables: #{schema[:total_tables]}",
|
|
338
345
|
""
|
|
339
346
|
]
|
|
@@ -594,31 +594,11 @@ module RailsAiContext
|
|
|
594
594
|
end
|
|
595
595
|
end
|
|
596
596
|
|
|
597
|
-
#
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if adapter.nil? || adapter == "static_parse" || adapter == "unknown"
|
|
603
|
-
config_data = ctx[:config]
|
|
604
|
-
if config_data.is_a?(Hash) && !config_data[:error]
|
|
605
|
-
live_adapter = config_data[:database_adapter] || config_data[:adapter]
|
|
606
|
-
adapter = live_adapter if live_adapter
|
|
607
|
-
end
|
|
608
|
-
end
|
|
609
|
-
|
|
610
|
-
# Try to resolve from gems as a fallback
|
|
611
|
-
if adapter.nil? || adapter == "static_parse" || adapter == "unknown"
|
|
612
|
-
gems_data = ctx[:gems]
|
|
613
|
-
if gems_data.is_a?(Hash) && !gems_data[:error]
|
|
614
|
-
notable = gems_data[:notable_gems] || []
|
|
615
|
-
adapter = "PostgreSQL" if notable.any? { |g| g[:name] == "pg" }
|
|
616
|
-
adapter = "MySQL" if notable.any? { |g| %w[mysql2 trilogy].include?(g[:name]) }
|
|
617
|
-
adapter = "SQLite" if notable.any? { |g| g[:name] == "sqlite3" }
|
|
618
|
-
end
|
|
619
|
-
end
|
|
620
|
-
|
|
621
|
-
adapter || "unknown"
|
|
597
|
+
# The gems loop here let the LAST match win, so an app carrying both pg
|
|
598
|
+
# and sqlite3 was told SQLite by onboard and PostgreSQL by the
|
|
599
|
+
# generated files. One seam, one answer.
|
|
600
|
+
def resolve_db_adapter(ctx, _schema = nil)
|
|
601
|
+
RailsAiContext::SchemaAdapter.label(ctx)
|
|
622
602
|
end
|
|
623
603
|
|
|
624
604
|
def central_models(models, limit = 5)
|
|
@@ -23,23 +23,38 @@ module RailsAiContext
|
|
|
23
23
|
kwargs = normalize_detail(kwargs)
|
|
24
24
|
Thread.current[:rails_ai_context_call_params] = session_params(kwargs)
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
26
|
+
# Held across the tool body: a concurrent live reload must not unload
|
|
27
|
+
# constants while this call is reading them.
|
|
28
|
+
#
|
|
29
|
+
# The rescue sits INSIDE the block on purpose. `executor.wrap` reports
|
|
30
|
+
# anything that crosses it to the host app's error_reporter as
|
|
31
|
+
# unhandled, which would page a team over a failure this method turns
|
|
32
|
+
# into an ordinary isError result. Nothing escapes the block, so
|
|
33
|
+
# nothing is reported.
|
|
34
|
+
RailsAiContext::CodeReloader.with_app_code do
|
|
35
|
+
begin
|
|
36
|
+
append_note(super(**kwargs), invalid_detail_note(discarded))
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
# A failed call must not leak its recorded params into the next
|
|
39
|
+
# call's session entry.
|
|
40
|
+
Thread.current[:rails_ai_context_call_params] = nil
|
|
41
|
+
failure_response(e)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
31
47
|
|
|
48
|
+
def failure_response(error)
|
|
32
49
|
label = respond_to?(:tool_name) ? tool_name : name
|
|
33
|
-
origin = Array(
|
|
34
|
-
text = +"Tool #{label} failed: #{
|
|
50
|
+
origin = Array(error.backtrace).first.to_s
|
|
51
|
+
text = +"Tool #{label} failed: #{error.class}: #{error.message.to_s.lines.first&.strip}\n"
|
|
35
52
|
text << "At: #{origin}\n" unless origin.empty?
|
|
36
53
|
text << "Recovery: retry with a narrower query (a single table, model, or " \
|
|
37
54
|
"controller), or run `rails-ai-context doctor` to check app health."
|
|
38
55
|
MCP::Tool::Response.new([ { type: "text", text: text } ], error: true)
|
|
39
56
|
end
|
|
40
57
|
|
|
41
|
-
private
|
|
42
|
-
|
|
43
58
|
# The value the caller sent, when this tool takes a DetailLevel detail
|
|
44
59
|
# and that value is not one of its levels.
|
|
45
60
|
def discarded_detail(kwargs)
|
|
@@ -48,7 +48,16 @@ module RailsAiContext
|
|
|
48
48
|
VALID_LEVELS = %w[syntax rails].freeze
|
|
49
49
|
|
|
50
50
|
def self.call(files:, level: "syntax", server_context: nil)
|
|
51
|
-
|
|
51
|
+
# Both spellings, because the JSON form this used to suggest is not
|
|
52
|
+
# something the CLI accepts - following the advice produced
|
|
53
|
+
# "file not found" with the literal brackets in the name.
|
|
54
|
+
if files.nil? || files.empty?
|
|
55
|
+
return text_response(
|
|
56
|
+
"No files provided. Pass paths relative to Rails root - " \
|
|
57
|
+
"CLI: `--files app/models/post.rb app/models/user.rb`, " \
|
|
58
|
+
"MCP: `files: [\"app/models/post.rb\"]`."
|
|
59
|
+
)
|
|
60
|
+
end
|
|
52
61
|
return text_response("Too many files (#{files.size}). Maximum is #{max_files} per call.") if files.size > max_files
|
|
53
62
|
return text_response("Unknown level: '#{level}'. Valid values: #{VALID_LEVELS.join(', ')}") unless VALID_LEVELS.include?(level)
|
|
54
63
|
|
|
@@ -72,6 +72,9 @@ module RailsAiContext
|
|
|
72
72
|
@last_fingerprint = Fingerprinter.compute(app)
|
|
73
73
|
|
|
74
74
|
$stderr.puts "[rails-ai-context] Changes detected, regenerating context files..."
|
|
75
|
+
# Without this the regenerated files describe the app as it was when the
|
|
76
|
+
# watcher started, not as it is now.
|
|
77
|
+
CodeReloader.reload!
|
|
75
78
|
result = RailsAiContext.generate_context(format: :all)
|
|
76
79
|
result[:written].each { |f| $stderr.puts " Updated: #{f}" }
|
|
77
80
|
result[:skipped].each { |f| $stderr.puts " Unchanged: #{f}" }
|
data/server.json
CHANGED
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.20.
|
|
4
|
+
version: 5.20.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- crisnahine
|
|
@@ -278,6 +278,7 @@ files:
|
|
|
278
278
|
- lib/rails_ai_context/boot_manager.rb
|
|
279
279
|
- lib/rails_ai_context/cli.rb
|
|
280
280
|
- lib/rails_ai_context/cli/tool_runner.rb
|
|
281
|
+
- lib/rails_ai_context/code_reloader.rb
|
|
281
282
|
- lib/rails_ai_context/confidence.rb
|
|
282
283
|
- lib/rails_ai_context/configuration.rb
|
|
283
284
|
- lib/rails_ai_context/count_phrase.rb
|
|
@@ -385,6 +386,7 @@ files:
|
|
|
385
386
|
- lib/rails_ai_context/redaction.rb
|
|
386
387
|
- lib/rails_ai_context/resources.rb
|
|
387
388
|
- lib/rails_ai_context/safe_file.rb
|
|
389
|
+
- lib/rails_ai_context/schema_adapter.rb
|
|
388
390
|
- lib/rails_ai_context/schema_hint.rb
|
|
389
391
|
- lib/rails_ai_context/schema_version.rb
|
|
390
392
|
- lib/rails_ai_context/serializers.rb
|