rails-ai-context 5.14.0 → 5.15.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/README.md +39 -0
  4. data/docs/COMPATIBILITY.md +219 -0
  5. data/exe/rails-ai-context +74 -18
  6. data/lib/rails_ai_context/app_kind.rb +22 -0
  7. data/lib/rails_ai_context/confidence.rb +16 -4
  8. data/lib/rails_ai_context/configuration.rb +13 -1
  9. data/lib/rails_ai_context/engine.rb +5 -1
  10. data/lib/rails_ai_context/introspector.rb +38 -4
  11. data/lib/rails_ai_context/introspectors/controller_introspector.rb +24 -9
  12. data/lib/rails_ai_context/introspectors/gem_introspector.rb +1 -0
  13. data/lib/rails_ai_context/introspectors/listeners/mongoid_fields_listener.rb +27 -0
  14. data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +372 -0
  15. data/lib/rails_ai_context/introspectors/migration_introspector.rb +4 -0
  16. data/lib/rails_ai_context/introspectors/model_introspector.rb +133 -4
  17. data/lib/rails_ai_context/introspectors/route_introspector.rb +42 -0
  18. data/lib/rails_ai_context/introspectors/schema_introspector.rb +157 -21
  19. data/lib/rails_ai_context/path_resolver.rb +29 -0
  20. data/lib/rails_ai_context/static_app.rb +17 -0
  21. data/lib/rails_ai_context/tools/analyze_feature.rb +1 -1
  22. data/lib/rails_ai_context/tools/base_tool.rb +76 -2
  23. data/lib/rails_ai_context/tools/dependency_graph.rb +3 -0
  24. data/lib/rails_ai_context/tools/diagnose.rb +1 -1
  25. data/lib/rails_ai_context/tools/generate_test.rb +3 -3
  26. data/lib/rails_ai_context/tools/get_api.rb +3 -0
  27. data/lib/rails_ai_context/tools/get_callbacks.rb +4 -2
  28. data/lib/rails_ai_context/tools/get_component_catalog.rb +9 -0
  29. data/lib/rails_ai_context/tools/get_config.rb +7 -5
  30. data/lib/rails_ai_context/tools/get_context.rb +1 -15
  31. data/lib/rails_ai_context/tools/get_controllers.rb +5 -3
  32. data/lib/rails_ai_context/tools/get_conventions.rb +17 -15
  33. data/lib/rails_ai_context/tools/get_edit_context.rb +4 -4
  34. data/lib/rails_ai_context/tools/get_env.rb +2 -2
  35. data/lib/rails_ai_context/tools/get_frontend_stack.rb +3 -0
  36. data/lib/rails_ai_context/tools/get_gems.rb +3 -1
  37. data/lib/rails_ai_context/tools/get_job_pattern.rb +17 -6
  38. data/lib/rails_ai_context/tools/get_model_details.rb +26 -5
  39. data/lib/rails_ai_context/tools/get_partial_interface.rb +4 -1
  40. data/lib/rails_ai_context/tools/get_routes.rb +2 -0
  41. data/lib/rails_ai_context/tools/get_schema.rb +21 -0
  42. data/lib/rails_ai_context/tools/get_service_pattern.rb +1 -1
  43. data/lib/rails_ai_context/tools/get_stimulus.rb +11 -4
  44. data/lib/rails_ai_context/tools/get_test_info.rb +15 -13
  45. data/lib/rails_ai_context/tools/get_turbo_map.rb +15 -2
  46. data/lib/rails_ai_context/tools/get_view.rb +32 -7
  47. data/lib/rails_ai_context/tools/onboard.rb +4 -4
  48. data/lib/rails_ai_context/tools/performance_check.rb +3 -0
  49. data/lib/rails_ai_context/tools/query.rb +1 -1
  50. data/lib/rails_ai_context/tools/read_logs.rb +4 -4
  51. data/lib/rails_ai_context/tools/review_changes.rb +1 -1
  52. data/lib/rails_ai_context/tools/runtime_info.rb +1 -1
  53. data/lib/rails_ai_context/tools/search_code.rb +1 -1
  54. data/lib/rails_ai_context/tools/search_docs.rb +2 -2
  55. data/lib/rails_ai_context/tools/security_scan.rb +2 -2
  56. data/lib/rails_ai_context/tools/validate.rb +12 -12
  57. data/lib/rails_ai_context/version.rb +1 -1
  58. data/lib/rails_ai_context.rb +43 -3
  59. metadata +19 -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: 0250405dffbfbb2100bf513cc75840ae980f68970e1a76911c4b2cc5ba3fac77
