rails-ai-context 5.24.0 → 5.25.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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +251 -0
  3. data/CONTEXT.md +18 -0
  4. data/CONTRIBUTING.md +8 -0
  5. data/README.md +195 -467
  6. data/docs/ARCHITECTURE.md +26 -4
  7. data/docs/COMPATIBILITY.md +9 -6
  8. data/docs/CONFIGURATION.md +3 -3
  9. data/docs/FAQ.md +1 -1
  10. data/docs/GUIDE.md +8 -6
  11. data/docs/INTROSPECTORS.md +7 -6
  12. data/docs/TOOLS.md +4 -1
  13. data/exe/rails-ai-context +45 -260
  14. data/gemfiles/mcp_floor.gemfile +13 -0
  15. data/gemfiles/prism_floor.gemfile +1 -10
  16. data/gemfiles/prism_head.gemfile +1 -10
  17. data/gemfiles/rails.gemfile +34 -0
  18. data/lib/generators/rails_ai_context/install/install_generator.rb +2 -6
  19. data/lib/rails_ai_context/action_filters.rb +145 -0
  20. data/lib/rails_ai_context/app_kind.rb +2 -7
  21. data/lib/rails_ai_context/ast_cache.rb +0 -8
  22. data/lib/rails_ai_context/change_watch.rb +6 -15
  23. data/lib/rails_ai_context/cli/entry_boot.rb +214 -0
  24. data/lib/rails_ai_context/cli/tool_runner.rb +1 -1
  25. data/lib/rails_ai_context/concern_membership.rb +8 -1
  26. data/lib/rails_ai_context/configuration.rb +64 -9
  27. data/lib/rails_ai_context/doctor.rb +24 -17
  28. data/lib/rails_ai_context/engine.rb +6 -5
  29. data/lib/rails_ai_context/facts_formatter.rb +8 -11
  30. data/lib/rails_ai_context/fingerprinter.rb +48 -24
  31. data/lib/rails_ai_context/gem_lock.rb +97 -0
  32. data/lib/rails_ai_context/hydrators/controller_hydrator.rb +2 -9
  33. data/lib/rails_ai_context/hydrators/model_hints.rb +28 -0
  34. data/lib/rails_ai_context/hydrators/schema_hint_builder.rb +1 -8
  35. data/lib/rails_ai_context/hydrators/view_hydrator.rb +2 -9
  36. data/lib/rails_ai_context/install_mode.rb +3 -7
  37. data/lib/rails_ai_context/introspector.rb +21 -2
  38. data/lib/rails_ai_context/introspectors/action_resolver.rb +119 -2
  39. data/lib/rails_ai_context/introspectors/action_text_introspector.rb +2 -6
  40. data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +6 -18
  41. data/lib/rails_ai_context/introspectors/active_support_introspector.rb +1 -0
  42. data/lib/rails_ai_context/introspectors/api_introspector.rb +26 -36
  43. data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +9 -13
  44. data/lib/rails_ai_context/introspectors/auth_introspector.rb +18 -45
  45. data/lib/rails_ai_context/introspectors/config_introspector.rb +17 -22
  46. data/lib/rails_ai_context/introspectors/controller_introspector.rb +29 -55
  47. data/lib/rails_ai_context/introspectors/convention_introspector.rb +34 -74
  48. data/lib/rails_ai_context/introspectors/declared_constant.rb +18 -2
  49. data/lib/rails_ai_context/introspectors/eager_load.rb +65 -0
  50. data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +6 -4
  51. data/lib/rails_ai_context/introspectors/gem_introspector.rb +12 -35
  52. data/lib/rails_ai_context/introspectors/job_introspector.rb +51 -138
  53. data/lib/rails_ai_context/introspectors/listeners/method_call_listener.rb +31 -0
  54. data/lib/rails_ai_context/introspectors/listeners/methods_listener.rb +37 -5
  55. data/lib/rails_ai_context/introspectors/middleware_introspector.rb +14 -1
  56. data/lib/rails_ai_context/introspectors/migration_introspector.rb +32 -35
  57. data/lib/rails_ai_context/introspectors/model_introspector.rb +109 -74
  58. data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +13 -21
  59. data/lib/rails_ai_context/introspectors/performance_introspector.rb +13 -39
  60. data/lib/rails_ai_context/introspectors/schema_introspector.rb +12 -38
  61. data/lib/rails_ai_context/introspectors/security_introspector.rb +3 -6
  62. data/lib/rails_ai_context/introspectors/source_scan.rb +80 -0
  63. data/lib/rails_ai_context/introspectors/test_introspector.rb +7 -17
  64. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +276 -120
  65. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +19 -0
  66. data/lib/rails_ai_context/mcp_config_generator.rb +2 -2
  67. data/lib/rails_ai_context/payload.rb +76 -11
  68. data/lib/rails_ai_context/pending_migrations.rb +60 -0
  69. data/lib/rails_ai_context/presets.rb +34 -4
  70. data/lib/rails_ai_context/redaction.rb +32 -1
  71. data/lib/rails_ai_context/resources.rb +6 -13
  72. data/lib/rails_ai_context/route_coverage.rb +1 -1
  73. data/lib/rails_ai_context/safe_path.rb +72 -0
  74. data/lib/rails_ai_context/schema_version.rb +13 -0
  75. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +8 -9
  76. data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +21 -23
  77. data/lib/rails_ai_context/serializers/context_file_serializer.rb +15 -1
  78. data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +9 -13
  79. data/lib/rails_ai_context/serializers/copilot_serializer.rb +9 -11
  80. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +9 -13
  81. data/lib/rails_ai_context/serializers/markdown_serializer.rb +64 -87
  82. data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +4 -10
  83. data/lib/rails_ai_context/serializers/section_facts.rb +26 -3
  84. data/lib/rails_ai_context/serializers/section_marker_writer.rb +11 -1
  85. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +4 -21
  86. data/lib/rails_ai_context/serializers/test_command_detection.rb +1 -2
  87. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +1 -2
  88. data/lib/rails_ai_context/tasks/rails_ai_context.rake +15 -53
  89. data/lib/rails_ai_context/tools/analyze_feature.rb +18 -43
  90. data/lib/rails_ai_context/tools/base_tool.rb +68 -24
  91. data/lib/rails_ai_context/tools/dependency_graph.rb +3 -4
  92. data/lib/rails_ai_context/tools/diagnose.rb +27 -21
  93. data/lib/rails_ai_context/tools/get_api.rb +65 -12
  94. data/lib/rails_ai_context/tools/get_concern.rb +36 -136
  95. data/lib/rails_ai_context/tools/get_config.rb +16 -14
  96. data/lib/rails_ai_context/tools/get_context.rb +60 -107
  97. data/lib/rails_ai_context/tools/get_controllers.rb +46 -182
  98. data/lib/rails_ai_context/tools/get_edit_context.rb +9 -37
  99. data/lib/rails_ai_context/tools/get_env.rb +6 -35
  100. data/lib/rails_ai_context/tools/get_frontend_stack.rb +9 -14
  101. data/lib/rails_ai_context/tools/get_helper_methods.rb +2 -23
  102. data/lib/rails_ai_context/tools/get_job_pattern.rb +96 -165
  103. data/lib/rails_ai_context/tools/get_model_details.rb +15 -60
  104. data/lib/rails_ai_context/tools/get_partial_interface.rb +16 -40
  105. data/lib/rails_ai_context/tools/get_routes.rb +1 -1
  106. data/lib/rails_ai_context/tools/get_schema.rb +19 -16
  107. data/lib/rails_ai_context/tools/get_stimulus.rb +1 -17
  108. data/lib/rails_ai_context/tools/get_test_info.rb +38 -22
  109. data/lib/rails_ai_context/tools/get_turbo_map.rb +113 -454
  110. data/lib/rails_ai_context/tools/get_view.rb +27 -142
  111. data/lib/rails_ai_context/tools/migration_advisor.rb +6 -10
  112. data/lib/rails_ai_context/tools/onboard.rb +68 -76
  113. data/lib/rails_ai_context/tools/performance_check.rb +5 -3
  114. data/lib/rails_ai_context/tools/read_logs.rb +13 -43
  115. data/lib/rails_ai_context/tools/review_changes.rb +4 -7
  116. data/lib/rails_ai_context/tools/runtime_info.rb +2 -6
  117. data/lib/rails_ai_context/tools/safe_call.rb +4 -4
  118. data/lib/rails_ai_context/tools/search_code.rb +6 -6
  119. data/lib/rails_ai_context/tools/search_docs.rb +3 -8
  120. data/lib/rails_ai_context/tools/validate.rb +8 -45
  121. data/lib/rails_ai_context/tools/validate_semantics.rb +20 -20
  122. data/lib/rails_ai_context/version.rb +1 -1
  123. data/lib/rails_ai_context/vfs.rb +44 -102
  124. data/lib/rails_ai_context/view_file.rb +59 -0
  125. data/lib/rails_ai_context.rb +12 -8
  126. data/server.json +1 -1
  127. metadata +15 -4
  128. data/lib/rails_ai_context/source_line.rb +0 -208
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45c3818bdd2790c10c54e008bbbd22cf48b621195b89e869a1cd22e6f7e01e44
4
- data.tar.gz: 6374c7cf2bad6c28a58216e96b94a36dfc71e2af6aff5095a0b98a4d843d4ec3
3
+ metadata.gz: d80b8bd17147a84fb1f8ce3028b7e1291af63c0d639663704eec171069e8f1b7
4
+ data.tar.gz: 4ce32bd09b90f16b62f70621642d6db35258c3a47892196e1fdb6d329366b4ed
5
5
  SHA512:
