rails-ai-context 5.21.3 → 5.24.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 +342 -0
- data/CONTEXT.md +35 -1
- data/docs/CLI.md +23 -0
- data/docs/COMPATIBILITY.md +26 -12
- data/docs/CONFIGURATION.md +1 -1
- data/docs/GUIDE.md +1 -1
- data/exe/rails-ai-context +65 -130
- data/lib/generators/rails_ai_context/install/install_generator.rb +41 -148
- data/lib/rails_ai_context/ast_cache.rb +14 -3
- data/lib/rails_ai_context/boot_manager.rb +17 -1
- data/lib/rails_ai_context/change_watch.rb +71 -0
- data/lib/rails_ai_context/concern_membership.rb +65 -0
- data/lib/rails_ai_context/configuration.rb +40 -8
- data/lib/rails_ai_context/doctor.rb +3 -3
- data/lib/rails_ai_context/engine.rb +5 -0
- data/lib/rails_ai_context/fingerprinter.rb +27 -4
- data/lib/rails_ai_context/install/ai_tool.rb +10 -1
- data/lib/rails_ai_context/install/cleanup.rb +1 -0
- data/lib/rails_ai_context/install/initializer_file.rb +32 -0
- data/lib/rails_ai_context/install/program.rb +149 -0
- data/lib/rails_ai_context/install/selection_record.rb +68 -0
- data/lib/rails_ai_context/introspectors/action_resolver.rb +146 -0
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +70 -143
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +3 -1
- data/lib/rails_ai_context/introspectors/declared_constant.rb +80 -0
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +93 -11
- data/lib/rails_ai_context/introspectors/job_introspector.rb +48 -20
- data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +11 -0
- data/lib/rails_ai_context/introspectors/migration_replay.rb +272 -0
- data/lib/rails_ai_context/introspectors/model_introspector.rb +93 -51
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/route_introspector.rb +3 -0
- data/lib/rails_ai_context/introspectors/schema_conventions.rb +101 -0
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +9 -487
- data/lib/rails_ai_context/introspectors/schema_reader.rb +69 -2
- data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +196 -0
- data/lib/rails_ai_context/introspectors/view_introspector.rb +2 -1
- data/lib/rails_ai_context/legacy_cleanup.rb +5 -0
- data/lib/rails_ai_context/live_reload.rb +37 -50
- data/lib/rails_ai_context/mcp_edge.rb +25 -3
- data/lib/rails_ai_context/middleware.rb +1 -13
- data/lib/rails_ai_context/payload.rb +113 -0
- data/lib/rails_ai_context/route_coverage.rb +29 -0
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +9 -13
- data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +8 -26
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +15 -2
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +6 -7
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +10 -16
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +6 -7
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +11 -14
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +5 -29
- data/lib/rails_ai_context/serializers/section_facts.rb +43 -0
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +63 -58
- data/lib/rails_ai_context/server.rb +4 -12
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +46 -132
- data/lib/rails_ai_context/tools/analyze_feature.rb +19 -10
- data/lib/rails_ai_context/tools/generate_test.rb +12 -4
- data/lib/rails_ai_context/tools/get_callbacks.rb +3 -4
- data/lib/rails_ai_context/tools/get_context.rb +2 -16
- data/lib/rails_ai_context/tools/get_controllers.rb +10 -5
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +3 -1
- data/lib/rails_ai_context/tools/get_helper_methods.rb +5 -0
- data/lib/rails_ai_context/tools/get_i18n.rb +50 -5
- data/lib/rails_ai_context/tools/get_model_details.rb +19 -11
- data/lib/rails_ai_context/tools/get_routes.rb +4 -8
- data/lib/rails_ai_context/tools/get_schema.rb +14 -17
- data/lib/rails_ai_context/tools/get_service_pattern.rb +2 -2
- data/lib/rails_ai_context/tools/get_stimulus.rb +4 -5
- data/lib/rails_ai_context/tools/get_test_info.rb +11 -1
- data/lib/rails_ai_context/tools/get_view.rb +2 -2
- data/lib/rails_ai_context/tools/onboard.rb +22 -37
- data/lib/rails_ai_context/tools/runtime_info.rb +21 -5
- data/lib/rails_ai_context/tools/search_code.rb +13 -2
- data/lib/rails_ai_context/tools/validate_semantics.rb +17 -14
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/watcher.rb +17 -32
- data/server.json +1 -1
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45c3818bdd2790c10c54e008bbbd22cf48b621195b89e869a1cd22e6f7e01e44
|
|
4
|
+
data.tar.gz: 6374c7cf2bad6c28a58216e96b94a36dfc71e2af6aff5095a0b98a4d843d4ec3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 29b3386f4b8638e8059c42a39558547f08e7a1d5ac21197d8c09ba62a319852af5e6c7f206a63eafe378e03eac233e24f3784eb7bdf14f4ca40ff86afd12c2d3
|
|
7
|
+
data.tar.gz: 94e6915c0fd3e9472bb5792d1cd866f9c41711143ce877e39bdd461e8dfcd40280cffabeca4b4974efbd2d3f52514d438530b73cd16df033e43215a6bb519eb3
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,348 @@ 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.24.0] - 2026-08-17
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
Defects found by a QA round against GitLab, OpenProject, Canvas LMS,
|
|
13
|
+
Discourse and Mastodon. Every one of them exits 0.
|
|
14
|
+
|
|
15
|
+
- **The commands that read the app degrade the way `tool` does.** `context`,
|
|
16
|
+
`inspect`, `facts`, `preset`, `watch` and `init` called a bare boot guard,
|
|
17
|
+
so on a repo you have just cloned - the case an agent most needs a
|
|
18
|
+
`CLAUDE.md` for, and the case least likely to boot - every tool answered
|
|
19
|
+
and the command that writes the files exited 1 having written nothing.
|
|
20
|
+
`init` was worse: it writes its config files first, so a boot failure left
|
|
21
|
+
the app half set up and called that a failure. They allow the static tier
|
|
22
|
+
now and each takes `--no-boot`; `doctor` still fails, because diagnosing
|
|
23
|
+
the boot is its job. Writing under `--no-boot` then exposed its own bug -
|
|
24
|
+
the context writer asked `Rails.application` for the output directory,
|
|
25
|
+
which raises `NameError` on the path where Rails is never loaded at all.
|
|
26
|
+
- **`tool --list` reads the app's config when the app cannot boot.** Boot is
|
|
27
|
+
what normally loads `.rails-ai-context.yml`, so the listing fell back to
|
|
28
|
+
the gem's defaults: on Mastodon it advertised 45 tools while the MCP server
|
|
29
|
+
offered 43 and the CLI itself answered `Unknown tool 'query'` for one it
|
|
30
|
+
had just listed.
|
|
31
|
+
- **`search_extensions` is documented as the fallback's list, which is what
|
|
32
|
+
it is.** Making ripgrep honour it did make the two backends agree, and it
|
|
33
|
+
cost the reach that makes the tool useful: on Mastodon `gem 'devise'` went
|
|
34
|
+
from 5 results to none, because a Gemfile carries no listed extension - and
|
|
35
|
+
the same for a Rakefile, a `.md`, a `.sql`. The docs, the attr comment and
|
|
36
|
+
the line the install generator writes now say plainly that the list is the
|
|
37
|
+
Ruby fallback's and that ripgrep searches every file.
|
|
38
|
+
- **A table the replay cannot name is not reported.** Canvas has a migration
|
|
39
|
+
that calls `create_table table_name do |t|` with a local computed at run
|
|
40
|
+
time; the replay kept the entry under a nil key, and the first serializer
|
|
41
|
+
to sort the table names took the whole context run down. Reachable only
|
|
42
|
+
once `context` could enter the static tier at all.
|
|
43
|
+
- **Coverage says nothing when there is nothing to measure against.** With a
|
|
44
|
+
`default_locale` the app configures but ships no file for, every locale
|
|
45
|
+
scored zero and all of them were named as untranslated.
|
|
46
|
+
- **A mixin in a nested concerns directory is not a model.** The skip only saw
|
|
47
|
+
the top-level `concerns/` that Rails autoloads, so OpenProject's
|
|
48
|
+
`app/models/queries/operators/concerns` contributed four mixins to a model
|
|
49
|
+
count of 978 where the app has 974. A nested `concerns/` is an ordinary
|
|
50
|
+
namespace, though, so the directory name alone does not decide it: a file
|
|
51
|
+
there that declares a class is a model like any other.
|
|
52
|
+
- **`docs/COMPATIBILITY.md` describes the static tier the gem actually has.**
|
|
53
|
+
It said 6 introspectors answer without a booted app and "the other 34 have
|
|
54
|
+
no static path", naming eight examples - all of which answer. The real
|
|
55
|
+
split is 23 files-only, 9 alternate-source and 8 runtime-only: 32 of 40.
|
|
56
|
+
A guard spec derives all three lists from `INTROSPECTOR_MAP`.
|
|
57
|
+
|
|
58
|
+
- **A controller is named by the constant its source declares.** Zeitwerk
|
|
59
|
+
resolves a path through the app's own inflector, which the static tier
|
|
60
|
+
never loads, so camelizing the path invented `Activitypub::` for the 12
|
|
61
|
+
controllers Mastodon declares as `ActivityPub::` and `Oauth::` for the 4
|
|
62
|
+
it declares as `OAuth::` - names that appear nowhere in an app that
|
|
63
|
+
registers those acronyms, and a `NameError` for anyone who uses one. The
|
|
64
|
+
path stays the answer where the source does not carry the whole name
|
|
65
|
+
(`application_cable/channel.rb`) and where the declared name does not name
|
|
66
|
+
that file, since Prism recovers a syntax error into a partial tree.
|
|
67
|
+
- **An interceptor is told from a mailer by its framework hook.**
|
|
68
|
+
`app/mailers` is also where ActionMailer interceptors live, and every `.rb`
|
|
69
|
+
under it counted: OpenProject's `Interceptors::DefaultHeaders` arrived as a
|
|
70
|
+
mailer with `delivering_email` - a hook the framework calls - listed as an
|
|
71
|
+
email an agent could send, 2 of its 11 entries. What an interceptor has and
|
|
72
|
+
a mailer does not is one of `delivering_email`, `previewing_email` or
|
|
73
|
+
`delivered_email`; requiring a class instead would have dropped GitLab's 20
|
|
74
|
+
`Emails::*` modules, which hold every notification it sends.
|
|
75
|
+
- **Migration replay reads what a migration does, not what it undoes.** A
|
|
76
|
+
`def down` says how to reverse the change, so replaying it alongside `up`
|
|
77
|
+
cancelled the migration out. On a migrations-only app the ordinary
|
|
78
|
+
up-creates/down-drops pair erased two tables OpenProject really has (35
|
|
79
|
+
to 37), and the reverse pair would have invented one it dropped. The same
|
|
80
|
+
now holds for the block spellings, `reversible { |dir| dir.down { ... } }`
|
|
81
|
+
and `revert`, and for the `t.timestamps` inside them - those were found by
|
|
82
|
+
a separate walk over the whole file, so a down body's timestamps landed on
|
|
83
|
+
the last table created on the way up.
|
|
84
|
+
- **i18n coverage groups the locales below its own rounding floor instead of
|
|
85
|
+
listing them.** A language-name lookup table under `config/locales`
|
|
86
|
+
contributes a top-level key per language, and Rails does load each as an
|
|
87
|
+
available locale. Scoring them produced a row each saying every key was
|
|
88
|
+
missing: on Discourse, 138 of 186 coverage rows described a translation
|
|
89
|
+
effort nobody had started. Those 138 are now summarised in one line, with a
|
|
90
|
+
shared key count stated once. The rows still exist - asking for one by name
|
|
91
|
+
answers with its numbers, so a translation genuinely started below the floor
|
|
92
|
+
is not written off as untranslated. The available list still matches a
|
|
93
|
+
booted Rails, and a locale's keys are read from its own file and any shared
|
|
94
|
+
file together, so one that lives outside the naming convention is scored.
|
|
95
|
+
- **A recorded tool selection no longer switches off `.ai-context.json`.**
|
|
96
|
+
The install generator always records one, and every later `rails
|
|
97
|
+
ai:context` passed that list to the serializer. The machine artifact is
|
|
98
|
+
not an AI tool and no install menu offers it, so it silently stopped being
|
|
99
|
+
written on the one install path the docs describe - while the generator
|
|
100
|
+
went on adding it to `.gitignore`.
|
|
101
|
+
- **`skip_tools` accepts the symbol spelling its neighbours use.** Every
|
|
102
|
+
other key in the generated initializer takes symbols, and `%i[]` here
|
|
103
|
+
skipped nothing at all: both readers compare against a tool name, which is
|
|
104
|
+
a String.
|
|
105
|
+
|
|
106
|
+
- **A controller carries the file it was read from.** Reading the name from
|
|
107
|
+
the source fixed the name and broke every path derived from it:
|
|
108
|
+
`ActivityPub::CollectionsController`.underscore is `activity_pub/...` and
|
|
109
|
+
the file is under `activitypub/`, so `rails_get_context` answered "Could
|
|
110
|
+
not extract source code" for a file that exists and asked the routes for a
|
|
111
|
+
controller key Rails does not use. Both tiers emit `file:` - the booted one
|
|
112
|
+
asks Ruby where the class was defined - and the consumers read it through
|
|
113
|
+
`Payload`.
|
|
114
|
+
- **The locale files are indexed once, not once per locale.** Asking every
|
|
115
|
+
file about every locale is O(locales x files): on Discourse, 187 over 108,
|
|
116
|
+
it took the i18n answer from 4.6 seconds to four and a half minutes. Each
|
|
117
|
+
entry also records the locales it serves, so asking for one by name finds
|
|
118
|
+
its files even when the filename spells the gem rather than the locale.
|
|
119
|
+
|
|
120
|
+
- **Every consumer that turns a controller name back into a path reads the
|
|
121
|
+
file it was read from.** Carrying `file:` fixed six of them and left five
|
|
122
|
+
answering a confident negative, which is the answer an agent acts on
|
|
123
|
+
without checking. On Mastodon's 18 inflected controllers: `rails_test_info`
|
|
124
|
+
reported 16 specs that exist as missing, `rails_get_routes` reported 4
|
|
125
|
+
routes that exist as absent, `rails_get_view` 2 views, and
|
|
126
|
+
`rails_generate_test` wrote a spec whose body was `skip "no routes found"`
|
|
127
|
+
for a controller with a route. `rails_analyze_feature` listed them as
|
|
128
|
+
untested. `Payload.controller_route_key` is the one derivation now, and a
|
|
129
|
+
spec drives all five tools with an inflected name.
|
|
130
|
+
- **The install path's files require their own stdlib.** `init` loads six
|
|
131
|
+
files before Rails and before the entry file, on purpose - so the entry
|
|
132
|
+
file's `require "set"` never runs for them. `legacy_cleanup`'s `[...].to_set`
|
|
133
|
+
raised `NoMethodError` on Ruby 3.1, where `Set` is not autoloaded, and 3.2+
|
|
134
|
+
hid it: only the CI matrix ever saw it.
|
|
135
|
+
- **`rails_runtime_info` reports cache numbers, not the cache object.** The
|
|
136
|
+
MemoryStore branch passed `cache.inspect` straight through, so the answer
|
|
137
|
+
carried `#<ActiveSupport::Cache::MemoryStore entries=0, size=0, options={...}>`
|
|
138
|
+
- the shape this gem has shipped as a defect before. Entries and size are
|
|
139
|
+
now facts, and a store that answers `#stats` gets its hash rendered as pairs
|
|
140
|
+
rather than a Ruby literal.
|
|
141
|
+
- **A model's table comes from its file, not from its name.** Naming a model by
|
|
142
|
+
the constant its source declares made `underscore` stop being the way back:
|
|
143
|
+
`OAuthClientConfig` underscores to `o_auth_client_config`, and the file is
|
|
144
|
+
`oauth_client_config.rb`. `rails_model_details` reported the table as
|
|
145
|
+
`o_auth_client_configs`, which no app has, so the columns section vanished
|
|
146
|
+
and the schema hint pointed at nothing - on Canvas that hit
|
|
147
|
+
`OAuthClientConfig`, `OAuthRequest`, `AuthenticationProvider::OAuth` and
|
|
148
|
+
`OAuth2`, on OpenProject `OAuthClient` and `OAuthClientToken`. The file's own
|
|
149
|
+
name already carries the inflection, because Zeitwerk resolved the constant
|
|
150
|
+
from it. `rails_generate_test`, `rails_get_callbacks`, `rails_test_info` and
|
|
151
|
+
the five `rails_validate` checks that key models by path read it too.
|
|
152
|
+
- **An abstract base class is not a model in the static tier either.** A
|
|
153
|
+
booted Rails rejects `abstract_class?`, and a namespaced base class is one:
|
|
154
|
+
GitLab's `Ci::ApplicationRecord`, `PackageMetadata::ApplicationRecord` and
|
|
155
|
+
`SecApplicationRecord` were counted, so the same app answered 895 models
|
|
156
|
+
without a boot and 888 with one. OpenProject: 974 against 971.
|
|
157
|
+
- **A model carries its file too, and is named by the constant its source
|
|
158
|
+
declares.** `rails_model_details` rebuilt `app/models/<underscored>.rb` from
|
|
159
|
+
the name in four places, so for a model in a pack or an engine the custom
|
|
160
|
+
validations, the source-defined methods, the method signatures and the class
|
|
161
|
+
structure all went quietly missing from an answer that otherwise looked
|
|
162
|
+
complete. The static tier also camelized the path into the name, so an app
|
|
163
|
+
registering an inflection got a constant it does not have - and one the
|
|
164
|
+
booted tier, which reads the real class, would never agree with.
|
|
165
|
+
- **`rails_validate_semantics` checks the views under an inflected directory.**
|
|
166
|
+
A view directory names the route key, so camelizing `app/views/oauth/` back
|
|
167
|
+
gave a constant the app never declares and the undefined-ivar check stopped
|
|
168
|
+
running for that whole tree without saying so.
|
|
169
|
+
- **A test-gap claim is not made from a scan that stopped early.**
|
|
170
|
+
`rails_analyze_feature` scans the first 500 files per glob; Discourse has
|
|
171
|
+
1,672 specs, so the scan never reached the ones covering the feature and
|
|
172
|
+
every controller in it was reported as having no test. The Tests section
|
|
173
|
+
disclosed the cap, the gaps section asserted through it.
|
|
174
|
+
- **"Global before_actions" in the generated files means global.** The scan
|
|
175
|
+
matched `skip_before_action` too, and ignored `only:` / `except:` / `if:`.
|
|
176
|
+
Mastodon's `CLAUDE.md` listed five, of which three were false - including
|
|
177
|
+
`verify_authenticity_token`, which that controller skips.
|
|
178
|
+
|
|
179
|
+
### Added
|
|
180
|
+
|
|
181
|
+
- **`--no-boot` on `context`, `inspect`, `facts`, `preset`, `watch` and
|
|
182
|
+
`init`.** The static tier was reachable from `tool` and `serve` only.
|
|
183
|
+
- **`--no-boot` reads a repo that has source but no `config/`.** An engine
|
|
184
|
+
keeps its dummy app under `spec/dummy`, so the guard against describing an
|
|
185
|
+
empty directory has to measure source, not boot files.
|
|
186
|
+
|
|
187
|
+
### Changed
|
|
188
|
+
|
|
189
|
+
- **The generated initializer has one indent from top to bottom.** The AI
|
|
190
|
+
Tools section was written at the configure body's indent and every section
|
|
191
|
+
after it flush, so the file an app commits changed indent two lines in and
|
|
192
|
+
stayed there.
|
|
193
|
+
|
|
194
|
+
## [5.23.0] - 2026-08-16
|
|
195
|
+
|
|
196
|
+
### Fixed
|
|
197
|
+
|
|
198
|
+
- **`AstCache.parse_string` caches, as its name always said.** Keyed by
|
|
199
|
+
content digest, sharing the store and its eviction; oversize sources
|
|
200
|
+
bypass instead of raising. A controller was parsed up to ten times per
|
|
201
|
+
static run because every extractor received the text and re-parsed it.
|
|
202
|
+
- **`rails_validate` says which checks it skipped.** When the AST parse
|
|
203
|
+
fails, the three rules with no regex twin used to vanish silently and a
|
|
204
|
+
partially-checked file read as clean; the response now names them. The
|
|
205
|
+
tool also gains its first spec file.
|
|
206
|
+
- **A downstream app exception is the app's again.** The middleware's
|
|
207
|
+
rescue used to convert any error from the app behind it - non-MCP
|
|
208
|
+
requests included - into an MCP error frame. `McpEdge.rack_call` now owns
|
|
209
|
+
the whole Rack-shaped request (path match, session scope, dispatch,
|
|
210
|
+
containment) for the middleware and the standalone server, and the
|
|
211
|
+
pass-through branch runs outside the rescue.
|
|
212
|
+
|
|
213
|
+
### Changed
|
|
214
|
+
|
|
215
|
+
- **The cache fingerprint watches what the resolvers read.** Packs,
|
|
216
|
+
engines, `extra_app_paths`, every concern home, the `Gemfile`,
|
|
217
|
+
`config/locales` and `config/environments` now feed the fingerprint, so
|
|
218
|
+
an edit there cannot serve a stale answer that looks fresh.
|
|
219
|
+
- **One change-detection loop.** `ChangeWatch` owns the watch list, the
|
|
220
|
+
Listen wiring, the fingerprint gate and the code reload; `Watcher` and
|
|
221
|
+
`LiveReload` keep only their reactions (regenerate files; refresh caches
|
|
222
|
+
and notify clients) and their own missing-`listen` policy.
|
|
223
|
+
- **The initializer guard and the `tool_mode` line each have one home.**
|
|
224
|
+
`Install::InitializerFile` holds the guard patterns the generator writes
|
|
225
|
+
and the doctor diagnoses; `SelectionRecord.write_tool_mode` replaces the
|
|
226
|
+
rake task's hand-rolled three-branch rewriter, and only rewrites an
|
|
227
|
+
uncommented line, so the generated commented-out default stays a comment.
|
|
228
|
+
- **Split-rule targets derive from the `Install::AiTool` table.** The
|
|
229
|
+
rules serializers read `rules_dir` (and the AGENTS.md pair) from the
|
|
230
|
+
table doctor and cleanup already read, so a moved path changes every
|
|
231
|
+
surface at once.
|
|
232
|
+
- **The stack helper writes through `SafeFile.atomic_write`** instead of a
|
|
233
|
+
private copy of it, and its app-tree scans take a root, so they are
|
|
234
|
+
covered by specs for the first time.
|
|
235
|
+
- **Every serializer is proven against a context real introspectors
|
|
236
|
+
produced** (`IntrospectedFixture`), so no serializer can read a shape
|
|
237
|
+
production does not make - the class of defect that kept the engines and
|
|
238
|
+
turbo sections dead while hand-built fixtures stayed green.
|
|
239
|
+
|
|
240
|
+
## [5.22.0] - 2026-08-16
|
|
241
|
+
|
|
242
|
+
### Fixed
|
|
243
|
+
|
|
244
|
+
- **A class nested inside a controller, mailer, model or helper file no
|
|
245
|
+
longer contributes its methods to the host's interface.** "Which public
|
|
246
|
+
methods are this class's own" had five transcriptions with five filter
|
|
247
|
+
sets; they now share `ActionResolver`, which filters listener output by the
|
|
248
|
+
owner field the listener always emitted, walks app-owned ancestors, and
|
|
249
|
+
falls back to reflection with the #136 base subtraction. Mailers had kept
|
|
250
|
+
the pre-#136 reflection answer, so a public helper on a non-abstract
|
|
251
|
+
`ApplicationMailer` arrived as a deliverable action; they subtract now, and
|
|
252
|
+
static mailer actions apply the same underscore rule as controllers. (#149)
|
|
253
|
+
|
|
254
|
+
- **Every generated context file states each fact through one renderer.**
|
|
255
|
+
`SectionFacts` owns the models line, the database line, the associations
|
|
256
|
+
list (eight copies, three of them in tools) and the warnings section -
|
|
257
|
+
which now renders in AGENTS.md and .github/copilot-instructions.md too, so
|
|
258
|
+
a half-failed run cannot look clean in two of four files. AGENTS.md stops
|
|
259
|
+
shadowing the shared architecture and footer renderers with poorer copies,
|
|
260
|
+
and its controller list goes through the shared renderer with named
|
|
261
|
+
actions as a depth choice rather than a fork. `Payload` is the reading
|
|
262
|
+
side of the introspection hash: list readers whose key pairs a spec pins
|
|
263
|
+
against the producing introspector's real output, so the next renamed key
|
|
264
|
+
fails a test instead of silently emptying a section. (#151)
|
|
265
|
+
- **API-only apps are told "does not apply", not "not found".**
|
|
266
|
+
`rails_get_helper_methods` and `rails_get_frontend_stack` now consult the
|
|
267
|
+
api-only note like the other eleven call sites. The two hand-rolled
|
|
268
|
+
paginations (`rails_get_schema`, `rails_get_stimulus`) go through the
|
|
269
|
+
shared `paginate`, one offset/limit behavior across all fourteen paginated
|
|
270
|
+
surfaces. (#152)
|
|
271
|
+
- **The interactive install is one program with three voices.** The
|
|
272
|
+
generator, the standalone binary and the rake task each held a full copy
|
|
273
|
+
of the prompts, the removed-tool cleanup, the gitignore append and the
|
|
274
|
+
MCP-config write - about 470 lines that had already drifted: two labels
|
|
275
|
+
for one mode, a menu with file lists in two entries and bare names in the
|
|
276
|
+
third, a byte-identical gitignore block three times. `Install::Program`
|
|
277
|
+
now owns the steps and the wording; each entry supplies its say/ask
|
|
278
|
+
surface and keeps only its closing instructions. The rake menu shows each
|
|
279
|
+
tool's files, and every entry uses one mode label.
|
|
280
|
+
- **Configuration loads the same way on every entry point.** The engine now
|
|
281
|
+
runs `auto_load!` at boot, so `.rails-ai-context.yml` works on the rake
|
|
282
|
+
tasks, the middleware and the engine controller the way the docs always
|
|
283
|
+
said - initializer first, the loader steps aside when a configure block
|
|
284
|
+
ran. `Configuration#ai_tools` answers from `SelectionRecord` when nothing
|
|
285
|
+
set it, so `rails ai:context` stops re-asking a question the record
|
|
286
|
+
already answers, and the recorded `tool_mode` reaches in-app surfaces
|
|
287
|
+
through the record's new reader. A bad YAML value now warns and keeps the
|
|
288
|
+
default the way bad syntax always did, instead of dying raw out of the
|
|
289
|
+
CLI. (#147, #148)
|
|
290
|
+
- **The three static schema sources follow one set of conventions, and every
|
|
291
|
+
schema question can be asked of any of them.** `SchemaReader.for(root)`
|
|
292
|
+
chooses schema.rb, structure.sql or migration replay behind one
|
|
293
|
+
question-level seam, so introspectors asking "does this table carry this
|
|
294
|
+
column" now get answers on structure.sql and migrations-only apps instead
|
|
295
|
+
of nothing. The migration replay is repaired: a replayed `create_table`
|
|
296
|
+
seeds its implicit `id`, `change_column_null` applies in both directions
|
|
297
|
+
(the listener now captures the positional boolean), and `t.references` is
|
|
298
|
+
typed per adapter instead of hardcoded bigint. The conventions live in
|
|
299
|
+
`SchemaConventions`, shared by all three sources the way #140's
|
|
300
|
+
foreign-key fix pioneered; the SQL parser and the replay engine moved to
|
|
301
|
+
`StructureSqlReader` and `MigrationReplay`, and the schema introspector
|
|
302
|
+
halves in size, keeping only presentation. (#150)
|
|
303
|
+
- **Every surface answers "which concerns count" and "how many routes" the
|
|
304
|
+
same way.** Five renderers carried five concern filter sets:
|
|
305
|
+
`rails_get_callbacks` hid every namespaced concern unless it started with
|
|
306
|
+
"App", `rails_analyze_feature` hid all of them, and two ignored the
|
|
307
|
+
`excluded_concerns` config. `ConcernMembership` now decides the payload
|
|
308
|
+
sense once, at the introspector seam, for both tiers - the static
|
|
309
|
+
controller answer goes through `MixinsListener`, so `prepend` counts and a
|
|
310
|
+
singleton-class `include` does not, exactly as booted. Route populations
|
|
311
|
+
(app vs framework split, PUT/PATCH dedup) moved beside the coverage suffix
|
|
312
|
+
into `RouteCoverage`, replacing four hand-kept computations, one of them a
|
|
313
|
+
wholesale copy. CLAUDE.md's per-model concern list now names the concerns
|
|
314
|
+
the app defines, namespaced ones included.
|
|
315
|
+
- **Mounted engines render again in `rails_onboard` and the context files.**
|
|
316
|
+
`EngineIntrospector` emits `:mounted_engines`; the tool read `:engines` then
|
|
317
|
+
`:mounted`, and the stack line read `:mounted`, so both fell back to "none"
|
|
318
|
+
forever. Both now read the introspector's keys and the element's real
|
|
319
|
+
`engine:`/`path:` shape, and the spec fixtures use that shape instead of one
|
|
320
|
+
production never produced. (#144)
|
|
321
|
+
- **The Hotwire stack line and `rails_onboard`'s Real-Time and Frontend
|
|
322
|
+
sections read turbo keys that exist.** `turbo[:frames]`, `[:streams]` and
|
|
323
|
+
`[:broadcasts]` were never emitted (`:turbo_frames`, `:turbo_streams` and
|
|
324
|
+
`:model_broadcasts` are), so the line was dead everywhere and Real-Time
|
|
325
|
+
always fell through to its view-scan fallback. (#145)
|
|
326
|
+
- **`rails_get_context` no longer offers a Related Services section it could
|
|
327
|
+
never fill.** The `:services` key it read has no introspector behind it; the
|
|
328
|
+
dead branch is gone. (#146)
|
|
329
|
+
- **`rails ai:context` stops re-prompting a YAML-configured app on every
|
|
330
|
+
run.** The rake path now loads `.rails-ai-context.yml` the way the
|
|
331
|
+
standalone binary always did; the initializer still wins when its configure
|
|
332
|
+
block ran. (#147)
|
|
333
|
+
- **The standalone binary's `--format` help names opencode and codex.** A
|
|
334
|
+
guard spec pins the help text to `Install::AiTool`, which the Thor class
|
|
335
|
+
body cannot ask at definition time. (#153)
|
|
336
|
+
- **`rails ai:serve` and `rails ai:serve_http` boot under the same timeout
|
|
337
|
+
and rescue as the binary.** A hanging initializer used to hang the task
|
|
338
|
+
forever where `rails-ai-context serve` gives up at 60s; both tasks now boot
|
|
339
|
+
through `BootManager.guard`, and `RAILS_AI_CONTEXT_BOOT_TIMEOUT` is parsed
|
|
340
|
+
in one place. (#154)
|
|
341
|
+
- **doctor's tool list and `rails_search_code`'s exclusion list derive from
|
|
342
|
+
`Install::AiTool`.** Both were hand-typed restatements sitting below the
|
|
343
|
+
ownership spec's detection floor; the search exclusions now also cover the
|
|
344
|
+
generated `app/models/AGENTS.md` pair they previously missed. (#155)
|
|
345
|
+
- **An e2e example asserts `call` and `static_call` answer the same keys.**
|
|
346
|
+
The declaration contract was already enforced (ADR-0002); the shape contract
|
|
347
|
+
was not, so a key added to the booted tier could silently vanish from the
|
|
348
|
+
static one. (#156)
|
|
349
|
+
|
|
8
350
|
## [5.21.3] - 2026-08-12
|
|
9
351
|
|
|
10
352
|
### Fixed
|
data/CONTEXT.md
CHANGED
|
@@ -28,14 +28,48 @@ Two senses, one per module, and neither is bare "path" in a name.
|
|
|
28
28
|
|
|
29
29
|
**How a path is written down** - `PortablePath` rewrites one so it means the same thing on another machine, because what it touches ends up in `.ai-context.json` and the app commits that file. App paths go app-relative, gem paths keep the gem and version and drop the install prefix. "Relativize" always means this.
|
|
30
30
|
|
|
31
|
+
## Declared constant
|
|
32
|
+
|
|
33
|
+
What a source file calls its own class, as opposed to the **path name** - the constant its path camelizes to. The two differ wherever the app registers an inflection, because Zeitwerk resolves a path through the app's own inflector and the static tier has never loaded it: `app/controllers/activitypub/` is `ActivityPub` in Mastodon, and `Oauth` is a constant nothing defines. `DeclaredConstant` reads the class the source declares, and since an inflection only ever changes case, the declaration that names a file is the one equal to the path name ignoring case. Anything else - a second class in the file, a nested error class, a tree Prism recovered from a syntax error - is not this file's class, and there the path name stays the answer: it is the only thing carrying the namespace when the source does not.
|
|
34
|
+
|
|
35
|
+
Distinct from the **Static tier** sense of "declared": that one is about declaring a tier's capability up front rather than detecting it at runtime. This one is about a constant's spelling.
|
|
36
|
+
|
|
37
|
+
The other half of the same problem is the reverse trip. Once a name is the declared one, no consumer can rebuild the path from it - and a pack or an in-repo engine breaks that derivation too, inflection or not. So controllers and models carry `file:` from whichever tier found them, and consumers read it through `Payload` (`controller_file`, `controller_route_key`, `controller_for_route_key`, `model_file`). The rule: a tool that needs a path for a name reads it, never underscores it.
|
|
38
|
+
|
|
31
39
|
## Static tier
|
|
32
40
|
|
|
33
41
|
The mode where the app did not boot, or `--no-boot` was passed. What an introspector answers here is what it declared, never what a runtime check happened to detect. Every introspector in `INTROSPECTOR_MAP` extends `StaticTier` and names one of three kinds:
|
|
34
42
|
|
|
35
|
-
**files-only** - `call` runs unchanged against the static app handle, because it only ever read files. Most of the map.
|
|
43
|
+
**files-only** - `call` runs unchanged against the static app handle (`StaticApp`), because it only ever read files. Most of the map.
|
|
36
44
|
|
|
37
45
|
**alternate-source** - `static_call` answers from a different source than `call` does: `db/schema.rb` instead of the connection, `config/routes.rb` instead of the route set.
|
|
38
46
|
|
|
39
47
|
**runtime-only** - needs `app.config`, `app.routes` or a live database, and refuses honestly.
|
|
40
48
|
|
|
41
49
|
An undeclared introspector fails the suite, and every files-only declaration is proven against `spec/fixtures/static_app` with nothing booted. Declaring files-only while defining `static_call` (or the reverse) is also a spec failure.
|
|
50
|
+
|
|
51
|
+
## Concern
|
|
52
|
+
|
|
53
|
+
Three senses inside the gem, and the payload one is wider than either everyday Rails reading.
|
|
54
|
+
|
|
55
|
+
**Concern (as payload)** - a module in a class's ancestor chain, minus framework noise. Not only `ActiveSupport::Concern`, and not only files under a concerns directory: anything reached by `include` or `prepend` counts, which is why `extend` and a singleton-class `include` do not. This is what `:concerns` holds in model and controller data and what a "Concerns" section renders. The booted tier reads `ancestors`, so it also sees what the superclass and other concerns pulled in; the static tier sees the class's own file alone. `ConcernMembership` decides membership for both tiers; renderers trust the payload rather than re-filtering.
|
|
56
|
+
|
|
57
|
+
**Concerns directory** - `app/*/concerns` as a place, glob-derived the way Rails autoloads it, never a hardcoded list. `ConcernPaths` is the one answer to "where does this app keep its concerns" for every surface that lists or counts them.
|
|
58
|
+
|
|
59
|
+
**Mixin** - the static detection feeding the payload: any `include`, `prepend` or `extend` with a constant argument, as `MixinsListener` reports it. Named mixin because it captures more than concerns; its `ancestor` flag marks the subset that becomes one.
|
|
60
|
+
|
|
61
|
+
## Action
|
|
62
|
+
|
|
63
|
+
The callable interface of a class as this gem reports it: the class's own public instance methods - a class nested in the same file is a separate owner, not part of the interface - minus framework-shaped `_` names, read source-first, with reflection minus the app-owned base as the honest fallback. `ActionResolver` is the one answer; controller and mailer are configurations of it, and a channel's "stream methods" are a narrower selection of the same reading.
|
|
64
|
+
|
|
65
|
+
## Payload
|
|
66
|
+
|
|
67
|
+
The introspection context hash, in the sense consumers read it. `Payload` is the reading side: a section guard and pinned list readers whose key pairs a spec checks against the producing introspector's own output, so a renamed key fails a test instead of silently emptying a section everywhere. Facts rendered from it live once (`SectionFacts`); membership judgments are applied at the introspector seam, and readers trust the payload rather than re-filtering it.
|
|
68
|
+
|
|
69
|
+
## Standalone
|
|
70
|
+
|
|
71
|
+
Two senses on different axes. Qualify it; the bare word does not say which.
|
|
72
|
+
|
|
73
|
+
**Standalone install** - how the gem got installed: `gem install rails-ai-context`, driven through its own binary, never in the host app's Gemfile, so the `rails ai:*` rake tasks do not exist. `InstallMode.standalone?` is the decider, and the `standalone:` flag on `McpConfigGenerator` means this - it picks whether the written MCP command is the bare binary or a bundled invocation. `docs/STANDALONE.md` documents this sense only.
|
|
74
|
+
|
|
75
|
+
**Standalone server** - how MCP is served: the entry point that is its own process, `rails ai:serve_http` starting `Server` with `transport: :http`, serving MCP and nothing else. The middleware and the engine controller answer the same requests from inside the app's web server; the standalone server does not need one running. It says nothing about the install - an in-Gemfile app starts it from a rake task. ADR-0003 and `McpEdge` use this sense.
|
data/docs/CLI.md
CHANGED
|
@@ -37,6 +37,7 @@ rails-ai-context serve --transport http --port 6029 # HTTP transport
|
|
|
37
37
|
|:-------|:--------|:------------|
|
|
38
38
|
| `--transport` | `stdio` | `stdio` or `http` |
|
|
39
39
|
| `--port` | `6029` | HTTP listen port |
|
|
40
|
+
| `--no-boot` | off | Skip booting the app; answer from source alone |
|
|
40
41
|
|
|
41
42
|
### `tool`
|
|
42
43
|
|
|
@@ -58,6 +59,28 @@ rails-ai-context tool model_details --model User
|
|
|
58
59
|
|:-------|:------------|
|
|
59
60
|
| `--list` | List all available tools |
|
|
60
61
|
| `--json` | Output as JSON |
|
|
62
|
+
| `--no-boot` | Skip booting the app; answer from source alone |
|
|
63
|
+
|
|
64
|
+
### The static tier and `--no-boot`
|
|
65
|
+
|
|
66
|
+
Every command that reads the app takes `--no-boot`: `tool`, `serve`, `context`,
|
|
67
|
+
`inspect`, `facts`, `preset`, `watch` and `init`. It skips the boot and answers
|
|
68
|
+
from source alone, which is what you want on a repo you have just cloned, on an
|
|
69
|
+
app whose boot is broken, and in CI where booting costs more than the answer.
|
|
70
|
+
|
|
71
|
+
The same tier is entered automatically when a boot fails, so you get an answer
|
|
72
|
+
either way. Answers that need a running app are marked `[UNAVAILABLE: ...]`
|
|
73
|
+
rather than guessed, and everything else is tagged `[STATIC]` instead of
|
|
74
|
+
`[VERIFIED]`. `docs/COMPATIBILITY.md` lists which of the 40 introspectors answer
|
|
75
|
+
in which tier.
|
|
76
|
+
|
|
77
|
+
`doctor` is the exception: diagnosing the boot is its job, so it refuses
|
|
78
|
+
`--no-boot` and still exits 1 when the app cannot start.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
rails-ai-context tool models --no-boot # no boot, no database, no Gemfile
|
|
82
|
+
rails-ai-context context --no-boot # writes CLAUDE.md from source
|
|
83
|
+
```
|
|
61
84
|
|
|
62
85
|
### Tool name resolution
|
|
63
86
|
|
data/docs/COMPATIBILITY.md
CHANGED
|
@@ -70,23 +70,37 @@ Two tiers, both reachable over the CLI and MCP (stdio and HTTP):
|
|
|
70
70
|
(ActiveRecord connections, `Rails.application.routes`, loaded classes) plus the
|
|
71
71
|
Prism AST layer for source-level facts (scopes, callbacks, strong params).
|
|
72
72
|
|
|
73
|
-
**STATIC** - the app didn't boot, or `--no-boot` was passed.
|
|
74
|
-
|
|
73
|
+
**STATIC** - the app didn't boot, or `--no-boot` was passed. 32 of the 40
|
|
74
|
+
introspectors answer here; each declares which of three kinds it is
|
|
75
|
+
(ADR-0002), so what a tier can say is a declaration rather than a guess.
|
|
76
|
+
|
|
77
|
+
**files-only** (23) run unchanged against the static app handle, because they
|
|
78
|
+
only ever read files: `gems`, `views`, `view_templates`, `turbo`, `stimulus`,
|
|
79
|
+
`active_storage`, `action_text`, `auth`, `tests`, `rake_tasks`, `assets`,
|
|
80
|
+
`devops`, `action_mailbox`, `migrations`, `seeds`, `middleware`, `env_config`,
|
|
81
|
+
`multi_database`, `components`, `performance`, `frontend_frameworks`,
|
|
82
|
+
`credentials` and `env`.
|
|
83
|
+
|
|
84
|
+
**alternate-source** (9) have a `static_call` that reads a different source
|
|
85
|
+
from the booted path:
|
|
75
86
|
|
|
76
87
|
| Introspector | Static source |
|
|
77
88
|
|:---|:---|
|
|
78
|
-
| `schema` | `db/schema.rb` / `db/structure.sql` / migration
|
|
79
|
-
| `migrations` | migration file list + `db/structure.sql`'s trailing `schema_migrations` insert |
|
|
80
|
-
| `routes` | `config/routes.rb` parsed with a dedicated Prism listener |
|
|
89
|
+
| `schema` | `db/schema.rb` / `db/structure.sql` / migration replay |
|
|
81
90
|
| `models` | `app/models/**/*.rb` (plus packs/engines/extra paths) parsed, not constantized |
|
|
91
|
+
| `routes` | `config/routes.rb` parsed with a dedicated Prism listener |
|
|
82
92
|
| `controllers` | `app/controllers/**/*.rb` (plus packs/engines/extra paths) parsed, not constantized |
|
|
83
|
-
| `
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
| `jobs` | `app/jobs`, `app/mailers` and `app/channels` parsed for classes and their public methods |
|
|
94
|
+
| `i18n` | every top-level key across `config/locales`, and the default locale read from `config/` |
|
|
95
|
+
| `api` | serializers, API controllers and route constraints read from source |
|
|
96
|
+
| `engines` | `config/routes.rb` mounts, plus the Gemfile |
|
|
97
|
+
| `active_support` | concern and core-extension use read from source |
|
|
98
|
+
|
|
99
|
+
**runtime-only** (8) report `{ unavailable: reason }` here, and only these:
|
|
100
|
+
`conventions`, `database_stats`, `config`, `initializers`, `autoload`,
|
|
101
|
+
`connection_pool`, `security` and `observability`. The fallback message comes
|
|
102
|
+
from `Introspector#run_introspector` (`lib/rails_ai_context/introspector.rb`)
|
|
103
|
+
and is the same whatever put the gem in the static tier.
|
|
90
104
|
|
|
91
105
|
`doctor` never enters the static tier - its job is diagnosing why boot failed,
|
|
92
106
|
so it always requires a bootable app.
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -113,7 +113,7 @@ preset: full
|
|
|
113
113
|
|:-------|:-----|:--------|:------------|
|
|
114
114
|
| `max_search_results` | Integer | `200` | Maximum search results |
|
|
115
115
|
| `max_validate_files` | Integer | `50` | Maximum files for validation |
|
|
116
|
-
| `search_extensions` | Array | `["rb", "js", "erb", "yml", "yaml", "json", "ts", "tsx", "vue", "svelte", "haml", "slim"]` | File extensions
|
|
116
|
+
| `search_extensions` | Array | `["rb", "js", "erb", "yml", "yaml", "json", "ts", "tsx", "vue", "svelte", "haml", "slim"]` | File extensions the Ruby fallback searches (ripgrep, when installed, searches every file) |
|
|
117
117
|
| `concern_paths` | Array | `nil` (discovers `app/*/concerns`) | Paths to scan for concerns. Setting it replaces discovery, so it can narrow as well as widen |
|
|
118
118
|
| `frontend_paths` | Array | `nil` (auto-detect) | Override frontend file paths |
|
|
119
119
|
| `extra_app_paths` | Array | `[]` | Extra directories under the app root to treat as application code |
|
data/docs/GUIDE.md
CHANGED
|
@@ -1353,7 +1353,7 @@ end
|
|
|
1353
1353
|
| `excluded_concerns` | Array | framework regex patterns | Regex patterns for concerns to hide from model output |
|
|
1354
1354
|
| `excluded_filters` | Array | `verify_authenticity_token`, etc. | Framework filter names hidden from controller output |
|
|
1355
1355
|
| `excluded_middleware` | Array | standard Rails middleware | Default middleware hidden from config output |
|
|
1356
|
-
| `search_extensions` | Array | `rb js erb yml yaml json ts tsx vue svelte haml slim` | File extensions
|
|
1356
|
+
| `search_extensions` | Array | `rb js erb yml yaml json ts tsx vue svelte haml slim` | File extensions the Ruby fallback searches (ripgrep, when installed, searches every file) |
|
|
1357
1357
|
| `concern_paths` | Array | `nil` (discovers `app/*/concerns`) | Where to look for concern source files. Setting it replaces discovery |
|
|
1358
1358
|
|
|
1359
1359
|
### Root file generation
|