4
+ data.tar.gz: 25606a4264641a0a1be621bb30f3476c9ce1ecc1fd8dc37f7719ab15acee5e14
5
5
  SHA512:
6
- metadata.gz: e5f33eee0cbaedf8d21d6983e8672b72de97f58baab5a4c12ced685389d8e78b103f3f55974ba30a9d786e200ee5609ef3930a156614e502caafe80a56222e77
7
- data.tar.gz: bcb99dcf463633f7762636ee7a75b247fde3e2438e2eddfb76e977014d84bb2ab65bc6f88d0df6932f5e16adfa3d533712bab8f36fa96f45bd442b990cd7f448
6
+ metadata.gz: 88b6c43ddb2d47e8c7d428e0188fc9060584252d7457bcd5d5b580cba6029a73f383684582d04228c79aee1a5c014f5d65494439697e718d38844900a3b5890a
7
+ data.tar.gz: 00e2c762d693c804f0260b5e0d054ffb0443afd8271299a8711638f23b7a6aeaeaee68ea833ed788225086fc38ddb289b5de562fb3e5379d78e8a84cd6c75f3b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,57 @@ 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.15.0] - 2026-07-11
9
+
10
+ ### Added
11
+
12
+ - Static tier: `serve` and `tool` fall back to source-file analysis when the
13
+ app cannot boot, instead of exiting. Routes (new `config/routes.rb` Prism
14
+ walker), models, controllers, schema, and migrations answer statically;
15
+ other sections report `[UNAVAILABLE]` with the reason; every response
16
+ carries a tier banner.
17
+ - CLI flags: `--no-boot` (serve/tool), `--app-path` (all app-reading
18
+ commands), and `--environment` (all commands).
19
+ - `[STATIC]` and `[UNAVAILABLE: <reason>]` confidence tags.
20
+ - structure.sql parsing is dialect-aware: MySQL (backticks, inline
21
+ KEY/CONSTRAINT definitions, ENGINE trailers) and SQLite (quoted
22
+ identifiers, IF NOT EXISTS) now parse; output gains a `dialect` key.
23
+ - Shape-aware discovery: models and controllers are found in packwerk
24
+ `packs/`, in-repo `engines/`, and configured `extra_app_paths`.
25
+ - Multi-database schema dumps (`db/*_schema.rb`, `db/*_structure.sql`)
26
+ reported under `secondary_databases`.
27
+ - Mongoid detection: honest `[UNAVAILABLE]` schema signal and basic static
28
+ model extraction (fields, embeds, store_in) for Mongoid apps.
29
+ - API-only apps get explicit "not applicable" answers from view/frontend
30
+ tools instead of empty listings.
31
+ - **Loud Rails 9 warning in the standalone CLI.** In-Gemfile installs already
32
+ fail Bundler resolution against Rails 9 (`railties < 9.0`); the standalone
33
+ CLI has no such check, so it used to boot silently against an untested
34
+ Rails version. It now prints a stderr warning naming the installed Rails
35
+ version and pointing at checking for a newer gem release.
36
+ - **`docs/COMPATIBILITY.md`** publishes the supported-version matrix, the CI
37
+ version grid, the RUNTIME/STATIC tier contract with the confidence-tag
38
+ vocabulary, and a shape-by-introspector matrix that cites the spec or QA
39
+ run proving every cell.
40
+
41
+ ### Changed
42
+
43
+ - Boot failure in `tool`/`serve` no longer exits 1; it degrades to the
44
+ static tier with a stderr notice. `doctor` keeps requiring a bootable app.
45
+ - CI runs Rails 8.x on Ruby 3.2 again. The exclusion assumed Rails 8
46
+ required Ruby 3.3+; both railties 8.0.5 and 8.1.3 declare
47
+ `required_ruby_version >= 3.2.0`.
48
+
49
+ ### Fixed
50
+
51
+ - `config.auto_mount = true` set in a user initializer now takes effect
52
+ (the middleware initializer ran before user initializers loaded).
53
+ - `prism` and `concurrent-ruby` are now capped below their next majors
54
+ (`< 2.0`, `< 3.0`) instead of an open-ended floor.
55
+ - A syntax error in one model file now costs exactly that model - the rest
56
+ of the models section, and every other section, still answers normally
57
+ (verified end-to-end in the static tier).
58
+
8
59
  ## [5.14.0] - 2026-07-11