6
- metadata.gz: 29b3386f4b8638e8059c42a39558547f08e7a1d5ac21197d8c09ba62a319852af5e6c7f206a63eafe378e03eac233e24f3784eb7bdf14f4ca40ff86afd12c2d3
7
- data.tar.gz: 94e6915c0fd3e9472bb5792d1cd866f9c41711143ce877e39bdd461e8dfcd40280cffabeca4b4974efbd2d3f52514d438530b73cd16df033e43215a6bb519eb3
6
+ metadata.gz: 53ecb39c98209ddd59050431dca51b448c848e7f6e4d93fe092523ee87b014e6279aa25d97b283e57c66368eb0b315baaa4b75d0ea349d21bc6b0b1109ec0d2d
7
+ data.tar.gz: 25c7854ce8e4e4c1e16996e13a2031dc35d6cfee245d46515a48833c4fdcc48d35eb3a38b5365cfa4518847096340a104dc79dbfda2dfc314565a1508ea5dd7b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,257 @@ 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.25.0] - 2026-09-03
9
+
10
+ ### Added
11
+
12
+ - **`sprockets-rails`, `sprockets`, `sorcery` and `clearance` join the notable
13
+ gem table**, so `rails_get_gems` lists them and the `rails_get_config` assets
14
+ and auth lines name them from the lockfile rather than from a loaded
15
+ constant.
16
+
17
+ ### Fixed
18
+
19
+ Defects found by a second survey round over the whole surface, and the
20
+ duplicated mechanisms behind them.
21
+
22
+ - **`max_view_total_size` and `max_view_file_size` said they capped view
23
+ reads.** Only `doctor` reads either one, as the threshold for its view-size
24
+ warning; the config comments and the docs rows now say that.
25
+ - **`excluded_concerns` hid a concern from a model's list but not from the
26
+ catalogue.** `rails_get_concern` and the `rails_get_active_support` concern
27
+ registry still listed and counted a concern the key names; all three now
28
+ apply the same predicate.
29
+ - **An in-Gemfile app's `.rails-ai-context.yml` was inert.** The generated
30
+ initializer holds a `configure` block, and the YAML was skipped whenever one
31
+ had run, so a booted command took the defaults while `--no-boot` read the
32
+ file and the two disagreed (45 tools against 43 with `skip_tools` set).
33
+ Precedence is a merge: the file is applied once, before
34
+ `config/initializers`, so an initializer may assign a key or edit it in
35
+ place and both survive; a later call from the standalone binary or the
36
+ CLI's boot path is a no-op. A block wins the keys it assigns wherever it
37
+ lives, so one in `config/application.rb` or an environment file, which runs
38
+ before the load, keeps them too.
39
+ - **The static tier named the app after its directory.** `onboard` and every
40
+ generated context file were headed "mastodon" for an app that declares
41
+ `module Mastodon`. The name now comes from the module enclosing
42
+ `class Application < Rails::Application` in `config/application.rb`, and
43
+ falls back to the directory only when that file names nothing.
44
+ - **The static tier counted every class under `app/models` as a model.**
45
+ Namespace modules, form objects, filters and plain service classes were
46
+ listed and rendered as models of a table (Mastodon answered 195 models
47
+ against 117 booted). A static model is now a class whose superclass chain
48
+ reaches `ApplicationRecord`, a namespaced `*ApplicationRecord` or
49
+ `ActiveRecord::Base`, STI subclasses included.
50
+ - **An unreadable `.rails-ai-context.yml` aborted the boot.** A directory or a
51
+ file the process cannot read at that path raised out of the engine
52
+ initializer; it now warns and keeps the defaults, the way broken YAML does.
53
+ - **A model under a per-connection abstract base was missing from the static
54
+ list.** The multi-database shape - `class AnimalsRecord < ApplicationRecord;
55
+ self.abstract_class = true` in its own file, then `class Dog <
56
+ AnimalsRecord` - dropped every model on that connection from the static
57
+ list, the count and the schema listing. An abstract base is still walked so
58
+ chains resolve through it; it is left out of the result, as before.
59
+ - **`rails_get_api` stated a filesystem finding for a section nobody had
60
+ read.** In the static tier the whole section but the mode was declared
61
+ unavailable, and nothing read that declaration, so an app with
62
+ `app/controllers/api/v1/` was told "not detected (no app/controllers/api/v*
63
+ directories)". Every detection in that section is a file read, so the
64
+ static tier now answers all of them, and a key a section does name as
65
+ unanswered renders as `[UNAVAILABLE: ...]` rather than as a negative
66
+ finding.
67
+ - **An existing `.ai-context.json` holding anything but a JSON object
68
+ aborted the whole generation run.** The skip check reads it as a file to
69
+ replace.
70
+ - **`rails_get_schema`'s table heading named every model on the table**, so
71
+ an STI table with thirty subclasses filled the cheap summary with one
72
+ heading. It names five and counts the rest, the way the file's other lists
73
+ do.
74
+ - **`rails_get_test_info` blamed the app root for a name it refused as
75
+ sensitive.** The refusal names what the check covers: the name leaves the
76
+ app root or names a sensitive file.
77
+ - **`rails_onboard`'s Getting Started block told the reader to `cd` into the
78
+ app's class name underscored**, which is not the directory the clone lands
79
+ in. It names the app directory.
80
+ - **`init` refused a tree with app source but no `config/environment.rb`
81
+ after it had already written the config files**, leaving it half set up
82
+ with no `CLAUDE.md`. Every command that can serve the static tier now
83
+ reads such a tree, the way `--no-boot` already did.
84
+ - **A tree with app source but no `config/environment.rb` printed a boot
85
+ failure before falling back to static analysis.** No boot can succeed
86
+ without that file, so the static tier now takes over at once and its banner
87
+ names the missing file; `doctor` refuses the same tree with that reason.
88
+ The banner calls that tree static mode rather than a boot failure, since
89
+ no boot ran.
90
+ - **`rails_runtime_info` and `rails_query` told a tree with no
91
+ `config/environment.rb` to fix a boot failure or drop `--no-boot`**, neither
92
+ of which had happened. Such a tree now reads "This tree has no
93
+ `config/environment.rb`; add one (or run from the app root) for runtime
94
+ data."
95
+ - **A source path was contained without a separator and against an
96
+ unresolved root.** The frontend framework introspector's own containment
97
+ check let `/app-old` pass for `/app`. The pre-v5.8.1 bug, still in one
98
+ place.
99
+ - **`rails_get_env` printed Dockerfile `ENV` and `ARG` values verbatim at
100
+ detail full.** Every default now leaves through the redaction gate, and a
101
+ surrounding quote pair is stripped, so `ENV FOO="bar"` prints `bar`.
102
+ - **`rails_read_logs` matched the `search` term before redacting**, so a
103
+ hidden value could be probed a character at a time. The search runs on the
104
+ redacted text now.
105
+ - **The middleware introspector fabricated an empty stack in the static
106
+ tier.** It declares an alternate source and answers only its file facts,
107
+ rather than reporting an app with no middleware.
108
+ - **`rails_get_config` and `rails-ai-context facts` read gem keys no
109
+ introspector emits**, so the Auth line and the Key Dependencies section
110
+ never rendered at all.
111
+ - **Jobs and mailers carry `file:`.** `rails_get_job_pattern` reads the
112
+ carried file instead of camelizing a basename, and its listing comes from
113
+ the payload, so a job in a pack lists.
114
+ - **The auth introspector keyed Devise models by basename**, so
115
+ `app/models/admin/user.rb` overwrote `User`.
116
+ - **An out-of-order migration merge was reported as none pending** by the
117
+ migrations section while the schema section counted it. Both derive from
118
+ one applied set now, and an unknown applied set answers no pending key
119
+ rather than "everything" or "nothing".
120
+ - **Substring lockfile scans reported `bugsnag-capistrano` as Bugsnag** and
121
+ `database_cleaner-redis` as database_cleaner. One lockfile reader with
122
+ exact names answers every gem question, across the GEM, GIT and PATH
123
+ sections.
124
+ - **The hydrators warned about a model they had just resolved in another
125
+ spelling**, and emitted one hint block twice.
126
+ - **The channel and mailer eager loads had no per-constant recovery**, so a
127
+ single unloadable file emptied the list.
128
+ - **`YAML.safe_load` refused the `&default` anchors** every stock webpacker
129
+ or shakapacker config carries, so the source path fell back to the
130
+ convention.
131
+ - **VFS: `controllers/admin/posts` fell into the action handler** and
132
+ `routes/PostsController` returned zero routes.
133
+ - **`Payload.section` accepted a refused (`unavailable`) section**, so a
134
+ static-tier context file rendered a bare heading.
135
+ - **Every generated file counts the same controller set.**
136
+ `config.excluded_controllers` is honoured everywhere, not only by
137
+ `rails_get_controllers`.
138
+ - **The doctor counted models and controllers under `app/` only.** Packs and
139
+ engines count, and the freshness check reads the one watch scope.
140
+ - **The watcher stopped noticing `config/routes.rb` and `db/schema.rb`.** The
141
+ fingerprint, the watcher and the doctor share one scope covering `config`
142
+ and `db` whole, and the fingerprint mark is taken before the read it
143
+ protects.
144
+ - **The diagnose tool's git probe ran outside a repository** and leaked its
145
+ stderr into the response.
146
+ - **`rails_get_stimulus` re-read every controller file** to recompute a
147
+ lifecycle the payload already carried.
148
+ - **MCP config removal wrote non-atomically**, the dead `AstCache.invalidate`
149
+ is gone, and the doctor checks that `.codex/config.toml` is gitignored.
150
+ - **Composing tools decided whether a sub-tool answered by scraping its prose
151
+ for "not found".** A real answer whose body mentioned those words was
152
+ dropped. Responses carry an empty marker in MCP `meta` now.
153
+ - **`rails_get_test_info` guarded a caller-supplied name with its own
154
+ containment.** It reads through the shared guard.
155
+ - **The asset pipeline introspector's literal `none` was rendered as if it
156
+ named a pipeline**, so every generated context file carried
157
+ `- Assets: none`. The line shows only the parts that name something, and
158
+ disappears when there are none.
159
+ - **`rails_get_view`'s ivar list counted an `@` inside an email address and a
160
+ `@@class_variable`.** Neither is an instance variable.
161
+ - **A controller ivar compared, not assigned, was reported as set.** `return
162
+ unless @post == current_user` no longer names `@post`; `||=`, `+=`, `<<=` and
163
+ `>>=` still count.
164
+ - **`rails_get_context` resolved an action name case-sensitively** while
165
+ `rails_get_controllers` did not, so `action: "Show"` skipped the ivar
166
+ cross-check.
167
+ - **The ivar cross-check ran with no view templates section**, reporting every
168
+ controller ivar as unused in the view. It is skipped instead.
169
+ - **`rails_get_turbo_map` printed a Turbo Stream response as a Ruby hash.**
170
+ It renders `PostsController#create`, the way the file's other sections name
171
+ a controller action.
172
+ - **A second `context` run rewrote the generated files** for their timestamp
173
+ alone, so a repo that commits the context files saw a diff from a run that
174
+ found nothing new. One rule now covers them all: a file that differs only
175
+ in its `generated_at` key, or in the full-mode header's `> Generated:`
176
+ line, is skipped.
177
+ - **The schema listing named one model per table.** A table an STI child or a
178
+ namespaced second model shares listed whichever came first in the payload,
179
+ which could be the emptier one. Every model on the table is listed now,
180
+ the one carrying the most detail first.
181
+ - **Every static-tier context file carried `Rails [UNAVAILABLE: app not
182
+ booted]` mid-sentence.** The version comes from the lockfile, and the
183
+ marker is left for a tree whose lockfile does not carry rails.
184
+ - **`rails-ai-context preset " FULL "` exited 1 with the listing.** A preset
185
+ name is matched whatever its case or padding.
186
+ - **A `--no-boot` run that found no app printed the doctor hint**, which asks
187
+ for a boot error that never happened. `doctor` and `init` also accept a
188
+ tree that has app source but no `config/environment.rb`, and the refusal
189
+ names that file instead of telling the user to go to the app root they are
190
+ standing in.
191
+
192
+ ### Changed
193
+
194
+ - **`RailsAiContext.generate_context(format: nil)` means the recorded AI-tool
195
+ selection**, and all of them when nothing is recorded. It was `:all`.
196
+ - **Static-tier migration names are the class-style name** (`CreatePosts`),
197
+ matching what the booted tier reports.
198
+ - **Names in the turbo, auth, attachments and multi-database sections carry
199
+ their namespace** (`Admin::User`, not `User`), and packs and in-repo
200
+ engines count everywhere the app's source is walked.
201
+ - **The `.env.example` reader judges a value by its shape and length only.** A
202
+ default in Ruby source or a Dockerfile is condemned by a secret-shaped name
203
+ as well.
204
+ - **Turbo wiring lives in the introspector payload with file and line** -
205
+ frames, model broadcasts, explicit broadcasts and stream subscriptions -
206
+ and `rails_get_turbo_map` renders it. A `turbo_frame_tag dom_id(@post,
207
+ :edit)` frame renders as that call, and a symbol `turbo_stream_from :posts`
208
+ now pairs with its broadcast. A subscription argument that carries its own
209
+ commas renders whole: `turbo_stream_from [current_user, :notifications]` and
210
+ `dom_id(@post, :x)` keep every character they were written with.
211
+ - **The public-methods lists come from the parser.** `rails_get_concern`,
212
+ `rails_get_helper_methods`, `rails_get_model_details` and
213
+ `rails_get_controllers` read `private def` and `class_methods do`
214
+ correctly.
215
+ - **The standalone binary boots through `CLI::EntryBoot`**, and presets run
216
+ through `Presets.run` in both the binary and the rake task.
217
+ - **`rails_get_controllers` and the controller-action resource decide which
218
+ filters apply through one module, `ActionFilters`.** Every inherited filter
219
+ carries the `(from Parent)` annotation, after_action and
220
+ except-constrained ones included; a filter constrained with `except:` shows
221
+ that constraint; `skip_after_action` and `skip_around_action` count as
222
+ skips, and a skipped filter is struck through in the whole-controller view
223
+ as well as the per-action one.
224
+ - **A config value longer than 40 characters is filtered on every path.** The
225
+ Ruby-source and Dockerfile readers capped at 30 before; `.env.example` was
226
+ already 40.
227
+ - **`schema[:pending_migrations]`, reachable through the `rails://schema`
228
+ resource, is a list of `{ version:, name: }` entries** rather than version
229
+ strings.
230
+ - **`rails-ai-context://routes/{controller}` prefers an exact route-key
231
+ match**, so `routes/posts` no longer also returns `admin/posts`.
232
+ - **`unavailable_sections`, where a section carries it, is a list of
233
+ section keys**, not a reason string. The middleware section's static
234
+ answer is the one that still emits it.
235
+ - **The models resource template advertises
236
+ `rails-ai-context://models/{name}`**, and every scheme resolves through the
237
+ VFS rather than an exact-match legacy reader. `rails://models/{name}` is
238
+ still accepted.
239
+ - **`controllers[:controllers]` can carry `{ error: "unreadable" }` entries**
240
+ for a file over the size cap or otherwise unreadable, and the listing and
241
+ the count include them.
242
+
243
+ ### Removed
244
+
245
+ - **`SourceLine`, the hand-rolled Ruby lexer.** Every source read goes through
246
+ Prism.
247
+ - **`Fingerprinter.reset_gem_lib_fingerprint!` and `AstCache.invalidate`** -
248
+ no callers.
249
+ - **Support for the mcp gem below 0.13.** The gemspec floor is `>= 0.13`, the
250
+ first release whose tool responses carry `meta`.
251
+ - **`Presets.names` and `Presets.fetch`** - the listing and the run read
252
+ `DEFINITIONS` directly.
253
+ - **`SchemaHintBuilder.build_many`** - no callers.
254
+ - **`Fingerprinter.changed?`** - no callers; `stale?` answers the question.
255
+ - **The public constant `ChangeWatch::WATCH_DIRS`.**
256
+ - **`RailsAiContext.configured_via_block?`** - no callers once the config file
257
+ applies once per configuration and a block wins its keys by name.
258
+
8
259
  ## [5.24.0] - 2026-08-17
