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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +342 -0
  3. data/CONTEXT.md +35 -1
  4. data/docs/CLI.md +23 -0
  5. data/docs/COMPATIBILITY.md +26 -12
  6. data/docs/CONFIGURATION.md +1 -1
  7. data/docs/GUIDE.md +1 -1
  8. data/exe/rails-ai-context +65 -130
  9. data/lib/generators/rails_ai_context/install/install_generator.rb +41 -148
  10. data/lib/rails_ai_context/ast_cache.rb +14 -3
  11. data/lib/rails_ai_context/boot_manager.rb +17 -1
  12. data/lib/rails_ai_context/change_watch.rb +71 -0
  13. data/lib/rails_ai_context/concern_membership.rb +65 -0
  14. data/lib/rails_ai_context/configuration.rb +40 -8
  15. data/lib/rails_ai_context/doctor.rb +3 -3
  16. data/lib/rails_ai_context/engine.rb +5 -0
  17. data/lib/rails_ai_context/fingerprinter.rb +27 -4
  18. data/lib/rails_ai_context/install/ai_tool.rb +10 -1
  19. data/lib/rails_ai_context/install/cleanup.rb +1 -0
  20. data/lib/rails_ai_context/install/initializer_file.rb +32 -0
  21. data/lib/rails_ai_context/install/program.rb +149 -0
  22. data/lib/rails_ai_context/install/selection_record.rb +68 -0
  23. data/lib/rails_ai_context/introspectors/action_resolver.rb +146 -0
  24. data/lib/rails_ai_context/introspectors/controller_introspector.rb +70 -143
  25. data/lib/rails_ai_context/introspectors/convention_introspector.rb +3 -1
  26. data/lib/rails_ai_context/introspectors/declared_constant.rb +80 -0
  27. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +93 -11
  28. data/lib/rails_ai_context/introspectors/job_introspector.rb +48 -20
  29. data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +11 -0
  30. data/lib/rails_ai_context/introspectors/migration_replay.rb +272 -0
  31. data/lib/rails_ai_context/introspectors/model_introspector.rb +93 -51
  32. data/lib/rails_ai_context/introspectors/performance_introspector.rb +1 -1
  33. data/lib/rails_ai_context/introspectors/route_introspector.rb +3 -0
  34. data/lib/rails_ai_context/introspectors/schema_conventions.rb +101 -0
  35. data/lib/rails_ai_context/introspectors/schema_introspector.rb +9 -487
  36. data/lib/rails_ai_context/introspectors/schema_reader.rb +69 -2
  37. data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +196 -0
  38. data/lib/rails_ai_context/introspectors/view_introspector.rb +2 -1
  39. data/lib/rails_ai_context/legacy_cleanup.rb +5 -0
  40. data/lib/rails_ai_context/live_reload.rb +37 -50
  41. data/lib/rails_ai_context/mcp_edge.rb +25 -3
  42. data/lib/rails_ai_context/middleware.rb +1 -13
  43. data/lib/rails_ai_context/payload.rb +113 -0
  44. data/lib/rails_ai_context/route_coverage.rb +29 -0
  45. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +9 -13
  46. data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +8 -26
  47. data/lib/rails_ai_context/serializers/context_file_serializer.rb +15 -2
  48. data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +6 -7
  49. data/lib/rails_ai_context/serializers/copilot_serializer.rb +10 -16
  50. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +6 -7
  51. data/lib/rails_ai_context/serializers/markdown_serializer.rb +1 -1
  52. data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +11 -14
  53. data/lib/rails_ai_context/serializers/opencode_serializer.rb +5 -29
  54. data/lib/rails_ai_context/serializers/section_facts.rb +43 -0
  55. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +63 -58
  56. data/lib/rails_ai_context/server.rb +4 -12
  57. data/lib/rails_ai_context/tasks/rails_ai_context.rake +46 -132
  58. data/lib/rails_ai_context/tools/analyze_feature.rb +19 -10
  59. data/lib/rails_ai_context/tools/generate_test.rb +12 -4
  60. data/lib/rails_ai_context/tools/get_callbacks.rb +3 -4
  61. data/lib/rails_ai_context/tools/get_context.rb +2 -16
  62. data/lib/rails_ai_context/tools/get_controllers.rb +10 -5
  63. data/lib/rails_ai_context/tools/get_frontend_stack.rb +3 -1
  64. data/lib/rails_ai_context/tools/get_helper_methods.rb +5 -0
  65. data/lib/rails_ai_context/tools/get_i18n.rb +50 -5
  66. data/lib/rails_ai_context/tools/get_model_details.rb +19 -11
  67. data/lib/rails_ai_context/tools/get_routes.rb +4 -8
  68. data/lib/rails_ai_context/tools/get_schema.rb +14 -17
  69. data/lib/rails_ai_context/tools/get_service_pattern.rb +2 -2
  70. data/lib/rails_ai_context/tools/get_stimulus.rb +4 -5
  71. data/lib/rails_ai_context/tools/get_test_info.rb +11 -1
  72. data/lib/rails_ai_context/tools/get_view.rb +2 -2
  73. data/lib/rails_ai_context/tools/onboard.rb +22 -37
  74. data/lib/rails_ai_context/tools/runtime_info.rb +21 -5
  75. data/lib/rails_ai_context/tools/search_code.rb +13 -2
  76. data/lib/rails_ai_context/tools/validate_semantics.rb +17 -14
  77. data/lib/rails_ai_context/version.rb +1 -1
  78. data/lib/rails_ai_context/watcher.rb +17 -32
  79. data/server.json +1 -1
  80. metadata +12 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8afb263c0004a822a803eee097300e87dcb5a3a2de27d996a3cc5f11ea801051
4
- data.tar.gz: 462a8ca8d7f4a66cd617af71322356908f513aa88ac7aec2046f32cf4279805f
3
+ metadata.gz: 45c3818bdd2790c10c54e008bbbd22cf48b621195b89e869a1cd22e6f7e01e44
4
+ data.tar.gz: 6374c7cf2bad6c28a58216e96b94a36dfc71e2af6aff5095a0b98a4d843d4ec3
5
5
  SHA512:
6
- metadata.gz: 2f48e61abb15f942c9a3b41eced790ce41618f091e337d84c1d598486e74e3a58e3331c9acf971ad2fea0d0a0bb8a909caef4c6b9fd3196fba51dbe318532e97
7
- data.tar.gz: 07e4b2759758da12d51805c0861e8a101fcdefdff55cba0db7714c6464473e486842fbd93c55b6480c8ce7b87325138fa18eb0920c38e219f0df3556a1ec0b19
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
 
@@ -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. Only introspectors
74
- that define a `static_call` path can answer without a booted app:
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 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 |
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
- | `env_config` | `config/environments/*.rb` read from disk - file-based, so the static tier serves the same data as a booted app |
84
-
85
- The other 34 introspectors (views, jobs, gems, turbo, i18n, active_storage,
86
- auth, api, and the rest) have no static path and report `{ unavailable: reason
87
- }` in this tier - by construction, not by shape: `Introspector#run_introspector`
88
- (`lib/rails_ai_context/introspector.rb`) falls back to the same message
89
- regardless of what triggered the static tier.
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.
@@ -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 to search |
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 for Ruby fallback search |
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