9
60
 
10
61
  ### 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
 
@@ -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/exe/rails-ai-context CHANGED
@@ -39,18 +39,23 @@ class RailsAiContextCLI < Thor
39
39
  # and `-v` fall through to "Could not find command" instead of the `version` task.
40
40
  map %w[--version -v] => :version
41
41
 
42
+ class_option :app_path, type: :string,
43
+ desc: "Path to the Rails app root (default: current directory)"
44
+ class_option :environment, type: :string,
45
+ desc: "RAILS_ENV to run under (default: the ambient RAILS_ENV or development)"
46
+
42
47
  desc "tool [NAME]", "Run an MCP tool. Use --list to see all."
43
48
  option :json, type: :boolean, default: false, desc: "Output as JSON envelope"
44
49
  option :list, type: :boolean, default: false, desc: "List available tools"
50
+ option :no_boot, type: :boolean, default: false, desc: "Skip booting the app; serve static analysis only"
45
51
  def tool(name = nil, *args)
46
52
  if options[:list] || name.nil?
47
53
  print_tool_list
48
54
  return
49
55
  end
50
56
 
51
- boot_rails!
52
-
53
57
  if args.include?("--help") || args.include?("-h")
58
+ require "rails_ai_context"
54
59
  tool_class = ::RailsAiContext::CLI::ToolRunner.new(name, []).tool_class
55
60
  puts ::RailsAiContext::CLI::ToolRunner.tool_help(tool_class)
56
61
  return
@@ -64,6 +69,13 @@ class RailsAiContextCLI < Thor
64
69
  json_mode = true if args.delete("--json")
65
70
  json_mode = true if args.delete("-j")
66
71
 
72
+ # stop_on_unknown_option! passes post-name flags through into args;
73
+ # honor --no-boot in either position, mirroring the --json handling.
74
+ no_boot = options[:no_boot]
75
+ no_boot = true if args.delete("--no-boot")
76
+
77
+ boot_rails!(allow_static: true, no_boot: no_boot)
78
+
67
79
  runner = ::RailsAiContext::CLI::ToolRunner.new(name, args, json_mode: json_mode)
68
80
  puts runner.run
69
81
  exit 1 if runner.error
@@ -77,8 +89,9 @@ class RailsAiContextCLI < Thor
77
89
  desc "serve", "Start MCP server (stdio transport)"
78
90
  option :transport, type: :string, default: "stdio", desc: "Transport: stdio or http"
79
91
  option :port, type: :numeric, default: 6029, desc: "HTTP port (only for http transport)"
92
+ option :no_boot, type: :boolean, default: false, desc: "Skip booting the app; serve static analysis only"
80
93
  def serve
81
- boot_rails!
94
+ boot_rails!(allow_static: true)
82
95
 
83
96
  transport = options[:transport].to_sym
84
97
  if transport == :http
@@ -550,7 +563,7 @@ class RailsAiContextCLI < Thor
550
563
  require "rails_ai_context"
551
564
  puts ::RailsAiContext::CLI::ToolRunner.tool_list
552
565
  puts ""
553
- puts "Run inside a Rails app to execute tools."
566
+ puts "Run inside a Rails app to execute tools - static source analysis works even when the app can't boot (see --no-boot)."
554
567
  rescue LoadError, StandardError => e