9
260
 
10
261
  ### Fixed
data/CONTEXT.md CHANGED
@@ -28,6 +28,24 @@ 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
+ ## Safe path
32
+
33
+ A caller-supplied path resolved once, through `SafePath`, before anything reads it. Its refusal order is the contract, not an implementation detail: traversal, sensitive name, realpath, containment, sensitive realpath, file, size. A caller that reorders those checks, or repeats one of them itself, gets a different answer on a symlink or a dotfile than every other tool does, which is the divergence the module exists to end. `safe_glob` is the globbed-path form: the same guard applied to each path a pattern yields.
34
+
35
+ Not the same as a path that merely looks harmless, and not a caller's own containment check - "the tool guards this parameter" means it hands the parameter to `SafePath` and renders whatever refusal comes back.
36
+
37
+ ## Carried path
38
+
39
+ A path the payload already holds because the gem's own walk found it - a controller's or a model's `file:`. It is re-read with `SafeFile.read(File.join(root, relative))`, which applies the size cap and nothing else. The reason is the walk: `SourceScan` deliberately keeps the spelling the app uses rather than the realpath, so a pack or an in-repo engine symlinked out of the root is spelled inside it, and `SafePath`'s realpath containment would refuse the very file the payload just named - the source comes back nil and a section silently empties.
40
+
41
+ Not an exception to the **safe path** rule, the other side of it: a caller-supplied path is untrusted and goes through `SafePath`; a carried path was produced by this gem and only needs the cap.
42
+
43
+ ## Source scan
44
+
45
+ The one walk over a kind of app source, across every directory `PathResolver` resolves for it: conventional layout, packs, in-repo engines. `SourceScan.paths` stats only, `each` reads the source on top of it, and `classes` names each file by its declared constant. An introspector that globs `app/<kind>` itself is the mistake this entry exists to name: it misses every pack and engine, and it names files by their path rather than by what they declare.
46
+
47
+ Distinct from the **eager load** of the booted tier (`EagerLoad`), which makes constants exist rather than reading files, and from a **targeted walk** - one named file handed to `SourceIntrospector` with the listeners that file needs.
48
+
31
49
  ## Declared constant
32
50
 
33
51
  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.
data/CONTRIBUTING.md CHANGED
@@ -82,6 +82,14 @@ bundle exec rspec spec/lib/ # Just lib specs
82
82
  bundle exec rubocop --parallel # Lint check
83
83
  ```
84
84
 
85
+ ### Static fixture
86
+
87
+ `spec/fixtures/static_app` is the app the static tier is exercised against, and
88
+ `spec/lib/rails_ai_context/serializers/real_shape_smoke_spec.rb` pins its model
89
+ count in two places (`Models 7` and `Models (7)`).
90
+
91
+ Adding or removing a fixture model means changing that spec in the same commit.
92
+
85
93
  ## Pull Request Process
86
94
 
87
95
  1. Fork the repo and create your branch from `main`