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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -0
- data/README.md +39 -0
- data/docs/COMPATIBILITY.md +219 -0
- data/exe/rails-ai-context +74 -18
- data/lib/rails_ai_context/app_kind.rb +22 -0
- data/lib/rails_ai_context/confidence.rb +16 -4
- data/lib/rails_ai_context/configuration.rb +13 -1
- data/lib/rails_ai_context/engine.rb +5 -1
- data/lib/rails_ai_context/introspector.rb +38 -4
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +24 -9
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +1 -0
- data/lib/rails_ai_context/introspectors/listeners/mongoid_fields_listener.rb +27 -0
- data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +372 -0
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +4 -0
- data/lib/rails_ai_context/introspectors/model_introspector.rb +133 -4
- data/lib/rails_ai_context/introspectors/route_introspector.rb +42 -0
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +157 -21
- data/lib/rails_ai_context/path_resolver.rb +29 -0
- data/lib/rails_ai_context/static_app.rb +17 -0
- data/lib/rails_ai_context/tools/analyze_feature.rb +1 -1
- data/lib/rails_ai_context/tools/base_tool.rb +76 -2
- data/lib/rails_ai_context/tools/dependency_graph.rb +3 -0
- data/lib/rails_ai_context/tools/diagnose.rb +1 -1
- data/lib/rails_ai_context/tools/generate_test.rb +3 -3
- data/lib/rails_ai_context/tools/get_api.rb +3 -0
- data/lib/rails_ai_context/tools/get_callbacks.rb +4 -2
- data/lib/rails_ai_context/tools/get_component_catalog.rb +9 -0
- data/lib/rails_ai_context/tools/get_config.rb +7 -5
- data/lib/rails_ai_context/tools/get_context.rb +1 -15
- data/lib/rails_ai_context/tools/get_controllers.rb +5 -3
- data/lib/rails_ai_context/tools/get_conventions.rb +17 -15
- data/lib/rails_ai_context/tools/get_edit_context.rb +4 -4
- data/lib/rails_ai_context/tools/get_env.rb +2 -2
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +3 -0
- data/lib/rails_ai_context/tools/get_gems.rb +3 -1
- data/lib/rails_ai_context/tools/get_job_pattern.rb +17 -6
- data/lib/rails_ai_context/tools/get_model_details.rb +26 -5
- data/lib/rails_ai_context/tools/get_partial_interface.rb +4 -1
- data/lib/rails_ai_context/tools/get_routes.rb +2 -0
- data/lib/rails_ai_context/tools/get_schema.rb +21 -0
- data/lib/rails_ai_context/tools/get_service_pattern.rb +1 -1
- data/lib/rails_ai_context/tools/get_stimulus.rb +11 -4
- data/lib/rails_ai_context/tools/get_test_info.rb +15 -13
- data/lib/rails_ai_context/tools/get_turbo_map.rb +15 -2
- data/lib/rails_ai_context/tools/get_view.rb +32 -7
- data/lib/rails_ai_context/tools/onboard.rb +4 -4
- data/lib/rails_ai_context/tools/performance_check.rb +3 -0
- data/lib/rails_ai_context/tools/query.rb +1 -1
- data/lib/rails_ai_context/tools/read_logs.rb +4 -4
- data/lib/rails_ai_context/tools/review_changes.rb +1 -1
- data/lib/rails_ai_context/tools/runtime_info.rb +1 -1
- data/lib/rails_ai_context/tools/search_code.rb +1 -1
- data/lib/rails_ai_context/tools/search_docs.rb +2 -2
- data/lib/rails_ai_context/tools/security_scan.rb +2 -2
- data/lib/rails_ai_context/tools/validate.rb +12 -12
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context.rb +43 -3
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0250405dffbfbb2100bf513cc75840ae980f68970e1a76911c4b2cc5ba3fac77
|
|
4
|
+
data.tar.gz: 25606a4264641a0a1be621bb30f3476c9ce1ecc1fd8dc37f7719ab15acee5e14
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
647
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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
|
-
|
|
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
|