555
568
  $stderr.puts "Error: No Rails app found in #{Dir.pwd}"
556
569
  $stderr.puts "Run this command from your Rails app root directory."
@@ -577,11 +590,22 @@ class RailsAiContextCLI < Thor
577
590
  # capture references BEFORE Rails boot runs and re-register them afterwards.
578
591
  STANDALONE_REQUIRED_GEMS = %w[mcp json-schema addressable public_suffix].freeze
579
592
 
580
- def boot_rails!(context: nil)
593
+ def boot_rails!(context: nil, allow_static: false, no_boot: false)
594
+ app_root = File.expand_path(options[:app_path] || Dir.pwd)
595
+ # Relative paths inside the app (log/, tmp/, sqlite files) assume the
596
+ # process cwd is the app root, so honor --app-path by moving there.
597
+ Dir.chdir(app_root) if options[:app_path]
598
+ ENV["RAILS_ENV"] = options[:environment] if options[:environment]
599
+
600
+ if allow_static && (no_boot || options[:no_boot])
601
+ enter_static_tier!("static mode requested with --no-boot")
602
+ return
603
+ end
604
+
581
605
  config_path = File.join(Dir.pwd, "config", "environment.rb")
582
606
  unless File.exist?(config_path)
583
607
  $stderr.puts "Error: No Rails app found in #{Dir.pwd}"
584
- $stderr.puts "Run this command from your Rails app root directory."
608
+ $stderr.puts "Run this command from your Rails app root directory (or pass --app-path)."
585
609
  exit 1
586
610
  end
587
611
 
@@ -621,6 +645,18 @@ class RailsAiContextCLI < Thor
621
645
  result = ::RailsAiContext::BootManager.boot!(app_root: Dir.pwd, timeout: timeout)
622
646
 
623
647
  unless result.booted?
648
+ if allow_static
649
+ $stderr.puts "[rails-ai-context] App boot failed: #{result.failure_summary}"
650
+ if result.error.is_a?(::RailsAiContext::BootManager::BootTimeoutError)
651
+ $stderr.puts "[rails-ai-context] If the app is healthy but slow, raise RAILS_AI_CONTEXT_BOOT_TIMEOUT (seconds, current: #{timeout})."
652
+ end
653
+ $stderr.puts "[rails-ai-context] Serving static analysis; runtime-only data is marked [UNAVAILABLE]."
654
+ $stderr.puts "[rails-ai-context] Run `rails-ai-context doctor` for boot diagnostics."
655
+ restore_standalone_environment!(pre_boot_paths, pre_boot_specs)
656
+ enter_static_tier!(result.failure_summary)
657
+ return
658
+ end
659
+
624
660
  $stderr.puts "Error: Rails app failed to boot in #{Dir.pwd}"
625
661
  $stderr.puts " #{result.failure_summary}"
626
662
  if ENV["DEBUG"]
@@ -643,24 +679,44 @@ class RailsAiContextCLI < Thor
643
679
  exit 1
644
680
  end
645
681
 
646
- # Restore paths and specs that Bundler.setup stripped (standalone mode).
647
- # For Gemfile users, both restorations are no-ops.
682
+ restore_standalone_environment!(pre_boot_paths, pre_boot_specs)
683
+
684
+ require "rails_ai_context"
685
+
686
+ if defined?(Rails::VERSION::MAJOR) && Rails::VERSION::MAJOR >= 9
687
+ $stderr.puts "[rails-ai-context] WARNING: Rails #{Rails.version} is newer than this gem supports (< 9.0)."
688
+ $stderr.puts "[rails-ai-context] Introspection is untested on this version and may be wrong or incomplete."
689
+ $stderr.puts "[rails-ai-context] Check for a newer rails-ai-context release."
690
+ end
691
+
692
+ RailsAiContext::Configuration.auto_load!
693
+ end
694
+
695
+ # Bundler.setup during app boot strips $LOAD_PATH and the spec registry
696
+ # to Gemfile-resolved gems; in standalone mode our own transitive deps
697
+ # (mcp, json-schema) vanish. Splice back what was captured pre-boot.
698
+ def restore_standalone_environment!(pre_boot_paths, pre_boot_specs)
648
699
  (pre_boot_paths - $LOAD_PATH).each { |p| $LOAD_PATH << p }
