rails-ai-context 5.36.9 → 5.37.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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1061 -0
  3. data/README.md +1 -1
  4. data/app/controllers/rails_ai_context/mcp_controller.rb +0 -10
  5. data/docs/ARCHITECTURE.md +1 -1
  6. data/docs/CLI.md +7 -1
  7. data/docs/COMPATIBILITY.md +21 -0
  8. data/docs/CONFIGURATION.md +1 -1
  9. data/docs/CUSTOM_TOOLS.md +2 -2
  10. data/docs/FAQ.md +1 -1
  11. data/docs/GUIDE.md +3 -3
  12. data/docs/INTROSPECTORS.md +10 -7
  13. data/docs/PROMPTS.md +1 -1
  14. data/docs/SECURITY.md +1 -1
  15. data/docs/SETUP.md +2 -2
  16. data/docs/TOOLS.md +11 -4
  17. data/docs/adr/0005-workspace-mcp-entries.md +1 -1
  18. data/exe/rails-ai-context +39 -8
  19. data/lib/generators/rails_ai_context/install/install_generator.rb +12 -12
  20. data/lib/rails_ai_context/action_filters.rb +1 -2
  21. data/lib/rails_ai_context/app_databases.rb +2 -2
  22. data/lib/rails_ai_context/app_inflections.rb +59 -0
  23. data/lib/rails_ai_context/base_mixins.rb +3 -3
  24. data/lib/rails_ai_context/brackets.rb +11 -0
  25. data/lib/rails_ai_context/change_watch.rb +2 -1
  26. data/lib/rails_ai_context/cli/entry_boot.rb +14 -2
  27. data/lib/rails_ai_context/cli/tool_runner.rb +19 -4
  28. data/lib/rails_ai_context/code_reloader.rb +1 -1
  29. data/lib/rails_ai_context/concern_macros.rb +46 -60
  30. data/lib/rails_ai_context/concern_membership.rb +10 -13
  31. data/lib/rails_ai_context/concern_paths.rb +132 -92
  32. data/lib/rails_ai_context/confidence.rb +8 -0
  33. data/lib/rails_ai_context/database_family/sqlite_runtime.rb +1 -1
  34. data/lib/rails_ai_context/database_family.rb +9 -4
  35. data/lib/rails_ai_context/doctor.rb +15 -20
  36. data/lib/rails_ai_context/facts_formatter.rb +4 -14
  37. data/lib/rails_ai_context/fingerprinter/snapshot.rb +3 -3
  38. data/lib/rails_ai_context/fingerprinter.rb +65 -21
  39. data/lib/rails_ai_context/gem_lock.rb +5 -0
  40. data/lib/rails_ai_context/hydration_result.rb +3 -3
  41. data/lib/rails_ai_context/hydrators/hydration_formatter.rb +0 -5
  42. data/lib/rails_ai_context/hydrators/model_hints.rb +6 -14
  43. data/lib/rails_ai_context/hydrators/view_hydrator.rb +2 -4
  44. data/lib/rails_ai_context/install/ai_tool.rb +2 -1
  45. data/lib/rails_ai_context/install/cleanup.rb +48 -21
  46. data/lib/rails_ai_context/install/program.rb +10 -4
  47. data/lib/rails_ai_context/install/selection_record.rb +4 -15
  48. data/lib/rails_ai_context/install/uninstall.rb +8 -2
  49. data/lib/rails_ai_context/install_mode.rb +1 -1
  50. data/lib/rails_ai_context/introspector.rb +3 -4
  51. data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +11 -3
  52. data/lib/rails_ai_context/introspectors/action_presence.rb +2 -2
  53. data/lib/rails_ai_context/introspectors/action_resolver.rb +0 -10
  54. data/lib/rails_ai_context/introspectors/active_support_introspector.rb +28 -26
  55. data/lib/rails_ai_context/introspectors/api_introspector.rb +7 -7
  56. data/lib/rails_ai_context/introspectors/association_class.rb +108 -0
  57. data/lib/rails_ai_context/introspectors/ast_walk.rb +18 -1
  58. data/lib/rails_ai_context/introspectors/auth_introspector.rb +5 -4
  59. data/lib/rails_ai_context/introspectors/autoload_introspector.rb +1 -3
  60. data/lib/rails_ai_context/introspectors/base.rb +4 -0
  61. data/lib/rails_ai_context/introspectors/call_site_expansion.rb +5 -2
  62. data/lib/rails_ai_context/introspectors/component_introspector.rb +4 -4
  63. data/lib/rails_ai_context/introspectors/controller_filters.rb +7 -10
  64. data/lib/rails_ai_context/introspectors/controller_introspector.rb +54 -34
  65. data/lib/rails_ai_context/introspectors/controller_settings.rb +8 -5
  66. data/lib/rails_ai_context/introspectors/convention_introspector.rb +19 -19
  67. data/lib/rails_ai_context/introspectors/credentials_introspector.rb +125 -15
  68. data/lib/rails_ai_context/introspectors/declared_constant.rb +248 -48
  69. data/lib/rails_ai_context/introspectors/devops_introspector.rb +11 -11
  70. data/lib/rails_ai_context/introspectors/eager_load.rb +45 -20
  71. data/lib/rails_ai_context/introspectors/env_config_introspector.rb +1 -1
  72. data/lib/rails_ai_context/introspectors/env_introspector.rb +0 -1
  73. data/lib/rails_ai_context/introspectors/env_references.rb +16 -1
  74. data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +18 -10
  75. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +4 -4
  76. data/lib/rails_ai_context/introspectors/includers.rb +49 -10
  77. data/lib/rails_ai_context/introspectors/job_introspector.rb +40 -21
  78. data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +93 -11
  79. data/lib/rails_ai_context/introspectors/listeners/branch_conditions.rb +5 -1
  80. data/lib/rails_ai_context/introspectors/listeners/chained_call_listener.rb +1 -1
  81. data/lib/rails_ai_context/introspectors/listeners/class_definition_listener.rb +6 -1
  82. data/lib/rails_ai_context/introspectors/listeners/component_structure_listener.rb +1 -0
  83. data/lib/rails_ai_context/introspectors/listeners/enums_listener.rb +15 -11
  84. data/lib/rails_ai_context/introspectors/listeners/gemfile_dsl_listener.rb +5 -5
  85. data/lib/rails_ai_context/introspectors/listeners/grape_api_listener.rb +6 -1
  86. data/lib/rails_ai_context/introspectors/listeners/inflections_listener.rb +123 -0
  87. data/lib/rails_ai_context/introspectors/listeners/methods_listener.rb +14 -51
  88. data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +5 -3
  89. data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +18 -22
  90. data/lib/rails_ai_context/introspectors/listeners/mixins_listener.rb +10 -4
  91. data/lib/rails_ai_context/introspectors/listeners/mount_listener.rb +37 -27
  92. data/lib/rails_ai_context/introspectors/listeners/owner_scope.rb +84 -21
  93. data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +572 -108
  94. data/lib/rails_ai_context/introspectors/listeners/routes_walk.rb +108 -0
  95. data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +37 -30
  96. data/lib/rails_ai_context/introspectors/middleware_introspector.rb +1 -1
  97. data/lib/rails_ai_context/introspectors/migration_introspector.rb +16 -3
  98. data/lib/rails_ai_context/introspectors/migration_replay/helpers.rb +8 -7
  99. data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +1080 -94
  100. data/lib/rails_ai_context/introspectors/migration_replay.rb +48 -23
  101. data/lib/rails_ai_context/introspectors/model_introspector.rb +37 -18
  102. data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +1 -1
  103. data/lib/rails_ai_context/introspectors/n_plus_one_scan.rb +49 -25
  104. data/lib/rails_ai_context/introspectors/observability_introspector.rb +1 -1
  105. data/lib/rails_ai_context/introspectors/performance_introspector.rb +5 -15
  106. data/lib/rails_ai_context/introspectors/pg_naming.rb +13 -19
  107. data/lib/rails_ai_context/introspectors/rails_version.rb +22 -0
  108. data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +3 -3
  109. data/lib/rails_ai_context/introspectors/render_calls.rb +237 -0
  110. data/lib/rails_ai_context/introspectors/rendered_record.rb +163 -24
  111. data/lib/rails_ai_context/introspectors/route_introspector.rb +53 -33
  112. data/lib/rails_ai_context/introspectors/schema_conventions.rb +33 -2
  113. data/lib/rails_ai_context/introspectors/schema_introspector.rb +30 -19
  114. data/lib/rails_ai_context/introspectors/schema_reader.rb +16 -2
  115. data/lib/rails_ai_context/introspectors/security_introspector.rb +34 -11
  116. data/lib/rails_ai_context/introspectors/seeds_introspector.rb +13 -5
  117. data/lib/rails_ai_context/introspectors/source_calls.rb +3 -2
  118. data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +481 -85
  119. data/lib/rails_ai_context/introspectors/superclass_chain.rb +1 -1
  120. data/lib/rails_ai_context/introspectors/table_name.rb +13 -32
  121. data/lib/rails_ai_context/introspectors/test_introspector.rb +23 -23
  122. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +24 -12
  123. data/lib/rails_ai_context/introspectors/view_introspector.rb +9 -25
  124. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +39 -76
  125. data/lib/rails_ai_context/legacy_cleanup.rb +7 -5
  126. data/lib/rails_ai_context/markup_source.rb +455 -0
  127. data/lib/rails_ai_context/mcp_config_generator.rb +144 -49
  128. data/lib/rails_ai_context/package_json.rb +8 -0
  129. data/lib/rails_ai_context/path_resolver.rb +106 -21
  130. data/lib/rails_ai_context/payload.rb +7 -4
  131. data/lib/rails_ai_context/percent.rb +1 -1
  132. data/lib/rails_ai_context/portable_path.rb +0 -4
  133. data/lib/rails_ai_context/resources.rb +8 -9
  134. data/lib/rails_ai_context/route_coverage.rb +16 -4
  135. data/lib/rails_ai_context/run_cache.rb +5 -0
  136. data/lib/rails_ai_context/safe_file.rb +42 -4
  137. data/lib/rails_ai_context/safe_path.rb +12 -0
  138. data/lib/rails_ai_context/secret_columns.rb +2 -6
  139. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +12 -7
  140. data/lib/rails_ai_context/serializers/context_file_serializer.rb +7 -4
  141. data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +1 -1
  142. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +3 -2
  143. data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +1 -1
  144. data/lib/rails_ai_context/serializers/section_marker_writer.rb +16 -14
  145. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +6 -5
  146. data/lib/rails_ai_context/server.rb +7 -19
  147. data/lib/rails_ai_context/sql_gate.rb +16 -12
  148. data/lib/rails_ai_context/tasks/rails_ai_context.rake +18 -5
  149. data/lib/rails_ai_context/test_framework.rb +204 -6
  150. data/lib/rails_ai_context/test_helper.rb +6 -2
  151. data/lib/rails_ai_context/tools/analyze_feature.rb +39 -68
  152. data/lib/rails_ai_context/tools/base_tool.rb +51 -12
  153. data/lib/rails_ai_context/tools/dependency_graph.rb +65 -117
  154. data/lib/rails_ai_context/tools/generate_test.rb +74 -18
  155. data/lib/rails_ai_context/tools/get_api.rb +1 -1
  156. data/lib/rails_ai_context/tools/get_concern.rb +77 -51
  157. data/lib/rails_ai_context/tools/get_context.rb +3 -6
  158. data/lib/rails_ai_context/tools/get_controllers.rb +13 -13
  159. data/lib/rails_ai_context/tools/get_edit_context.rb +13 -18
  160. data/lib/rails_ai_context/tools/get_env.rb +45 -38
  161. data/lib/rails_ai_context/tools/get_frontend_stack.rb +2 -5
  162. data/lib/rails_ai_context/tools/get_gems.rb +1 -1
  163. data/lib/rails_ai_context/tools/get_helper_methods.rb +26 -28
  164. data/lib/rails_ai_context/tools/get_partial_interface.rb +46 -141
  165. data/lib/rails_ai_context/tools/get_routes.rb +9 -8
  166. data/lib/rails_ai_context/tools/get_schema.rb +8 -3
  167. data/lib/rails_ai_context/tools/get_stimulus.rb +7 -4
  168. data/lib/rails_ai_context/tools/get_test_info.rb +75 -27
  169. data/lib/rails_ai_context/tools/get_turbo_map.rb +5 -4
  170. data/lib/rails_ai_context/tools/get_view.rb +37 -29
  171. data/lib/rails_ai_context/tools/migration_advisor.rb +64 -19
  172. data/lib/rails_ai_context/tools/onboard.rb +6 -1
  173. data/lib/rails_ai_context/tools/query.rb +1 -1
  174. data/lib/rails_ai_context/tools/read_logs.rb +1 -2
  175. data/lib/rails_ai_context/tools/review_changes.rb +40 -41
  176. data/lib/rails_ai_context/tools/runtime_info.rb +1 -6
  177. data/lib/rails_ai_context/tools/safe_call.rb +1 -1
  178. data/lib/rails_ai_context/tools/search_code.rb +1 -1
  179. data/lib/rails_ai_context/tools/search_docs.rb +6 -2
  180. data/lib/rails_ai_context/tools/security_scan.rb +2 -2
  181. data/lib/rails_ai_context/tools/session_context.rb +2 -1
  182. data/lib/rails_ai_context/tools/validate.rb +2 -0
  183. data/lib/rails_ai_context/tools/validate_semantics.rb +46 -31
  184. data/lib/rails_ai_context/update_check.rb +1 -1
  185. data/lib/rails_ai_context/version.rb +1 -1
  186. data/lib/rails_ai_context/vfs.rb +5 -5
  187. data/lib/rails_ai_context/view_file.rb +13 -1
  188. data/lib/rails_ai_context/watcher.rb +3 -1
  189. data/lib/rails_ai_context.rb +4 -3
  190. metadata +8 -1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,1067 @@ 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.37.0] - 2026-10-25
