rails-ai-context 5.35.0 → 5.35.1

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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +826 -0
  3. data/CONTEXT.md +36 -6
  4. data/CONTRIBUTING.md +30 -1
  5. data/README.md +9 -9
  6. data/SECURITY.md +2 -2
  7. data/docs/ARCHITECTURE.md +12 -4
  8. data/docs/CLI.md +44 -15
  9. data/docs/COMPATIBILITY.md +29 -8
  10. data/docs/CONFIGURATION.md +28 -5
  11. data/docs/CUSTOM_TOOLS.md +8 -2
  12. data/docs/FAQ.md +5 -5
  13. data/docs/GUIDE.md +33 -65
  14. data/docs/INTROSPECTORS.md +4 -4
  15. data/docs/QUICKSTART.md +1 -1
  16. data/docs/RECIPES.md +5 -5
  17. data/docs/SECURITY.md +217 -64
  18. data/docs/SETUP.md +1 -1
  19. data/docs/STANDALONE.md +2 -2
  20. data/docs/TOOLS.md +137 -88
  21. data/docs/TROUBLESHOOTING.md +96 -8
  22. data/docs/_config.yml +1 -1
  23. data/docs/adr/0008-query-runs-only-where-it-can-guard.md +38 -0
  24. data/docs/adr/0009-dbhub-shapes-in-ruby.md +26 -0
  25. data/docs/adr/0010-one-config-tool-with-sections.md +32 -0
  26. data/docs/index.md +2 -2
  27. data/exe/rails-ai-context +24 -9
  28. data/lib/generators/rails_ai_context/install/install_generator.rb +2 -0
  29. data/lib/rails_ai_context/app_databases.rb +377 -0
  30. data/lib/rails_ai_context/app_kind.rb +2 -0
  31. data/lib/rails_ai_context/cli/app_root.rb +1 -0
  32. data/lib/rails_ai_context/cli/tool_runner.rb +110 -99
  33. data/lib/rails_ai_context/configuration.rb +15 -8
  34. data/lib/rails_ai_context/database_family/mysql_runtime.rb +171 -0
  35. data/lib/rails_ai_context/database_family/postgresql_plan.rb +109 -0
  36. data/lib/rails_ai_context/database_family/postgresql_runtime.rb +211 -0
  37. data/lib/rails_ai_context/database_family/sqlite_runtime.rb +363 -0
  38. data/lib/rails_ai_context/database_family.rb +278 -0
  39. data/lib/rails_ai_context/database_file.rb +61 -7
  40. data/lib/rails_ai_context/database_yml.rb +7 -29
  41. data/lib/rails_ai_context/doctor.rb +15 -16
  42. data/lib/rails_ai_context/instrumentation.rb +1 -1
  43. data/lib/rails_ai_context/introspectors/connection_pool_introspector.rb +8 -23
  44. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +6 -70
  45. data/lib/rails_ai_context/introspectors/env_introspector.rb +1 -1
  46. data/lib/rails_ai_context/introspectors/env_references.rb +1 -1
  47. data/lib/rails_ai_context/introspectors/job_introspector.rb +1 -1
  48. data/lib/rails_ai_context/introspectors/migration_introspector.rb +8 -11
  49. data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +9 -9
  50. data/lib/rails_ai_context/introspectors/model_introspector.rb +28 -27
  51. data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +24 -30
  52. data/lib/rails_ai_context/introspectors/node_source.rb +2 -0
  53. data/lib/rails_ai_context/introspectors/performance_introspector.rb +81 -24
  54. data/lib/rails_ai_context/introspectors/pg_partitions.rb +1 -4
  55. data/lib/rails_ai_context/introspectors/schema_conventions.rb +6 -8
  56. data/lib/rails_ai_context/introspectors/schema_dump_path.rb +12 -14
  57. data/lib/rails_ai_context/introspectors/schema_introspector.rb +47 -27
  58. data/lib/rails_ai_context/introspectors/schema_reader.rb +1 -1
  59. data/lib/rails_ai_context/introspectors/source_scan.rb +39 -4
  60. data/lib/rails_ai_context/migration_status.rb +18 -46
  61. data/lib/rails_ai_context/payload.rb +38 -1
  62. data/lib/rails_ai_context/pending_migrations.rb +3 -4
  63. data/lib/rails_ai_context/run_cache.rb +5 -0
  64. data/lib/rails_ai_context/schema_adapter.rb +13 -18
  65. data/lib/rails_ai_context/secret_columns.rb +480 -0
  66. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +2 -2
  67. data/lib/rails_ai_context/serializers/markdown_serializer.rb +13 -9
  68. data/lib/rails_ai_context/serializers/section_facts.rb +12 -2
  69. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +5 -2
  70. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +35 -7
  71. data/lib/rails_ai_context/server.rb +143 -29
  72. data/lib/rails_ai_context/sql_gate.rb +953 -0
  73. data/lib/rails_ai_context/tasks/rails_ai_context.rake +1 -2
  74. data/lib/rails_ai_context/test_helper.rb +3 -1
  75. data/lib/rails_ai_context/tools/analyze_feature.rb +11 -7
  76. data/lib/rails_ai_context/tools/base_tool.rb +71 -48
  77. data/lib/rails_ai_context/tools/dependency_graph.rb +1 -1
  78. data/lib/rails_ai_context/tools/diagnose.rb +105 -42
  79. data/lib/rails_ai_context/tools/generate_test.rb +4 -6
  80. data/lib/rails_ai_context/tools/get_active_support.rb +1 -6
  81. data/lib/rails_ai_context/tools/get_api.rb +1 -6
  82. data/lib/rails_ai_context/tools/get_autoload.rb +1 -6
  83. data/lib/rails_ai_context/tools/get_callbacks.rb +5 -5
  84. data/lib/rails_ai_context/tools/get_component_catalog.rb +6 -7
  85. data/lib/rails_ai_context/tools/get_concern.rb +6 -6
  86. data/lib/rails_ai_context/tools/get_config.rb +189 -12
  87. data/lib/rails_ai_context/tools/get_context.rb +111 -105
  88. data/lib/rails_ai_context/tools/get_controllers.rb +3 -4
  89. data/lib/rails_ai_context/tools/get_conventions.rb +1 -6
  90. data/lib/rails_ai_context/tools/get_engines.rb +1 -6
  91. data/lib/rails_ai_context/tools/get_env.rb +1 -6
  92. data/lib/rails_ai_context/tools/get_env_config.rb +2 -8
  93. data/lib/rails_ai_context/tools/get_frontend_stack.rb +1 -6
  94. data/lib/rails_ai_context/tools/get_gems.rb +1 -6
  95. data/lib/rails_ai_context/tools/get_helper_methods.rb +4 -5
  96. data/lib/rails_ai_context/tools/get_i18n.rb +1 -7
  97. data/lib/rails_ai_context/tools/get_job_pattern.rb +1 -1
  98. data/lib/rails_ai_context/tools/get_mailers.rb +1 -1
  99. data/lib/rails_ai_context/tools/get_model_details.rb +5 -8
  100. data/lib/rails_ai_context/tools/get_partial_interface.rb +3 -4
  101. data/lib/rails_ai_context/tools/get_schema.rb +10 -8
  102. data/lib/rails_ai_context/tools/get_service_pattern.rb +60 -24
  103. data/lib/rails_ai_context/tools/get_turbo_map.rb +1 -1
  104. data/lib/rails_ai_context/tools/migration_advisor.rb +47 -32
  105. data/lib/rails_ai_context/tools/onboard.rb +23 -20
  106. data/lib/rails_ai_context/tools/performance_check.rb +15 -2
  107. data/lib/rails_ai_context/tools/query.rb +141 -1355
  108. data/lib/rails_ai_context/tools/read_logs.rb +1 -1
  109. data/lib/rails_ai_context/tools/review_changes.rb +6 -7
  110. data/lib/rails_ai_context/tools/runtime_info.rb +119 -86
  111. data/lib/rails_ai_context/tools/safe_call.rb +69 -26
  112. data/lib/rails_ai_context/tools/search_code.rb +0 -4
  113. data/lib/rails_ai_context/tools/search_docs.rb +3 -1
  114. data/lib/rails_ai_context/tools/security_scan.rb +9 -5
  115. data/lib/rails_ai_context/tools/session_context.rb +18 -11
  116. data/lib/rails_ai_context/tools/validate.rb +1 -0
  117. data/lib/rails_ai_context/tools/validate_semantics.rb +5 -5
  118. data/lib/rails_ai_context/version.rb +1 -1
  119. data/lib/rails_ai_context.rb +4 -7
  120. data/server.json +2 -2
  121. metadata +14 -3