649
700
  pre_boot_specs.each { |name, spec| Gem.loaded_specs[name] ||= spec }
650
701
 
651
- # Loud warning if any required spec couldn't be restored — silent failure
652
- # here means tools will crash later with `undefined method 'full_gem_path'
653
- # for nil` from inside the MCP SDK.
654
702
  missing = STANDALONE_REQUIRED_GEMS.reject { |g| Gem.loaded_specs.key?(g) }
655
- if missing.any? && !ENV["BUNDLE_BIN_PATH"]
656
- $stderr.puts "[rails-ai-context] WARNING: standalone CLI could not restore gemspec(s): #{missing.join(', ')}."
657
- $stderr.puts "[rails-ai-context] Tool calls may crash with `undefined method 'full_gem_path' for nil`."
658
- $stderr.puts "[rails-ai-context] Try `gem install rails-ai-context` to ensure all transitive deps are installed."
659
- end
703
+ return if missing.none? || ENV["BUNDLE_BIN_PATH"]
660
704
 
661
- require "rails_ai_context"
705
+ $stderr.puts "[rails-ai-context] WARNING: standalone CLI could not restore gemspec(s): #{missing.join(', ')}."
706
+ $stderr.puts "[rails-ai-context] Tool calls may crash with `undefined method 'full_gem_path' for nil`."
707
+ $stderr.puts "[rails-ai-context] Try `gem install rails-ai-context` to ensure all transitive deps are installed."
708
+ end
662
709
 
663
- RailsAiContext::Configuration.auto_load!
710
+ # Static tier: the gem loads standalone (no app constants), introspection
711
+ # runs against the filesystem, and every tool response carries the tier
712
+ # banner so consumers know runtime data is absent.
713
+ def enter_static_tier!(reason)
714
+ require "rails_ai_context"
715
+ ::RailsAiContext.tier = :static
716
+ ::RailsAiContext.static_reason = reason
717
+ ::RailsAiContext.configuration.app_root = Dir.pwd
718
+ ::RailsAiContext::Configuration.auto_load!
719
+ $stderr.puts "[rails-ai-context] static tier active: #{reason}"
664
720
  end
665
721
 
666
722
  # Look up each required gem via Gem::Specification.find_by_name BEFORE
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ # Detects app-level facts that change which introspection applies, from
5
+ # artifacts that exist without booting (config files, Gemfile.lock).
6
+ module AppKind
7
+ module_function
8
+
9
+ # The four-space indent plus "(" matches Bundler's resolved-spec line
10
+ # exactly, so gems that merely contain the name do not false-positive.
11
+ def mongoid?(root)
12
+ root = root.to_s
13
+ return true if File.exist?(File.join(root, "config", "mongoid.yml"))
14
+
15
+ lock = File.join(root, "Gemfile.lock")
16
+ return false unless File.exist?(lock)
17
+
18
+ content = RailsAiContext::SafeFile.read(lock)
19
+ !!content&.include?(" mongoid (")
20
+ end
21
+ end
22
+ end
@@ -5,13 +5,25 @@ module RailsAiContext
5
5
  # Shared across tools and introspectors without creating
6
6
  # cross-layer dependencies.
7
7
  #
8
- # VERIFIED: value extracted from a static, deterministic AST node
9
- # (symbol literals, string literals, integers, booleans).
10
- # INFERRED: value involves dynamic expressions, metaprogramming,
11
- # or runtime-only constructs that AST cannot fully resolve.
8
+ # VERIFIED: value the gem can assert with certainty - runtime-confirmed data,
9
+ # or a static literal the AST resolves deterministically (see for_node).
10
+ # STATIC: derived from source files without a booted app - trustworthy structure,
11
+ # not runtime-confirmed.
12
+ # INFERRED: heuristic - dynamic expressions, metaprogramming, or runtime-only
13
+ # constructs the AST cannot fully resolve.
14
+ # UNAVAILABLE: the data source is absent entirely; the reasoned form says why.
12
15
  module Confidence