9
+
10
+ ### Security
11
+
12
+ - **Install and uninstall leave a `.gitignore` or initializer that links out of
13
+ the app as it is, and name it.** They wrote through such a link before, and
14
+ the legacy rule-file cleanup deletes nothing a symlinked rules folder carries
15
+ out of the app.
16
+ - **Uninstall and the dropped-tool cleanup no longer delete or edit a file that
17
+ a symlinked directory carries out of the app.** Such a file is left in place
18
+ and named as left behind a symlink the cleanup does not follow.
19
+
20
+ ### Changed
21
+
22
+ - **The Claude Code plugin needs rails-ai-context 5.36.9 or later**, the first
23
+ release whose `doctor` and update notice know it; an older one sent you to
24
+ `init`, which adds a second server beside the plugin's. With an older gem the
25
+ plugin's server connects with no tools and says to update it.
26
+ - **`rails_validate` with `level: "rails"` has Brakeman parse only the files it
27
+ checks and the app's models, so one file no longer costs a whole-app scan.**
28
+ The models keep a query on a model at the confidence a full scan gives it. On
29
+ a generated app of 30-line models they took one file's scan from 0.02s to
30
+ 0.32s at 250 models and from 0.04s to 0.92s at 700 (Brakeman 8.1.0, best of
31
+ 3). A view's warning that comes from its controller's params shows when both
32
+ files are passed; `rails_security_scan` still scans the whole app. (#529)
33
+ - **`TestHelper#execute_tool` raises when the tool returns an error, and
34
+ `execute_tool_with_error` fails the test when the call succeeds.** Before,
35
+ both helpers returned whatever the tool answered, so a test on a broken tool
36
+ passed. A suite that read an error text through `execute_tool` should call
37
+ `execute_tool_with_error` instead. (#593)
38
+ - **The env section reads each source file once and keeps its scan until the
39
+ files change.** The ENV scan and the HTTP client check share one read, and the
40
+ result lives beside the shared context, so a repeated call costs milliseconds
41
+ rather than about a second. The read runs without holding the shared cache's
42
+ lock, so other tools do not wait on it. (#597)
43
+ - **Doctor introspects once.** The introspector health check and the context
44
+ files' dry run read the same run, so doctor takes about half the time on a
45
+ large app, and the health check no longer reports introspectors that fail only
46
+ when run alone. (#598)
47
+
48
+ ### Fixed
49
+
50
+ - **A standalone run on a Windows Ruby reached through a link keeps this gem's
51
+ paths ahead of Ruby's own lib directories.** setup-ruby unpacks Ruby in one
52
+ folder and runs it through a link to it from the tool cache: there RbConfig
53
+ names Ruby's directories by the link's target while the load path names them
54
+ by the link. The paths put back after the app's boot were matched to
55
+ RbConfig's spelling, found none, and went last, behind Ruby's own copies, so a
56
+ gem with a default-gem twin (prism, json) could load half from each. Ruby's
57
+ own entries are now found by the mark Ruby gives them, the one RubyGems
58
+ inserts gem paths by.
59
+ - **A route in a resource block whose path has a dynamic segment takes no name,
60
+ and the resource's own route keeps its name.** A route like
61
+ `delete "groups/:group_id" => "users#remove_group"` inside `resources :users`
62
+ was named after the resource (`user`), so `DELETE /users/:id` lost its name.
63
+ Rails gives such a route no name unless it has `as:`.
64
+ - **A response cut at the size limit ends on a whole line.** It was cut
65
+ mid-line, so the last line could read as half a fact, such as an index named
66
+ on no columns.
67
+ - **A static route whose path ends in a slash or holds `//` gets the helper name
68
+ Rails gives it.** The name is built from the cleaned path, so
69
+ `/reports/daily/` is `reports_daily`, not `reports_daily_`. (#540)
70
+ - **`get_routes` and `analyze_feature` pass a glob segment to the route
71
+ helper.** `get "docs/*section", as: :doc` now reads `doc_path(:section)` with
72
+ `[section]`, since Rails requires the key. Both tiers list the glob among the
73
+ route's params. (#541)
74
+ - **A static verb route named by a symbol takes its URL from `path:`.** `get
75
+ :stats, path: "statistics"` in a member block is `/heroes/:id/statistics`, and
76
+ the helper and action still come from the symbol. A computed `path:` is
77
+ counted as a construct the static tier could not expand. (#542)
78
+ - **Both tiers name the root route when `GET /` also answers another verb or any
79
+ verb.** The static tier gave none for `match "/", via: [:get, :post]`, and the
80
+ booted tier gave none for `via: :all`. (#589)
81
+ - **Static routes read an in-repo engine's routes whatever order the
82
+ introspectors run in.** When the engines section ran first, the routes section
83
+ reported the engine as needing a boot. (#587)
84
+ - **`routes` lists a large table in seconds.** Summary folding and the PATCH/PUT
85
+ merge each rescanned every route per route. A namespace of 2000 controllers
86
+ took 24s and now takes 2s, with the same output. (#601)
87
+ - **Static routes list a resource's block and concern routes before its default
88
+ ones, the order Rails matches them.** The static list put `/heroes/:id` ahead
89
+ of `/heroes/search`, unlike Rails and the booted tier. A member block that
90
+ draws `post :update` no longer splits the resource's own PATCH and PUT update
91
+ routes into two rows. (#528)
92
+ - **Static routes match Rails for a singular resource's bare verb, a resource at
93
+ `path: ""`, and a multi-name `resources` block.** `get :avatar` in `resource
94
+ :profile` is `avatar_profile`, `resources :pages, path: ""` is `/:id`, and
95
+ `resources :users, :groups do ... end` draws the block under each. (#508)
96
+ - **Static routes name a resource nested in a `member` or `collection` block the
97
+ way Rails does.** `resources :posts do member { resources :votes } end` names
98
+ its routes `votes` and `vote_tally`, not `post_votes` and `post_vote_tally`.
99
+ - **A `scope` inside a static resource block goes before the resource's path and
100
+ name, as Rails draws it.** `scope "x"` inside `resource :settings` gives
101
+ `/x/settings/inner`, and `scope "y", as: "why"` inside `resources :posts`
102
+ names `why_post_qux`. A shallow resource's member and nested routes take the
103
+ scope after the shallow path and prefix.
104
+ - **A static route in a `scope` with `as:` inside a `member`, `collection` or
105
+ `new` block keeps that `as:` in its name, as Rails gives it.** `member {
106
+ scope("qq", as: "qux") { get :mm } }` in `resources :heroes` is
107
+ `mm_qux_hero`, not `mm_hero`.
108
+ - **Static routes draw a canonical action given in a member, collection or new
109
+ scope at that scope's path, named without the action.** `get :show, on:
110
+ :member, path: "sh"` in `resources :heroes` is named `hero`, not `show_hero`,
111
+ and without `path:` it is `/heroes/:id`. A singular resource's bare verbs are
112
+ member routes, so `get :index` in `resource :profile` is `/profile`, not
113
+ `/profile/index`.
114
+ - **Static routes give the controller Rails does for a route inside a namespace
115
+ or `scope module:` within a resource block.** `resources :posts do namespace
116
+ :admin do get :foo end end` routes to `admin/posts#foo`, not `posts#foo`.
117
+ - **get_autoload lists an app's custom inflections whatever the inflections
118
+ block calls its parameter.** A rule written as `inflections(:en).acronym "X"`,
119
+ on `_1` or `it`, on a local holding `inflections(:en)`, or inside
120
+ `instance_eval` is listed too, as Rails applies all of them. (#583)
121
+ - **Without a boot, a table and a route helper get the names Rails gives them
122
+ under the app's own inflections.** The static tier now reads the literal
123
+ `irregular`, `plural`, `singular`, `uncountable`, `acronym` and `human` rules
124
+ in the app's initializers and applies them for that run only, in tool answers,
125
+ generated context files and resource reads alike, so `Hero` reads `heroes` and
126
+ `resources :staffers` names `staff_path`. The rules never reach another app,
127
+ another thread or the booted tier. (#521)
128
+ - **Without a boot, a constant's file is the one Zeitwerk loads it from.** With
129
+ `acronym 'JsonLd'`, `include JsonLdHelper` reads
130
+ `app/helpers/json_ld_helper.rb`, found by the file name and not the
131
+ underscored constant, so its filters are no longer reported as not read, and a
132
+ lib module's `helper_method`s are listed when its path splits an app acronym.
133
+ Where only a Zeitwerk inflector override spells a name, the file counts when
134
+ it declares that constant, and when several such files share a name, the one
135
+ that declares it is taken. (#521)
136
+ - **Without a boot, get_mailers names a mailbox and its routing target with the
137
+ app's acronym.** Each mailbox takes the class its file declares, and a route
138
+ takes the mailbox whose file its target names, so an app with
139
+ `inflect.acronym "SMS"` gets `SMSMailbox`, as Action Mailbox and Zeitwerk do,
140
+ even when the acronym is computed in a loop. (#571)
141
+ - **The static tier reads a PostgreSQL unlogged table.** A `CREATE UNLOGGED
142
+ TABLE` in `db/structure.sql` or in a migration's `execute` was missing from
143
+ the schema, though Rails lists it as a table. (#543)
144
+ - **The static tier keeps a float column's limit as Rails dumps it.** A MySQL
145
+ `double` reads as `float, limit: 53` and a PostgreSQL `real` as `float, limit:
146
+ 24` (Rails 7.2.2 and later), from structure.sql and from replayed migrations.
147
+ A PostgreSQL `real[]` column reads with limit 24, as Rails reads its element.
148
+ (#546)
149
+ - **A table whose name only starts with `schema_migrations` or
150
+ `ar_internal_metadata` is listed.** Tables such as `schema_migrations_archive`
151
+ were hidden on both tiers, and the static tier called them missing. The booted
152
+ tier now hides the bookkeeping tables under the app's `table_name_prefix` and
153
+ `table_name_suffix`, as SchemaDumper does. `rails_query` no longer treats a
154
+ prefixed `ar_internal_metadata.key` as a secret column. The static schema
155
+ summary no longer reports `schema_migrations` missing when db/schema.rb
156
+ declares it; the declared-table list leaves out the bookkeeping tables, as the
157
+ table list does. (#547)
158
+ - **A hand-written `db/schema.rb` gets its `t.timestamps` columns and index
159
+ names on the static tier.** `created_at` and `updated_at` were missing, and an
160
+ index declared without `name:` had a blank name instead of Rails'
161
+ `index_<table>_on_<columns>`. In a `Schema[4.2]` or older file the timestamps
162
+ allow NULL, as Rails' 4.2 compatibility gives them. (#592)
163
+ - **A `get_schema` JSON page lists only its own tables' check constraints and
164
+ generated columns.** On a large app those app-wide lists filled the response
165
+ cap and pushed out `tables_without_model_file` and `gem_owned_tables`. (#595)
166
+ - **`migration_advisor` writes migrations that run and roll back to the same
167
+ column.** A `references` or `belongs_to` type on `add_column` now writes
168
+ `add_reference` with the key's `type:` and `foreign_key:`, keeping a
169
+ `foreign_key:` that `options` gives, and its strong_migrations hints add no
170
+ key when that is `foreign_key: false`. `change_type`'s `down` and
171
+ `remove_column` carry the column's limit, precision, scale, `array:`, null and
172
+ default, a boolean or number default as its literal, and `create_table` puts
173
+ `options` on its line. `create_table` keeps the app's key type unless the
174
+ options really pass `id:`; a string option that contains "id:" no longer drops
175
+ it. A reference whose table the app does not have (`editor` with no `editors`
176
+ table) is named as a role: the answer says to place it with `add_association`
177
+ and gives the `belongs_to` its model needs, and no longer offers the generator
178
+ line that would add a foreign key to a missing table; with strong_migrations,
179
+ it notes that `add_reference` builds its index without
180
+ `algorithm: :concurrently`. (#507)
181
+ - **`migration_advisor` refuses a `new_name` or `create_table` column name that
182
+ is not a plain identifier, and writes the index `title:string:index` asks
183
+ for.** It returns the same "Invalid column name" error other actions give, and
184
+ `uniq` writes a unique index. `title:index` is an indexed string, and a
185
+ reference's `uniq` writes `index: { unique: true }`, as the Rails generator
186
+ reads them. (#526)
187
+ - **Schema replay leaves an existing column or index alone when `add_column` or
188
+ `add_index` passes `if_not_exists: true`.** The static schema no longer swaps
189
+ the column's type or lists the index twice. From Rails 7.1 an `add_reference`
190
+ keeps an existing column and index the same way on every adapter. An existing
191
+ foreign key or check constraint is skipped under `if_not_exists: true` outside
192
+ SQLite, whose table rebuild adds them again. A check constraint
193
+ exists when its name matches, the given one or the `chk_rails_` name Rails
194
+ derives from the table and expression. A foreign key exists when one to the
195
+ same table matches the way the app's Rails compares them: 7.0 by table alone,
196
+ 7.1 and 8.0 also by a column the call gives, and 8.1 by the column, the
197
+ default `<table>_id` when none is given, and a given primary key. (#545)
198
+ - **Schema replay moves foreign keys through `rename_table` and
199
+ `rename_column`.** A key keeps pointing at the renamed table and column, as
200
+ the database's dump shows. A column inside a composite foreign key or
201
+ composite primary key follows the rename too. (#512)
202
+ - **Schema replay drops every table `drop_table` names, and the join table
203
+ `drop_join_table` names.** The static schema no longer lists tables those
204
+ migrations removed. (#544)
205
+ - **Recent migrations list only the schema calls a migration makes.**
206
+ `change_column_null` no longer also reads as `change_column`, `remove_columns`
207
+ reads as `remove_column`, and a name in a comment is not counted. A call on
208
+ the migration's connection still counts, also through a local or a
209
+ `with_connection` block parameter. (#548)
210
+ - **A class built with `::Class.new(...) do ... end` inside a model gets its own
211
+ associations, scopes and validations on the static tier.** They were credited
212
+ to the model around it, and the nested class showed none. (#538)
213
+ - **Static `model_details` lists the values of `enum :status, active: 0,
214
+ archived: 1`.** Rails reads every bare keyword after the name as a value,
215
+ `prefix:` included, and the static tier now does the same, where it used to
216
+ show the source text as computed. (#537)
217
+ - **A concern macro called with `**hash` no longer takes a keyword's default
218
+ when the hash may set it.** The rules that depend on that keyword are listed
219
+ as not decided from the source, unless a literal `key:` pair follows the
220
+ spread. (#579)
221
+ - **The conventions section reads every model file when it detects patterns.**
222
+ An app with more than 500 model files lost versioning, polymorphic and the
223
+ other patterns that only showed up in a later file. (#585)
224
+ - **A booted STI base lists only its direct subclasses as its children.** A
225
+ grandchild was listed beside them, which disagreed with the static tier and
226
+ with the grandchild's own parent. (#531)
227
+ - **A file that does not compile leaves out only the files whose constants
228
+ resolve to it.** Names now resolve the way Ruby resolves them where they are
229
+ written, so a broken `Legacy::ApplicationRecord` no longer marks every
230
+ top-level model as unloadable, and a broken `Admin::ApplicationController` no
231
+ longer takes every top-level controller. Inside a namespace that defines its
232
+ own class of that name, a broken top-level one blocks nothing. (#578)
233
+ - **`rails_get_concern` names a concern that another concern includes, and the
234
+ classes that get it through that concern.** Before, a concern included only by
235
+ another concern read as "Nothing under app/ includes this concern." A module
236
+ passes its concern on the same way wherever its file sits, so the classes that
237
+ include it are listed too. Each includer is labelled by its own declaration,
238
+ `(concern)` or `(module)`, never by its folder, a module that wires a
239
+ validator is named by the module holding the `validates` call, and a plugin's
240
+ `concerns/group.rb` declaring `Billing::Group` is no longer taken for the
241
+ app's `Group`. A constant written inside `module A::B` resolves the way Ruby
242
+ resolves it there, never through `A`, for includers, a model's or controller's
243
+ concerns, a controller's base class and a spec's described class. A class
244
+ written `class ::Foo` inside a module is named Foo, for everything declared in
245
+ it, and a constant used in a `Class.new` block resolves from the scope around
246
+ the assignment. A class written `class Shop::Draft` inside `module Shop` is
247
+ Shop::Draft, as Ruby's lexical lookup finds the enclosing Shop, and
248
+ Shop::Shop::Draft below a `Shop = Class.new`: a constant written with `=`,
249
+ with `||=` where the name does not exist yet, or in a multiple assignment
250
+ counts as declared for every name read after it, in models, tests, Grape APIs
251
+ and mixin records alike. A module read out of another file finds its includes
252
+ through its own nesting, so `include Marks` in `module Shop::Tagging` inside
253
+ `module Shop`, in `module Foo::Tagging` inside `class ::Foo`, or in a
254
+ `Module.new` block reaches the module Ruby includes. A `Struct.new`,
255
+ `Module.new` or `Data.define` block owns its includes and declarations as a
256
+ `Class.new` block does. A receiver such as `Post.prepend(X)` inside
257
+ `Blog::Post` resolves through the nesting it is written in. A class assigned
258
+ as `Shop::Draft = Class.new(Base) do` inside `module Shop` is Shop::Draft, and
259
+ `class Billing::Invoice` inside another module is Billing::Invoice when the
260
+ file declares Billing above it, as Ruby's lexical lookup finds them. A model's
261
+ `self.table_name` is read off the class its written name resolves to the same
262
+ way, so `class Shop::Order` inside `module Shop` and `class ::Order` beside it
263
+ each keep their own. An `||=` and every segment of a compact path find a
264
+ constant the superclass declares when the file declares that superclass above
265
+ the class, as Ruby's lookup does, and so does a route constant read in a class
266
+ whose superclass writes it. `self::Sub =` inside Ledger is Ledger::Sub, and a
267
+ class opened in a `Class.new` block is named from the scope around the block,
268
+ as Ruby defines it. What a `Foo ||= Class.new(Base) do ... end` block declares
269
+ belongs to Foo when the `||=` assigns it, so
270
+ `Part ||= Class.new(ApplicationRecord) do` inside Widget is the model
271
+ Widget::Part with its associations; when Foo already exists the block never
272
+ runs, so nothing in it, a class opened in it included, is credited to
273
+ anything. (#514)
274
+ - **Static model associations match the model's reflections.** A name declared
275
+ twice is listed once, with the options of the declaration that runs last, and
276
+ the associations a concern adds sit where the concern is included. Across a
277
+ class and its bases they list as `reflect_on_all_associations` does on Rails
278
+ 7.0 to 8.1: the outermost base's first, each where it last ran. A child's
279
+ association replaces a base's of the same name whatever the macro, and one a
280
+ base's class method declares wins over the class's own, at the place the class
281
+ calls that method, when the class calls it after declaring its own.
282
+ - **A file that declares a module as `X = Module.new do ... end` is named for
283
+ that module, as the `module X` form is.** An acronym the app registers names
284
+ it, and such a file under app/controllers or app/serializers is read as a
285
+ mixin, not as a controller or serializer class. (#514)
286
+ - **`rails_dependency_graph` gives a polymorphic belongs_to only the models
287
+ whose `as:` association points at its own model.** Two models with `belongs_to
288
+ :record, polymorphic: true` no longer share targets, and the false cycle that
289
+ came from it is gone. An `as:` pointed at an STI subclass or base reaches the
290
+ classes of that family above and below it. A polymorphic `as:` whose class
291
+ cannot be named adds its model to no belongs_to list. (#551)
292
+ - **A focused `rails_dependency_graph(model:)` costs about what the whole graph
293
+ does.** It finds the links into each model from one index built once, where it
294
+ used to rescan every model for every node. (#602)
295
+ - **`rails_onboard` names a payment model by a whole word of its name.**
296
+ Airplane, Explanation, FloorPlan and Discharge are no longer listed as payment
297
+ models; PaymentPlan, Billing::Invoice, Subscription and Plan still are. (#553)
298
+ - **Seeds list only the models the app has.** Plain Ruby classes built in a seed
299
+ file (`Date`, `Time`, `Random`, `Set`, a service object) are no longer
300
+ reported as seeded models; booted, a record class a gem defines, such as
301
+ `ActiveStorage::Blob`, still counts. (#570)
302
+ - **`rails_dependency_graph` with `show_sti` shows every level of an STI tree.**
303
+ A grandchild such as `SportsCar < Car < Vehicle` is now drawn under its own
304
+ parent, on both tiers, where before only the base's direct subclasses showed.
305
+ - **`rails_generate_test` writes no failing `validate_uniqueness_of` for a
306
+ uniqueness rule with `conditions:`.** A lambda option on a validation read
307
+ from reflection is now kept as `(proc)`, so the generator falls back to its
308
+ plain example, as it does for other options it cannot read.
309
+ - **A controller declared beside another in its file gets only its own filters
310
+ and strong params.** The booted tier read skips and filter options from the
311
+ whole file, and both tiers read strong params from it, so one class showed the
312
+ other's; each is now read from its own class body. A booted job in such a file
313
+ also shows its own `queue_as` block source.
314
+ - **performance_check keeps a controller's N+1 risks when one of its views
315
+ renders records whose `to_partial_path` is computed.** The scan used to crash
316
+ on that render and drop every risk in the controller, without a word. (#573)
317
+ - **performance_check rates an N+1 as preloaded only when a preload call names
318
+ that association at its top level.** A `where(user: ...)`, a `joins(:user)` or
319
+ a nested `includes(comments: :user)` no longer turns a missing
320
+ `includes(:user)` into "no action needed". A preload written without
321
+ parentheses (`includes :user`), in an action or a scope, is read as one.
322
+ (#574)
323
+ - **The N+1 scan no longer slows down with actions times views.** It builds its
324
+ template and partial lookups once, so 2,400 actions over 2,700 views take
325
+ about 5s where they took 20s. (#517)
326
+ - **performance_check reads every model the app loads, whatever its superclass
327
+ is written as.** A subclass of a second abstract base, an STI child or a class
328
+ under `ActiveRecord::Base` now gets the missing counter_cache, Model.all and
329
+ eager-load checks. (#568)
330
+ - **The GraphQL resolver list names only resolver classes.** An app that keeps
331
+ its resolvers in `app/graphql/resolvers` no longer gets a fake `Resolvers`
332
+ entry for the folder. (#588)
333
+ - **API versioning lists only `api/v<number>` folders.** A resource namespace
334
+ such as `api/vendors` or `api/videos` is no longer reported as an API version.
335
+ (#569)
336
+ - **`get_controllers` action detail lists every private method the action
337
+ calls.** One passed as an argument (`paginate(scope)`) or with a call chained
338
+ on it (`visible_posts.size`) now shows with its source, and a name that only
339
+ appears in a comment no longer does. (#586)
340
+ - **A controller is an API controller only when its chain reaches
341
+ `ActionController::API`.** Without booting, a parent named `APIController` or
342
+ `API::BaseController` under `ApplicationController` was read as an API
343
+ controller, so it lost its layout and got JSON tests. It now keeps its layout
344
+ and gets HTML tests. A child of an API-only `ApplicationController` is now
345
+ marked as an API controller. (#539)
346
+ - **The security section no longer says CSRF protection is on in an API-only
347
+ app.** When `ApplicationController` inherits `ActionController::API`, which
348
+ leaves out `RequestForgeryProtection`, `csrf` reports it as not included. An
349
+ API app that includes `RequestForgeryProtection` back reports it as enabled
350
+ only when `protect_from_forgery` turns it on, and as off when it never does.
351
+ The answer says CSRF is off when `verify_authenticity_token` does not run,
352
+ names a `skip_before_action :verify_authenticity_token` after protection as
353
+ skipped, and, when ApplicationController cannot load, reads
354
+ `default_protect_from_forgery` and the file's own protect and skip calls.
355
+ (#572)
356
+ - **A controller or job file that declares a second controller or job lists both
357
+ without booting.** Each class is read from its own body, so one no longer
358
+ takes the other's actions, filters or queue, an error class nested in a worker
359
+ is not a worker, and a second controller routes by its own name. A class
360
+ declared beside a job is listed only when its own chain reaches a job or
361
+ worker base, and a controller declared in a file named for another routes by
362
+ its own class name, as Rails' `controller_path` does. (#513)
363
+ - **Views list the templates of a folder named layouts anywhere but
364
+ app/views/layouts.** An admin/layouts controller and an app checked out under
365
+ a folder named layouts kept no templates, because the check read the path on
366
+ disk. (#575)
367
+ - **A layout in a subfolder of app/views/layouts is listed, by its path under
368
+ layouts/.** The views answer and `get_view controller:"layouts"` read only the
369
+ top level, so `layouts/admin/application` and its slots were missing, and a
370
+ nested file of the same name would have merged with the top-level layout.
371
+ get_turbo_map finds the morph meta tag in a layout in a subfolder of
372
+ `layouts/`. (#576)
373
+ - **get_view shows the locals each partial is rendered with.** Both `render
374
+ "posts/form", post: @post` and `render partial: "posts/form", locals: { ... }`
375
+ are read, from templates and from other partials, and only from calls that
376
+ name that partial. `rails_get_partial_interface` reads locals by the same
377
+ rule: a call split over lines keeps every key, a nested hash's keys are not
378
+ listed, and `render "x", locals: {...}` is one local named `locals`, as Rails
379
+ passes it. `rails_get_partial_interface` reads `json.partial!` in hash form
380
+ the way jbuilder does (every key beside `partial:`, `as:`, `collection:` and
381
+ `cached:` is a local unless `locals:` is given); a partial rendered with
382
+ `collection:` or `object:` lists the record local Rails gives it (the `as:`
383
+ name or the partial's own, plus `_counter` and `_iteration` for a collection);
384
+ and a bare name Rails finds under `application/` reaches that partial in both
385
+ tools. (#525)
386
+ - **get_view returns a view named by path exactly as written.** Inline SVGs and
387
+ repeated class strings were rewritten in the text an agent edits from. The
388
+ full layouts listing shows each layout as written, and a controller's full
389
+ listing says when it shortened icons and repeated classes. (#511)
390
+ - **The generated guide names a partial in app/views/application as
391
+ `application/<name>`.** It printed a `shared/` path that does not exist.
392
+ (#536)
393
+ - **get_turbo_map reads a parenthesized `turbo_frame_tag(:comments)` or
394
+ `turbo_stream_from(:notifications)` call.** The frame id and stream name used
395
+ to come out as `(dynamic)`, and the map warned about a stream that does not
396
+ exist. Only `turbo_stream_from` itself counts, so `my_turbo_stream_from` is no
397
+ phantom subscription, and `turbo_stream_from (:a), :b` lists both streams.
398
+ (#577)
399
+ - **rails_get_stimulus writes the value and class attributes Stimulus reads for
400
+ names with an acronym or a digit.** `apiURL` is `data-x-api-u-r-l-value` and
401
+ `step2Count` is `data-x-step2-count-value`, from Stimulus's own `dasherize`.
402
+ (#560)
403
+ - **partial_interface credits a local only with calls made on the local
404
+ itself.** `@user.admin?` and `post.user.avatar` no longer show up as calls on
405
+ a `user` local. (#563)
406
+ - **helper_methods counts a view as using a helper only where it calls the
407
+ helper by name.** `@post.subtitle`, the CSS class `icon-edit` and `helper_m10`
408
+ no longer count, and in an ERB view neither do plain markup and strings. A
409
+ framework helper is named only where a view calls it by its own name, so
410
+ `@current_user_id` and `current_user_path` no longer list Devise's
411
+ `current_user`. A HAML or Slim view is read the way haml and slim parse it, so
412
+ a tag such as `%title`, page text such as `follow @shop`, and comments name no
413
+ helper and no ivar. Attribute lists, object refs, continued lines, `:ruby`
414
+ filters and `#{}` in text still count, and a quote inside `#{...}` no longer
415
+ hides the Ruby after it. Ruby strings in a view are read the way Ruby reads
416
+ them, so `"by #@author"` counts `@author` and an escaped `"\#{@x}"` counts
417
+ nothing. (#562)
418
+ - **helper_methods `detail:"full"` takes time in step with the views' total size
419
+ instead of its square.** On 2,000 views (16 MB) it runs about three times
420
+ faster. Each view is read once, and files no longer join into one name across
421
+ their boundary. (#596)
422
+ - **component_catalog lists a prop's allowed values when its constant table ends
423
+ in `.freeze`.** (#581)
424
+ - **view and context schema hints no longer warn about a missing model for an
425
+ ivar that holds no model.** `@stats` and `@chart_data` used to warn about
426
+ `Stat` and `ChartDatum` models that never existed. (#580)
427
+ - **`rails_security_scan` and `rails_validate` read `./app/x.rb` (and
428
+ `././app/x.rb`) as `app/x.rb` and a folder name as that folder only.** A `./`
429
+ path used to be reported clean and `app/controllers/admin` picked up
430
+ `admin_controller.rb`. (#522)
431
+ - **`rails_review_changes` shows a file's staged and unstaged changes together,
432
+ as `git diff HEAD` does.** A staged validation removal is still reported after
433
+ a later edit to the same file. (#523)
434
+ - **`rails_review_changes` shows a namespaced controller's own routes and names
435
+ a namespaced model with its namespace.**
436
+ `app/controllers/admin/posts_controller.rb` no longer gets the top-level
437
+ `PostsController`'s routes. (#557)
438
+ - **`rails_review_changes` counts a foreign key as indexed only by an
439
+ `add_index` on its own table that leads with it, or with its polymorphic
440
+ `[type, id]` pair.** An index on another table or column, or one that names
441
+ the key only in its `where:`, no longer hides a missing index, and one
442
+ `index: false` flags only its own reference. (#558)
443
+ - **`rails_review_changes` warns when a change removes a `validates_presence_of`
444
+ line or any other `validates_*` macro.** A line rewritten as `validates :name,
445
+ ...` still reads as kept. A method named `validate_email` is no longer read as
446
+ a validation. (#559)
447
+ - **`rails_validate` asks for no migration for a validated attribute the model
448
+ defines as a method, and finds a callback method a parent class defines on the
449
+ static tier.** A parent outside the app's files is no longer reported as
450
+ missing the method. (#509)
451
+ - **`rails_validate` warns about `Model.all` in the controller
452
+ `rails_performance_check` found it in.** It matches the full path, so
453
+ `admin/posts_controller.rb` does not take `posts_controller.rb`'s finding.
454
+ (#594)
455
+ - **`rails_get_edit_context` takes the method that actually holds the match.** A
456
+ line below a method that already ended keeps its window and names only the
457
+ class, and an endless method no longer takes in the next one. (#524)
458
+ - **`rails_validate` checks the instance variables of views under a folder named
459
+ `layouts`, such as `app/views/admin/layouts/`.** Only `app/views/layouts/` is
460
+ skipped, since that is where the app's layouts are.
461
+ - **`rails_get_test_info` with `detail: "full"` lists each factory's attributes
462
+ and traits.** It looked for the file under a doubled path, so it never found
463
+ one and printed only the factory names. Each factory file's traits stay under
464
+ its own path, so two files named `users.rb` in different folders no longer
465
+ lose one's traits; the JSON context's `factory_traits` are keyed by that path,
466
+ as `factory_names` already were. Each factory's attributes are read as
467
+ FactoryBot declares them, `do ... end` blocks, `association`, `sequence` and
468
+ bare names included, with transient ones marked, callbacks left out, and each
469
+ trait's and child factory's attributes listed under it. (#561)
470
+ - **`rails_generate_test` and `rails_get_test_info` no longer give
471
+ `UsersController` the spec of `UserController`.** The app's learned test
472
+ layout tried the singular name for controllers too. (#555)
473
+ - **`rails_generate_test` writes a Minitest file that parses when a model
474
+ declares an association with a computed name.** That association is left out,
475
+ as the RSpec output already did. (#554)
476
+ - **`rails_generate_test` writes a Minitest `ORDER BY` assert only for a scope
477
+ that sorts.** A scope that joins `orders` or filters a `bordered` column no
478
+ longer gets a test that fails on its first run. (#556)
479
+ - **`rails_generate_test` writes shoulda matchers that pass for an optional
480
+ `belongs_to`, a scoped or case-insensitive uniqueness, and an enum with
481
+ explicit values, string values, a prefix, or `scopes: false` or
482
+ `instance_methods: false`.** Each matcher now carries the qualifier the
483
+ declaration needs. Where an option can't be read or told to the matcher, it
484
+ falls back to the plain example: a reflection check for the association, the
485
+ validation's kind for the uniqueness, the values for the enum. The hash form
486
+ (`enum status: {...}, _instance_methods: false`) reads that key as the option,
487
+ not as a second enum, and on Rails 7.0, whose enum ignores
488
+ `instance_methods:`, the enum keeps its value methods. (#510)
489
+ - **`rails_analyze_feature` lists a service's or job's ENV names only when its
490
+ path under `app/services` or `app/jobs` holds the feature word.** A folder
491
+ above the app (`Users`, the app's own name) matched every file. (#549)
492
+ - **`rails_analyze_feature` says a model, controller, job or service has no test
493
+ unless a test is named for it or describes it.** Before, any test whose path
494
+ merely contained the name hid the gap, such as `postmark_event_spec.rb` for
495
+ Post or `user_mailer_spec.rb` for User. `rails_review_changes` reads the same
496
+ rule, so a controller has no test changes unless a changed test is named for
497
+ it or describes it, and the `posts_routing_spec.rb` and
498
+ `posts_request_spec.rb` rspec-rails writes count for `PostsController`. Those
499
+ names count only in a controller test folder, and a controller's name is never
500
+ singularized, so `spec/models/post_spec.rb` is no test of `PostsController`
501
+ and `user_controller_spec.rb` none of `UsersController`. A test under a
502
+ namespace folder covers only the subject in that namespace:
503
+ `spec/controllers/admin/posts_controller_spec.rb` no longer counts for a
504
+ top-level `PostsController`. A model spec named `widget_model_spec.rb` counts
505
+ for `Widget`, the suffix `rails_get_test_info` already looks up. The tests an
506
+ in-repo engine or pack keeps in its own `spec/` or `test/` count for that
507
+ component's classes only. A test filed anywhere else counts when any of its
508
+ example groups (`describe`, `context`, `feature`, nested or not) names the
509
+ subject's declared constant, resolved from the modules around it as Ruby
510
+ resolves it, or its minitest class does without `Test`, so
511
+ `spec/jobs/sweep_spec.rb` describing `Jobs::Sweep` covers
512
+ `app/jobs/scheduled/sweep.rb`. A test filed under a subject's name counts
513
+ unless its top-level example groups or test class describe another class the
514
+ app declares, and a controller test outside `controllers/` and `functional/`
515
+ (request, routing, integration, system), or a minitest
516
+ `ActionDispatch::IntegrationTest` anywhere, counts for the controller it is
517
+ filed as whatever it describes, since it reaches that controller through the
518
+ routes. (#552)
519
+ - **`rails_get_context` with `feature:` adds the schema of only the models its
520
+ feature word names as whole words.** `post` no longer brings in
521
+ `PostmarkEvent` or controllers that merely start with the word. (#550)
522
+ - **Running the install again keeps an `ai_tools`, `tool_mode` or
523
+ `context_files` line it does not rewrite, and says so.** An array spread over
524
+ several lines got its first line replaced and the initializer stopped parsing.
525
+ (#504)
526
+ - **Context files, MCP configs and the selection record write through a symlink
527
+ to the file it names, and keep that file's mode.** A link that leads out of
528
+ the app, or nowhere, is left as it is and reported. Before, the write replaced
529
+ the link with a copy and reset the mode. A write into a directory a symlink
530
+ carries out of the app is refused too. Removing the gem's MCP entry through a
531
+ symlinked config takes it out of the file the link names and keeps the link,
532
+ and nothing a symlink carries out of the app is deleted. A fetched guide in
533
+ `rails_search_docs` is still answered when `tmp/` is a symlink out of the app;
534
+ it is not cached there, and the answer names no path. Uninstall and the
535
+ dropped-tool cleanup take the gem's block out of a root file that is a symlink
536
+ into the app (CLAUDE.md -> AGENTS.md), and a context file under a folder
537
+ symlink that leads nowhere is reported as left instead of stopping the run.
538
+ (#505)
539
+ - **Removing the Codex entry leaves the rest of `.codex/config.toml` as it
540
+ was.** Blank lines inside a `"""` or `'''` string were squeezed, which changed
541
+ the value Codex reads. Your blank lines around the removed table are kept;
542
+ only the one the cut leaves is dropped. (#519)
543
+ - **A context run keeps an MCP entry whose command the gem did not write, such
544
+ as SETUP.md's Docker entry, and says so.** When it rewrites its own entry it
545
+ merges into it, so keys and env vars it never writes (`cwd`,
546
+ `startup_timeout_sec`, `RAILS_ENV`) stay, and so do comments in the gem's
547
+ Codex table and its env table. A Codex env var set as a dotted key in the
548
+ gem's table survives, and a workspace write keeps a bare `rails-ai-context`
549
+ entry that starts the server through Docker, naming it in its note. A Codex
550
+ env var whose name needs quotes keeps them, and a line you typed in the gem's
551
+ env table stays. (#506)
552
+ - **`.claude/rules/rails-models.md` lists the first 30 models and counts the
553
+ rest**, as the other models rules do. On a large app it grew to about 190 KB
554
+ and loaded on every model edit. (#530)
555
+ - **`database_stats` and the runtime table sizes leave out `schema_migrations`
556
+ and `ar_internal_metadata` on PostgreSQL and MySQL too.** Every database
557
+ family now applies the schema's own rule, including the app's table prefix and
558
+ suffix, so the counts name only the tables the schema lists.
559
+ - **A static run of an app with no `config/routes.rb` answers no routes, as the
560
+ booted tier does.** Doctor no longer lists `routes` as a failing introspector
561
+ there, and the routes section says there was no route file to parse.
562
+ - **`facts` lists the tables of every database in `database.yml`.** A second
563
+ database's tables were missing from the list and the count. Each one now reads
564
+ `- dogs (animals; 2 cols, 0 indexes, 0 FKs)`. (#527)
565
+ - **The `facts` title reads `Rails App` for an app whose Application class has
566
+ no module.** It used to read `# - Schema Facts`. (#604)
567
+ - **`rails 'ai:tool[NAME]'` accepts a trailing `JSON=1` or `RAILS_ENV=test`, the
568
+ form `docs/GUIDE.md` shows.** The task passed them to the tool as params, so
569
+ it failed. (#534)
570
+ - **`session_context` status keeps one row per call when a param holds `|` or a
571
+ line break.** (#600)
572
+ - **`tool search_code --pattern -h` searches for `-h`.** `-h` and `-j` right
573
+ after a flag that takes a value are read as that value. After a boolean flag
574
+ they are still help and JSON. `tool NAME --json -h` prints the tool's help
575
+ without booting the app. (#535)
576
+ - **`tool --json` and `JSON=1` answer every refusal with the envelope on stdout,
577
+ at exit 1.** An unknown tool, unknown param, missing value or failed boot
578
+ printed nothing on stdout. Now `"error": true` carries the message, "did you
579
+ mean" included. A call whose `-j` is left for the tool answers a failed boot
580
+ with the envelope too. A run that stops before the tool starts says "The tool
581
+ did not run: rails-ai-context stopped before it could. stderr says why." from
582
+ the binary and from `rails 'ai:tool[NAME]'` alike. (#590)
583
+ - **The standalone server's bind warning describes its own Host check on every
584
+ mcp SDK.** Below mcp 0.23 it said every tool answers anyone, but the server
585
+ refuses a non-loopback Host there too. (#599)
586
+ - **A cached answer goes stale when a root file a reader reads changes, and when
587
+ a file is renamed.** The fingerprint now covers the Dockerfile, Procfile,
588
+ compose, CI and deploy configs, `.env` examples, JS lockfiles, framework and
589
+ bundler configs, the manifests in a declared `frontend_paths` folder (inside
590
+ the app or outside it) and in the JS workspace root above the app, Ruby
591
+ version files, `bin/` scripts and config `.yaml`, each taken from the list its
592
+ reader reads, and hashes each file's path with its mtime. `watch` and live
593
+ reload also watch `bin/`, `.github/workflows` and the other folders those
594
+ files sit in. A `.github/workflows` folder that is itself a symlink is never
595
+ walked or watched. (#515)
596
+ - **An app whose folder path has brackets, braces, a star or a question mark
597
+ gets the same answers as one under a plain folder.** The fingerprint walk,
598
+ doctor and every introspector globbed with the app root inside the pattern, so
599
+ `/Users/me/work [old]/shop` found no models, jobs, helpers, rake tasks, packs,
600
+ locales, environments, credentials, mailer previews, migrations or test files,
601
+ and watch and live reload never saw a change. Every glob now matches below the
602
+ root and reads the root as a path; patterns the app configures still glob as
603
+ before. (#532)
604
+ - **`watch --no-boot` picks up a folder added under `app/` while it runs.** Each
605
+ rewrite drops the cached folder lists, as a server does, so the new folder's
606
+ models reach the context files. (#533)
607
+ - **The env section names a service on a `.co.uk`, `.com.au`, `.co.nz` or
608
+ `.or.jp` host by its own label.** Such services were shown as "Co" or "Com",
609
+ and two in one file merged into one row. (#566)
610
+ - **A resource read answers from the shared context the tools read.** Each read
611
+ ran a full introspection outside the shared cache; it now costs what a tool
612
+ call costs, and concurrent reads introspect once. (#516)
613
+ - **Percentages print the exact share.** 29 of 100 i18n keys showed 28.9%
614
+ because the floor ran on a float a hair under 29; the math is now exact.
615
+ (#591)
616
+ - **Gemfile groups match Bundler.** A gem's `groups:` option is read, and
617
+ `group:` inside a `group` block adds to the block's groups instead of
618
+ replacing them. (#582)
619
+ - **The context file's env section lists every name rails_get_config lists.**
620
+ Lowercase, one-letter and underscore-first ENV names are no longer dropped.
621
+ (#567)
622
+ - **A lambda in `config.log_tags` reads as computed at request time.** The
623
+ active_support and observability sections no longer print a Proc address and
624
+ an absolute path that change on every boot. (#584)
625
+ - **The frontend framework label keeps a version range as written.** `^18.2.0 ||
626
+ ^19.0.0` read as "React 18.2.019.0.0"; a single requirement still loses its
627
+ leading operator. (#564)
628
+ - **The credentials section lists key names under the file Rails read.** With
629
+ `config/credentials/<env>.yml.enc` present, its keys are no longer shown under
630
+ `config/credentials.yml.enc`, and `master_key_source` names the key Rails
631
+ uses, or `missing`; a static run picks that key by the locked Rails version's
632
+ rule. A credentials file that is a symlink counts as the file it stands in
633
+ for. Credentials at a custom `config.credentials.content_path` get their own
634
+ `custom` entry with the key names, and a path outside the app root shows as
635
+ `(outside the app root)`. (#565)
636
+ - **A static run reports the credentials files it can see.** The `default` entry
637
+ no longer comes back as `error: "inspection failed"` without a boot. It lists
638
+ the file and key-file presence, and no key names, since nothing is decrypted
639
+ without a boot.
640
+ - **A static run reports `require_master_key` as the app sets it.** Without a boot
641
+ the credentials section said `false` whatever the config said; it now reads a
642
+ literal `config.require_master_key = true/false` from `config/application.rb`,
643
+ the environment file or an initializer, following a `require_relative` into
644
+ `config/` the way Ruby runs it, and answers Rails' default, `false`, when none
645
+ of them sets it. A value it cannot read (an expression, a condition, a `load`
646
+ or a required file outside `config/`) reports as unavailable. Only a call, or a
647
+ send that names the setting, counts as a mention, so the name in a string or
648
+ after `__END__` leaves the answer at Rails' default.
649
+ - **`rails_analyze_feature` names each related model the class Rails would
650
+ load.** In the static tier a through association reads its class from its
651
+ source, and a singular association keeps its name singular, so
652
+ `belongs_to :preferences` is `Preferences` and
653
+ `has_many :followers, through: :subscriptions, source: :user` is `User`. The
654
+ dependency graph, the N+1 scan, the turbo map and the foreign key index check
655
+ use the same rule, and rendered records read a through association's class off
656
+ its source on the hop's model, wherever an autoload root keeps that model, a
657
+ pack or an engine included. The receiver a view renders through counts as a
658
+ model only when the model directories hold its file, so `render @cart.items`
659
+ with a `Cart` service still renders items. A hop or source counts when the
660
+ model's file, its included concerns or its parent models declare it, with its
661
+ `class_name`, a parent being read by the constant its superclass names; one
662
+ the app's source does not declare names no model. A `class_name` wins over
663
+ `source_type`, as Rails reads it, here and in the dependency graph. A
664
+ polymorphic `belongs_to` names no related model there, in the N+1 scan or in
665
+ the turbo map, since Rails reads its class from each row; the dependency graph
666
+ still draws it as a polymorphic interface.
667
+ - **A rendered record whose model constant only a Zeitwerk `inflect` override
668
+ spells keeps its parent's associations and its own `to_partial_path`.**
669
+ `render @html_page.links`, a through association across such a model and
670
+ `render @html_pages` name the partial Rails renders, in the view,
671
+ partial-interface and N+1 answers.
672
+ - **A view's "renders:" list names the partial Rails renders for a bare
673
+ `render @records`.** It reads the model's own `to_partial_path` and the view's
674
+ controller namespace, the way partial_interface does, so `render @comments` in
675
+ an admin view lists `admin/comments/comment`, and a has_many with a
676
+ `class_name` lists that class's partial. It used to list the plural model
677
+ name. The records' model comes from the names the view writes
678
+ (`@post.comments`, `@comments`), read from the model directories only, so a
679
+ service or presenter under the view's namespace is never taken for the
680
+ records. A lone name with no model file falls back to its default partial,
681
+ prefixed with the controller namespace, when that partial exists in any view
682
+ root; otherwise a local keeps its name and an ivar names none, since Rails
683
+ never looks a partial up by the ivar's name. A form helper's block parameter,
684
+ whatever it is called and in a `do` or a brace block, renders the partial its
685
+ builder's `to_partial_path` names (`form` for FormBuilder), and a builder's
686
+ own `fields` and `fields_for` keep its builder class, as
687
+ `FormBuilder#fields_for` passes it on, while a builder handed to a partial
688
+ as a local reads as FormBuilder's and a local assigned a builder reads as that
689
+ builder. Which locals a template binds is read from its Ruby the way Ruby
690
+ binds them, so a `<script>`, an HTML or Slim/HAML attribute or a string under
691
+ the same name never turns a builder's `fields` block into a record's, nor, in
692
+ ERB and Phlex, another block's parameter; the builder is read only off a real
693
+ helper call and the block that helper takes, so prose naming fields, a
694
+ `fields:` key, a `%i[fields]` symbol before a loop, a record's own `fields`
695
+ called on an ivar, a constant, a chain or a local the view assigns or a plain
696
+ block binds (`post = Post.new`, `[Post.new].each do |post|`), or iterated
697
+ (`@post.fields(:visible).each do |field|`), or a brace block inside the
698
+ helper's arguments changes nothing. This holds in the "renders:" list and in
699
+ partial_interface, which reads builders from the same comment-stripped markup
700
+ as the view scan and finds a bare `render form` at the form partial beside the
701
+ view, as get_view does.
702
+ - **partial_interface finds a record's default partial kept in another view
703
+ root.** It reads the same partials set the view scan reads, built once a call,
704
+ so a partial in a root added with `config.paths["app/views"] <<` is credited
705
+ to the views that render it.
706
+ - **performance_check follows the partial Rails renders for `render @records` in
707
+ a namespaced controller.** The controller's namespace prefixes the record's
708
+ partial whether or not that file exists, and
709
+ `prefix_partial_path_with_controller_namespace = false` turns the prefix off,
710
+ the rule views and partial_interface use.
711
+ - **get_concern finds a concern nested in its namespace's file.**
712
+ `module Shop; module Tagging; extend ActiveSupport::Concern` kept in `shop.rb`
713
+ is looked up as `Shop::Tagging`, as Zeitwerk loads it with `Shop`, and the
714
+ concern listing and rails_get_active_support name it instead of the namespace
715
+ that only holds it. Each concern a file holds is read from its own module:
716
+ rails_get_concern and rails_get_active_support read its macros, callbacks and
717
+ `included` blocks from its own body, leaving out the modules it nests, so a
718
+ concern no longer shows a nested concern's associations or counts its
719
+ `included` block, and rails_get_concern with a namespace's name answers not
720
+ found and suggests the concerns it holds. A file whose module holds only
721
+ constant writes and modules is a namespace too,
722
+ `Error = Class.new(StandardError)` included, a concern defined beside a file's
723
+ class is listed and answered by name while the class is not, and a module
724
+ nested in a concern is listed only when it extends ActiveSupport::Concern
725
+ itself, whether it is written as a module, with `self::` or with an assigning
726
+ `||=`. A not-found answer from any tool suggests the nearest spellings and a
727
+ namespace's members together, five at most.
728
+ - **A concern's associations, filters, devise modules, actions and strong params
729
+ leave out the modules it nests, a `Module.new` one included, unless the
730
+ concern includes or prepends that module itself.** Ruby runs a nested module's
731
+ body for that module alone, so the model and controller tools no longer credit
732
+ a nested concern's `included` block or macros to the outer concern's
733
+ includers. A module the concern includes or prepends is the constant Ruby's
734
+ lookup reaches first, for every tool that reads it (associations, callbacks,
735
+ validations, scopes, filters, layouts, actions, devise modules and strong
736
+ params): one declared in the file that names it, or nested in its outer
737
+ constant's file, comes before a top-level module of the same name, unless that
738
+ file declares it below an include that runs as the file loads. A top-level
739
+ module the concern includes is followed too, through three levels of
740
+ includes, for its actions, devise modules and strong params.
741
+ - **A class's own include finds a module nested in one it included earlier.**
742
+ After `include Holdy`, a later `include Taggable` in the class body reads
743
+ `Holdy::Taggable` before the top-level `Taggable`, as Ruby looks constants up
744
+ through the class's ancestors, and a module it prepended comes before the ones
745
+ it includes after it. A mixin written from the top level (`include ::Taggable`)
746
+ reads the top-level module only, in the class body, a concern's `included`
747
+ block and a hook's `base.include`. A module only the superclass includes is
748
+ not read this way yet.
749
+ - **Static routes draw a block over a literal list once per element, as Rails
750
+ runs it.** `%w[groups g].each do |root_path|` listed one set of routes under
751
+ `/[INFERRED]/...`. It now lists `/groups/...` and `/g/...`, each with the name
752
+ Rails gives it, and a name an earlier pass took stays unnamed. A route inside
753
+ a block over a list the source does not spell out counts as a construct the
754
+ static tier could not expand. A `draw` inside the block follows each element's
755
+ file, and a bound element reads as a literal in `only:` and `except:`. A
756
+ concern defined in the block keeps its element wherever it is applied, and a
757
+ block that assigns or changes its own variable is drawn once with that
758
+ variable unknown, so its routes count as dynamic constructs; a call in it to a
759
+ method the routes file defines counts too, so the table never reads as
760
+ complete. The same goes for a block that hands its string to a pattern match
761
+ (`x => y`) or an attribute setter. Inside a block Ruby runs more than once
762
+ that the static tier does not unroll, such as `2.times`, `LOC.each_key` or an
763
+ `each` over a list it cannot read, every routing call and every call to a
764
+ method the routes file defines counts as such a construct, since Rails draws
765
+ them once per pass, while a mount or a verb route to a Rack app in such a
766
+ block is listed once as a mounted app; a `tap`, `then` or `yield_self` block
767
+ still draws in place, the calls it makes on the mapper it takes as its
768
+ parameter (`tap { |m| m.get "x" }`, an optional `|m = 1|` too) included, and
769
+ so does a `tap` or `yield_self` with no receiver. A list of symbols, or of
770
+ strings under `# frozen_string_literal: true`, cannot be changed, so its block
771
+ is unrolled unless it assigns the variable.
772
+ - **A static route to an app a constant holds is drawn where Rails draws it.**
773
+ `mount APPS[:admin] => "/admin"` and `mount HANDLERS[:x], at: "/x"` are listed
774
+ at their path with an inferred app, where they were named `APPS.[]` and
775
+ `HANDLERS.[]`. A verb route to such a value, such as
776
+ `get "y", to: HANDLERS[:x]` or `get "y" => HANDLERS[:x]`, counts as a
777
+ construct the static tier could not expand: it was listed as a mounted app
778
+ named `HANDLERS.[]`, so the table showed one route fewer than Rails draws. A
779
+ verb route to a constant the routes file assigns once, at its top level,
780
+ inside a module, through a path such as `Cfg::HOME =` or with `||=`, reads it
781
+ as Ruby looks the reference up, through Module.nesting where it is read and
782
+ then the top level: with `TO = "posts#index"`, `get "a", to: TO` is a route to
783
+ posts#index, `ALIAS = HealthCheck` names the app HealthCheck, and a lambda
784
+ counts as a construct the static tier could not expand. An app the file builds
785
+ with `.new` is named as booted Rails names it: an instance
786
+ (`Rack::Builder.new { ... }`, `HealthCheck.new`, `AL.new` with
787
+ `AL = HealthCheck`) by its class, a `Class.new` by its constant. A constant
788
+ holding any other call reads as that call written in place: a verb route to
789
+ `"posts#index".freeze` or `ENV.fetch(...)` counts as a construct the static
790
+ tier could not expand, a mount of `HealthCheck.app(1)` held in a constant is
791
+ named `HealthCheck.app`, one of `APPS[:a]` is listed with an `[INFERRED]` app,
792
+ and one of a call with a block (`Rack::Builder.app do`) counts as not
793
+ expanded. A verb route to a bare all-caps constant the file does not assign,
794
+ or assigns more than once, is no longer listed as a mounted app.
795
+ - **A static route option given as a constant is no longer read as the
796
+ constant's name.** A `controller:`, `action:`, `as:`, `param:` or
797
+ `shallow_prefix:` given as a constant is marked inferred, in the names of the
798
+ shallow routes under it too. A scope, namespace, resource or route whose path,
799
+ module, verbs, `shallow:` or `on:` come from a constant, such as
800
+ `scope path: PREFIX`, `namespace :api, module: MOD`, `match "x", via: VERBS`
801
+ or `get :preview, on: ON`, or from a computed prefix (`scope PREFIX do`),
802
+ counts as a construct the static tier could not expand.
803
+ - **A static verb route to a redirect, a held value or a Rack app takes the name
804
+ Rails gives it.** So does a mount, so a later `get "health_check"` after
805
+ `mount HealthCheck` is unnamed: an engine takes the railtie name its file
806
+ gives it (an isolated engine its namespace's), a constant ending in `Engine`
807
+ whose file is not in the app takes none, a bare `mount UI` inside the
808
+ `module Admin` that declares it is named Admin::UI and takes admin_ui, and
809
+ `as: nil` leaves a mount unnamed on Rails 8.1, and when no Rails version is
810
+ known, and named before 8.1. The route drawn at the same path or name after it
811
+ is unnamed, as it is live, where it used to take the name.
812
+ - **A resource's own `shallow_path:` and `shallow_prefix:` place and name its
813
+ shallow routes and its children's, as the scope Rails draws it in does.**
814
+ `resources :posts, shallow_path: "sp"` with a shallow child lists `/sp/c/:id`,
815
+ where it listed `/c/:id`.
816
+ - **A mount inside `scope nil` keeps its path.** `scope nil` adds no path
817
+ segment, as Rails reads it.
818
+ - **A Rack app mounted or matched inside `resources`, `member` or `collection`
819
+ is listed at the path Rails attaches it, such as `/posts/:post_id/hc`.** It
820
+ was listed at the bare `at:` path. One inside a resource the static walk
821
+ cannot read, or a block in a resource that does not draw in place, has no
822
+ known path.
823
+ - **A Rack app in a routing concern, a routes-file method, a literal list's
824
+ loop or a `resources :a, :b` block is listed at each place Rails draws it,
825
+ with that place's path.** A concern's app is listed at each `concerns` call
826
+ and `concerns:` option (`/adm/hc` under `namespace :adm`, `/posts/:post_id/hc`
827
+ on a resource), with the conditions around that call, then those the concern
828
+ or method is defined under, then its own. A concern or method defined in each
829
+ arm of an `if`/`else` or `case` is drawn at the site once per arm, under that
830
+ arm's condition; a later definition replaces an earlier one when every branch
831
+ it is written under is one the earlier one is written under (a branch counts
832
+ by its text, so a second `if ENV["A"]` block is the same branch), and an
833
+ earlier one that a later definition under one `if`/`unless` may replace
834
+ carries the opposite of that branch. Each arm names its routes from the names
835
+ taken before the site, and a later route finds a name taken if either arm
836
+ took it. A loop's `at:`, path, scope or
837
+ `as:` is read through the variable it binds. A concern the file never applies,
838
+ applies only in a file another `draw` reads, or redefines before any
839
+ `concerns` call still lists its apps once with no path, and a method called
840
+ with arguments lists them once where it is written. A constant a replayed body
841
+ writes counts as one write.
842
+ - **A route in a routing concern or routes-file method defined under
843
+ `if`/`unless`/`case` carries that condition, and a concern defined in each arm
844
+ draws each arm's routes.** Only the last arm's routes were listed, with no
845
+ condition.
846
+ - **A mount path written as a symbol is the path Rails draws.**
847
+ `mount App, at: :hc`, `mount App => :hc` and `get :metrics, to: App` list
848
+ `/hc` and `/metrics`, where they listed no path. A symbol naming a canonical
849
+ action inside a member, collection or singular resource scope, or with `on:`,
850
+ sits at the scope's own path (`get :index, to: App` in a collection is
851
+ `/posts`), as a verb route's symbol does; a string keeps its segment.
852
+ - **The booted tier names a module mounted as a Rack app by the module's name,
853
+ as `rails routes` prints it.** It printed `Module`.
854
+ - **A replayed column of a native type name reads the way the adapter reads it
855
+ back.** On PostgreSQL, `t.column :ratio, :real` is a float of limit 24 on
856
+ Rails 7.2.2 and later, and an array spelling such as `text[]` stays an array
857
+ of its element type. A MySQL `double`, `real` or `float8` is a float of limit
858
+ 53, and MySQL's `serial` is a not null unsigned bigint, as a key or a column,
859
+ and the unique index MySQL adds for a serial column is listed under its name,
860
+ or `<name>_2` when that is taken. A `tinyint(1)` is a boolean on MySQL and an
861
+ integer of limit 1 on SQLite. An `int2` is an integer of limit 2, a
862
+ `varchar(40)` a string of limit 40, and a `bool` a boolean, as Rails' dump
863
+ writes them. A MySQL table an `execute` creates reads with MySQL's rules, and
864
+ an adapter with no family reads with PostgreSQL's.
865
+ - **A PostgreSQL serial reads by the same name from schema.rb, structure.sql, a
866
+ replayed migration and the booted tier.** A column whose nextval default names
867
+ its own `<table>_<column>_seq` is a `serial`, or a `bigserial` when it is a
868
+ bigint, as Rails' dump writes it. An integer key `create_table` makes is a
869
+ serial unless a default is given or Migration[5.0] gives it one. A bigserial
870
+ that is the table's whole key reads as the default bigint key, while a
871
+ bigserial column of a composite key stays a bigserial. `change_column` keeps a
872
+ serial a serial whatever the new type, until a new default replaces the
873
+ sequence, and keeps a key with no sequence of its own
874
+ (`id: :bigint, default: nil`, a `Migration[5.0]` key, an `execute`'s
875
+ `bigint PRIMARY KEY`) an integer. A key `create_table` declares `serial` or
876
+ `smallserial` stays a serial through `change_column`. Once a serial's sequence
877
+ no longer follows its name (after `rename_column`, after `rename_table` for
878
+ any column but the key, after `change_column_default`, or in a
879
+ `CREATE TABLE AS` copy) it reads as the integer the dump writes, with the old
880
+ sequence as its default where one stays. A serial renamed away and back is a
881
+ serial again, as the dump reads a serial by the names it has at the end, and a
882
+ serial that `change_column` retypes once its sequence no longer follows its
883
+ names keeps the new type. A new serial whose `<table>_<column>_seq` a table,
884
+ an index or a renamed-away serial already holds gets the `_seq1` (or the next
885
+ free) sequence PostgreSQL picks, and every path reads a numbered `seq` suffix
886
+ as the serial's own. `rename_table` renames the key's sequence only where the
887
+ app's Rails version would: 7.1 to 8.1 cut the table name to fit PostgreSQL's
888
+ 63 bytes, while 7.0 does not and PostgreSQL cuts the result. A nextval of any
889
+ other sequence stays the column's default, and an INHERITS child, from
890
+ structure.sql or a replayed `execute`, gets its parent's columns that way, the
891
+ default bigint key's nextval included.
892
+ - **A replayed PostgreSQL child table follows the parent it INHERITS.** A
893
+ parent's `change_column`, `change_column_default` or `change_column_null` runs
894
+ on each child's column as PostgreSQL applies it, even where the parent already
895
+ had that value; a column the parent adds or renames changes in the child too,
896
+ one the parent drops goes unless the child declares it, and a renamed sequence
897
+ shows in the child's default. A child that declares one of its parent's
898
+ columns NOT NULL keeps it NOT NULL until such a statement on the parent
899
+ reaches it. A `create_table` with `INHERITS (...)` in its options gets its
900
+ parents' columns and keeps a parent it cannot find as unresolved, as an
901
+ executed CREATE TABLE does, and a drop under CASCADE drops the tables that
902
+ inherit from it and the foreign keys to it.
903
+ - **A replayed MySQL column is unsigned where the statement that makes it says
904
+ so.** A key, a column, a reference or an `unsigned_integer` keeps it, and
905
+ `change_column` drops it unless the call gives it again, as Rails rebuilds the
906
+ column from the call's options.
907
+ - **A replayed MySQL or MariaDB auto-increment column, a serial among them,
908
+ stays NOT NULL through `change_column_null` and `change_column`, as the server
909
+ keeps it.** `auto_increment: false` releases it.
910
+ - **A replayed reference's id column reads its type and sizes as the dump writes
911
+ them.** `type: :unsigned_integer` is an unsigned integer on MySQL, and
912
+ `limit:` reads as the dump writes it, in `create_join_table` too.
913
+ - **A key column a migration declares reads as the key `create_table` implies.**
914
+ `t.primary_key` with or without a type, `primary_key: true` and `add_column`
915
+ of a `primary_key` type give a not null key of the declared type, and an
916
+ integer key its own sequence on PostgreSQL.
917
+ - **A replayed composite primary key's columns are not null on PostgreSQL and
918
+ MySQL, as those databases make them.** SQLite leaves them nullable.
919
+ - **A replayed `timestamp` reads as the datetime the PostgreSQL and SQLite dumps
920
+ write, and stays a timestamp on MySQL.** A time, timestamp or timestamptz
921
+ keeps the precision schema.rb writes on every path: only a datetime's 6 is
922
+ left out, and only MySQL drops a 0. A dump that writes `precision: nil`
923
+ (PostgreSQL's plain timestamp, MySQL's `datetime(0)`) reads with no precision.
924
+ - **A replayed `create_table` the migration skips stays skipped.** A temporary
925
+ table named by a string no longer appears in the replayed schema, and
926
+ `if_not_exists: true` on a table that exists no longer adds the block's
927
+ columns to it. An executed `CREATE TABLE IF NOT EXISTS` leaves an existing
928
+ table as it is, and a `create_table ..., force:` replaces the table it names.
929
+ - **A replayed PostgreSQL nextval default reads as the dump writes it, and
930
+ follows a key sequence that `rename_table` renames.** One that names the
931
+ column's own sequence is a serial.
932
+ - **A replayed column whose default is nil has no default, as the dump writes
933
+ it, and a `change_column_default` or `add_column` with a proc default keeps
934
+ that default.** A reverted `change_column_default` sets its `from:` value. A
935
+ proc default reads as the dump writes it (`-> { "now()" }`), and one whole
936
+ PostgreSQL literal in one, such as `-> { "'y'::character varying" }`, reads as
937
+ Rails decodes it, booleans, arrays and dates through the column's type, while
938
+ any other expression keeps its proc; a bare MySQL literal or number in one
939
+ reads as its value. A replayed SQLite proc default reads as the app's Rails
940
+ reads it back: a quoted or bare literal is its value, and an expression with
941
+ no function is no default. A boolean proc default goes through Rails' boolean
942
+ cast on SQLite and MySQL, PostgreSQL's `TRUE`, `('a')::text` and boolean array
943
+ words read as the server stores them, and a bare MySQL number reads as MySQL
944
+ stores it for the column's type (`2.5` on an integer reads `3`, `007` on a
945
+ string `7`, `1e3` anywhere `1000`, a decimal's rounded to the column's scale
946
+ with no negative zero, `07` on a year `2007` whether create_table or
947
+ change_column_default sets it, and one with a trailing point such as `1.` as
948
+ its value); on a date or time column, for a value MySQL refuses, and for one
949
+ no double holds on a float or double column, it keeps its proc, and the rest
950
+ of the migration file still replays.
951
+ - **A UNIQUE that a replayed or hand-written CREATE TABLE declares is the key
952
+ the database makes.** MySQL lists an index named for its first column, and
953
+ PostgreSQL one constraint per column list named `<table>_<columns>_key`, or
954
+ the unique index behind it for a Rails 7.0 app.
955
+ - **The migration replay reads Rails' PostgreSQL unique constraints.**
956
+ `t.unique_constraint` in `create_table` and `change_table`,
957
+ `add_unique_constraint` and `remove_unique_constraint` add and drop the
958
+ constraint the dump lists, and `using_index:` takes the index over.
959
+ - **A unique constraint's `nulls_not_distinct` shows on every schema path.**
960
+ structure.sql, a replayed migration, schema.rb and the booted connection read
961
+ it as Rails 8.0 and later dump it, Rails 7.1 and 7.2 dumps leave it out, and
962
+ `rails_get_schema` prints it beside the constraint. A structure.sql index
963
+ reads `NULLS NOT DISTINCT` only for Rails 7.1 on, since Rails 7.0 cannot dump
964
+ the option.
965
+ - **A replayed decimal reads as the dump writes it.** A precision with no scale
966
+ is scale 0 on every adapter, and a default at scale 0 is whole as Rails casts
967
+ it (`2.5` reads `2`), while MySQL's own `decimal(10,0)` still rounds a default
968
+ the server gets unscaled; any other default reads in the column's scale
969
+ (`"0.0"`). A `numeric` column is a decimal with no sizes, as `type_to_sql`
970
+ writes it, and MySQL's plain `decimal` has precision 10.
971
+ - **Array and decimal defaults read as Rails dumps them on every schema path.**
972
+ Each PostgreSQL array element goes through the column's type, with NULL as
973
+ nil, quotes and backslashes decoded and floats as floats, and an `ARRAY[...]`
974
+ default is no longer cut at its first comma. A decimal default is rounded to
975
+ its scale and cut to a whole number at scale 0, so `numeric(5) DEFAULT 2.5`
976
+ reads `2`. A replayed PostgreSQL array default written as a Ruby array or an
977
+ array-literal string reads the same way, so `[1, 2.5]` on a float column reads
978
+ `[1.0, 2.5]` and `"{a,b}"` reads `["a", "b"]`.
979
+ - **A PostgreSQL column default that Rails cannot read as a value or a function
980
+ shows as no default, as schema.rb has it.** The structure.sql reader and the
981
+ migration replay showed `ARRAY[...]`, `'a' || 'b'`, `(1 + 1)`, `CURRENT_TIME`
982
+ and similar expressions as `-> { ... }`; they now follow Rails'
983
+ `has_default_function?` and give a proc only for what it accepts.
984
+ - **A structure.sql column with a quoted name that is not a plain word is
985
+ read**, PostgreSQL's `?column?` and names with spaces or doubled quotes among
986
+ them, with their `ALTER COLUMN` defaults.
987
+ - **A MySQL `CREATE TABLE` in a migration's `execute` or a hand-written
988
+ structure.sql keeps an index written without quotes or a name as an index.**
989
+ `INDEX (a)`, `KEY k (b)` and `FULLTEXT (t)` read as made-up columns. They now
990
+ take the name MySQL gives them, and in a structure.sql a functional key reads
991
+ as Rails' connection reads it.
992
+ - **A table a migration's `execute` makes with `CREATE TABLE ... AS SELECT`, or
993
+ `create_table` with `as:`, is read with the columns its database gives it,
994
+ where the replay can trace them.** MySQL keeps their not null and defaults,
995
+ PostgreSQL keeps neither, and SQLite types each column by its affinity. A
996
+ column it cannot trace stays untyped, and a column it cannot name or a query
997
+ it cannot read is counted in the schema note, so such a table never reads as
998
+ complete when it is not. It reads the query's own FROM, outside parentheses
999
+ and quotes, reads past DISTINCT and keeps the column `DISTINCT ON (k)` or
1000
+ `DISTINCT(k)` names, and reads each select item with the database's own
1001
+ quotes and numbers (dollar-quoted strings, `0x` and `0b` numbers, `x''`
1002
+ blobs, a timestamp's precision, interval fields), so it keeps a bare alias
1003
+ after any complete expression: a quoted one with every character in it, a
1004
+ string one on MySQL and SQLite, a bare PostgreSQL one folded to lower case,
1005
+ and a keyword that ends the item (`time w` names `w`). MySQL reads adjacent
1006
+ strings as one, named by the first (`'a' 'b'` names `a`), and a number with a
1007
+ leading zero reads in base ten. A computed column is named as each database
1008
+ names it: on PostgreSQL as its FigureColname does (a function's name, a
1009
+ cast's argument or its type, a CASE's ELSE or `case`, a scalar subquery its
1010
+ own first column's name, a subquery selecting a star unnamed,
1011
+ `IS NORMALIZED` `is_normalized`, `?column?` for an operator or a constant),
1012
+ on MySQL and SQLite by its text. A literal column
1013
+ is typed as MySQL 8.4's dump types it; MariaDB gives one no default and types
1014
+ some integers smaller, which the replay cannot tell apart. MySQL's form
1015
+ without AS makes its table, a MySQL column list before the SELECT keeps the
1016
+ keys it declares and orders the columns as MySQL does (the declared ones the
1017
+ query does not select, then the selected ones), and a query in parentheses
1018
+ reads its own columns.
1019
+ - **A migration's `create_table` or `change_table` block reads its columns,
1020
+ indexes and references whatever its parameter is named, `it` and `_1`
1021
+ included.** Rails hands the block the table under any name, but the static
1022
+ tier read a block's calls only on `t`, so `do |table|` added an empty table. A
1023
+ block parameter of any other kind (`reversible do |dir|`,
1024
+ `find_each { |t| ... }`) is still not read as the table.
1025
+ - **A SQLite structure.sql types each column the way Rails' SQLite type map
1026
+ reads it.** A `REAL`, `NUM` or other type the map does not match reads with no
1027
+ type, as booted Rails reads it, where it read as float or as its own name;
1028
+ `number`, `clob`, `point`, `nvarchar`, `jsonb` and `smalldatetime` read as
1029
+ decimal, text, integer, string, json and datetime; an uppercase `BIGINT` reads
1030
+ as integer, as the dumper writes it; and a column that declares no type is
1031
+ listed, not dropped. An `UNSIGNED BIG INT` reads as a plain integer, since
1032
+ only MySQL's columns carry an unsigned mark, and a default reads as Rails'
1033
+ SQLite adapter reads it: without the parentheses SQLite drops, a boolean's
1034
+ `1`, `0`, `'t'` or `'f'` as Rails' boolean type casts it, an empty string as
1035
+ no default, and an expression that is no function as no default. A primary key
1036
+ declared without NOT NULL allows NULL, as SQLite and booted Rails read it. A
1037
+ `DEFAULT TRUE` or `DEFAULT FALSE` reads as the app's Rails dumps it: no
1038
+ default before 8.1; on 8.1.0 to 8.1.3 the uppercase words, which a number
1039
+ column casts to 0 or 0.0 and a date or time column to none; and on 8.1.4
1040
+ either case as a boolean, which a string column dumps as "t" or "f", a float
1041
+ or decimal as 1.0 or 0.0, and an integer, a whole decimal or a date or time
1042
+ column as no default.
1043
+ - **A MySQL structure.sql column of a type the reader does not name is typed as
1044
+ Rails reads it.** A `point` or `multipoint` column and a `year` read as
1045
+ integers, as Rails' MySQL type map matches them, a `bit` reads as binary with
1046
+ its limit, and a `geometry` has no type, as booted Rails gives. A bare `bit`
1047
+ reads as the `bit(1)` MySQL makes it, limit 1, in a structure.sql, an executed
1048
+ CREATE TABLE and a replayed migration, and a MariaDB `uuid` has no type, as
1049
+ booted Rails reads it.
1050
+ - **A migration helper module assigned with `Module.new`, such as
1051
+ `MigrationHelpers = Module.new do`, replays the helpers it defines as a
1052
+ `module` statement does.** A migration that includes it no longer drops the
1053
+ columns those helpers add from the replayed schema.
1054
+ - **A migration's `self.execute` replays like a bare `execute`, and a migration
1055
+ table helper class whose name keeps an underscore, such as `Shop_Items`, names
1056
+ its table by its file.**
1057
+
1058
+ ### Removed
1059
+
1060
+ - **An expression index read from `db/schema.rb` no longer carries an
1061
+ `expression: true` key.** It now has the same name, columns and uniqueness
1062
+ shape as the structure.sql reader and the booted tier give it. (#605)
1063
+ - **`ActionResolver.own_actions`, `PortablePath.relativize_all` and
1064
+ `McpConfigGenerator.sub_table` are gone.** Nothing called them. (#518)
1065
+ - **`McpController.mcp_transport` and `.reset_transport!` are gone.** They only
1066
+ forwarded to `McpEdge`. The bind warning and the Host check now read one
1067
+ loopback list. (#603)
1068
+
8
1069
  ## [5.36.9] - 2026-10-23
9
1070
 
10
1071
  ### Security