rails-ai-context 5.30.1 → 5.31.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +295 -0
  3. data/docs/CLI.md +1 -1
  4. data/docs/COMPATIBILITY.md +23 -7
  5. data/docs/CONFIGURATION.md +5 -2
  6. data/docs/FAQ.md +1 -1
  7. data/docs/GUIDE.md +8 -2
  8. data/docs/INTROSPECTORS.md +6 -1
  9. data/docs/SECURITY.md +23 -1
  10. data/docs/TOOLS.md +11 -2
  11. data/exe/rails-ai-context +3 -4
  12. data/lib/rails_ai_context/action_filters.rb +68 -6
  13. data/lib/rails_ai_context/base_mixins.rb +44 -16
  14. data/lib/rails_ai_context/boot_manager.rb +21 -5
  15. data/lib/rails_ai_context/cli/entry_boot.rb +3 -2
  16. data/lib/rails_ai_context/cli/tool_runner.rb +22 -3
  17. data/lib/rails_ai_context/concern_macros.rb +698 -92
  18. data/lib/rails_ai_context/concern_membership.rb +6 -4
  19. data/lib/rails_ai_context/concern_paths.rb +31 -4
  20. data/lib/rails_ai_context/configuration.rb +8 -8
  21. data/lib/rails_ai_context/erb_source.rb +22 -3
  22. data/lib/rails_ai_context/git_ignore.rb +13 -11
  23. data/lib/rails_ai_context/introspector.rb +13 -2
  24. data/lib/rails_ai_context/introspectors/action_presence.rb +5 -3
  25. data/lib/rails_ai_context/introspectors/action_resolver.rb +2 -1
  26. data/lib/rails_ai_context/introspectors/call_site_expansion.rb +198 -18
  27. data/lib/rails_ai_context/introspectors/controller_introspector.rb +46 -19
  28. data/lib/rails_ai_context/introspectors/declared_constant.rb +15 -6
  29. data/lib/rails_ai_context/introspectors/eager_load.rb +22 -10
  30. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +4 -3
  31. data/lib/rails_ai_context/introspectors/job_introspector.rb +76 -17
  32. data/lib/rails_ai_context/introspectors/listeners/associations_listener.rb +12 -0
  33. data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +3 -3
  34. data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +28 -1
  35. data/lib/rails_ai_context/introspectors/listeners/generic_macro_listener.rb +3 -1
  36. data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +9 -1
  37. data/lib/rails_ai_context/introspectors/listeners/mixins_listener.rb +21 -28
  38. data/lib/rails_ai_context/introspectors/listeners/mount_listener.rb +13 -6
  39. data/lib/rails_ai_context/introspectors/listeners/owner_scope.rb +67 -0
  40. data/lib/rails_ai_context/introspectors/listeners/proc_literal_listener.rb +42 -0
  41. data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +81 -25
  42. data/lib/rails_ai_context/introspectors/listeners/with_options_scope.rb +49 -12
  43. data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +15 -0
  44. data/lib/rails_ai_context/introspectors/migration_replay.rb +2 -0
  45. data/lib/rails_ai_context/introspectors/model_introspector.rb +291 -121
  46. data/lib/rails_ai_context/introspectors/route_introspector.rb +62 -4
  47. data/lib/rails_ai_context/introspectors/schema_conventions.rb +8 -0
  48. data/lib/rails_ai_context/introspectors/schema_introspector.rb +7 -4
  49. data/lib/rails_ai_context/introspectors/schema_reader.rb +2 -0
  50. data/lib/rails_ai_context/introspectors/source_scan.rb +5 -3
  51. data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +13 -4
  52. data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +107 -23
  53. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +18 -16
  54. data/lib/rails_ai_context/method_name.rb +31 -0
  55. data/lib/rails_ai_context/migration_status.rb +6 -1
  56. data/lib/rails_ai_context/output_guard.rb +6 -4
  57. data/lib/rails_ai_context/pending_migrations.rb +3 -2
  58. data/lib/rails_ai_context/portable_path.rb +14 -0
  59. data/lib/rails_ai_context/redaction.rb +8 -2
  60. data/lib/rails_ai_context/safe_file.rb +4 -1
  61. data/lib/rails_ai_context/safe_path.rb +17 -4
  62. data/lib/rails_ai_context/schema_adapter.rb +6 -1
  63. data/lib/rails_ai_context/serializers/context_file_serializer.rb +2 -2
  64. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +5 -0
  65. data/lib/rails_ai_context/server.rb +26 -7
  66. data/lib/rails_ai_context/tasks/rails_ai_context.rake +1 -1
  67. data/lib/rails_ai_context/tools/base_tool.rb +20 -10
  68. data/lib/rails_ai_context/tools/diagnose.rb +4 -0
  69. data/lib/rails_ai_context/tools/generate_test.rb +19 -0
  70. data/lib/rails_ai_context/tools/get_callbacks.rb +14 -6
  71. data/lib/rails_ai_context/tools/get_concern.rb +1 -1
  72. data/lib/rails_ai_context/tools/get_conventions.rb +5 -7
  73. data/lib/rails_ai_context/tools/get_job_pattern.rb +8 -2
  74. data/lib/rails_ai_context/tools/get_model_details.rb +5 -4
  75. data/lib/rails_ai_context/tools/get_partial_interface.rb +18 -4
  76. data/lib/rails_ai_context/tools/get_routes.rb +19 -9
  77. data/lib/rails_ai_context/tools/get_schema.rb +26 -4
  78. data/lib/rails_ai_context/tools/get_service_pattern.rb +44 -25
  79. data/lib/rails_ai_context/tools/get_test_info.rb +8 -0
  80. data/lib/rails_ai_context/tools/performance_check.rb +5 -13
  81. data/lib/rails_ai_context/tools/query.rb +125 -33
  82. data/lib/rails_ai_context/tools/runtime_info.rb +5 -3
  83. data/lib/rails_ai_context/tools/search_code.rb +64 -22
  84. data/lib/rails_ai_context/tools/search_docs.rb +4 -5
  85. data/lib/rails_ai_context/tools/security_scan.rb +3 -1
  86. data/lib/rails_ai_context/tools/validate_semantics.rb +49 -13
  87. data/lib/rails_ai_context/version.rb +1 -1
  88. data/lib/rails_ai_context.rb +1 -1
  89. data/server.json +1 -1
  90. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e31095f85a3f90825fbd66bf2a7deae9224ac4fe94474c8d60da20c46ed52b47