13
16
  VERIFIED = "[VERIFIED]"
14
17
  INFERRED = "[INFERRED]"
18
+ STATIC = "[STATIC]"
19
+ UNAVAILABLE = "[UNAVAILABLE]"
20
+
21
+ # Reasoned unavailability tag: "[UNAVAILABLE: requires a booted Rails app]".
22
+ # Reasons are collapsed to their first line so a raised exception message
23
+ # can be passed through without flooding the tag.
24
+ def self.unavailable(reason)
25
+ "[UNAVAILABLE: #{reason.to_s.lines.first.to_s.strip}]"
26
+ end
15
27
 
16
28
  # Determine confidence level for a Prism call node's arguments.
17
29
  # Returns VERIFIED if all arguments are static literals,
@@ -18,7 +18,7 @@ module RailsAiContext
18
18
  live_reload live_reload_debounce auto_mount http_path http_bind http_port
19
19
  output_dir skip_tools excluded_models excluded_controllers
20
20
  excluded_route_prefixes excluded_filters excluded_middleware excluded_association_names excluded_paths
21
- sensitive_patterns search_extensions concern_paths frontend_paths
21
+ sensitive_patterns search_extensions concern_paths frontend_paths extra_app_paths
22
22
  max_file_size max_test_file_size max_schema_file_size max_view_total_size
23
23
  max_view_file_size max_search_results max_validate_files
24
24
  query_timeout query_row_limit query_redacted_columns allow_query_in_production
@@ -107,6 +107,12 @@ module RailsAiContext
107
107
  # Output directory for generated context files
108
108
  attr_accessor :output_dir
109
109
 
110
+ # Filesystem root of the app under analysis. Set by the CLI (--app-path
111
+ # or the working directory) for the static tier; nil means "ask Rails".
112
+ # Deliberately not a YAML key: the config file lives inside the app it
113
+ # would point at.
114
+ attr_accessor :app_root
115
+
110
116
  # Models/tables to exclude from introspection
111
117
  attr_accessor :excluded_models
112
118
 
@@ -229,6 +235,11 @@ module RailsAiContext
229
235
  # Frontend framework detection (optional overrides - auto-detected if nil)
230
236
  attr_accessor :frontend_paths # User-declared frontend dirs (e.g. ["app/frontend", "../web-client"])
231
237
 
238
+ # Additional app-root-relative directories that also contain Rails app
239
+ # code (e.g. ["src", "vendor/internal"]). Code discovery scans each
240
+ # entry's app/models, app/controllers, and app/views subdirectories.
241
+ attr_accessor :extra_app_paths
242
+
232
243
  # Database query tool settings (rails_query)
233
244
  attr_accessor :query_timeout # Statement timeout in seconds (default: 5)
234
245
  attr_accessor :query_row_limit # Max rows returned (default: 100, hard cap: 1000)
@@ -297,6 +308,7 @@ module RailsAiContext
297
308
  @search_extensions = %w[rb js erb yml yaml json ts tsx vue svelte haml slim]
298
309
  @concern_paths = %w[app/models/concerns app/controllers/concerns]
299
310
  @frontend_paths = nil
311
+ @extra_app_paths = []
300
312
  @query_timeout = 5
301
313
  @query_row_limit = 100
302
314
  @query_redacted_columns = %w[
@@ -9,7 +9,11 @@ module RailsAiContext
9
9
  end
10
10
 
11
11
  # Auto-mount MCP HTTP middleware when configured
12
- initializer "rails_ai_context.middleware" do |app|
12
+ # Must run after config/initializers so a user's
13
+ # `config.auto_mount = true` is visible when the mount decision is made;
14
+ # the middleware stack is built later in the boot finisher, so adding to
15
+ # it here still takes effect.
16
+ initializer "rails_ai_context.middleware", after: :load_config_initializers do |app|
13
17
  if RailsAiContext.configuration.auto_mount
14
18
  require_relative "middleware"
15
19
  app.middleware.use RailsAiContext::Middleware