rails-ai-context 5.14.0 → 5.16.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +273 -0
  3. data/README.md +42 -2
  4. data/config/routes.rb +3 -1
  5. data/docs/COMPATIBILITY.md +219 -0
  6. data/docs/CUSTOM_TOOLS.md +17 -3
  7. data/docs/TROUBLESHOOTING.md +17 -0
  8. data/exe/rails-ai-context +121 -22
  9. data/lib/generators/rails_ai_context/install/install_generator.rb +1 -1
  10. data/lib/rails_ai_context/app_kind.rb +31 -0
  11. data/lib/rails_ai_context/cli/tool_runner.rb +1 -1
  12. data/lib/rails_ai_context/confidence.rb +16 -4
  13. data/lib/rails_ai_context/configuration.rb +13 -1
  14. data/lib/rails_ai_context/doctor.rb +121 -3
  15. data/lib/rails_ai_context/engine.rb +5 -1
  16. data/lib/rails_ai_context/introspector.rb +42 -5
  17. data/lib/rails_ai_context/introspectors/controller_introspector.rb +40 -10
  18. data/lib/rails_ai_context/introspectors/gem_introspector.rb +1 -0
  19. data/lib/rails_ai_context/introspectors/job_introspector.rb +19 -1
  20. data/lib/rails_ai_context/introspectors/listeners/mongoid_fields_listener.rb +27 -0
  21. data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +372 -0
  22. data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +3 -0
  23. data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +21 -4
  24. data/lib/rails_ai_context/introspectors/migration_introspector.rb +4 -0
  25. data/lib/rails_ai_context/introspectors/model_introspector.rb +161 -8
  26. data/lib/rails_ai_context/introspectors/route_introspector.rb +64 -0
  27. data/lib/rails_ai_context/introspectors/schema_introspector.rb +313 -24
  28. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +15 -2
  29. data/lib/rails_ai_context/path_resolver.rb +35 -0
  30. data/lib/rails_ai_context/safe_file.rb +5 -1
  31. data/lib/rails_ai_context/schema_version.rb +9 -4
  32. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +2 -2
  33. data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +20 -9
  34. data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +1 -1
  35. data/lib/rails_ai_context/serializers/copilot_serializer.rb +13 -2
  36. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +1 -1
  37. data/lib/rails_ai_context/serializers/markdown_serializer.rb +1 -1
  38. data/lib/rails_ai_context/serializers/section_guard.rb +16 -0
  39. data/lib/rails_ai_context/server.rb +49 -7
  40. data/lib/rails_ai_context/source_line.rb +208 -0
  41. data/lib/rails_ai_context/static_app.rb +17 -0
  42. data/lib/rails_ai_context/test_helper.rb +6 -1
  43. data/lib/rails_ai_context/tools/analyze_feature.rb +12 -5
  44. data/lib/rails_ai_context/tools/base_tool.rb +91 -2
  45. data/lib/rails_ai_context/tools/dependency_graph.rb +3 -0
  46. data/lib/rails_ai_context/tools/diagnose.rb +8 -4
  47. data/lib/rails_ai_context/tools/generate_test.rb +15 -3
  48. data/lib/rails_ai_context/tools/get_api.rb +3 -0
  49. data/lib/rails_ai_context/tools/get_callbacks.rb +4 -2
  50. data/lib/rails_ai_context/tools/get_component_catalog.rb +9 -0
  51. data/lib/rails_ai_context/tools/get_config.rb +13 -5
  52. data/lib/rails_ai_context/tools/get_context.rb +1 -15
  53. data/lib/rails_ai_context/tools/get_controllers.rb +5 -3
  54. data/lib/rails_ai_context/tools/get_conventions.rb +45 -23
  55. data/lib/rails_ai_context/tools/get_edit_context.rb +5 -5
  56. data/lib/rails_ai_context/tools/get_env.rb +5 -2
  57. data/lib/rails_ai_context/tools/get_frontend_stack.rb +7 -4
  58. data/lib/rails_ai_context/tools/get_gems.rb +3 -1
  59. data/lib/rails_ai_context/tools/get_job_pattern.rb +36 -12
  60. data/lib/rails_ai_context/tools/get_model_details.rb +41 -8
  61. data/lib/rails_ai_context/tools/get_partial_interface.rb +4 -1
  62. data/lib/rails_ai_context/tools/get_routes.rb +10 -0
  63. data/lib/rails_ai_context/tools/get_schema.rb +44 -0
  64. data/lib/rails_ai_context/tools/get_service_pattern.rb +10 -2
  65. data/lib/rails_ai_context/tools/get_stimulus.rb +11 -4
  66. data/lib/rails_ai_context/tools/get_test_info.rb +15 -13
  67. data/lib/rails_ai_context/tools/get_turbo_map.rb +42 -10
  68. data/lib/rails_ai_context/tools/get_view.rb +32 -7
  69. data/lib/rails_ai_context/tools/onboard.rb +4 -4
  70. data/lib/rails_ai_context/tools/performance_check.rb +3 -0
  71. data/lib/rails_ai_context/tools/query.rb +5 -1
  72. data/lib/rails_ai_context/tools/read_logs.rb +4 -4
  73. data/lib/rails_ai_context/tools/review_changes.rb +1 -1
  74. data/lib/rails_ai_context/tools/runtime_info.rb +5 -1
  75. data/lib/rails_ai_context/tools/search_code.rb +2 -2
  76. data/lib/rails_ai_context/tools/search_docs.rb +4 -4
  77. data/lib/rails_ai_context/tools/security_scan.rb +2 -2
  78. data/lib/rails_ai_context/tools/validate.rb +12 -12
  79. data/lib/rails_ai_context/version.rb +1 -1
  80. data/lib/rails_ai_context.rb +43 -3
  81. metadata +21 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab9892f397e65d076b20e20f7a01da83cd70b1bfb2a2d71fe9949e09616d7bdb