data/CHANGELOG.md CHANGED
@@ -5,6 +5,832 @@ 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.35.1] - 2026-10-13
9
+
10
+ ### Security
11
+
12
+ - **`rails_query` sends no statement to an adapter with no database family.**
13
+ On SQL Server, Oracle, CockroachDB and any other adapter outside PostgreSQL,
14
+ MySQL and SQLite it ran the statement with the text checks alone, with no
15
+ read-only transaction and no timeout. It now answers with the adapter's
16
+ name, and it opens no connection to do so.
17
+ - **MySQL bounds a statement that starts with WITH or EXPLAIN ANALYZE.** The
18
+ `MAX_EXECUTION_TIME` hint bound only a statement that starts with SELECT, so
19
+ one that starts with WITH or EXPLAIN ANALYZE ran with no time limit. The
20
+ session's `max_execution_time` now bounds those too and is put back after the
21
+ statement, also when the statement fails. It does not cover `EXPLAIN` or
22
+ `SHOW`, where a wait on a lock ends at the session's `lock_wait_timeout`.
23
+ - **`rails_query` keeps a column secret when `Redaction`'s secret words name
24
+ it.** Columns such as `passwd`, `salt`, `credentials` and `pepper` were
25
+ returned by `rails_query` while log and config redaction filtered those
26
+ words. They are now refused by name and filtered in results, so a column
27
+ name is secret in every tool or in none.
28
+ - **An `encrypts` column stays secret when `query_allowed_columns` lists it.**
29
+ The statement check let an allowed name through, and on MySQL and SQLite a
30
+ query that gave the column an alias returned the stored value. The check now
31
+ refuses the name on every database family, and the refusal says the allowed
32
+ list does not release it.
33
+ - **On PostgreSQL a statement whose plan cannot be read does not run.** The
34
+ checks that keep a secret column out of a result read the statement's plan,
35
+ and a statement ran without them when planning timed out or failed. Every
36
+ statement except `SHOW` now runs only with a plan. An `EXPLAIN` is planned
37
+ as the statement it explains. A planning timeout is answered as the timeout,
38
+ and a planning error as the SQL error.
39
+ - **`rails_query` refuses `LOCK IN SHARE MODE` and `FOR KEY SHARE`.** MySQL
40
+ and MariaDB ran `LOCK IN SHARE MODE` inside the read-only transaction, where
41
+ it locks the rows it reads. The gate now refuses it wherever it refuses
42
+ `FOR SHARE`, with `NOWAIT`, `SKIP LOCKED`, `OF` or `WAIT` after it too.
43
+ - **A view cannot rename a secret column past `rails_query`.** On MySQL,
44
+ MariaDB and SQLite a view that selects a secret column under another name
45
+ returned its values. A statement that names such a view is now refused with
46
+ the view and the column. So is a view built on it, a view whose definition
47
+ has a wildcard and names a table that holds a secret column, and a view
48
+ whose definition cannot be read on a database that holds secret columns. A
49
+ definition is read with its comments taken out, and a view's name is matched
50
+ whatever characters it holds. `query_allowed_columns` releases a column by
51
+ name as before, and an `encrypts` column stays secret. On PostgreSQL a plain
52
+ view keeps the filter on the one column. A materialized view stores its
53
+ rows, so no result column of it is traced to a table column, and one whose
54
+ definition names a secret column returned it under another name. A
55
+ statement that reads such a materialized view is now refused, through a
56
+ view over it too.
57
+ - **A generated column that reads a secret column is secret.** On every database
58
+ family its values were returned under its own name. A statement that names it
59
+ is now refused and its values come back `[FILTERED]`. Two tables can each have
60
+ a generated column of one name. That name is secret when either column reads a
61
+ secret column, and it is held as an `encrypts` column when either reads one.
62
+ - **The secret column rule knows every relation a statement reads.** It read the
63
+ columns of the connection's own database, or of the search path, and no
64
+ others. So a secret column of a table anywhere else came back under an alias
65
+ or through an expression. The rule now follows what a statement reaches. On
66
+ MySQL and MariaDB it reads every database the statement's text names. On
67
+ PostgreSQL it reads materialized views and foreign tables, every schema the
68
+ statement's text names, and every relation the plan names. On SQLite it reads
69
+ the temp schema and every attached database. Then it reads the databases or
70
+ schemas that the definitions of the views in the statement name, and follows
71
+ those views three reads deep. On PostgreSQL only a materialized view's
72
+ definition is followed, since the plan reads through a plain view. A statement
73
+ that names a view that leads deeper is refused with the view's name. A
74
+ relation whose columns cannot be read is not queried.
75
+ - **On PostgreSQL two aliases that differ only by case are two relations.**
76
+ The plan checks folded an alias to lower case, so a whole-row read of one
77
+ relation was taken for a read of the other. An alias is now read as the plan
78
+ prints it.
79
+ - **On SQLite the database file's raw pages are not read.** `sqlite_dbpage`,
80
+ `sqlite_dbdata` and `sqlite_dbptr` return the file's pages and cells, and
81
+ every stored value is in them, secret columns included. A statement that
82
+ names one is now refused.
83
+ - **On SQLite a shadow table of a virtual table is not read.** A virtual
84
+ table, such as a full-text one, keeps what it stores in shadow tables under
85
+ other column names, so a secret column's values came back from there. A
86
+ statement that names a shadow table is now refused, and the refusal names
87
+ the virtual table to query. Before SQLite 3.37 a table whose name starts
88
+ with a virtual table's name and an underscore is read as a shadow table.
89
+ - **On SQLite a virtual table that holds a secret column is refused whole.** A
90
+ search or a function over a virtual table reads a column by its position, with
91
+ no name written, so it returned or confirmed a secret column's values. A
92
+ statement that names such a table is now refused. So is one that names an
93
+ `fts5vocab` or `fts4aux` table over it, which lists its words, and one that
94
+ names a view over any of these tables. Only the columns a virtual table
95
+ declares decide this, so a full-text table whose own name reads like a secret
96
+ name runs. A virtual table whose definition names a refused view, or a table
97
+ that is not read, is refused with its `fts5vocab` or `fts4aux` table too.
98
+ - **`rails_query` also keeps nonce and initialization vector columns secret.**
99
+ A column whose name holds `nonce`, or is `iv` or ends in `_iv`, is refused
100
+ by name and filtered in results. `query_allowed_columns` releases one by
101
+ name, as for any name the heuristic flags.
102
+ - **`authentication_string` is a built-in secret name.** MySQL keeps password
103
+ hashes in a column of that name, and `rails_query` returned it. A statement
104
+ that names it is now refused, and its values come back `[FILTERED]`.
105
+ - **A column's name is compared the way the database compares it.** MySQL
106
+ folds a letter that Ruby's `downcase` leaves as it is, so a secret column's
107
+ name written with that letter passed the name check. Names in a statement
108
+ and in the schema are now folded the same way before they are compared.
109
+ - **A column-alias list is refused in every spelling.** `rails_query` refuses
110
+ a list such as `t(a, b, c)`, since it renames the columns a wildcard
111
+ returns. The check read one spelling of the list, so a list written another
112
+ way passed it and returned secret columns under other names. The gate now
113
+ reads the list by its shape on every family, whatever the quoting of its
114
+ names, with or without `AS`, and with `MATERIALIZED` or `NOT MATERIALIZED`
115
+ before its body. One name after a function call, such as
116
+ `generate_series(1, 5) AS g(n)`, still runs, and so does a list over
117
+ `VALUES` rows.
118
+ - **On MySQL, MariaDB and SQLite a `*` inside a subquery is refused when the
119
+ statement names a table that holds a secret column.** Such a statement names
120
+ no secret column, so it ran, and a comparison with the subquery or a new name
121
+ around it could tell or return a secret value. It is now refused with the
122
+ table and the column, and the refusal says to name the columns in the
123
+ subquery. `EXISTS (SELECT * ...)` and `count(*)` still run, and MySQL's
124
+ `(TABLE t)` counts as a `*`. PostgreSQL keeps its plan checks. SQLite reads a
125
+ string as a table's name wherever a name can stand, so there a string equal to
126
+ such a table's name counts as that name. SQLite's `x IN t` is its shorthand
127
+ for `x IN (SELECT * FROM t)`. It ran with no `*` written, and it is now held
128
+ to the same rule.
129
+ - **On MySQL and MariaDB the statement text checks hold under both readings of a
130
+ backslash in a string.** MySQL reads a backslash in a string as an escape,
131
+ unless the session's sql_mode holds `NO_BACKSLASH_ESCAPES`. The checks read it
132
+ as an escape alone. In a session with that mode a string ended earlier than
133
+ the checks took it to, and the text after it ran unchecked. Each check now
134
+ reads the statement both ways, and a statement that either reading refuses is
135
+ refused. A refusal that the second reading alone gives says so. `rails_query`
136
+ does not read the session's mode.
137
+ - **On SQLite a result column named `name:1` is filtered.** SQLite gives a
138
+ second result column of the same name the name `name:1`. The result filter
139
+ read that whole name, so a secret column that came back twice was returned
140
+ once in clear. The filter now reads the name without the suffix.
141
+ - **On SQLite a dollar-quoted span is read as code.** SQLite has no dollar
142
+ quoting, and the checks skipped the text between two `$tag$` marks as if it
143
+ were a string. They now read it.
144
+ - **`SHOW` with a `WHERE` clause is refused.** MySQL's statement timeout holds
145
+ `SELECT` only, so a `SHOW ... WHERE` filter ran past `query_timeout`. It is
146
+ now refused on every family and `SHOW ... LIKE` still runs. The refusal
147
+ names the `information_schema` query to use.
148
+ - **More `SHOW` forms are refused.** `SHOW FULL PROCESSLIST`,
149
+ `SHOW REPLICA STATUS`, `SHOW ALL SLAVES STATUS`, `SHOW ALL REPLICAS STATUS`,
150
+ `SHOW BINARY LOGS`, `SHOW BINARY LOG STATUS`, `SHOW RELAYLOG EVENTS`,
151
+ `SHOW ENGINE ... STATUS`, `SHOW PROFILE`, `SHOW PROFILES`,
152
+ `SHOW OPEN TABLES`, `SHOW EXPLAIN FOR`, `SHOW ANALYZE FOR`,
153
+ `SHOW USER_STATISTICS`, `SHOW CLIENT_STATISTICS` and the `SHOW WSREP_` forms
154
+ ran on MySQL and MariaDB. Most of them tell of other sessions' statements,
155
+ of replication hosts and users, or of the rows a log holds. They now get the
156
+ refusal `SHOW PROCESSLIST` gets.
157
+ - **A function that returns a whole table as one value is refused.**
158
+ PostgreSQL's `table_to_xml`, `schema_to_xml`, `database_to_xml` and the rest
159
+ of the `_to_xml` family returned a table, its secret columns included, as
160
+ one XML value. A call to one is now refused by name.
161
+ - **Relations that hold sampled values, other sessions' statements or stored
162
+ credentials are not read.** No check on a column's name applies to what they
163
+ hold, so a query on one ran and returned it. A statement that names one is now
164
+ refused. On PostgreSQL they include `pg_stats`, `pg_stats_ext`,
165
+ `pg_statistic`, `pg_statistic_ext`, `pg_largeobject`, `pg_stat_activity` and
166
+ the functions it reads through, `pg_stat_statements`,
167
+ `pg_prepared_statements`, `pg_cursors`, `pg_authid`, `pg_shadow`,
168
+ `pg_db_role_setting`, `pg_user_mapping`, `pg_user_mappings`,
169
+ `pg_subscription`, `pg_foreign_server`, `pg_foreign_data_wrapper`,
170
+ `pg_foreign_table`, `pg_settings`, `pg_show_all_settings()`,
171
+ `pg_show_all_file_settings()`, `pg_hba_file_rules` and
172
+ `pg_ident_file_mappings`. There the plan is checked too, so a view over one of
173
+ them is refused, the `information_schema` views of user mappings and foreign
174
+ servers among them. On MySQL and MariaDB they include the whole `mysql`
175
+ schema, `performance_schema`, `sys`, `processlist`, `general_log`, `slow_log`,
176
+ `column_statistics`, `column_stats`, `global_priv`, and in
177
+ `information_schema` `INNODB_TRX`, `INNODB_LOCKS`, `INNODB_LOCK_WAITS`, the
178
+ `INNODB_FT_` tables, `USER_STATISTICS`, `CLIENT_STATISTICS`, `PROFILING` and
179
+ `OPTIMIZER_TRACE`. On SQLite they include `sqlite_stat1`, `sqlite_stat3`,
180
+ `sqlite_stat4` and `sqlite_stmt`, which lists the connection's own
181
+ statements with their literals. docs/SECURITY.md lists every name. Each name
182
+ is refused on the database family that has it, wherever it stands in the
183
+ statement. So an app table called `slow_log` or `processlist` is refused on
184
+ MySQL and MariaDB.
185
+ - **On PostgreSQL a role's password hash and its stored settings are secret
186
+ columns.** `pg_authid.rolpassword` and `pg_db_role_setting.setconfig` were
187
+ returned, also through the views PostgreSQL builds on them. Both names are
188
+ now built-in secret names. A view that reads neither column still runs, so
189
+ `pg_roles` and `pg_user` answer, and `pg_roles.rolconfig` comes back
190
+ `[FILTERED]`.
191
+ - **PostgreSQL settings that can hold a password are not read.**
192
+ `SHOW primary_conninfo` and `current_setting('primary_conninfo')` returned a
193
+ connection string with its password, and `SHOW ALL` listed it.
194
+ `primary_conninfo`, `ssl_passphrase_command`, `archive_command`,
195
+ `restore_command`, `archive_cleanup_command` and `recovery_end_command` are
196
+ now refused, and so is a setting whose own name reads as a secret, such as
197
+ `app.jwt_secret`. On PostgreSQL `SHOW ALL` is refused and `SHOW` takes a
198
+ setting's plain name. `current_setting` runs only with the name written as
199
+ one plain string, so the gate can read which setting it returns.
200
+ - **`EXPLAIN FOR CONNECTION` is refused.** On MySQL and MariaDB it showed the
201
+ plan of the statement another session was running. It is now refused there,
202
+ also when it is written with `DESCRIBE` or `DESC`.
203
+ - **A function called by a quoted name is refused.** The checks that read a
204
+ function's name read it bare, so a function whose name was written in
205
+ quotes, backticks or brackets passed them. A quoted name followed by a
206
+ parenthesis is now refused on every family. On PostgreSQL the functions a
207
+ statement's text names are checked beside those its plan prints, since a
208
+ plan prints no row of a `VALUES` list and no `LIMIT` expression.
209
+ - **A database whose columns cannot be read is not queried.** When the schema
210
+ read failed, `rails_query` ran the statement with no column of the schema to
211
+ check its text against. It now answers "The columns of the <name> database
212
+ could not be read, so its secret columns cannot be told and no query runs on
213
+ it." The primary alone falls back to the schema in the cached context.
214
+ - **On PostgreSQL a result column is filtered when its table's columns cannot
215
+ be read and that table holds a secret column.** The trace from a result
216
+ column to its table column skipped a table whose column names it could not
217
+ read, so a secret column of that table could come back under another name.
218
+ Such a column is now filtered when its table holds a secret column. When the
219
+ table cannot be told either, it is filtered when any table does.
220
+ - **The reads a `rails_query` call makes beside the query have a time limit on
221
+ PostgreSQL, MySQL and MariaDB.** Those reads, such as the schema read behind
222
+ the secret column rule, had no time limit, so a call against a table another
223
+ session had locked waited until the lock went. Each family now bounds them the
224
+ way it bounds the query, a wait on a lock included. The query has
225
+ `query_timeout` to itself. The schema read, the PostgreSQL plan with its
226
+ function lookup, and the column trace share a second `query_timeout`, and each
227
+ is charged the time it ran. A read that runs out of time ends the call with an
228
+ error that names those reads, so the statements of a call take about twice
229
+ `query_timeout` at most.
230
+ On MySQL and MariaDB a wait on a lock ends at `query_timeout` rounded up to a
231
+ whole second, and `explain: true` is bounded while it waits on a lock too.
232
+ `SHOW`, `DESCRIBE` and a plain `EXPLAIN` have that lock wait limit alone
233
+ there, since the statement limit does not cover them. On SQLite those reads
234
+ run on the app's own connection with no statement limit, and a wait on another
235
+ writer ends at that connection's busy timeout.
236
+ - **`rails_runtime_info` reads table sizes, index usage and pending migrations
237
+ under `query_timeout` on PostgreSQL, MySQL and MariaDB.** They had no time
238
+ limit, so a locked table held the answer until the lock went. Each read now
239
+ ends at `query_timeout`, a wait on a lock included.
240
+ - **A `query_timeout` under a millisecond still bounds the statement.** On
241
+ PostgreSQL and MySQL it rounded to zero, which turns the statement limit
242
+ off. PostgreSQL, MySQL and MariaDB now bound the statement at one
243
+ millisecond.
244
+ - **SQLite no longer chooses the app's own connection from an error message.**
245
+ `rails_query` runs a SQLite statement in a child process that it ends at
246
+ `query_timeout`. It ran the statement again on the app's own connection, with
247
+ no time limit, when the child's error message read like a missing function,
248
+ module or collation, and text from the statement could reach that message. The
249
+ choice is now made from which step failed in the child. A statement runs on
250
+ the app's own connection in four cases: an in-memory database, a platform
251
+ without `fork`, a statement the child could not prepare, and a transaction
252
+ open on the app's connection. A statement that fails while it runs in the
253
+ child is answered with its SQL error and is not run again. On
254
+ the app's own connection a statement that returns rows slowly now ends a few
255
+ rows after `query_timeout` passes, since an interrupt lands only between two
256
+ rows. One long step still cannot be ended there, so the answer keeps its
257
+ "without a time limit" note.
258
+ - **On MySQL and MariaDB no statement runs on a connection Rails replaced.**
259
+ Rails reconnects a connection it lost before the transaction begins, and the
260
+ new session has no read-only transaction and no time limit. The statement is
261
+ now sent only on the session that holds both. On any other the answer is an
262
+ error that starts "The <name> database stopped answering during the query:"
263
+ and says the statement was not sent.
264
+ - **`rails_query` holds `LIMIT ALL`, `LIMIT NULL` and a negative limit to the
265
+ row cap.** The three mean no limit, and the query fetched every row before
266
+ the cap cut the answer. They now read as no limit of the query's own and
267
+ fetch one row past the cap, with the note that rows were held back.
268
+ - **A SQLite database whose file is not there is refused by name, and no file
269
+ is created.** In 5.35.0 the connection created an empty database file.
270
+ `rails_query` now answers with an error that names the database and
271
+ `bin/rails db:prepare`, with or without `database`. `rails_runtime_info`
272
+ does the same for a named database. With none, its Database section says the
273
+ primary does not exist yet and the other sections still answer. A database
274
+ written as a `file:` URI is checked at the path inside the URI, or at the
275
+ whole name on a SQLite build that reads no URIs.
276
+ - **`rails_runtime_info` creates no Solid Cache database.** Under an
277
+ environment whose Solid Cache SQLite file was not there, the cache section's
278
+ read created an empty one. The section now says the database does not exist
279
+ yet.
280
+ - **`skip_tools` holds inside composite answers.**
281
+ `rails_get_context(include:)` ran `rails_security_scan` and `rails_get_env`
282
+ when `skip_tools` listed them. An include whose tool is turned off is now
283
+ answered by saying so, and that tool does not run. `rails_get_context`,
284
+ `rails_diagnose` and `rails_review_changes` build every other section the
285
+ same way: from a built-in tool only when the app serves it. A section left
286
+ out is named in one line at the end of the answer.
287
+
288
+ ### Removed
289
+
290
+ Ten tools that each answered one section of the app's configuration leave
291
+ `tools/list`, and `rails_get_config` answers all of them through a new
292
+ `section` parameter (ADR-0010). The tool count is now **35** (was 45).
293
+
294
+ - **Ten tools are now sections of `rails_get_config`.** They are
295
+ `rails_get_gems` (`section:"gems"`), `rails_get_frontend_stack`
296
+ (`"frontend"`), `rails_get_api` (`"api"`), `rails_get_engines`
297
+ (`"engines"`), `rails_get_conventions` (`"conventions"`),
298
+ `rails_get_autoload` (`"autoload"`), `rails_get_active_support`
299
+ (`"active_support"`), `rails_get_env` (`"env"`), `rails_get_env_config`
300
+ (`"env_config"`) and `rails_get_i18n` (`"i18n"`). `rails_get_config()` with
301
+ no `section` answers `app`, what it answered before.
302
+ - **The sections' parameters ride on `rails_get_config`.** They are `detail`
303
+ (read by `frontend`, `api` and `env`), `category` (`gems`), `environment`
304
+ (`env_config`), `locale` (`i18n`), and `offset` and `limit` (`gems`,
305
+ `env_config`, `i18n`). A parameter the asked section does not read is named
306
+ in the answer as ignored.
307
+ - **`rails_onboard` drops `quick`.** It takes `summary`, `standard` and
308
+ `full`, the levels every other tool with `detail` takes, and `summary` is
309
+ the one-paragraph overview `quick` gave.
310
+ - **`tools/list` is smaller.** Measured as the JSON of every tool's
311
+ definition, it was 47,802 characters (about 11,950 tokens at four characters
312
+ per token) and is now 41,275 characters (about 10,320 tokens).
313
+
314
+ ### Migration notes
315
+
316
+ - **Old names keep working where a name is typed.** The CLI resolves `gems`,
317
+ `rails_get_gems`, `frontend_stack` and every other old short and full name to
318
+ its section, and `--json` reports `rails_get_config`. A call to a retired
319
+ name, from the CLI or over MCP, is answered from its section and ends with a
320
+ line naming the call to make instead. That line is the command under
321
+ `tool_mode: :cli` and the MCP call under `:mcp`, and it carries only the
322
+ arguments the section reads. A value goes into the call when it is a number, a
323
+ boolean, or at most 80 characters of letters, digits, `_`, `.` and `-`. Any
324
+ other value is named after the call, as in "_rails_get_i18n is now
325
+ `rails_get_config(section:"i18n")`, with the same `locale`; call that
326
+ instead._" `rails_session_context(mark:)` takes a retired name too, and
327
+ records it as `rails_get_config` with its section. Update saved prompts to the
328
+ new call: `rails_get_gems(category:"auth")` becomes
329
+ `rails_get_config(section:"gems", category:"auth")`.
330
+ - **`skip_tools` keeps its meaning.** An old tool name turns its section off,
331
+ so an app that skipped `rails_get_env` still keeps the `env` section off.
332
+ `rails_get_config` turns off the `app` section only. A section that is off
333
+ leaves the published `section` list, the tool description, the parameters
334
+ only it reads and the guide row. A call that names it anyway is told it is
335
+ turned off. The tool leaves `tools/list` only when all eleven names are
336
+ listed.
337
+ - **With the `app` section off, `section` is required.** `app` is the default,
338
+ so once `skip_tools` lists `rails_get_config` the published schema marks
339
+ `section` required and the description says so. A call with no `section` is
340
+ answered with the sections that are on, and the guide row shows a call to
341
+ the first of them.
342
+ - **A custom `rails_get_config` still replaces the built-in.** An app that
343
+ lists `rails_get_config` in `skip_tools` and registers a custom tool with
344
+ that name gets the custom tool in the built-in's place. The ten sections go
345
+ with the built-in, so a retired name such as `rails_get_gems` is then an
346
+ unknown tool, and no answer points at a section.
347
+ - **A custom tool that reuses a retired name is served as its own tool.** A
348
+ custom `rails_get_gems` answers a call to that name. The gem's own hints
349
+ still mean the built-in section, and they print it as
350
+ `rails_get_config(section:"gems")`.
351
+ - **Ruby callers are unchanged.** `RailsAiContext::Tools::GetGems.call` and
352
+ the other nine classes still answer. They are out of the MCP registry, so
353
+ `Tools::BaseTool.registered_tools` no longer lists them. The test helper's
354
+ `execute_tool("rails_get_gems")` still finds the class.
355
+ - **`rails_get_context(include:)` takes the same keys.** `config`, `gems`,
356
+ `env` and `conventions` now run as `rails_get_config` sections.
357
+ - **The session context names `rails_get_config`.** A section call is recorded
358
+ as `rails_get_config` with its section, also when a retired name made the
359
+ call. A section call answered with an error is not recorded.
360
+ - **The CLI's `--environment` is a section's filter only after `env_config`.**
361
+ After `config --section env_config`, in any capitalisation, it is the
362
+ section's filter and leaves `RAILS_ENV` alone, as it does after
363
+ `env_config`. After any other section it sets `RAILS_ENV`.
364
+ - **`rails_onboard(detail:"quick")` becomes `detail:"summary"`.** Over MCP a
365
+ `quick` call is refused with the three valid levels. The CLI's
366
+ `--detail quick` warns with them and answers at `standard`.
367
+ - **Apps on SQL Server, Oracle or CockroachDB lose `rails_query`.** It answers
368
+ with the adapter's name and runs nothing (ADR-0008). It ran there with no
369
+ read-only transaction and no timeout. On SQL Server and Oracle the `LIMIT`
370
+ its row cap appended is a syntax error, so a query without its own row limit
371
+ failed. Add `rails_query` to `skip_tools` to take it out of `tools/list`.
372
+ `rails_runtime_info` still answers, and says table sizes and index usage are
373
+ not available for the adapter.
374
+ - **`query_allowed_columns` no longer releases an `encrypts` column.** A query
375
+ that names one is refused, and the refusal says the allowed list does not
376
+ release it. Its values come back `[FILTERED]` under `SELECT *`. Take the
377
+ column out of the query.
378
+ - **`rails_query` refuses more column names.** A column named `salt`, `passwd`,
379
+ `credentials`, `pepper`, `authentication_string`, `rolpassword` or
380
+ `setconfig`, one whose name holds `nonce`, and one named `iv` or ending in
381
+ `_iv` is refused by name and comes back `[FILTERED]` under `SELECT *`. Each of
382
+ `salt`, `passwd`, `credentials` and `pepper` counts as a whole name and as one
383
+ part of a name split at its underscores. So `salt_rounds`, `aws_credentials`
384
+ and `pepper_spray` are refused, and `salty`, `basalt` and `peppermint` are
385
+ not. To read one that holds no secret, list its name in
386
+ `query_allowed_columns`.
387
+ - **On MySQL, MariaDB and SQLite a query over a view that reads a secret
388
+ column is refused.** It ran on 5.35.0 and returned what the view selected.
389
+ The refusal names the view and the column. Query the tables the view reads,
390
+ or list the column in `query_allowed_columns` when it holds no secret. An
391
+ `encrypts` column is not released that way. On PostgreSQL the same holds for
392
+ a materialized view.
393
+ - **A secret column in another database or schema is refused by name.** A
394
+ statement that names a schema or database beside the connection's own, such as
395
+ `SELECT api_token AS t FROM other_db.users`, ran on 5.35.0. The rule now reads
396
+ the columns there, so the statement is refused. Name the other columns, or
397
+ list the name in `query_allowed_columns` when it holds no secret. A statement
398
+ that names a view that leads through more than three reads of further
399
+ databases or schemas is not run, and the answer names the view.
400
+ - **On SQLite a virtual table with a secret column, a shadow table and the
401
+ page modules are refused.** A query on a full-text table that holds a secret
402
+ column ran on 5.35.0. So did one on a shadow table of a virtual table, on an
403
+ `fts5vocab` table over it and on `sqlite_dbpage`. Keep the secret column out
404
+ of the virtual table, or query the table it was copied from. A shadow
405
+ table's refusal names the virtual table to query.
406
+ - **On SQLite only the pragma functions that describe the schema or the file
407
+ run.** They are `pragma_table_info`, `pragma_table_xinfo`,
408
+ `pragma_table_list`, `pragma_index_list`, `pragma_index_info`,
409
+ `pragma_index_xinfo`, `pragma_foreign_key_list`, `pragma_foreign_key_check`,
410
+ `pragma_integrity_check`, `pragma_quick_check`, `pragma_user_version`,
411
+ `pragma_application_id`, `pragma_schema_version`, `pragma_page_size`,
412
+ `pragma_page_count`, `pragma_freelist_count`, `pragma_encoding`,
413
+ `pragma_auto_vacuum`, `pragma_journal_mode`, `pragma_compile_options` and
414
+ `pragma_pragma_list`. Every other `pragma_*` function ran on 5.35.0 and is
415
+ refused now, and so are `changes()`, `total_changes()` and
416
+ `last_insert_rowid()`. Read those values on the app's own connection, in
417
+ `bin/rails runner` or the console.
418
+ - **A statement over 1,000,000 bytes is refused.** 5.35.0 took a statement of
419
+ any length. Every text check reads the whole statement, so its length is
420
+ checked before any of them. Send a shorter statement: 100,000 ids in an `IN`
421
+ list are about 700,000 bytes. The text checks also give up when they have run
422
+ longer than `query_timeout`, with a refusal that says so.
423
+ - **A generated column that reads a secret column is refused by name.** A
424
+ query that names it ran on 5.35.0. Take it out of the query, or list its
425
+ name in `query_allowed_columns` when it holds no secret.
426
+ - **On MySQL, MariaDB and SQLite `SELECT *` inside a subquery is refused over a
427
+ table with a secret column.**
428
+ `WITH x AS (SELECT * FROM users) SELECT * FROM x` ran on 5.35.0, and is
429
+ refused now when `users` holds a secret column. Name the columns in the
430
+ subquery. On SQLite `WHERE id IN users` ran too, and is
431
+ refused the same way: write `IN (SELECT id FROM users)`. There a string equal
432
+ to the name of such a table, as in `WHERE role = 'users'`, refuses a statement
433
+ with a `*` inside a subquery or with that `IN` form.
434
+ - **On MySQL and MariaDB a statement has to pass under both readings of a
435
+ backslash.** A statement with a backslash before a quote inside a string, such
436
+ as `'it\'s'`, ran on 5.35.0 when the checks passed it with the backslash read
437
+ as an escape. It is refused now when the checks refuse it with the backslash
438
+ read as an ordinary character, as MySQL reads it under `NO_BACKSLASH_ESCAPES`.
439
+ Write a quote inside a string doubled, as in `'it''s'`, so that both readings
440
+ agree.
441
+ - **`SHOW ... WHERE` is refused.** Use `SHOW ... LIKE`, or select from
442
+ `information_schema`: `SELECT table_name FROM information_schema.tables
443
+ WHERE table_schema = DATABASE()`.
444
+ - **`rails_query` no longer reads statistics, session and credential
445
+ relations.** A query on one ran on 5.35.0 and is refused now. Each name is
446
+ refused on the database family that has it. On PostgreSQL they are
447
+ `pg_stat_activity`, `pg_stats`, `pg_settings` and the other catalog names
448
+ docs/SECURITY.md lists. On MySQL and MariaDB they are `performance_schema`,
449
+ `processlist`, `general_log`, `slow_log`, `column_statistics`,
450
+ `column_stats`, `global_priv`, the listed `information_schema` tables and
451
+ anything under `mysql.` or `sys.`. On SQLite they are `sqlite_stat1`,
452
+ `sqlite_stat3`, `sqlite_stat4` and `sqlite_stmt`. A name is matched as a
453
+ whole word wherever it stands, inside a string literal too, so an app table
454
+ named `slow_log` is refused on MySQL and MariaDB. The `mysql` and `sys`
455
+ schemas are matched as a name before a dot, with string literals masked, so
456
+ `'mysql.com'` runs. An alias named `sys` or `mysql` before a dot is refused
457
+ there, and so is a double-quoted string that holds `mysql.`. Give such an
458
+ alias another name, and write such a string in single quotes. Read the
459
+ refused relations with the database's own client.
460
+ - **On PostgreSQL `SHOW ALL` and six settings are refused.** `SHOW ALL`,
461
+ `SHOW "work_mem"` with quotes, and `SHOW` or `current_setting` of
462
+ `primary_conninfo`, `ssl_passphrase_command`, `archive_command`,
463
+ `restore_command`, `archive_cleanup_command` or `recovery_end_command` ran
464
+ on 5.35.0. Show one setting by its plain name: `SHOW work_mem`, or
465
+ `current_setting('work_mem')`. On PostgreSQL a statement that holds one of
466
+ the six names anywhere is refused.
467
+ - **A function whose name is written in quotes cannot be called.**
468
+ `SELECT "upper"('x')` ran on 5.35.0 and is refused now. Write the name
469
+ without quotes. A function whose name needs quotes, such as a mixed-case one
470
+ on PostgreSQL, cannot be called through `rails_query`.
471
+ - **The `rails_query` refusals say "secret column".** They said "sensitive
472
+ column", and now use the glossary's word. A script that matches on the old
473
+ words in a refusal has to match on the new ones. The config keys keep their
474
+ names.
475
+ - **`query_timeout` takes a positive number only.** Zero, a negative value or
476
+ a value that is no number raises `ArgumentError` in an initializer, as
477
+ `query_row_limit` does. In `.rails-ai-context.yml` it is reported and the
478
+ default of 5 seconds is kept. In 5.35.0 zero turned the timeout off on
479
+ PostgreSQL and MySQL. Set a larger number for a slow query. A fraction of a
480
+ second is allowed.
481
+ - **A CLI call with a value its parameter cannot take exits 1.** `--format xml`
482
+ on `schema`, `--limit abc` and `--no-limit` exited 0 in 5.35.0, the first two
483
+ with a warning on stderr and the parameter's default. A script that leaned on
484
+ the fallback has to pass a value the parameter takes. The Fixed entry "The CLI
485
+ refuses a value a parameter does not take" gives the answers. `--detail` alone
486
+ still warns and falls back.
487
+ - **Some CLI calls that exited 0 now exit 1.** Beside the refused values above,
488
+ they are a `rails_query` timeout, a `rails_query` call the production guard
489
+ refuses, a database that cannot be reached, a failing `explain: true`, a
490
+ failed Brakeman scan in the bundle or outside it, a crash inside a section
491
+ of `rails_get_context`, `rails_diagnose` or
492
+ `rails_review_changes`, and an unexpected failure in `rails_onboard`,
493
+ `rails_diagnose`, `rails_generate_test`, `rails_review_changes`,
494
+ `rails_runtime_info`, `rails_session_context`, `rails_get_context` or
495
+ `rails_search_code`. Over MCP the same answers carry `isError`.
496
+ - **Regenerate the context files.** Run `rails ai:context` or
497
+ `rails-ai-context context` so the tool guide and tables name the new call.
498
+
499
+ ### Added
500
+
501
+ - **`rails_query` and `rails_runtime_info` take `database`.** It is an app
502
+ database's name from `config/database.yml`, checked when the tool runs. With
503
+ none they read the primary, the database `ActiveRecord::Base` is connected
504
+ to. A misspelled name is answered with the closest one. A database only
505
+ other environments configure, a replica and one with `database_tasks` off
506
+ included, is answered with the environments that configure it. One only a
507
+ dump names is refused with the reason. On the static tier both tools are
508
+ unavailable as a whole, with or without `database`.
509
+ - **Another database is reached through a connection handler of its own.** The
510
+ handler lasts for the call, so the app's pools are untouched and two calls
511
+ at once never share a pool. The database's secret columns are read from its
512
+ own schema. It has `query_timeout`, rounded up to whole seconds, to connect
513
+ in unless its entry sets `connect_timeout`. On MySQL and MariaDB a read on
514
+ it ends after twice `query_timeout` plus 5 seconds unless its entry sets
515
+ `read_timeout`, so a server that goes silent ends the call. PostgreSQL's
516
+ driver has no read timeout, so a PostgreSQL server that goes silent after it
517
+ connects is not ended by the client.
518
+ - **A query that misses a table another database holds names that database.**
519
+ The SQL error ends with the database that holds the table and the `database`
520
+ value to use. A failing `explain: true` carries the same pointer.
521
+ - **`rails_runtime_info` checks `database` whatever the `section`.** A name it
522
+ cannot reach is an error for every section, and a section that does not read
523
+ `database` says so.
524
+ - **`rails_runtime_info` names a database no connection reaches.** It says what
525
+ stopped the connection and what to do, as the `rails_query` answer under Fixed
526
+ does. For a named database that is an error for the call. With none, the
527
+ Database section says so about the primary and the other sections still
528
+ answer.
529
+ - **A database whose adapter gem is not in the bundle is named with the gem to
530
+ add.** The answer reads "The <name> database cannot be reached: its
531
+ <adapter> adapter did not load. Add the <gem> gem to the Gemfile and run
532
+ `bundle install`." `rails ai:doctor` gives the same reason and fix, and
533
+ `rails_runtime_info` with no `database` still answers its other sections.
534
+ - **Under a named database `rails_runtime_info` shows that database's own
535
+ pending migrations.** For a replica it says Rails runs no migrations on it.
536
+ For a database whose entry turns `database_tasks` off it says "_Rails runs
537
+ no migrations on <name>: its database.yml entry turns `database_tasks`
538
+ off._"
539
+ - **A SQLite database other than the primary that lives in memory is refused
540
+ by name.** That is `:memory:`, or a `file:` URI with `mode=memory`. It
541
+ exists only inside the process that created it, and a new connection would
542
+ open an empty one. `rails ai:doctor` and `rails_runtime_info` leave such a
543
+ database out of their pending migrations check.
544
+ - **`.ai-context.json` carries `primary_name` where the primary has another
545
+ name.** The key sits in the `multi_database` section and holds the primary's
546
+ `config/database.yml` name. An app whose primary is named `primary` has no
547
+ such key.
548
+
549
+ ### Changed
550
+
551
+ - **Statistics say when an adapter has none.** On an adapter with no database
552
+ family, `rails_runtime_info` says table sizes and index usage are not
553
+ available for that adapter, and the database stats say the same of row
554
+ estimates.
555
+ - **`rails_query` reads the schema in a few statements, whatever the number of
556
+ tables.** The secret column rule read each table's columns with a statement of
557
+ its own on every call, about 1.5 seconds on a 300-table PostgreSQL schema and
558
+ 2 seconds on MySQL. One catalogue read per family now lists every table's
559
+ columns, and one each reads the view definitions and the generated columns.
560
+ With the lookups beside them that is four statements at most on MySQL and
561
+ MariaDB, five on PostgreSQL and six on SQLite. A statement that names another
562
+ schema or database, or a view that names one, adds the reads of that one. Any
563
+ other statement adds none.
564
+ - **`rails_get_service_pattern` says which files its count leaves out.** A
565
+ listing that counts fewer services than the service directories hold Ruby
566
+ files adds one line with the number left out and the kinds: base classes,
567
+ concerns, modules with no entry point that other code mixes in, mailers and
568
+ files too large to read. It names up to five of each kind, and the base
569
+ classes are named above the listing as before.
570
+
571
+ ### Fixed
572
+
573
+ - **Every file requires the standard library it names.** Twelve files used
574
+ `JSON`, `YAML`, `URI`, `Pathname`, `FileUtils`, `RbConfig`, `DidYouMean`,
575
+ `Prism`, `Time.parse` or `iso8601` and worked only when another library had
576
+ loaded it first. On a Ruby that does not load the library by itself that
577
+ showed as "uninitialized constant" or "undefined method".
578
+ - **`rails_onboard` answers on the static tier for an app with Solid
579
+ databases.** On a default Rails 8 app `tool onboard --no-boot` answered
580
+ "Onboard error: undefined method 'to_sentence'". The gem called an
581
+ ActiveSupport extension that only a booted app had loaded. It now loads the
582
+ extension itself.
583
+ - **A tool that fails unexpectedly is an error result.** `rails_onboard`,
584
+ `rails_diagnose`, `rails_generate_test`, `rails_review_changes`,
585
+ `rails_runtime_info`, `rails_session_context`, `rails_get_context` and
586
+ `rails_search_code` answered an exception they did not plan for as ordinary
587
+ text, so the CLI exited 0. They now answer as every other tool does:
588
+ `isError: true` over MCP, exit 1 from the CLI, and a text that starts "Tool
589
+ <name> failed:".
590
+ - **A crash inside a section of a composite answer makes the answer an error.**
591
+ `rails_get_context`, `rails_diagnose` and `rails_review_changes` build their
592
+ sections from other tools. When one of those raised an exception, the answer
593
+ was ordinary text and the CLI exited 0. It is now an error response on both
594
+ surfaces. The text keeps every section, and the crashed one reads "Tool <name>
595
+ failed: <Class>: <message>". A section that answers an error for another
596
+ reason, such as a refusal or a failed Brakeman run, stays a section of an
597
+ ordinary answer.
598
+ - **A stack overflow or a `SecurityError` in a tool is an error response.**
599
+ Either one ended the stdio server and printed a backtrace on the CLI. The
600
+ answer is now an error such as "Tool <name> failed: SystemStackError: stack
601
+ level too deep", and the server keeps running.
602
+ - **A failed Brakeman scan is an error result.** `rails_security_scan` answered
603
+ "Brakeman scan failed: ..." as an ordinary answer, an unknown check name
604
+ included. The text is the same, and it now carries `isError` over MCP and
605
+ exits 1 from the CLI. A brakeman run from outside the bundle that started and
606
+ failed is an error too, with the text it had. One that could not start stays
607
+ an ordinary answer.
608
+ - **`serve --transport http` starts on rack 2 with rackup 1.x.** It stopped
609
+ with "uninitialized constant Rackup::Handler" on an app that pins rack below
610
+ 3 and bundles the rackup gem.
611
+ - **The `rails_get_config` app section names the database family.** Its
612
+ Database line showed `mysql2` where every other surface says "MySQL". It now
613
+ reads "MySQL", "PostgreSQL" or "SQLite", and an adapter with no family as
614
+ database.yml writes it. The JSON keeps adapter names as written.
615
+ - **`rails_onboard` words one Solid database in the singular.** For one
616
+ framework with one database it said "keeps their tables in databases of
617
+ their own". It now says "Solid Cache keeps its tables in a database of its
618
+ own: cache."
619
+ - **`rails_runtime_info` names a Solid Cache store "SolidCache::Store".** The
620
+ cache section's Store line read "Store".
621
+ - **`rails_runtime_info` says when a database family has no index usage.** At
622
+ full detail on MySQL, MariaDB and SQLite it printed nothing where PostgreSQL
623
+ lists index usage. It now says "_Index usage is not available for the MySQL
624
+ database family._", or SQLite.
625
+ - **`rails_runtime_info` reads pending migrations in every database configured
626
+ here.** It said "all up to date" while another database had pending
627
+ migrations. The line now names the databases it read, as in "**Migrations:**
628
+ all up to date in primary, queue". A database it could not read gets a line
629
+ of its own with the reason, and the databases it did read are still
630
+ reported.
631
+ - **A booted MySQL app reads "MySQL".** A connection on mysql2 or Trilogy
632
+ showed its driver name in the generated context and in `rails_runtime_info`.
633
+ It now reads "MySQL" there, as the static tier already said. PostGIS reads
634
+ "PostgreSQL".
635
+ - **Migration advice for an undecided database names no database.** When the
636
+ app's database is one of two candidates, `rails_migration_advisor` no longer
637
+ gives PostgreSQL and MySQL advice at once.
638
+ - **Migration advice follows the database that holds the table.** Index and
639
+ lock advice took the primary's database family for a table another database
640
+ holds. It now takes the family of the database that holds the table.
641
+ - **Every surface names the same databases.** On a default Rails 8 app, where
642
+ the running environment configures only the primary and production configures
643
+ queue, cache and cable, the schema section, the multi-database section, the
644
+ stack overview and the model listing now all name the four databases. The
645
+ migrations section names each one whose migrations directories hold files. A
646
+ database only other environments configure is marked with every one of them,
647
+ "(production only)" or "(staging and production)", and one only a committed
648
+ dump names is marked "(committed dump only)". The mark is on every line that
649
+ names such a database: the `rails_get_schema` Secondary databases line, each
650
+ database `rails_onboard` names, the performance check's "Not checked" line,
651
+ the Database, Migrations and Databases lines of the compact generated files,
652
+ and the headings of the full generated file and of the schema rules file, as
653
+ in "## cache (production only, SQLite, 1 table)".
654
+ - **An app whose first database is not named `primary` is read by its own
655
+ names.** Where another database is the one named `primary`, the generated
656
+ files called both of them primary. They now name the first database by its own
657
+ name, as in "## Database Schema: animals (2 tables)". Migration advice for a
658
+ table the database named `primary` holds gave no `--database`, and now carries
659
+ `--database primary`.
660
+ - **A `replica` entry is read as Rails reads it.** `replica: "yes"`, or any
661
+ other value Ruby counts as true, marks a replica. The static tier took `true`
662
+ alone.
663
+ - **The static tier reads an environment whose whole value is a URL.** A
664
+ `config/database.yml` environment written as one URL string was left out. It
665
+ is now read as that environment's one database.
666
+ - **A secondary dump too large to read is named.** It keeps an entry in the
667
+ schema section that gives the reason, and a database whose migrations are
668
+ replayed instead no longer has its existing dump called missing.
669
+ - **`rails_onboard` says when a database's dump was not read.** For a secondary
670
+ whose dump is too large to read it said "(0 tables)" and counted the database
671
+ in its summary. It now says "plus the warehouse database (tables not read)",
672
+ and the summary reads "5 tables in 2 databases, the warehouse database not
673
+ read". The unread database is left out of both counts.
674
+ - **The performance check covers every database.** Missing indexes and counter
675
+ caches are checked in each database's dump, a finding outside the primary
676
+ names its database, and a database it could not read is listed under "Not
677
+ checked" with the reason.
678
+ - **Migration advice counts rows only on the primary.** The row estimate comes
679
+ from the primary database, so a table on another database is no longer given
680
+ the primary's count.
681
+ - **A `.rails-ai-context.yml` that holds no settings is reported and
682
+ ignored.** A file that holds a bare string, a list or a number crashed the
683
+ loader with a `NoMethodError`. It now warns on stderr that the file holds no
684
+ `key: value` settings, and reads nothing from it.
685
+ - **Of two tools under a built-in's name the server keeps the built-in.** With
686
+ no `skip_tools` entry the warning said it kept the built-in, while MCP could
687
+ serve an app's `BaseTool` subclass under that name. The server now keeps the
688
+ gem's own tool, as the warning says and as the CLI runs.
689
+ - **`tool --list` shows the tools the MCP server serves.** A custom tool that
690
+ shares a built-in's name is listed once, and the CLI's single-tool lookup
691
+ picks the tool the server serves while still loading only that tool's class.
692
+ - **No answer points at a tool the app turned off.** Every pointer to another
693
+ tool, in `_Next:` hints, fix steps, recovery lines and lists, goes through
694
+ one helper, and a tool `skip_tools` names is left out with its hint. A
695
+ custom tool in a built-in's place is left out the same way, since a hint
696
+ carries the built-in's arguments.
697
+ - **The CLI refuses a value a parameter does not take.**
698
+ `rails-ai-context tool schema --format xml` warned on stderr and answered in
699
+ the default format with exit 0, and so did `--limit abc` on a parameter that
700
+ takes an integer. Each now exits 1 and names what the parameter takes: the
701
+ listed values, "an integer", "a string", "an array", or "true or false" for a
702
+ boolean. An empty `--limit=` is refused too. So is `--no-<param>` on a
703
+ parameter that is no boolean, which passed `false` to the tool: `--no-limit`
704
+ answers "**Error:** `--no-limit` is not a valid `limit`. Valid: an integer".
705
+ A `--detail` level a tool does not have still falls back to the default with
706
+ a warning, since every level answers the same question.
707
+ - **`tool <name> --help` describes the tool the app serves.** Help read no
708
+ configuration, so `tool config --help` said "Default: app." in an app that
709
+ turned the `app` section off. Inside an app, or with `--app-path`, help now
710
+ reads `.rails-ai-context.yml` without booting. A tool or section `skip_tools`
711
+ turned off is answered as turned off at exit 1, and `tool config --help` says
712
+ "Required: the app section is turned off in this app." when `app` is off.
713
+ Outside an app the output is unchanged. A `skip_tools` set in an initializer
714
+ is not seen without a boot.
715
+ - **A call to a skipped tool shows in instrumentation.** Over MCP it left the
716
+ `rails_ai_context.tools.call` payload with no tool name and no error. The
717
+ payload now carries `tool_name` and `error: :tool_skipped`.
718
+ - **A model class in a `concerns` directory is listed.** A file in a `concerns`
719
+ directory of a model root holds a model when the class its path names has a
720
+ model superclass. Such a class was missing from the static tier's models, and
721
+ from the booted listing until something loaded it. It is now listed under the
722
+ constant its file declares. A concern module that holds an inner class stays a
723
+ concern, the mixins beside it stay out, and `rails_validate` gives the model's
724
+ file the model checks. On the static tier a class whose superclass is a gem's
725
+ own record base is not listed.
726
+ - **`rails_get_context(include:)` lists all twelve keys it takes.** It also
727
+ names a key it does not take, and a key given twice, in any capitalisation,
728
+ runs once.
729
+ - **`rails_get_context` says what is not there with a tool turned off.** With
730
+ `rails_get_controllers` in `skip_tools`, an `action` the controller does not
731
+ have is answered "Action 'nope' not found in PostsController. Available:
732
+ create, destroy, ...". With `rails_analyze_feature` in `skip_tools`, a
733
+ `feature` keyword that only model names hold is answered "The model names that
734
+ hold 'comment': Comment."
735
+ - **The performance check finds a `has_paper_trail` model's unindexed versions
736
+ table on the static tier.** paper_trail adds `has_many :versions, as: :item`
737
+ when the app boots, so only the booted tier reported
738
+ `add_index :versions, [:item_type, :item_id]`. The static tier now reads the
739
+ macro, and the versions class its options name (`versions: { class_name: ...
740
+ }`), so both tiers give the same finding.
741
+ - **A database `rails_query` cannot reach is an error that names it.** A server
742
+ that was down, or a database its server does not hold, came back as an
743
+ ordinary answer headed "Database unavailable" or "Database not found", with no
744
+ database named and advice to try `RAILS_ENV=test`. With or without `database`,
745
+ the answer is now an error that names the database, says what stopped the
746
+ connection and says what to do: start its server or fix its
747
+ `config/database.yml` entry, or run `bin/rails db:create`.
748
+ - **A connection lost while a statement runs is answered as one.** On Rails
749
+ 7.1 and later the answer is an error that starts "The <name> database
750
+ stopped answering during the query:".
751
+ - **`rails_query` refused in production is an error.** With
752
+ `allow_query_in_production` off, the refusal came back as an ordinary answer
753
+ and the CLI exited 0. The text is the same, and it is now an error response
754
+ like the tool's other refusals.
755
+ - **A locked SQLite database is answered as locked.** With another connection
756
+ holding the database, `rails_query` answered "Query exceeded 5 second timeout"
757
+ although nothing ran. It now answers "The database is locked by another
758
+ connection, so the query did not run. Run it again when the other connection
759
+ has finished." The query process waits on a lock for 80% of `query_timeout`
760
+ first. The process tells its parent the moment it meets a lock, so the answer
761
+ is the locked one however slow the machine is. A lock met while the connection
762
+ opens, as on the CLI or on a server's first call, gets the same answer.
763
+ - **On SQLite a statement sent inside an open transaction reads that
764
+ transaction's rows.** It ran in the query process, which opens the file
765
+ itself, so it answered from committed rows alone. With a transaction open on
766
+ the app's connection the statement now runs on that connection, and the
767
+ answer carries the "ran without a time limit" note.
768
+ - **On SQLite a pragma function no longer answers for the wrong connection.**
769
+ `pragma_foreign_keys`, `changes()` and their like answered for the
770
+ connection `rails_query` runs on, which is its own and has values the tool
771
+ set. They are now refused, and the Migration note lists the pragma functions
772
+ that run.
773
+ - **On PostgreSQL a wildcard over a partitioned table is filtered in place.**
774
+ `SELECT *` over a partitioned table, or over an inheritance parent, was
775
+ refused with a reason about a CTE or subquery when the table holds a secret
776
+ column. It now runs with that column `[FILTERED]`, also with an expression
777
+ beside the wildcard, as in `SELECT *, id + 1 AS next FROM events`. A whole-row
778
+ read of such a table stays refused when any of its partitions holds a secret
779
+ column.
780
+ - **A `rails_query` timeout is an error.** A query that ran past
781
+ `query_timeout` came back as an ordinary answer. With the same text it is
782
+ now an error response, for a query and for `explain: true`: the CLI exits
783
+ non-zero and an MCP client sees `isError`.
784
+ - **A guarded read leaves an open transaction as it found it.** On a
785
+ connection already inside a transaction, as in a transactional test,
786
+ PostgreSQL's `SET TRANSACTION READ ONLY` left the caller's transaction
787
+ read-only. PostgreSQL now guards inside a savepoint, and the outer
788
+ transaction stays writable. MySQL and MariaDB cannot make an open
789
+ transaction read-only, so there the statement is not sent: "Query failed:
790
+ MySQL cannot make a transaction that is already open read-only, so the
791
+ statement was not sent. Run it outside the transaction."
792
+ - **`rails_query` runs `JOIN ... USING (x, y)` and a row comparison.** The
793
+ column-alias check read `JOIN b USING (x, y)` and `WHERE (a, b) IN (...)` as
794
+ an alias list and refused them.
795
+ - **On PostgreSQL a subquery or join that reads no secret column runs.**
796
+ `SELECT (SELECT count(*) FROM users) AS n` was refused when `users` holds a
797
+ secret column, although `count(*)` reads none. It runs now, and so do
798
+ `count(id)`, `max(id)` and `EXISTS (SELECT 1 ...)` in its place. An
799
+ aggregate over a secret column, a whole row and `count(users)` stay refused.
800
+ - **On PostgreSQL a quoted mixed-case secret column is filtered in place.**
801
+ `SELECT *` over a table with a secret column such as `"ApiToken"` was refused
802
+ whole, where a lowercase one is filtered.
803
+ - **CSV output with no rows is the header row.** `format: "csv"` answered a
804
+ query that returned no rows with the sentence the table format prints.
805
+ - **EXPLAIN on MySQL leaves a row with no table out of the scan summary.**
806
+ `explain: true` on a statement that reads no table, such as `SELECT 1`,
807
+ listed a scan of `?`.
808
+ - **EXPLAIN on SQLite reads a constant row and an alias for what they are.**
809
+ `explain: true` on `SELECT 1` listed a full table scan of `CONSTANT` with a
810
+ warning, and now shows no scan summary. A table the statement gives another
811
+ name was listed as a table named by its alias, and now reads
812
+ `u (a name the statement gives)`.
813
+ - **Only a timeout is reported as a timeout.** `rails_query` read any SQL
814
+ error that mentioned "timeout" as a timeout, so an unknown column named
815
+ `timeout` came back as one. A timeout is now read from the error's class or
816
+ the database's native code, and MariaDB's 1969 counts.
817
+ - **MySQL table sizes count base tables only.** The row estimates already did.
818
+ - **An `explain: true` failure is an error.** A failing EXPLAIN, or one asked
819
+ of a statement other than SELECT or WITH, came back as an ordinary answer.
820
+ The CLI now exits non-zero and an MCP client sees `isError`. Asked of
821
+ another statement it answered "EXPLAIN only supports SELECT queries." and
822
+ now answers "`explain: true` takes a SELECT or WITH statement and explains
823
+ it.", with "The statement is already an EXPLAIN." in front when it is one.
824
+ - **An SQL error no longer ends in a colon.** Rails ends an error's first line
825
+ with a colon where the statement follows, and `rails_query` printed it.
826
+ - **`rails_query` runs a string literal that only reads like a refused
827
+ phrase.** The `OR 1=1` check is gone: the caller writes the whole statement,
828
+ so it protected nothing and refused `LIKE '%OR 1=1%'`. On PostgreSQL a
829
+ locking clause is read with literals masked, since its read-only transaction
830
+ refuses a locking read, so `'%ready for update%'` runs there. MySQL's
831
+ read-only transaction allows `FOR SHARE`, so there and on SQLite the raw
832
+ text still decides.
833
+
8
834
  ## [5.35.0] - 2026-10-12
9
835
 
10
836
  ### Added