4
- data.tar.gz: 190a7d48230b4ae98300b2619a0d2cf7a1ddb8a6e24619a7a54cfa3a6e5b233b
3
+ metadata.gz: 44b4fb64c009d96449f8f05375947d6713b07d0a45c7f27e779e0ac271c0bfa6
4
+ data.tar.gz: d0045d00ef00250f97eeae2ff105b09c7f991a3746c9198bc8126b3e79d8dc2a
5
5
  SHA512:
6
- metadata.gz: f91198dc48ae6f259c0f4d170b0fbae5d2174fa142a64530ef4ac46f2e6c51c9e22e54057e7f64712f06fc76a085e45994da2a9dc592edfe9ee3f0366608d642
7
- data.tar.gz: cee2a7a2d636322fddeedcffa151812d0ae8c52cde17d99394fd7dded5e30e5bddb8db288bc9b31b0fe1281a9a33270739c000735957fab6d64cbafdfec8a149
6
+ metadata.gz: e60a1a843d881a90404c7f87b6a29e829a60210195375c94a90151b82850d749820828b752e9e6f5f60b22dd92bf948bb8275f207c7df801ce954bd9a53705cd
7
+ data.tar.gz: 4aa95471cb0a3a4ad1cec127db6322b9b0b5d1f39f00826b1c73dd246cf0acfadc51e14f3e9f16b1cd6f3602acb618ee520eadc2b674825d2f95e675dfa7e6c1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,301 @@ 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.31.0] - 2026-10-05
9
+
10
+ ### Changed
11
+
12
+ - **A committed placeholder is not a secret.** A file whose name ends in
13
+ `.example`, `.sample`, `.template` or `.dist` (`.env.example`) is readable by
14
+ `rails_get_edit_context` and the other file tools, although the default
15
+ `.env.*` pattern matches it, and `doctor` no longer lists it as unread or
16
+ says to gitignore it. A `sensitive_patterns` entry that names the file
17
+ exactly, with no glob characters, still blocks it, and so does a path
18
+ pattern with a `/` (`.ssh/*` keeps `.ssh/id_rsa.template` blocked).
19
+ Redaction treats the four suffixes alike. `rails_search_code` now drops
20
+ sensitive files from ripgrep's output where it used to exclude them by glob,
21
+ so a sensitive file ripgrep cannot open is not reported as an error and does
22
+ not send a no-match search through the Ruby fallback. (#267)
23
+ - **`rails_query` on a SQLite file stops at `query_timeout`.** The query runs
24
+ in a child process with its own read-only connection, which is stopped at the
25
+ deadline. sqlite3 2.x's `statement_timeout=` interrupts any statement of more
26
+ than a few thousand steps whatever the value, and the extension holds the GVL
27
+ while a statement runs, so no in-process limit exists. The child takes the
28
+ app's `extensions:` and waits on a locked database as the app would. An
29
+ in-memory database, a platform without `fork`, or a query that needs a
30
+ function, virtual-table module, collation or encryption key only the app's
31
+ own connection has runs in-process; the table and EXPLAIN answers then say it
32
+ ran without a time limit, and CSV output stays plain data. (#256)
33
+ - **`rails_get_callbacks` lists callbacks in the order Rails runs them**: base
34
+ classes first, a concern's callbacks where its `include` line stands, then
35
+ the model's own, and a `before_` or `around_` callback with `prepend: true`
36
+ first. `after_commit` and `after_rollback` list last declared first, as
37
+ Rails runs them, unless `run_after_transaction_callbacks_in_order_defined`
38
+ is on (`load_defaults 7.1` or later). The static tier reads that from
39
+ `config/application.rb` and the initializers, the last line that sets it
40
+ winning, and when the config does not tell, `rails_get_callbacks` and
41
+ `rails_get_model_details` say so. A method declared twice under one callback
42
+ type shows the declaration Rails keeps, the later one, which corrects the
43
+ 5.30.0 entry that said each declaration is shown. A declaration inside a
44
+ method body counts where the class calls the method, and not at all when
45
+ nothing does, so an uncalled method that redeclares a callback no longer
46
+ hides it. A call counts written bare or as `self.name`, made inside a class
47
+ method it runs, or made from a concern's `included` block. A module joins a
48
+ class chain once, at the first place the chain adds it: the outermost class
49
+ that adds it, at its first include there, which can be a call to a class
50
+ method that includes it. A Concern's `included` block runs there only; a
51
+ plain `self.included` hook runs on every include. A call runs the definition
52
+ Ruby's lookup finds as of the call: a module the class prepends to its
53
+ singleton class, the class's own class methods, then the modules it extends
54
+ and its concerns' class methods, the last added first, then the same for
55
+ each base, then what every model has: class methods an initializer writes in
56
+ `ActiveRecord::Base.class_eval` or a reopened base, the file Rails loads
57
+ later winning (Canvas's `validates_locale` adds its `before_validation` to
58
+ Account, User and Course), then the modules mixed into the base. One an
59
+ initializer writes on `ApplicationRecord` replaces ApplicationRecord's own.
60
+ A class's own class methods are its `def self.`, an `alias_method` in
61
+ `class << self` (it runs what its original name ran at the alias), and those
62
+ a Concern's `included` block or a hook writes on the class (`def self.x` or
63
+ `class << self`, in the block, in `base.class_eval` or in `class << base`),
64
+ each from its line where that code runs. A module counts from the line
65
+ adding it, where that line runs: an `extend`, `singleton_class.include` or
66
+ `singleton_class.prepend` (which counts as prepended) in the class, in a
67
+ hook or in a Concern's `included` block, and an `extend` in a class method a
68
+ call reaches (an `acts_as_x` that extends its methods, whose `self.extended`
69
+ hook then runs); a plain module's nested `ClassMethods` counts only when
70
+ something extends it. A module a hook includes runs its own hooks. Two
71
+ concerns' modules of one name (each concern's `ClassMethods`) stay two: each
72
+ name is the constant it resolves to where it is written, and one that
73
+ resolves to nothing no longer hides the other. A `super` runs the next
74
+ definition, at the `super`. An instance method or a nested class's method of
75
+ the same name never runs. A module a class method includes joins only when a
76
+ call reaches that method, and only calls after it see its methods. A nested
77
+ class's callbacks are its own, and a module nested in the model's file reads
78
+ like any concern: `include` runs its `included` block, `extend` its methods
79
+ and `self.extended` only. A module a plugin's lib loads by a glob `require`
80
+ is read from beside the file naming it, and from its definition rather than
81
+ an empty namespace stub above it, so OpenProject's journalized models list
82
+ `save_journals`. A Mongoid document's own callbacks list in the same run
83
+ order, one in a class method where the document calls it. Neither tier
84
+ evaluates `send`, `define_singleton_method` or a module included in
85
+ `class << self` for this, so the list can miss what they register or show
86
+ the definition they replace. (#253)
87
+ - **A lambda or Proc given to `queue_as` reads as what ActiveJob does with
88
+ it.** ActiveJob never calls it; it names the queue after the Proc's text.
89
+ Both tiers now say so, with the source, where the booted tier printed the
90
+ Proc's address and the static tier called it computed. A `queue_as` block is
91
+ the computed case and now shows its source too; a block argument
92
+ (`queue_as(&pick)`) reads as computed in both tiers. The 5.30.0 entry used
93
+ `queue_as -> { ... }` as its example of a computed queue; that was wrong.
94
+ (#271)
95
+ - **An MCP stdio session carries nothing but JSON-RPC on stdout for its whole
96
+ life.** Output written during a tool call (a logger built on first use, as
97
+ Sidekiq's is, `puts`, a child process) goes to stderr; only boot was guarded
98
+ before. (#255)
99
+ - **The booted `rails_security_scan` parses in its own process.** Brakeman's
100
+ forked parse workers could not return a parse error from an app that loads
101
+ web-console, and the scan failed where the static tier scanned. (#257)
102
+
103
+ ### Fixed
104
+
105
+ - **A concern's declarations name the module the include resolves to.**
106
+ `rails_get_callbacks` on OpenProject's WorkPackage lists
107
+ `after_save :save_journals` from `Acts::Journalized::SaveHooks`, where it
108
+ said `SaveHooks`.
109
+ - **A module declared in its outer module's file is read from there.**
110
+ Canvas's `Role::AssociationHelper` lives in role.rb, and its `included`
111
+ hook adds `before_save :resolve_cross_account_role` to Enrollment and its
112
+ subclasses, AccountUser and RoleOverride; the static tier called the
113
+ module unread and missed the callback. `rails_get_callbacks`,
114
+ `rails_get_model_details` and the controller filters and actions find it
115
+ there and read that module, not the outer class.
116
+ - **`rails_get_callbacks` lists models of one callback count by name**, so a
117
+ model gaining a callback no longer moves the sections around it.
118
+ - **A model macro written over `*args` reads each call's own arguments.**
119
+ The list binds to the call's positionals past the method's other
120
+ parameters, less the options hash the body takes off the end
121
+ (`args.extract_options!`), plus literals its leading statements push. A
122
+ block over the list is read once per item, so Canvas's
123
+ `validates_locale :locale, :browser_locale, allow_nil: true` lists an
124
+ inclusion validation on each field with its own `if: :<field>_changed?`, in
125
+ place of the macro's own row and one on a computed `field`. An options
126
+ hash, list or parameter the method changes in place (`options[:x] = 1`,
127
+ `options[:x] ||= 1`, `reverse_merge!`, `delete`) is not read as the call's
128
+ value. A declaration it decides, the method name included (`before_save
129
+ name` after `name.strip!`), is held back and listed under "Not decided from
130
+ the source (the method name or a condition)", the heading that was "Only
131
+ under a condition the source does not decide".
132
+ - **A `load_defaults` written after `belongs_to_required_by_default = false`
133
+ turns the default back on** in the static tier, as Rails does, so the
134
+ implicit presence of a required `belongs_to` is listed. A literal assignment
135
+ used to win wherever it stood. A value the source cannot evaluate
136
+ (`ENV.fetch(...) == "true"`), app-wide or in the class, makes the presence
137
+ conditional on it, named in the answer. A literal `nil` reads as off, as
138
+ Rails reads it, here and for the commit-order setting. A `belongs_to` with
139
+ `required: nil` is optional, and `required:` decides over `optional:` when
140
+ both are written, as in Rails.
141
+ - **`rails_get_callbacks` prints `on:`** for validation callbacks and any
142
+ callback whose type does not already name the event, before `if:` and
143
+ `unless:`. (#252)
144
+ - **`rails_performance_check` with `model` and `category` counts only what it
145
+ lists.** (#265)
146
+ - **`rails_search_docs` caches a fetched guide whole.** A guide with non-ASCII
147
+ text failed to write in the booted tier and left an empty file that read as a
148
+ cached guide for 24 hours; the write is now binary and atomic, and an empty
149
+ cache file is a miss. (#266)
150
+ - **`rails_search_code` trace finds one definition and no sibling of itself**:
151
+ `qa_ping` no longer matches `def qa_ping?`, `qa_ping!` or `qa_ping=`, and
152
+ `def self.qa_ping` is left out of its own sibling list, where a setter now
153
+ appears. Every method-body lookup (concerns, callbacks, actions, conventions)
154
+ ends a name the same way, and ripgrep and the Ruby fallback agree on
155
+ non-ASCII text, where any character outside ASCII is part of the name, as
156
+ Ruby reads it. (#258)
157
+ - **`rails_search_code` call sites skip comment lines** with the rule trace
158
+ uses, by file type: `#` in Ruby, `//` and `/*` in JS and TS, `/*` in CSS and
159
+ `//` too in SCSS, Sass and Less, `-#` in Haml, `/` in Slim. A stylesheet
160
+ line that starts with a `#id` selector is not a call site either. A line
161
+ holding a live ERB tag is a call site, and a spaced setter call
162
+ (`obj.qa_ping = 1`) is not a call of `qa_ping`. (#269)
163
+ - **Both search backends read the same excludes file.** The Ruby fallback read
164
+ `core.excludesFile` from the repository's own config, which ripgrep does not.
165
+ It now reads what ripgrep 15 reads: `~/.gitconfig`, then the XDG git config,
166
+ then the default ignore file. (#268)
167
+ - **A `db/structure.sql` table that `INHERITS` reads its parent's columns
168
+ first, then its own**, with multiple parents, chains, a parent defined later
169
+ in the file, and `ALTER COLUMN ... SET NOT NULL`/`SET DEFAULT`. A parent in
170
+ another schema resolves; one the file does not define is named. An index, key
171
+ or foreign key lands on the table its statement names, schema included. The
172
+ one-line form read its column type as `integer) INHERITS (...`. (#259)
173
+ - **A Stimulus controller under `app/javascript/controllers` keeps its
174
+ identifier** when another `controllers` directory holds the same path, and a
175
+ nested `controllers` directory stays in the name (`admin--controllers--x`).
176
+ (#264)
177
+ - **`query_allowed_columns` exempts a column from result redaction too**, as
178
+ it does from the pre-query check. Columns declared with `encrypts` stay
179
+ redacted. (#270)
180
+
181
+ ## [5.30.2] - 2026-10-01
182
+
183
+ ### Changed
184
+
185
+ - **`generate_root_files: false` leaves out `.cursorrules` too, and always
186
+ writes `.ai-context.json`.** `.cursorrules` is the same compact file as
187
+ `CLAUDE.md` at the repo root, so it is a root file and now reports "Not
188
+ applicable (root files disabled)". `.ai-context.json` is data for tools, not
189
+ an instruction file, and is written whatever the key says.
190
+ - **`serve --port` has no default of its own.** An explicit `--port` wins, then
191
+ `http_port` from the config, then 6029. The config key never reached `serve`
192
+ before, although `docs/STANDALONE.md` documents it, and the
193
+ `streamable_http` alias ignored `--port`.
194
+ - **A PostGIS connection is labelled PostgreSQL everywhere.** The booted schema
195
+ said PostGIS and the static tier PostgreSQL for the same app. The raw adapter
196
+ name stays in `.ai-context.json` as `adapter_source`.
197
+ - **The booted schema header lists pending migrations**, as the static tier
198
+ already did.
199
+
200
+ ### Fixed
201
+
202
+ - **No machine path in an introspector's error.** A model file that could not
203
+ load put the absolute path of the app into `rails_get_model_details`,
204
+ `CLAUDE.md`, the models rules file and `.ai-context.json`, and the controller,
205
+ component, middleware, migration and rake-task errors did the same. Every
206
+ section's errors are now written relative to the app root, the resolved
207
+ `/private/tmp` form included.
208
+ - **A declared, unmigrated table names the migration that adds it.** The booted
209
+ answer said the table was missing from the database and stopped there. Rails
210
+ 7.2+ refuses to load a migration dated in the future, which also emptied the
211
+ pending list; the list now comes from the applied versions and the files.
212
+ - **An unreadable model file still counts as the model file for its table**,
213
+ instead of listing the table under "Tables with no model file".
214
+ - **The static tier carries each `belongs_to` foreign key.** Static
215
+ association records had none unless the source spelled one, so
216
+ `rails_get_model_details`, the context files and `.ai-context.json` left it
217
+ out, and `rails_validate` stayed silent on an unindexed key where the booted
218
+ tier warned. A polymorphic key indexed as `[type, id]` counts as indexed.
219
+ - **A composite foreign key in `db/structure.sql` is read**, pg_dump and
220
+ mysqldump both, and renders as a column list like the schema.rb one.
221
+ - **Minitest tests from `rails_generate_test` can pass.** Absence,
222
+ confirmation, acceptance and exclusion validations asserted on a valid
223
+ fixture; each now sets up an invalid record, or emits `skip` where it cannot.
224
+ - **A mount at a computed path prints no path.** `mount X => ENV.fetch(...)`
225
+ printed `` at `[INFERRED]` ``, a bare constant printed its name as the path,
226
+ and a gem engine mounted that way read as "not mounted by the app's routes".
227
+ A path written without its leading slash gets one, as Rails does.
228
+ - **Static routes say which engine tables they could not read.** An engine
229
+ whose routes live in its gem (PgHero, LetterOpenerWeb) dropped out of the
230
+ static answer with no note; it is listed under "Mounted engines whose routes
231
+ were not read". A constant the app defines counts as an engine only when it
232
+ subclasses `Rails::Engine`. An engine mounted at several paths is one line
233
+ naming each.
234
+ - **Booted filter attribution matches the static tier.** An inherited
235
+ `after_action` lost its "from" credit when the child declared a
236
+ `before_action` of the same name, filters from a controller concern were not
237
+ credited to it, and a concern's `only:`/`except:`/`if:`/`unless:` and its
238
+ `skip_*_action` calls never reached the booted record. A body that
239
+ re-declares a concern's filter owns it, and a concern an ancestor already
240
+ includes stays the ancestor's.
241
+ - **An ERB comment is not read for instance variables**, while a Ruby comment
242
+ line inside a multi-line code tag no longer hides the code after it.
243
+ - **`rails_get_partial_interface` finds `render(partial: ...)`** with
244
+ parentheses, on one line or several, counts a nested render once, and a
245
+ multi-line snippet stops at the call's end.
246
+ - **Jobs on Rails 7.0 to 8.1.** A job with no `queue_as` read as "computed by a
247
+ block", since ActiveJob's default queue is a lambda; it now shows the
248
+ resolved default. The booted tier dropped every `retry_on`/`discard_on`, and
249
+ a job in a code root Zeitwerk does not manage.
250
+ - **A mailer outside `app/mailers` is a mailer in the booted tier too**, and
251
+ `rails_get_service_pattern` no longer answers a mailer, concern or mixin by
252
+ name when its listing leaves them out.
253
+ - **`app/concerns` reads as `other`** when the app root directory is itself
254
+ named `app`, as in Docker's `/app`.
255
+ - **The static default locale reads `Rails.application.config.i18n`** in an
256
+ initializer, where the last assignment wins.
257
+ - **A bad value for a required enum fails the call.**
258
+ `migration_advisor --action bogus` said "action is required" and exited 0;
259
+ it says `bogus` is not a valid `action`, names the allowed values and exits 1.
260
+ - **`rails_diagnose` never suggests a path it refused**, refuses it with or
261
+ without `--line`, and prints the tier footer once.
262
+ - **`rails_validate` names a `belongs_to` whose key column the table lacks**
263
+ instead of calling that column unindexed, and says nothing about a table's
264
+ indexes when its schema calls a helper the reader cannot interpret (Canvas's
265
+ `t.replica_identity_index`), since that helper may add the index.
266
+ - **A multi-name `t.references :a, :b, :c` adds every column**, with its index
267
+ and foreign key, in the schema.rb reader and the migration replay. The replay
268
+ also records `foreign_key: true` on a single reference, which it dropped.
269
+ - **A declaration inside a block the model may not run is kept but not
270
+ trusted.** `has_details_table do belongs_to :parent end` put a `parent_id`
271
+ key on the model's own table, and `rails_validate` checked it there. Such
272
+ declarations are still listed, and `rails_validate`'s schema, column and
273
+ `:dependent` checks skip them. `with_options`, `included`, `class_methods`,
274
+ `concerning`, `state_machine` and `aasm` blocks run on the model and are
275
+ trusted as before.
276
+ - **A foreign key given as a constant or an expression is not read as a column
277
+ name**, and a composite key stays a column list in both tiers instead of the
278
+ text of a Ruby array.
279
+ - **Every column type reads as a column in the static schema**, `t.vector`,
280
+ `t.citext`, `t.enum`, the range and PostGIS types included, and so does a
281
+ column whose options sit in braces. They were left out of the table.
282
+ - **`attr_reader` and `alias_attribute` names count as attributes** for
283
+ `rails_validate`'s "column not found" check.
284
+ - **Filters inherited through a Devise or Doorkeeper controller.** The static
285
+ walk stopped at the gem class, so a controller under `Devise::` or
286
+ `Doorkeeper::` lost every filter its app base declares and was credited with
287
+ a concern that base already includes. It now continues at the base the gem's
288
+ initializer names (`parent_controller`, `base_controller`).
289
+ - **In-repo route files are read statically.** A mounted in-repo engine's own
290
+ routes are listed under its mount, and a file that draws, appends or prepends
291
+ to the app's routes adds to the app's table; only an unmounted engine's file
292
+ still counts as not read.
293
+ - **A failed boot names missing gems once.** A bundle missing many gems listed
294
+ every name three times; the banner now gives the count and the first few,
295
+ and `doctor` and `DEBUG=1` keep the full list.
296
+ - **A test file over `max_test_file_size` is named as too large**, not missing.
297
+ - **`rails_runtime_info` says how many rows it left out** of the unused-index
298
+ list and the summary table sizes, which it cut at 10 and 5 with no note.
299
+ - **`rails_query` on MySQL** reads a `MAX_EXECUTION_TIME` interruption as the
300
+ timeout, and names `INTO OUTFILE`/`DUMPFILE` as a disk write.
301
+ - The defaults in `configuration.rb`'s comments match the values it sets.
302
+
8
303
  ## [5.30.1] - 2026-09-28
9
304
 
10
305
  ### Fixed
data/docs/CLI.md CHANGED
@@ -36,7 +36,7 @@ rails-ai-context serve --transport http --port 6029 # HTTP transport
36
36
  | Option | Default | Description |
37
37
  |:-------|:--------|:------------|
38
38
  | `--transport` | `stdio` | `stdio` or `http` |
39
- | `--port` | `6029` | HTTP listen port |
39
+ | `--port` | `http_port` from config, else `6029` | HTTP listen port |
40
40
  | `--no-boot` | off | Skip booting the app; answer from source alone |
41
41
 
42
42
  ### `tool`
@@ -215,7 +215,14 @@ Proof sources:
215
215
  PostgreSQL 17 with a partitioned table added, booted and static from
216
216
  schema.rb and structure.sql, a Rails 7.2 app whose schema.rb carries no
217
217
  partition marker, and Discourse, Mastodon, OpenFoodNetwork and Forem
218
- answering as before.
218
+ answering as before. In the v5.30.2 release QA: 14 real apps answered by
219
+ v5.30.1 and by the release branch side by side, and the lab regressions on
220
+ SQLite in all three tiers, PostgreSQL and MySQL. In the v5.31.0 release QA:
221
+ a private Rails 8.0 API app booted and static, and Discourse, Mastodon,
222
+ Forem, OpenFoodNetwork, OpenProject, Consul, Canvas and Errbit answered by
223
+ v5.30.2 and by the release branch side by side, every changed answer
224
+ checked against the app's source; the lab regressions on SQLite in all three
225
+ tiers, PostgreSQL and MySQL; and the unit suite on Ruby 3.1 with Rails 7.0.
219
226
  2. Non-crash coverage for every built-in tool including `get_view` in
220
227
  `spec/e2e/in_gemfile_install_spec.rb`'s full-tool sweep; output correctness
221
228
  (ivar cross-check, render-form detection, partial interfaces) verified
@@ -275,12 +282,21 @@ Postgres instance in `spec/e2e/postgres_install_spec.rb`, opt-in via
275
282
  stay marked `[UNAVAILABLE]`. A concern whose file cannot be found, a gem's
276
283
  module for example, is named under `Concerns` as not read.
277
284
  - **Callbacks are what the file declares.** In both tiers the callback list is
278
- what the model file, the app concerns it includes and the classes it
279
- inherits from declare. A callback a
280
- gem registers on include without an `include` line in the model file (Devise,
281
- counter caches, attachment purges) is not listed. Within one type the order
282
- is declaration order, the model file first and then its concerns, not the
283
- order Rails registered them in; the order across types is Rails' event order.
285
+ what the model file, the app concerns it includes and the classes it inherits
286
+ from declare. A callback a gem registers on include without an `include` line
287
+ in the model file (Devise, counter caches, attachment purges) is not listed.
288
+ Within one type the order is the order Rails runs them: base classes first, a
289
+ concern's callbacks where its `include` line stands, then the model's own,
290
+ and a `before_` or `around_` callback with `prepend: true` first.
291
+ `after_commit` and `after_rollback` run last declared first unless
292
+ `run_after_transaction_callbacks_in_order_defined` is on (`load_defaults 7.1`
293
+ or later; Rails 7.0 has no such setting). The booted tier reads the setting;
294
+ the static tier reads `load_defaults` and any assignment in
295
+ `config/application.rb` or an initializer, the last line that sets it
296
+ winning, and when those do not say, keeps declaration order and the answer
297
+ says so. The static tier does not read `config/environments/*.rb` for this
298
+ setting or for `belongs_to_required_by_default`, so a value set per
299
+ environment is missed. The order across types is Rails' event order.
284
300
  - **Concern-style Mongoid documents in runtime results.** Mongoid documents
285
301
  are invisible to ActiveRecord reflection, so `ModelIntrospector#call` falls
286
302
  back to the same source-parsing pass used in the static tier even when the
@@ -86,7 +86,7 @@ config file is written. Before v5.27.0 an empty list wrote every tool's files.
86
86
  | `preset` | Symbol | `:full` | `:full` (40 introspectors) or `:standard` (17 introspectors) |
87
87
  | `context_mode` | Symbol | `:compact` | `:compact` (context files capped at ~150 lines) or `:full` (no line cap) |
88
88
  | `introspectors` | Array of symbols | (from preset) | Override the introspector list directly |
89
- | `generate_root_files` | Boolean | `true` | Set `false` to generate split rules only, no root CLAUDE.md/AGENTS.md |
89
+ | `generate_root_files` | Boolean | `true` | Set `false` to generate split rules only: no CLAUDE.md, AGENTS.md, .cursorrules or copilot-instructions.md (`.ai-context.json` is still written) |
90
90
  | `anti_hallucination_rules` | Boolean | `true` | Embed 6-rule verification protocol in generated context files |
91
91
  | `claude_max_lines` | Integer | `150` | Max non-blank lines in a compact context file's gem-managed block, the `<!-- BEGIN/END rails-ai-context -->` markers included. Over budget, data lines are cut and the Commands, Warnings, Rules and MCP-tools sections kept whole |
92
92
 
@@ -213,6 +213,9 @@ fits neither that shape nor the conventional one.
213
213
  |:-------|:-----|:--------|:------------|
214
214
  | `sensitive_patterns` | Array | 34 patterns | File patterns blocked from search/read (`.env*`, `*.env`, `.envrc`, `*.key`, `*.pem`, `config/credentials.yml.enc`, `config/application.yml`, `config/settings.local.yml`, `.ssh/*`, etc.) |
215
215
 
216
+ A placeholder such as `.env.example` stays readable unless a path pattern or
217
+ its exact name blocks it; see [SECURITY.md](SECURITY.md).
218
+
216
219
  ### Extensibility
217
220
 
218
221
  | Option | Type | Default | Description |
@@ -247,7 +250,7 @@ Lightweight subset for faster generation:
247
250
  | AI Tool | Root File | Split Rules | MCP Config |
248
251
  |:--------|:----------|:------------|:-----------|
249
252
  | Claude Code | `CLAUDE.md` | `.claude/rules/*.md` | `.mcp.json` |
250
- | Cursor | - | `.cursor/rules/*.mdc` | `.cursor/mcp.json` |
253
+ | Cursor | `.cursorrules` (legacy fallback) | `.cursor/rules/*.mdc` | `.cursor/mcp.json` |
251
254
  | GitHub Copilot | `.github/copilot-instructions.md` | `.github/instructions/*.instructions.md` | `.vscode/mcp.json` |
252
255
  | OpenCode | `AGENTS.md` | `app/*/AGENTS.md` | `opencode.json` |
253
256
  | Codex CLI | (shares `AGENTS.md`) | (shares OpenCode rules) | `.codex/config.toml` |
data/docs/FAQ.md CHANGED
@@ -126,7 +126,7 @@ Yes. The file is applied first and a `configure` block wins only the keys it ass
126
126
 
127
127
  ### What's `generate_root_files`?
128
128
 
129
- When `true` (default), generates root files like CLAUDE.md, AGENTS.md. Set to `false` to only generate split rules (.claude/rules/, .cursor/rules/, etc.).
129
+ When `true` (default), generates the root files: CLAUDE.md, AGENTS.md, .cursorrules and copilot-instructions.md. Set to `false` to only generate split rules (.claude/rules/, .cursor/rules/, etc.). `.ai-context.json` is written either way.
130
130
 
131
131
  ---
132
132
 
data/docs/GUIDE.md CHANGED
@@ -826,7 +826,13 @@ rails_get_callbacks(model: "Order", detail: "full")
826
826
  → Order's callbacks with the actual method source code for each callback
827
827
  ```
828
828
 
829
- **Returns:** Callbacks grouped by type, in Rails event order. Includes concern-provided callbacks, with their bodies read from the concern file. Within one type the order is declaration order, not the order Rails registered them in.
829
+ **Returns:** Callbacks grouped by type, in Rails event order. Includes
830
+ concern-provided callbacks, with their bodies read from the concern file.
831
+ Within one type the order is the order Rails runs them, base classes first, a
832
+ `before_` or `around_` callback with `prepend: true` first, and
833
+ `after_commit`/`after_rollback` last declared first unless the app runs them in
834
+ order (`load_defaults 7.1`). A method declared again shows once with the later
835
+ declaration's conditions.
830
836
 
831
837
  ### rails_get_helper_methods
832
838
 
@@ -1353,7 +1359,7 @@ end
1353
1359
  | `live_reload_debounce` | Float | `1.5` | Debounce interval in seconds for live reload |
1354
1360
  | `server_name` | String | `"rails-ai-context"` | MCP server name |
1355
1361
  | `server_version` | String | gem version | MCP server version |
1356
- | `generate_root_files` | Boolean | `true` | Generate root files (CLAUDE.md, etc.) - set `false` for split rules only |
1362
+ | `generate_root_files` | Boolean | `true` | Set `false` to generate split rules only: no CLAUDE.md, AGENTS.md, .cursorrules or copilot-instructions.md (`.ai-context.json` is still written) |
1357
1363
  | `anti_hallucination_rules` | Boolean | `true` | Embed 6-rule Anti-Hallucination Protocol in generated context files - set `false` to skip |
1358
1364
  | `hydration_enabled` | Boolean | `true` | Inject schema hints into controller/view tool responses |
1359
1365
  | `hydration_max_hints` | Integer | `5` | Max schema hints per tool response |
@@ -186,7 +186,7 @@ It runs a single-pass Dispatcher that walks the AST once and feeds events to all
186
186
  | CallbacksListener | All AR callback types including `around_*`, `after_touch`, `after_initialize` and `after_find`; `after_commit` with `on:` resolution; a callback object by its constant, a block as `[inline_block]`; `if:`/`unless:` kept as the source wrote them, and the options of an enclosing `with_options` block |
187
187
  | MacrosListener | `encrypts`, `normalizes`, `delegate`, `has_secure_password`, `serialize`, `store`, `has_one_attached`, `has_many_attached`, `has_rich_text`, `generates_token_for`, `attribute` |
188
188
  | MethodsListener | `def`/`def self.`, visibility tracking, parameter extraction, `class << self`; `include_initialize: true` adds the constructor a caller reports on its own |
189
- | MixinsListener | `include`, `prepend`, `extend`, flagging the ones that reach the ancestor chain |
189
+ | MixinsListener | `include`, `prepend`, `extend`, `singleton_class.include` and `singleton_class.prepend` (`singleton_class.extend` is not read), flagging the ones that reach the ancestor chain, as reflection reports them |
190
190
 
191
191
  ### The targeted-walk listeners
192
192
 
@@ -214,8 +214,13 @@ Passed to `SourceIntrospector.walk(path, key => Listener)` when a specific file
214
214
  | MailboxRoutingListener | Action Mailbox `routing` and processing callbacks |
215
215
  | ModelReferenceListener | Model constants used in controllers: `Post.find`, `params.require(:post)`, ivar writes |
216
216
  | VariantCallListener | `variant` calls (ChainedCallListener with `:variant` preset) |
217
+ | ProcLiteralListener | Proc literals: line, source, assigned constant |
217
218
  | MethodCallListener | Call sites by name or pattern anywhere in a file, inside a `def`, a lambda or a block included, with arguments, options, receiver and line. Used by the Turbo introspector for broadcast calls and by `ActionFilters` for the skip macros |
218
219
 
220
+ `GenericMacroListener.new(*names, block_source: [:name])` adds `block`, the
221
+ one-line source of the block those macros are given. ProcLiteralListener is
222
+ what the job introspector reads a `queue_as` Proc with.
223
+
219
224
  ### Adding a listener
220
225
 
221
226
  1. Subclass `BaseListener` in `lib/rails_ai_context/introspectors/listeners/`. Use its helpers rather than re-reading nodes: `extract_symbol_args`, `extract_keyword_options`, `extract_arg_values` (source-slice fallback for expressions like `2.hours`), `extract_keyword_sources`, `extract_keyword_nodes`, `keyword_hash`, `constant_path_string`.
data/docs/SECURITY.md CHANGED
@@ -66,10 +66,23 @@ After validation, the query runs inside a transaction:
66
66
  |:---------|:----------|
67
67
  | PostgreSQL | `SET TRANSACTION READ ONLY` + `SET LOCAL statement_timeout` |
68
68
  | MySQL | `SET TRANSACTION READ ONLY` + `MAX_EXECUTION_TIME` hint |
69
- | SQLite | `PRAGMA query_only = ON` + progress handler for timeout |
69
+ | SQLite | A read-only connection in a child process, killed at timeout |
70
70
 
71
71
  All queries execute inside a transaction, then rollback (even if they could write, they can't).
72
72
 
73
+ A SQLite query runs in-process under `PRAGMA query_only = ON`, with no time
74
+ limit, for an in-memory database, on a platform without `fork`, or when it
75
+ needs a function, virtual-table module, collation or encryption key only the
76
+ app's own connection has. The table and EXPLAIN answers then say so; CSV output
77
+ stays plain data.
78
+
79
+ On sqlite3 1.x a query fails at its timeout, with the statement-timeout error,
80
+ when another connection in the same process held an exclusive lock on a
81
+ rollback-journal database as it started (`BEGIN EXCLUSIVE`): the child
82
+ inherits that lock record and it never clears. A lock held
83
+ by another process is waited on as usual, and an open write transaction or any
84
+ lock in WAL mode does not block it.
85
+
73
86
  ### Layer 3 - Row limit
74
87
 
75
88
  - Default: 100 rows
@@ -90,6 +103,9 @@ If one of your own columns merely looks sensitive (an `oauth_applications.secret
90
103
  config.query_allowed_columns = %w[secret]
91
104
  ```
92
105
 
106
+ The exemption covers the results too: an allowed name comes back unredacted. A
107
+ column declared with `encrypts` stays `[FILTERED]` either way.
108
+
93
109
  ### Environment guard
94
110
 
95
111
  > [!WARNING]
@@ -115,6 +131,12 @@ config/mongoid.yml config/redis.yml
115
131
  .ssh/* .aws/credentials .aws/config .netrc .pgpass .my.cnf
116
132
  ```
117
133
 
134
+ A placeholder whose name ends in `.example`, `.sample`, `.template` or
135
+ `.dist` (`.env.example`) is committed to be read, so a basename glob such as
136
+ `.env.*` does not block it. A pattern that names it exactly, with no glob
137
+ characters, still does, and so does a path pattern (one with a `/`, such as
138
+ `.ssh/*`), which covers everything under it.
139
+
118
140
  ### AI context file exclusions
119
141
 
120
142
  Search also excludes generated AI context files to prevent circular references:
data/docs/TOOLS.md CHANGED
@@ -179,8 +179,17 @@ Concerns as `[UNAVAILABLE]` rather than dropped.
179
179
  Callbacks grouped by type, in Rails event order, with source code. The list
180
180
  covers concern-declared callbacks too, with the body read from the concern
181
181
  file, and the "From Concerns" section says which concern declared each one.
182
- Within one type the order is declaration order (the model file first, then
183
- its concerns), not the order Rails registered them in.
182
+ Within one type the order is the order Rails runs them: base classes first, a
183
+ concern's callbacks where its `include` line stands, then the model's own, and
184
+ a `before_` or `around_` callback with `prepend: true` first. `after_commit`
185
+ and `after_rollback` are last declared first unless the app turns on
186
+ `run_after_transaction_callbacks_in_order_defined` (`load_defaults 7.1`). A
187
+ method declared again for one type shows once, with the later declaration's
188
+ conditions, as Rails keeps it. The order is read from source in both tiers,
189
+ and dynamic dispatch is not evaluated: a callback registered through
190
+ `send(:before_save, ...)`, through a method called with `send` or defined with
191
+ `define_singleton_method`, or through a module included in `class << self` is
192
+ not read, so the list can miss it or show the definition it replaces.
184
193
 
185
194
  | Parameter | Type | Default | Description |
186
195
  |:----------|:-----|:--------|:------------|
data/exe/rails-ai-context CHANGED
@@ -175,15 +175,14 @@ class RailsAiContextCLI < Thor
175
175
 
176
176
  desc "serve", "Start MCP server (stdio transport)"
177
177
  option :transport, type: :string, default: "stdio", desc: "Transport: stdio or http"
178
- option :port, type: :numeric, default: 6029, desc: "HTTP port (only for http transport)"
178
+ option :port, type: :numeric, desc: "HTTP port (only for http transport; default: config http_port, else 6029)"
179
179
  option :no_boot, type: :boolean, default: false, desc: "Skip booting the app; serve static analysis only"
180
180
  def serve
181
181
  boot_rails!(command: "serve", allow_static: true)
182
182
 
183
183
  transport = options[:transport].to_sym
184
- if transport == :http
185
- ::RailsAiContext.configuration.http_port = options[:port]
186
- end
184
+ # No Thor default: it would overwrite the config file's http_port.
185
+ ::RailsAiContext.configuration.http_port = options[:port] if options[:port]
187
186
 
188
187
  ::RailsAiContext.start_mcp_server(transport: transport)
189
188
  end