4
- data.tar.gz: 3f5021a82283731cc7ea915bfbab0b11f06950d5bbc9fd9b656ca70fe43d6cc9
3
+ metadata.gz: 59c335a48ab2976318ed2fc58d1cce4e2153c0db0144b61ca8d902f160bbeb76
4
+ data.tar.gz: 334cc340093515797006c8326b47fa97721c9e66f07f940f3ec0864db2ae241a
5
5
  SHA512:
6
- metadata.gz: e5f33eee0cbaedf8d21d6983e8672b72de97f58baab5a4c12ced685389d8e78b103f3f55974ba30a9d786e200ee5609ef3930a156614e502caafe80a56222e77
7
- data.tar.gz: bcb99dcf463633f7762636ee7a75b247fde3e2438e2eddfb76e977014d84bb2ab65bc6f88d0df6932f5e16adfa3d533712bab8f36fa96f45bd442b990cd7f448
6
+ metadata.gz: f0e5532e52f873a3cb659dbf90fde582974f4840609f0c8365318f5b862659475484232e9ead3fd0074ec1a079db565cd8046a157ceb4f1bc1099b08877feded
7
+ data.tar.gz: e6bd4fe02b30344a494a8af944ab6ca94e07f944d143d99bd4f116ca3f15099a5fbbe840b38c1b186398c43c3b23825889fd4b852593fc09342edc50910cd4e9
data/CHANGELOG.md CHANGED
@@ -5,6 +5,279 @@ 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.16.0] - 2026-07-12
9
+
10
+ ### Fixed
11
+
12
+ - **HTTP engine mount works again.** `mount RailsAiContext::Engine, at: "/mcp"`
13
+ 500'd with `uninitialized constant McpController`: the engine is not
14
+ namespace-isolated, so its route needed the fully-qualified
15
+ `rails_ai_context/mcp#handle` controller path. Verified with a real curl
16
+ MCP session (initialize + session-id + tools/call) against a booted app.
17
+ - **`config.custom_tools` is usable as documented.** Referencing an
18
+ `app/mcp_tools/` class constant in the initializer aborted boot with
19
+ `NameError` (app/ constants are not autoloadable while initializers run),
20
+ and a class-name string crashed every CLI tool invocation with
21
+ `undefined method 'tool_name' for an instance of String`. Entries may now
22
+ be classes or class-name strings; strings resolve lazily after boot, and
23
+ invalid entries are warn-skipped everywhere (server, CLI runner,
24
+ TestHelper) instead of taking down unrelated tools. Docs and the
25
+ initializer template now show the string form.
26
+ - **Mailers are no longer invisible in development.** `extract_mailers` read
27
+ `ActionMailer::Base.descendants` without the eager-load fallback that
28
+ channels already had, so every dev-mode run reported zero mailers
29
+ (`ai:inspect`, context files, onboard).
30
+ - **TestHelper is self-sufficient.** `require "rails_ai_context/test_helper"`
31
+ alone raised `uninitialized constant RailsAiContext::Tools` - the common
32
+ case, since a `group: :development` install is never Bundler-required in
33
+ the test environment. The helper now requires the gem core itself; docs
34
+ explain custom-tool name lookup in the test environment.
35
+ - **`claude_max_lines` counts physical lines.** Dual MCP/CLI tool examples
36
+ embed newlines inside single entries, so a default-capped CLAUDE.md could
37
+ ship 161 real lines with no trim marker. The cap (and the trim) now apply
38
+ to physical lines of the managed block.
39
+ - **turbo_map no longer flags correct wiring.** `broadcast_append_to post`
40
+ against `turbo_stream_from @post` warned "no matching turbo_stream_from";
41
+ stream matching now strips the ivar sigil before comparing.
42
+ - **frontend_stack's Turbo wiring line can actually render.** It read
43
+ `turbo[:broadcasts]`/`[:frames]` - keys the introspector never emits
44
+ (`model_broadcasts`/`turbo_frames`) - so the line was dead code for every
45
+ app; callback-style `broadcast_*_to` calls are now also detected.
46
+ - **generate_test emits runnable tests for arg-taking scopes.** A scope like
47
+ `scope :by_status, ->(s) { ... }` produced `Post.by_status` (ArgumentError
48
+ at runtime); scope lambdas' required params are now captured through the
49
+ AST layer and such scopes get an `assert_respond_to` test with a
50
+ fill-in-the-args call site. Verified by running the generated file for real.
51
+ - **job_pattern keeps `wait:` in retry configuration.** One ordered regex
52
+ dropped whichever of `wait:`/`attempts:` came first in `retry_on`; the two
53
+ options are now scanned independently.
54
+ - **get_env skips comment lines.** `# ENV["WEB_CONCURRENCY"]` in the stock
55
+ puma.rb comment was reported as a real environment variable.
56
+ - **diagnose omits an empty "Next Steps" section** instead of rendering a
57
+ bare header when no file/method context was parsed.
58
+ - **analyze_feature lists jbuilder templates and record renders**
59
+ (`render @post` / `render post`), matching what get_view reports.
60
+ - **service_pattern surfaces mailer dependencies** (`PostMailer.published_email`
61
+ style calls) and recognizes `perform_now`.
62
+ - **README observability example reads `event.payload[:duration]`** - the
63
+ bridge emits a zero-width event, so `event.duration` is always ~0ms.
64
+ - Singular/plural agreement in tool output ("1 result", "1 site",
65
+ "1 broadcast", "1 line", "1 table", "1 controller", "1 migration file").
66
+ - edit_context's footer no longer suggests pasting the line-numbered block
67
+ verbatim as an Edit old_string.
68
+ - **A syntax-broken model or controller file no longer kills the process in
69
+ the runtime tier.** Eager loading aborts at the first bad file with a
70
+ `SyntaxError` - a `ScriptError`, which sailed past every bare `rescue` and
71
+ took down the MCP server mid-session and every model-touching rake task.
72
+ Introspection now rescues `ScriptError` at the dispatch level, eager
73
+ loading falls back to per-constant loading, and `discover_models`
74
+ tolerates unloadable classes - one broken file costs exactly that file,
75
+ matching the static tier's contract.
76
+ - **Standalone CLI no longer mixes framework versions.** The RubyGems
77
+ binstub activates the newest installed railties before the app's
78
+ Gemfile.lock pins its own, leaving newer framework paths in $LOAD_PATH:
79
+ every generated fact reported the wrong Rails version (e.g. 8.1.3 for an
80
+ 8.0.5 app) with "already initialized constant" warnings polluting all
81
+ output. The exe now strips Rails-family gem paths at startup; the app's
82
+ bundle is the only framework source.
83
+ - **`--app-path` and `--environment` work after the tool name** (`tool
84
+ schema --table notes --app-path /x`), matching the `--json`/`--no-boot`
85
+ passthrough handling. Previously they were silently discarded and the
86
+ command ran against the wrong directory.
87
+ - **structure.sql parsing: single-line CREATE TABLE statements** (the shape
88
+ sqlite's `.schema` emits) no longer lose every column but the first;
89
+ bodies are split on top-level commas before per-line parsing.
90
+ - **structure.sql parsing: foreign keys land on the right table.** The
91
+ `ALTER TABLE ... FOREIGN KEY` regex could span statement boundaries and
92
+ attribute a FK to whichever table had the first `ADD CONSTRAINT`.
93
+ - **structure.sql parsing: NOT NULL columns are no longer reported as
94
+ nullable** (nullability was simply not extracted).
95
+ - **Runtime-only tools refuse in the static tier.** After a mid-boot
96
+ failure, `runtime_info` and `query` ran against the half-initialized app
97
+ and returned live, unmarked data contradicting the static-tier banner in
98
+ the same response; both now answer `[UNAVAILABLE: static tier]` with the
99
+ boot-failure reason. This also removes a raw NameError leak in
100
+ runtime_info's Cache section and query's wrong `--skip-active-record`
101
+ blame under `--no-boot`.
102
+ - **The static-tier banner carries the literal `[STATIC]` tag** that README
103
+ and COMPATIBILITY.md promise.
104
+ - **doctor's "Secrets in .gitignore" check understands globs.** Rails 8.1
105
+ generates `/config/*.key`; the substring check false-failed every default
106
+ 8.1 app and docked its readiness score.
107
+ - **model_details carries confidence tags**: `[VERIFIED]` on the runtime
108
+ model header, per-scope `[VERIFIED]`/`[INFERRED]` from the AST layer
109
+ (README promised tags this tool never rendered).
110
+ - **Route stats agree across generated context files.** CLAUDE.md counted
111
+ all routes over app controllers while copilot-instructions counted all
112
+ routes over all controllers ("35 across 1" vs "35 across 18"); every
113
+ serializer now reports app routes across app controllers with the
114
+ framework-inclusive total alongside.
115
+ - **get_routes counts engine mounts.** Verbless rack mounts (propshaft's
116
+ `/assets`) vanished from both the app list and the excluded-framework
117
+ count; the header now reports them explicitly.
118
+ - **Mongoid apps are detected from the Gemfile too** (previously only
119
+ Gemfile.lock or config/mongoid.yml), so a fresh checkout without a
120
+ lockfile gets honest Mongoid treatment instead of ActiveRecord answers.
121
+ - **`extra_app_paths` accepts entries ending in `/app`** (`custom/app`
122
+ previously globbed `custom/app/app/models` and silently missed).
123
+ - **analyze_feature no longer invents model names** ("user, payment, order")
124
+ on apps with zero models.
125
+ - **A missing schema is "unavailable", not "failed".** A greenfield app's
126
+ `context` output framed the absent db/schema.rb as "introspection failed";
127
+ absent data sources now use the `[UNAVAILABLE]` vocabulary.
128
+ - **conventions: `create!` flows are no longer attributed the save-and-branch
129
+ skeleton**, zero-signal test files are no longer credited as pattern
130
+ sources, and empty custom-directory descriptions drop the dangling dash.
131
+ - **get_config names the environment its values reflect** (cache store,
132
+ queue adapter, and Action Cable differ per environment).
133
+ - **get_env skips ERB-only false positives from comments** and standalone
134
+ `doctor` writes its report to stdout (`doctor > report.txt` works).
135
+ - docs: TROUBLESHOOTING covers RubyGems' "Resolving dependencies..." stdout
136
+ pollution of the MCP stdio stream and its fixes; a new doctor check ("MCP
137
+ stdio hygiene") detects the condition by re-running gem activation the way
138
+ an MCP client's binstub launch does.
139
+ - **Static schema answers include the implicit id primary key**, typed per
140
+ the adapter in config/database.yml (bigint everywhere, integer on SQLite);
141
+ composite-primary-key tables (`primary_key: [...]`) are left to their
142
+ explicitly dumped columns instead of gaining a phantom id.
143
+ - **Static schema answers name their source dialect and migration state**:
144
+ structure.sql parses report "Dialect", "Schema version" (from the
145
+ schema_migrations insert tail), and the exact pending migration files;
146
+ schema.rb parses report version plus files newer than it. Secondary
147
+ database dumps compare against their own db/<name>_migrate directories.
148
+ - Review-round regression fixes (found by adversarial review of this batch):
149
+ the missing-schema `{unavailable:}` result no longer leaks blank
150
+ "- Database:" lines into generated context files; the gitignore matcher
151
+ honors git's basename rule (`*.key`, bare `master.key`); multi-line
152
+ `retry_on` declarations keep their continuation-line options; a
153
+ syntax-broken `custom_tools` string entry warn-skips instead of crashing
154
+ the server and CLI; the Turbo broadcast scan ignores comments and method
155
+ definitions; redirect and lambda routes are not counted as engine mounts.
156
+ - Second review-round fixes: the gitignore security check honors `!`
157
+ negation patterns (a re-included secret now correctly fails the check);
158
+ `SafeFile.read` scrubs invalid UTF-8 so one bad byte in one file can no
159
+ longer raise out of any scan (previously it could erase every model's
160
+ Turbo broadcasts, or crash `get_turbo_map` outright); legacy zero-padded
161
+ migration versions (`001_` storing version "1") no longer read as
162
+ forever-pending; the synthesized primary-key type is resolved per
163
+ database from config/database.yml (a mixed postgres/sqlite multi-db app
164
+ types each dump correctly); endless-method broadcasts
165
+ (`def refresh = broadcast_replace_to(...)`) are detected while trailing
166
+ `#comments` (with or without spaces) are not, in both the introspector
167
+ and get_turbo_map's own scanners; `retry_on` options inside trailing
168
+ comments are not reported as real retry config; the doctor stdio-hygiene
169
+ probe launches from Bundler's original environment (a configured bundle
170
+ path no longer produces a spurious warning); a custom tool whose class
171
+ body raises during autoload is reported as broken rather than
172
+ "class not found"; the schema_migrations INSERT regex and the
173
+ section-usability predicate each have a single home.
174
+ - Third review-round fixes: source-line scans (Turbo broadcasts, retry_on
175
+ options) strip comments with a string-aware walker (`SourceLine`) instead
176
+ of a regex, so a `#` inside a string literal (`"#comments"`, `"##{id}"`)
177
+ no longer truncates the line and drops real calls, while comment text
178
+ still never reads as code - including comments after a continuation comma
179
+ in multi-line `retry_on`; `def self.broadcast_*_to` definitions are not
180
+ counted as broadcast calls; the doctor gitignore check now models git's
181
+ negation semantics (dir-only `!*/` entries, no re-inclusion under an
182
+ excluded parent, negations must match the file itself) - verified against
183
+ `git check-ignore` on a 16-case battery; the database.yml block lookup
184
+ no longer bleeds across whitespace-only lines into a sibling database's
185
+ adapter; and a custom tool whose body references a missing constant that
186
+ shares a name segment with the entry is no longer misreported as
187
+ "class not found".
188
+ - Fourth review-round fixes: the gitignore check now evaluates paths the
189
+ way git does (ancestor-directory recursion with last-match-wins instead
190
+ of descend globbing), closing a false-SAFE case where `config/` followed
191
+ by `!config/` read as still covering config/master.key - verified 25/25
192
+ against `git check-ignore`, including `**/`, whitelist preludes, and
193
+ re-included directories; it also honors case-insensitive filesystems the
194
+ way core.ignorecase does. `SourceLine.strip_comment` gained %-literal
195
+ support (`%w[cs#billing]` is content, `n % 2` is not a literal), an
196
+ O(n) walk (a 400k-char multibyte line dropped from 27s to 51ms), and a
197
+ no-`#` fast path. Broadcast def-blanking handles constant receivers
198
+ (`def User.broadcast_updates_to`) and multiple defs per line; multi-line
199
+ broadcast extraction reads comment-stripped context; `::`-prefixed
200
+ custom_tools entries classify correctly; config/database.yml is read
201
+ once, CRLF-normalized, per introspection run.
202
+ - Sixth-through-eighth review-round fixes (call-site scanning converged
203
+ after three more single-finding rounds): classic one-line methods
204
+ (`def x; broadcast_y_to(...); end`) contribute their body while
205
+ visibility-prefixed definitions (`private def`, `private_class_method
206
+ def`) contribute nothing; `;`/`=` inside string keyword defaults
207
+ (`sep: "; "`, `label: "a=b"`) and inside `#{...}` interpolation (with
208
+ nested quoted strings) no longer delimit a signature. A final review
209
+ round at a realistic-Rails bar returned zero findings.
210
+ - Fifth review-round fixes: comment stripping is now a whole-fragment
211
+ state machine (`SourceLine.strip_comments`) - literal state carries
212
+ across newlines, so a comment mid-way through a multi-line broadcast
213
+ call is removed while a string spanning lines keeps its tail; backtick
214
+ command literals (with interpolation) are modeled; singleton broadcast
215
+ definitions on any receiver (`def record.broadcast_status_to`,
216
+ `def self::...`) are excluded from broadcast detection; database.yml
217
+ without a final newline still resolves its last block's adapter; and
218
+ the case-insensitivity probe works for app roots with caseless names
219
+ (all-digit directories).
220
+
221
+ ### Changed
222
+
223
+ - Standalone `doctor` writes its report to stdout (previously stderr), so
224
+ `doctor > report.txt` captures it; update scripts that redirected `2>`.
225
+ - Custom tools resolved through the CLI are now validated as `MCP::Tool`
226
+ subclasses, matching what the MCP server has always enforced; duck-typed
227
+ tool classes that only ever worked via `rails-ai-context tool` are
228
+ warn-skipped with the reason.
229
+
230
+ ## [5.15.0] - 2026-07-11
231
+
232
+ ### Added
233
+
234
+ - Static tier: `serve` and `tool` fall back to source-file analysis when the
235
+ app cannot boot, instead of exiting. Routes (new `config/routes.rb` Prism
236
+ walker), models, controllers, schema, and migrations answer statically;
237
+ other sections report `[UNAVAILABLE]` with the reason; every response
238
+ carries a tier banner.
239
+ - CLI flags: `--no-boot` (serve/tool), `--app-path` (all app-reading
240
+ commands), and `--environment` (all commands).
241
+ - `[STATIC]` and `[UNAVAILABLE: <reason>]` confidence tags.
242
+ - structure.sql parsing is dialect-aware: MySQL (backticks, inline
243
+ KEY/CONSTRAINT definitions, ENGINE trailers) and SQLite (quoted
244
+ identifiers, IF NOT EXISTS) now parse; output gains a `dialect` key.
245
+ - Shape-aware discovery: models and controllers are found in packwerk
246
+ `packs/`, in-repo `engines/`, and configured `extra_app_paths`.
247
+ - Multi-database schema dumps (`db/*_schema.rb`, `db/*_structure.sql`)
248
+ reported under `secondary_databases`.
249
+ - Mongoid detection: honest `[UNAVAILABLE]` schema signal and basic static
250
+ model extraction (fields, embeds, store_in) for Mongoid apps.
251
+ - API-only apps get explicit "not applicable" answers from view/frontend
252
+ tools instead of empty listings.
253
+ - **Loud Rails 9 warning in the standalone CLI.** In-Gemfile installs already
254
+ fail Bundler resolution against Rails 9 (`railties < 9.0`); the standalone
255
+ CLI has no such check, so it used to boot silently against an untested
256
+ Rails version. It now prints a stderr warning naming the installed Rails
257
+ version and pointing at checking for a newer gem release.
258
+ - **`docs/COMPATIBILITY.md`** publishes the supported-version matrix, the CI
259
+ version grid, the RUNTIME/STATIC tier contract with the confidence-tag
260
+ vocabulary, and a shape-by-introspector matrix that cites the spec or QA
261
+ run proving every cell.
262
+
263
+ ### Changed
264
+
265
+ - Boot failure in `tool`/`serve` no longer exits 1; it degrades to the
266
+ static tier with a stderr notice. `doctor` keeps requiring a bootable app.
267
+ - CI runs Rails 8.x on Ruby 3.2 again. The exclusion assumed Rails 8
268
+ required Ruby 3.3+; both railties 8.0.5 and 8.1.3 declare
269
+ `required_ruby_version >= 3.2.0`.
270
+
271
+ ### Fixed
272
+
273
+ - `config.auto_mount = true` set in a user initializer now takes effect
274
+ (the middleware initializer ran before user initializers loaded).
275
+ - `prism` and `concurrent-ruby` are now capped below their next majors
276
+ (`< 2.0`, `< 3.0`) instead of an open-ended floor.
277
+ - A syntax error in one model file now costs exactly that model - the rest
278
+ of the models section, and every other section, still answers normally
279
+ (verified end-to-end in the static tier).
280
+
8
281
  ## [5.14.0] - 2026-07-11
9
282
 
10
283
  ### Added
data/README.md CHANGED
@@ -448,6 +448,44 @@ Both paths ask which AI tools you use (Claude Code, Cursor, GitHub Copilot, Open
448
448
 
449
449
  <br>
450
450
 
451
+ ## Static tier: works even when the app can't boot
452
+
453
+ `rails-ai-context` attempts a full boot for live reflection. When boot fails
454
+ (missing ENV vars, unreachable services, a broken initializer) the `serve`
455
+ and `tool` commands fall back to the **static tier** instead of dying:
456
+ routes come from parsing `config/routes.rb`, schema from `db/schema.rb` /
457
+ `db/structure.sql` / migrations, models and controllers from their source
458
+ files. Every response carries a banner explaining the degradation, static
459
+ data is tagged `[STATIC]`, and sections that genuinely need a booted app
460
+ report `[UNAVAILABLE]` with the reason.
461
+
462
+ Flags:
463
+
464
+ - `--no-boot` (serve, tool) - skip the boot attempt entirely and serve
465
+ static analysis. Fast, and immune to boot-time side effects.
466
+ - `--app-path PATH` (all app-reading commands) - run against a Rails app in
467
+ another directory.
468
+ - `--environment ENV` (all commands) - set RAILS_ENV for the boot attempt.
469
+
470
+ `rails-ai-context doctor` still requires a bootable app: its job is
471
+ diagnosing why boot fails.
472
+
473
+ <br>
474
+
475
+ ## App shapes
476
+
477
+ Beyond the conventional layout, code is discovered in packwerk packs
478
+ (`packs/*/app/*`), in-repo engines (`engines/*/app/*`), and any directories
479
+ listed in `extra_app_paths` in `.rails-ai-context.yml`. Rails multi-database
480
+ schema dumps (`db/queue_schema.rb` and friends) are reported under a
481
+ `Secondary databases` section. Mongoid apps get an honest
482
+ `[UNAVAILABLE]` schema signal plus basic static model data (fields, embedded
483
+ relations) instead of misleading empty output, and API-only apps get
484
+ "not applicable" answers from view and frontend tools instead of silent
485
+ empty listings.
486
+
487
+ <br>
488
+
451
489
  ## Documentation
452
490
 
453
491
  | | |
@@ -465,6 +503,7 @@ Both paths ask which AI tools you use (Claude Code, Cursor, GitHub Copilot, Open
465
503
  | **[Standalone](docs/STANDALONE.md)** | Use without Gemfile entry |
466
504
  | **[Troubleshooting](docs/TROUBLESHOOTING.md)** | Common issues and fixes |
467
505
  | **[FAQ](docs/FAQ.md)** | Frequently asked questions |
506
+ | **[Compatibility](docs/COMPATIBILITY.md)** | Supported versions, operating tiers, and the shape matrix with proof sources |
468
507
 
469
508
  <br>
470
509
 
@@ -484,7 +523,7 @@ class RailsGetBusinessMetrics < MCP::Tool
484
523
  end
485
524
 
486
525
  # config/initializers/rails_ai_context.rb
487
- config.custom_tools = [RailsGetBusinessMetrics]
526
+ config.custom_tools = ["RailsGetBusinessMetrics"]
488
527
  ```
489
528
 
490
529
  Test with the built-in `TestHelper` (works with RSpec and Minitest):
@@ -523,7 +562,8 @@ Every MCP tool call fires an `ActiveSupport::Notifications` event:
523
562
 
524
563
  ```ruby
525
564
  ActiveSupport::Notifications.subscribe("rails_ai_context.tools.call") do |event|
526
- Rails.logger.info "[MCP] #{event.payload[:method]} - #{event.duration}ms"
565
+ ms = (event.payload[:duration].to_f * 1000).round
566
+ Rails.logger.info "[MCP] #{event.payload[:tool_name]} - #{ms}ms"
527
567
  end
528
568
  ```
529
569
 
data/config/routes.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RailsAiContext::Engine.routes.draw do
4
- match "/", to: "mcp#handle", via: :all
4
+ # The engine is not namespace-isolated, so the controller must be
5
+ # referenced by its full path or Rails resolves a top-level McpController.
6
+ match "/", to: "rails_ai_context/mcp#handle", via: :all
5
7
  end
@@ -0,0 +1,219 @@
1
+ <div align="center" markdown="1">
2
+
3
+ # Compatibility
4
+
5
+ **What's actually supported, what's actually proven, and where each claim comes from.**
6
+
7
+ [Architecture](ARCHITECTURE.md) · [Introspectors](INTROSPECTORS.md) · [Troubleshooting](TROUBLESHOOTING.md) · [FAQ](FAQ.md)
8
+
9
+ </div>
10
+
11
+ ---
12
+
13
+ Every claim below cites the spec file or release that proves it. No entry in this
14
+ document is aspirational.
15
+
16
+ ## Supported versions
17
+
18
+ - **Ruby:** 3.1 - 4.0 (gemspec: `required_ruby_version >= 3.1.0`, no upper cap)
19
+ - **Rails (railties):** 7.0 - 8.1 (gemspec: `railties >= 7.0, < 9.0`)
20
+ - **mcp gem:** `>= 0.8, < 2.0`
21
+ - **thor:** `>= 1.0, < 3.0`
22
+ - **prism:** `>= 0.28, < 2.0`
23
+ - **concurrent-ruby:** `>= 1.2, < 3.0`
24
+
25
+ The gemspec's `railties` bound is wider than the CI matrix: point releases inside
26
+ 7.0-8.1 and any future 8.x minor satisfy Bundler's constraint without a gem
27
+ release, but only the combinations in the table below are actually exercised by
28
+ CI.
29
+
30
+ ### CI matrix
31
+
32
+ Source: `.github/workflows/ci.yml`. 17 of 25 possible Ruby x Rails combinations
33
+ run (`fail-fast: false`, so one failing cell doesn't hide the rest):
34
+
35
+ | Ruby \ Rails | 7.0 | 7.1 | 7.2 | 8.0 | 8.1 |
36
+ |:---|:---:|:---:|:---:|:---:|:---:|
37
+ | 3.1 | yes | yes | yes | - | - |
38
+ | 3.2 | - | yes | yes | yes | yes |
39
+ | 3.3 | - | yes | yes | yes | yes |
40
+ | 3.4 | - | yes | yes | yes | yes |
41
+ | 4.0 | - | - | - | yes | yes |
42
+
43
+ - Rails 7.0 runs only on Ruby 3.1 (7.0 predates upstream Ruby 3.2+ support).
44
+ - Rails 8.0/8.1 run on Ruby 3.2+ (both declare `required_ruby_version >= 3.2.0`
45
+ upstream; they never needed 3.3).
46
+ - Ruby 4.0 runs only against Rails 8.0/8.1 (7.x has no upstream Ruby 4
47
+ support).
48
+ - RuboCop runs once inside the matrix (Ruby 3.3 / Rails 8.0) and again in a
49
+ dedicated `lint` job pinned to Ruby 3.3.
50
+
51
+ ### Rails 9
52
+
53
+ Not supported, and the two install paths fail differently:
54
+
55
+ - **In-Gemfile:** the gemspec's `railties < 9.0` constraint makes `bundle
56
+ install`/`bundle exec` fail dependency resolution outright when the host
57
+ app's Gemfile locks Rails 9 - the gem never loads.
58
+ - **Standalone:** the CLI pre-loads the gem into its own `GEM_HOME` before the
59
+ host app boots, so no Bundler constraint applies. `exe/rails-ai-context`
60
+ prints a stderr warning after a successful boot when
61
+ `Rails::VERSION::MAJOR >= 9`, naming the installed Rails version and
62
+ pointing at checking for a newer gem release. Introspection itself is
63
+ untested on Rails 9 either way.
64
+
65
+ ## Operating tiers
66
+
67
+ Two tiers, both reachable over the CLI and MCP (stdio and HTTP):
68
+
69
+ **RUNTIME** - the app booted. Introspectors read live Rails reflection
70
+ (ActiveRecord connections, `Rails.application.routes`, loaded classes) plus the
71
+ Prism AST layer for source-level facts (scopes, callbacks, strong params).
72
+
73
+ **STATIC** - the app didn't boot, or `--no-boot` was passed. Only introspectors
74
+ that define a `static_call` path can answer without a booted app:
75
+
76
+ | Introspector | Static source |
77
+ |:---|:---|
78
+ | `schema` | `db/schema.rb` / `db/structure.sql` / migration files |
79
+ | `migrations` | migration file list + `db/structure.sql`'s trailing `schema_migrations` insert |
80
+ | `routes` | `config/routes.rb` parsed with a dedicated Prism listener |
81
+ | `models` | `app/models/**/*.rb` (plus packs/engines/extra paths) parsed, not constantized |
82
+ | `controllers` | `app/controllers/**/*.rb` (plus packs/engines/extra paths) parsed, not constantized |
83
+
84
+ The other 34 introspectors (views, jobs, gems, turbo, i18n, active_storage,
85
+ auth, api, and the rest) have no static path and report `{ unavailable: reason
86
+ }` in this tier - by construction, not by shape: `Introspector#run_introspector`
87
+ (`lib/rails_ai_context/introspector.rb`) falls back to the same message
88
+ regardless of what triggered the static tier.
89
+
90
+ `doctor` never enters the static tier - its job is diagnosing why boot failed,
91
+ so it always requires a bootable app.
92
+
93
+ Boot failure degrades `serve`/`tool` to the static tier automatically (proven
94
+ across four boot-failure modes - raises, prints to stdout, writes via the
95
+ `STDOUT` constant, and hangs past the timeout - in
96
+ `spec/e2e/boot_resilience_spec.rb`); `--no-boot` forces it without attempting a
97
+ boot at all (`spec/e2e/static_tier_spec.rb`).
98
+
99
+ ### Confidence vocabulary
100
+
101
+ Source: `lib/rails_ai_context/confidence.rb`.
102
+
103
+ | Tag | Meaning |
104
+ |:---|:---|
105
+ | `[VERIFIED]` | Runtime-confirmed value, or a static literal the AST resolves with certainty |
106
+ | `[STATIC]` | Derived from source files without a booted app - trustworthy structure, not runtime-confirmed |
107
+ | `[INFERRED]` | Heuristic: a dynamic expression, metaprogramming, or a runtime-only construct the AST can't fully resolve |
108
+ | `[UNAVAILABLE: reason]` | The data source is absent entirely |
109
+
110
+ `[VERIFIED]`/`[INFERRED]` are per-value tags applied inside the schema, model,
111
+ route, controller, and mailbox-routing introspectors as they walk the AST
112
+ (`Confidence.for_node`). `[STATIC]`/`[UNAVAILABLE]` are whole-response tags:
113
+ `[STATIC]` marks any answer that came from the static tier; `[UNAVAILABLE]`
114
+ marks a section with no static path, or (in either tier) a data source that
115
+ genuinely doesn't exist for this app.
116
+
117
+ ## Shape matrix
118
+
119
+ Rows are app shapes; columns are schema, models, routes, and controllers
120
+ (the four whose static behavior varies by shape), plus views for contrast.
121
+ `n/a` means the shape doesn't change that introspector's behavior - see
122
+ the stock full-stack row for its baseline proof. Reference numbers point at
123
+ the proof list below the table.
124
+
125
+ | Shape | Schema | Models | Routes | Controllers | Views |
126
+ |:---|:---|:---|:---|:---|:---|
127
+ | Stock full-stack (`schema.rb`) | runtime + static [1] | runtime + static [1] | runtime + static [1] | runtime + static [1] | runtime only [2] |
128
+ | API-only | runtime + static [1] | runtime + static [1] | runtime + static [1] | runtime + static [1] | runtime, "not applicable" [3] |
129
+ | `structure.sql` - PostgreSQL dialect | static [4] | n/a | n/a | n/a | n/a |
130
+ | `structure.sql` - MySQL/Trilogy dialect | static [4] | n/a | n/a | n/a | n/a |
131
+ | `structure.sql` - SQLite dialect | static [4] | n/a | n/a | n/a | n/a |
132
+ | Multi-DB `solid_*` schema dumps | static, `secondary_databases` [5] | n/a | n/a | n/a | n/a |
133
+ | Packwerk `packs/` | n/a | static [5] | n/a | n/a | n/a |
134
+ | In-repo `engines/` | n/a | n/a | n/a | static [5] | n/a |
135
+ | Mongoid | `[UNAVAILABLE]`, honest signal [6] | static (fields + embeds) [6] | n/a | n/a | n/a |
136
+ | Broken-boot (any full-stack app) | static [7] | static, per-file isolation [7] | static [7] | static [7] | `[UNAVAILABLE]` [8] |
137
+ | Packs + engines under `--no-boot` | n/a | static [5] | n/a | static [5] | n/a |
138
+ | Empty/greenfield app (no scaffold) | graceful, no crash [9] | graceful, no crash [9] | graceful, no crash [9] | graceful, no crash [9] | graceful, no crash [9] |
139
+ | Massive app (500 models/tables) | no crash at scale [10] | no crash at scale [10] | no crash at scale [10] | n/a | n/a |
140
+
141
+ Proof sources:
142
+
143
+ 1. `spec/e2e/in_gemfile_install_spec.rb`, `standalone_install_spec.rb`,
144
+ `zero_config_install_spec.rb` (schema, routes, model_details, and controllers
145
+ exercised across the install-path suite - in-Gemfile, standalone, zero-config);
146
+ real Rails 8.0 apps in the v5.14.0 release QA (`blog`, `sandbox`).
147
+ 2. Non-crash coverage for every built-in tool including `get_view` in
148
+ `spec/e2e/in_gemfile_install_spec.rb`'s full-tool sweep; output correctness
149
+ (ivar cross-check, render-form detection, partial interfaces) verified
150
+ against the real `blog` app in the v5.14.0 release QA.
151
+ 3. Unit specs for the "not applicable" API-only messaging: `get_view_spec.rb`,
152
+ `get_component_catalog_spec.rb`, `get_turbo_map_spec.rb`,
153
+ `get_partial_interface_spec.rb`, `get_stimulus_spec.rb`, `get_api_spec.rb`;
154
+ real Rails 8 `--api` app (`api_app`) in the v5.14.0 release QA.
155
+ 4. `spec/lib/rails_ai_context/introspectors/schema_introspector_spec.rb`
156
+ ("with a valid structure.sql fixture", "with a MySQL (mysqldump)
157
+ structure.sql", "with a SQLite structure.sql"). Unit-proven only - no
158
+ committed e2e fixture writes a `structure.sql` file. The MySQL/Trilogy
159
+ adapter itself (live queries, not the dump format) was also run for real
160
+ against MySQL 9.6 in the v5.14.0 release QA (`store` app,
161
+ `schema_format = :sql`).
162
+ 5. `spec/e2e/shapes_spec.rb`: "packs and engines code discovery (static
163
+ tier)" and "multi-database schema dumps".
164
+ 6. `spec/e2e/shapes_spec.rb`: "Mongoid app (bare directory, --app-path, no
165
+ boot possible)".
166
+ 7. `spec/e2e/boot_resilience_spec.rb` (four boot-failure modes: raises,
167
+ prints to stdout, writes via the `STDOUT` constant, hangs past the
168
+ timeout) plus `spec/e2e/static_tier_spec.rb` ("broken-boot app over the
169
+ CLI", "broken-boot app over MCP stdio", "syntax error in one model file").
170
+ 8. No introspector outside the five in the operating-tiers table defines
171
+ `static_call` (`lib/rails_ai_context/introspectors/view_introspector.rb`
172
+ has none); `Introspector#run_introspector` reports `{ unavailable: reason
173
+ }` for every such section regardless of shape.
174
+ 9. `spec/e2e/empty_app_spec.rb` - all 39 built-in tools swept against an app
175
+ with no scaffold, no models, no controllers beyond
176
+ `ApplicationController`, no routes beyond root.
177
+ 10. `spec/e2e/massive_app_spec.rb` - `schema`, `model_details`, and `routes`
178
+ (plus `context`, `onboard`, `analyze_feature`, `get_turbo_map`, `get_env`)
179
+ sampled against 500 generated models/tables under a 180-second cap.
180
+
181
+ Postgres itself - live query safety (read-only transactions, the
182
+ `BLOCKED_FUNCTIONS` guard against `pg_read_file`/`dblink`/`COPY ... PROGRAM`,
183
+ DDL rejection) alongside schema/routes - is proven end-to-end against a real
184
+ Postgres instance in `spec/e2e/postgres_install_spec.rb`, opt-in via
185
+ `TEST_POSTGRES=1` and skipped otherwise.
186
+
187
+ ## Known limits
188
+
189
+ - **Concern-style Mongoid documents in runtime results.** Mongoid documents
190
+ are invisible to ActiveRecord reflection, so `ModelIntrospector#call` falls
191
+ back to the same source-parsing pass used in the static tier even when the
192
+ app is booted (`lib/rails_ai_context/introspectors/model_introspector.rb`).
193
+ Fields or associations defined in an included concern module rather than
194
+ directly in the document class body are not resolved, in either tier.
195
+ - **Static-tier API-only detection.** `Tools::BaseTool#api_only_app?` reads
196
+ `app.config.api_only`. `RailsAiContext::StaticApp` exposes no `config`
197
+ method, so this check is always false in the static tier - the "not
198
+ applicable" messaging for view/frontend tools on API-only apps only fires
199
+ once the app has actually booted.
200
+ - **Live multi-DB connections are not iterated, only dumps.**
201
+ `MultiDatabaseIntrospector` (replicas, sharding, per-model connection
202
+ assignment) has no static path and reports `[UNAVAILABLE]` in the static
203
+ tier. Only the schema introspector's own secondary-database dump parsing
204
+ (`db/*_schema.rb`, `db/*_structure.sql`) works without a boot.
205
+ - **Constraints and lambda routes surface as a dynamic tally, not resolved
206
+ entries.** `RouteIntrospector#static_call` counts routes behind
207
+ `constraints do...end` blocks, lambdas, `devise_for`, and `concern`-based
208
+ route declarations into a `dynamic_routes` count rather than fabricating
209
+ per-route controller/action pairs it can't actually determine from source.
210
+
211
+ <br>
212
+
213
+ ---
214
+
215
+ <div align="center">
216
+
217
+ [Back to README](../README.md)
218
+
219
+ </div>
data/docs/CUSTOM_TOOLS.md CHANGED
@@ -75,13 +75,20 @@ Add your tool classes to the configuration:
75
75
  # config/initializers/rails_ai_context.rb
76
76
  if defined?(RailsAiContext)
77
77
  RailsAiContext.configure do |config|
78
- config.custom_tools = [RailsGetBusinessMetrics]
78
+ config.custom_tools = ["RailsGetBusinessMetrics"]
79
79
  end
80
80
  end
81
81
  ```
82
82
 
83
83
  Custom tools appear alongside built-in tools in the MCP server and CLI.
84
84
 
85
+ Use the class *name* as a string: classes under `app/` (including
86
+ `app/mcp_tools/`) are not autoloadable while initializers run, so a bare
87
+ constant reference there aborts boot with a `NameError`. String entries are
88
+ resolved after the app finishes booting. A bare constant still works for
89
+ classes already loaded when the initializer runs (e.g. defined in `lib/`
90
+ and required manually).
91
+
85
92
  ## Using BaseTool features
86
93
 
87
94
  For access to caching, pagination, and helper methods, you can use `RailsAiContext::Tools::BaseTool` utilities directly:
@@ -111,7 +118,14 @@ end
111
118
 
112
119
  ## Testing custom tools
113
120
 
114
- Use the built-in `TestHelper` module:
121
+ Use the built-in `TestHelper` module.
122
+
123
+ With the gem in `group: :development`, the initializer's guard makes
124
+ `config.custom_tools` a no-op in the test environment, so name-based lookup
125
+ (`execute_tool("rails_get_business_metrics")`) won't find custom tools
126
+ there. Either install the gem with `group: %i[development test]`, or pass
127
+ the class itself: `execute_tool(RailsGetBusinessMetrics)`. Built-in tools
128
+ resolve by name in any environment.
115
129
 
116
130
  ### With RSpec
117
131
 
@@ -191,7 +205,7 @@ If a custom tool replaces a built-in one, exclude the original:
191
205
 
192
206
  ```ruby
193
207
  RailsAiContext.configure do |config|
194
- config.custom_tools = [MyBetterSecurityScan]
208
+ config.custom_tools = ["MyBetterSecurityScan"]
195
209
  config.skip_tools = %w[rails_security_scan]
196
210
  end
197
211
  ```
@@ -100,6 +100,23 @@ For Codex CLI specifically, the env section in `.codex/config.toml` must match y
100
100
  2. Check schema exists: `ls db/schema.rb` or `ls db/structure.sql`
101
101
  3. Run doctor: `rails ai:doctor`
102
102
 
103
+ ### MCP client reports a JSON parse error on the first line ("Resolving dependencies...")
104
+
105
+ RubyGems can print `Resolving dependencies...` to stdout while activating the
106
+ standalone `rails-ai-context` binary - before any gem code runs - which breaks
107
+ the pure-JSON stdio framing. This happens when gem activation needs the full
108
+ dependency resolver: typically a `GEM_PATH` that is missing part of the
109
+ dependency graph, or launching from inside another Bundler project's
110
+ directory. Fixes:
111
+
112
+ 1. Launch `rails-ai-context serve` with the app root as the working directory
113
+ (the generated MCP configs do this by default).
114
+ 2. Make sure `GEM_PATH` covers the complete default gem path
115
+ (`gem env path`), not just `gem env gemdir` - Ruby's bundled gems
116
+ (racc, etc.) live in a separate directory.
117
+ 3. In-Gemfile installs are immune: `bundle exec rails-ai-context serve`
118
+ activates through the lockfile.
119
+
103
120
  ### MCP server responds slowly
104
121
 
105
122
  - Check `config.cache_ttl` - lower values mean more frequent re-introspection