rails-ai-context 5.28.0 → 5.30.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 (238) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1443 -0
  3. data/CONTEXT.md +37 -0
  4. data/CONTRIBUTING.md +2 -2
  5. data/README.md +2 -2
  6. data/SECURITY.md +1 -22
  7. data/docs/ARCHITECTURE.md +9 -6
  8. data/docs/CLI.md +9 -0
  9. data/docs/COMPATIBILITY.md +26 -16
  10. data/docs/CONFIGURATION.md +19 -13
  11. data/docs/GUIDE.md +19 -18
  12. data/docs/INTROSPECTORS.md +53 -18
  13. data/docs/STANDALONE.md +1 -1
  14. data/docs/TOOLS.md +91 -30
  15. data/docs/TROUBLESHOOTING.md +17 -3
  16. data/exe/rails-ai-context +72 -81
  17. data/lib/generators/rails_ai_context/install/install_generator.rb +10 -16
  18. data/lib/rails_ai_context/action_filters.rb +10 -6
  19. data/lib/rails_ai_context/action_outcome.rb +242 -0
  20. data/lib/rails_ai_context/app_commands.rb +36 -0
  21. data/lib/rails_ai_context/app_kind.rb +25 -4
  22. data/lib/rails_ai_context/ast_cache.rb +52 -10
  23. data/lib/rails_ai_context/base_mixins.rb +175 -0
  24. data/lib/rails_ai_context/brackets.rb +123 -0
  25. data/lib/rails_ai_context/cli/entry_boot.rb +67 -39
  26. data/lib/rails_ai_context/cli/tool_runner.rb +51 -33
  27. data/lib/rails_ai_context/concern_macros.rb +314 -25
  28. data/lib/rails_ai_context/concern_membership.rb +25 -1
  29. data/lib/rails_ai_context/concern_paths.rb +125 -6
  30. data/lib/rails_ai_context/configuration.rb +25 -13
  31. data/lib/rails_ai_context/detail_level.rb +2 -8
  32. data/lib/rails_ai_context/doctor.rb +102 -111
  33. data/lib/rails_ai_context/erb_source.rb +6 -0
  34. data/lib/rails_ai_context/file_walk.rb +37 -0
  35. data/lib/rails_ai_context/fingerprinter.rb +12 -12
  36. data/lib/rails_ai_context/gem_lock.rb +56 -10
  37. data/lib/rails_ai_context/git_ignore.rb +211 -0
  38. data/lib/rails_ai_context/hydrators/hydration_formatter.rb +3 -4
  39. data/lib/rails_ai_context/hydrators/schema_hint_builder.rb +4 -3
  40. data/lib/rails_ai_context/install/ai_tool.rb +9 -9
  41. data/lib/rails_ai_context/install/selection_record.rb +21 -89
  42. data/lib/rails_ai_context/install/surface.rb +16 -0
  43. data/lib/rails_ai_context/install_mode.rb +12 -0
  44. data/lib/rails_ai_context/introspector.rb +15 -15
  45. data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +1 -13
  46. data/lib/rails_ai_context/introspectors/action_presence.rb +127 -0
  47. data/lib/rails_ai_context/introspectors/action_resolver.rb +77 -31
  48. data/lib/rails_ai_context/introspectors/action_text_introspector.rb +1 -13
  49. data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +40 -58
  50. data/lib/rails_ai_context/introspectors/active_support_introspector.rb +15 -20
  51. data/lib/rails_ai_context/introspectors/api_introspector.rb +211 -49
  52. data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +14 -53
  53. data/lib/rails_ai_context/introspectors/ast_walk.rb +22 -0
  54. data/lib/rails_ai_context/introspectors/auth_introspector.rb +38 -35
  55. data/lib/rails_ai_context/introspectors/autoload_introspector.rb +15 -14
  56. data/lib/rails_ai_context/introspectors/base.rb +21 -0
  57. data/lib/rails_ai_context/introspectors/call_site_expansion.rb +517 -0
  58. data/lib/rails_ai_context/introspectors/component_introspector.rb +141 -114
  59. data/lib/rails_ai_context/introspectors/config_introspector.rb +1 -13
  60. data/lib/rails_ai_context/introspectors/connection_pool_introspector.rb +7 -25
  61. data/lib/rails_ai_context/introspectors/controller_filters.rb +49 -10
  62. data/lib/rails_ai_context/introspectors/controller_introspector.rb +141 -32
  63. data/lib/rails_ai_context/introspectors/convention_introspector.rb +38 -38
  64. data/lib/rails_ai_context/introspectors/credentials_introspector.rb +5 -15
  65. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +1 -15
  66. data/lib/rails_ai_context/introspectors/declared_constant.rb +103 -26
  67. data/lib/rails_ai_context/introspectors/devops_introspector.rb +1 -13
  68. data/lib/rails_ai_context/introspectors/eager_load.rb +1 -1
  69. data/lib/rails_ai_context/introspectors/engine_introspector.rb +34 -25
  70. data/lib/rails_ai_context/introspectors/env_config_introspector.rb +30 -20
  71. data/lib/rails_ai_context/introspectors/env_introspector.rb +13 -29
  72. data/lib/rails_ai_context/introspectors/env_references.rb +92 -0
  73. data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +102 -129
  74. data/lib/rails_ai_context/introspectors/gem_introspector.rb +57 -17
  75. data/lib/rails_ai_context/introspectors/gemfile_gems.rb +27 -0
  76. data/lib/rails_ai_context/introspectors/habtm_join_tables.rb +115 -0
  77. data/lib/rails_ai_context/introspectors/helper_names.rb +48 -0
  78. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +14 -17
  79. data/lib/rails_ai_context/introspectors/includers.rb +52 -0
  80. data/lib/rails_ai_context/introspectors/initializer_introspector.rb +5 -15
  81. data/lib/rails_ai_context/introspectors/interaction.rb +127 -0
  82. data/lib/rails_ai_context/introspectors/job_introspector.rb +585 -160
  83. data/lib/rails_ai_context/introspectors/listener_registration.rb +0 -7
  84. data/lib/rails_ai_context/introspectors/listeners/associations_listener.rb +14 -7
  85. data/lib/rails_ai_context/introspectors/listeners/autoload_paths_listener.rb +102 -0
  86. data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +211 -19
  87. data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +12 -13
  88. data/lib/rails_ai_context/introspectors/listeners/component_structure_listener.rb +3 -10
  89. data/lib/rails_ai_context/introspectors/listeners/config_assignment_listener.rb +2 -2
  90. data/lib/rails_ai_context/introspectors/listeners/enums_listener.rb +15 -3
  91. data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +2 -12
  92. data/lib/rails_ai_context/introspectors/listeners/generic_macro_listener.rb +16 -0
  93. data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +5 -3
  94. data/lib/rails_ai_context/introspectors/listeners/mailbox_routing_listener.rb +1 -1
  95. data/lib/rails_ai_context/introspectors/listeners/methods_listener.rb +1 -1
  96. data/lib/rails_ai_context/introspectors/listeners/middleware_config_listener.rb +47 -5
  97. data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +29 -48
  98. data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +108 -0
  99. data/lib/rails_ai_context/introspectors/listeners/mixins_listener.rb +44 -6
  100. data/lib/rails_ai_context/introspectors/listeners/model_reference_listener.rb +1 -1
  101. data/lib/rails_ai_context/introspectors/listeners/mount_listener.rb +132 -24
  102. data/lib/rails_ai_context/introspectors/listeners/preview_paths_listener.rb +48 -0
  103. data/lib/rails_ai_context/introspectors/listeners/rake_task_dsl_listener.rb +6 -25
  104. data/lib/rails_ai_context/introspectors/listeners/route_files_listener.rb +93 -0
  105. data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +159 -35
  106. data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +5 -32
  107. data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +22 -8
  108. data/lib/rails_ai_context/introspectors/listeners/validations_listener.rb +82 -7
  109. data/lib/rails_ai_context/introspectors/listeners/with_options_scope.rb +105 -0
  110. data/lib/rails_ai_context/introspectors/middleware_introspector.rb +125 -70
  111. data/lib/rails_ai_context/introspectors/migration_introspector.rb +1 -13
  112. data/lib/rails_ai_context/introspectors/migration_replay/helpers.rb +259 -0
  113. data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +285 -0
  114. data/lib/rails_ai_context/introspectors/migration_replay.rb +220 -207
  115. data/lib/rails_ai_context/introspectors/model_introspector.rb +676 -129
  116. data/lib/rails_ai_context/introspectors/module_aliases.rb +157 -0
  117. data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +84 -78
  118. data/lib/rails_ai_context/introspectors/node_source.rb +46 -0
  119. data/lib/rails_ai_context/introspectors/observability_introspector.rb +1 -13
  120. data/lib/rails_ai_context/introspectors/performance_introspector.rb +122 -96
  121. data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +2 -10
  122. data/lib/rails_ai_context/introspectors/retry_policy.rb +38 -0
  123. data/lib/rails_ai_context/introspectors/route_introspector.rb +275 -52
  124. data/lib/rails_ai_context/introspectors/schema_conventions.rb +87 -9
  125. data/lib/rails_ai_context/introspectors/schema_introspector.rb +89 -25
  126. data/lib/rails_ai_context/introspectors/schema_reader.rb +11 -5
  127. data/lib/rails_ai_context/introspectors/security_introspector.rb +12 -42
  128. data/lib/rails_ai_context/introspectors/seeds_introspector.rb +23 -33
  129. data/lib/rails_ai_context/introspectors/service_classes.rb +101 -0
  130. data/lib/rails_ai_context/introspectors/source_calls.rb +86 -0
  131. data/lib/rails_ai_context/introspectors/source_introspector.rb +17 -1
  132. data/lib/rails_ai_context/introspectors/source_scan.rb +12 -2
  133. data/lib/rails_ai_context/introspectors/static_tier.rb +1 -1
  134. data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +408 -79
  135. data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +123 -32
  136. data/lib/rails_ai_context/introspectors/superclass_chain.rb +149 -0
  137. data/lib/rails_ai_context/introspectors/table_name.rb +152 -9
  138. data/lib/rails_ai_context/introspectors/test_introspector.rb +57 -73
  139. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +61 -82
  140. data/lib/rails_ai_context/introspectors/view_introspector.rb +59 -60
  141. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +286 -95
  142. data/lib/rails_ai_context/mcp_config_generator.rb +1 -6
  143. data/lib/rails_ai_context/option_text.rb +17 -0
  144. data/lib/rails_ai_context/path_resolver.rb +246 -10
  145. data/lib/rails_ai_context/payload.rb +43 -7
  146. data/lib/rails_ai_context/percent.rb +17 -0
  147. data/lib/rails_ai_context/portable_path.rb +7 -0
  148. data/lib/rails_ai_context/redaction.rb +288 -49
  149. data/lib/rails_ai_context/resources.rb +1 -1
  150. data/lib/rails_ai_context/route_coverage.rb +50 -4
  151. data/lib/rails_ai_context/run_cache.rb +37 -0
  152. data/lib/rails_ai_context/safe_file.rb +6 -2
  153. data/lib/rails_ai_context/safe_path.rb +0 -2
  154. data/lib/rails_ai_context/schema_adapter.rb +80 -15
  155. data/lib/rails_ai_context/schema_hint.rb +1 -13
  156. data/lib/rails_ai_context/serializers/base.rb +13 -0
  157. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +8 -12
  158. data/lib/rails_ai_context/serializers/claude_serializer.rb +1 -1
  159. data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +98 -58
  160. data/lib/rails_ai_context/serializers/context_file_serializer.rb +4 -5
  161. data/lib/rails_ai_context/serializers/context_mode_dispatch.rb +2 -7
  162. data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +1 -7
  163. data/lib/rails_ai_context/serializers/copilot_serializer.rb +10 -89
  164. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +1 -7
  165. data/lib/rails_ai_context/serializers/json_serializer.rb +1 -7
  166. data/lib/rails_ai_context/serializers/markdown_serializer.rb +76 -45
  167. data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +3 -9
  168. data/lib/rails_ai_context/serializers/opencode_serializer.rb +2 -17
  169. data/lib/rails_ai_context/serializers/section_facts.rb +85 -5
  170. data/lib/rails_ai_context/serializers/section_marker_writer.rb +3 -3
  171. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +54 -42
  172. data/lib/rails_ai_context/serializers/test_command_detection.rb +1 -6
  173. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +1 -3
  174. data/lib/rails_ai_context/server.rb +3 -0
  175. data/lib/rails_ai_context/tasks/rails_ai_context.rake +22 -30
  176. data/lib/rails_ai_context/test_framework.rb +234 -0
  177. data/lib/rails_ai_context/tools/analyze_feature.rb +53 -40
  178. data/lib/rails_ai_context/tools/base_tool.rb +173 -78
  179. data/lib/rails_ai_context/tools/dependency_graph.rb +144 -43
  180. data/lib/rails_ai_context/tools/diagnose.rb +59 -66
  181. data/lib/rails_ai_context/tools/generate_test.rb +517 -134
  182. data/lib/rails_ai_context/tools/get_active_support.rb +30 -15
  183. data/lib/rails_ai_context/tools/get_api.rb +43 -8
  184. data/lib/rails_ai_context/tools/get_callbacks.rb +9 -4
  185. data/lib/rails_ai_context/tools/get_component_catalog.rb +34 -11
  186. data/lib/rails_ai_context/tools/get_concern.rb +241 -136
  187. data/lib/rails_ai_context/tools/get_config.rb +48 -18
  188. data/lib/rails_ai_context/tools/get_context.rb +32 -20
  189. data/lib/rails_ai_context/tools/get_controllers.rb +16 -15
  190. data/lib/rails_ai_context/tools/get_conventions.rb +39 -59
  191. data/lib/rails_ai_context/tools/get_edit_context.rb +6 -3
  192. data/lib/rails_ai_context/tools/get_engines.rb +19 -6
  193. data/lib/rails_ai_context/tools/get_env.rb +69 -90
  194. data/lib/rails_ai_context/tools/get_env_config.rb +1 -1
  195. data/lib/rails_ai_context/tools/get_frontend_stack.rb +7 -1
  196. data/lib/rails_ai_context/tools/get_gems.rb +27 -20
  197. data/lib/rails_ai_context/tools/get_helper_methods.rb +33 -37
  198. data/lib/rails_ai_context/tools/get_i18n.rb +10 -5
  199. data/lib/rails_ai_context/tools/get_job_pattern.rb +265 -96
  200. data/lib/rails_ai_context/tools/get_mailers.rb +51 -3
  201. data/lib/rails_ai_context/tools/get_model_details.rb +105 -46
  202. data/lib/rails_ai_context/tools/get_partial_interface.rb +68 -80
  203. data/lib/rails_ai_context/tools/get_routes.rb +77 -8
  204. data/lib/rails_ai_context/tools/get_schema.rb +163 -56
  205. data/lib/rails_ai_context/tools/get_service_pattern.rb +165 -88
  206. data/lib/rails_ai_context/tools/get_stimulus.rb +74 -32
  207. data/lib/rails_ai_context/tools/get_test_info.rb +100 -51
  208. data/lib/rails_ai_context/tools/get_turbo_map.rb +44 -34
  209. data/lib/rails_ai_context/tools/get_view.rb +72 -73
  210. data/lib/rails_ai_context/tools/migration_advisor.rb +9 -6
  211. data/lib/rails_ai_context/tools/onboard.rb +57 -72
  212. data/lib/rails_ai_context/tools/query.rb +13 -26
  213. data/lib/rails_ai_context/tools/read_logs.rb +5 -22
  214. data/lib/rails_ai_context/tools/review_changes.rb +4 -4
  215. data/lib/rails_ai_context/tools/runtime_info.rb +5 -20
  216. data/lib/rails_ai_context/tools/safe_call.rb +16 -1
  217. data/lib/rails_ai_context/tools/search_code.rb +148 -74
  218. data/lib/rails_ai_context/tools/search_docs.rb +9 -28
  219. data/lib/rails_ai_context/tools/security_scan.rb +283 -52
  220. data/lib/rails_ai_context/tools/session_context.rb +7 -2
  221. data/lib/rails_ai_context/tools/validate.rb +8 -11
  222. data/lib/rails_ai_context/tools/validate_semantics.rb +138 -121
  223. data/lib/rails_ai_context/version.rb +1 -1
  224. data/lib/rails_ai_context/vfs.rb +27 -3
  225. data/lib/rails_ai_context/view_file.rb +41 -11
  226. data/lib/rails_ai_context.rb +30 -3
  227. data/server.json +1 -1
  228. metadata +38 -13
  229. data/demo/demo-trace.gif +0 -0
  230. data/demo/demo-trace.tape +0 -21
  231. data/demo/demo.gif +0 -0
  232. data/demo/demo.tape +0 -33
  233. data/docs/social-preview.html +0 -164
  234. data/gemfiles/mcp_floor.gemfile +0 -19
  235. data/gemfiles/prism_floor.gemfile +0 -13
  236. data/gemfiles/prism_head.gemfile +0 -12
  237. data/gemfiles/rails.gemfile +0 -42
  238. data/lib/rails_ai_context/introspectors/listeners/mongoid_fields_listener.rb +0 -27
data/CHANGELOG.md CHANGED
@@ -5,6 +5,1449 @@ 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.30.0] - 2026-09-28
9
+
10
+ A pass over the whole tree for code that did not need to exist, with one base
11
+ class for the introspectors and one for the serializers, and one reader where
12
+ several files each kept a copy. Then four real-app QA passes in every tier
13
+ (Mastodon, Discourse, OpenProject, Canvas, Forem, Whitehall, Consul,
14
+ OpenFoodNetwork, Huginn, Errbit, Diaspora, Plots2 and a private API app) and
15
+ five review rounds found the wrong answers below. Their fixes grew `lib/` by
16
+ about 8,500 lines, most of it reading what the gem used to guess.
17
+
18
+ ### Added
19
+
20
+ - **`rails_get_engines` and the generated files name the app's own in-repo
21
+ engines, plugins and modules**: an "In-Repo Engines" section with each
22
+ one's path and the models the model scan found under it (unavailable, not
23
+ zero, when the model scan failed), and
24
+ in the compact files one Stack line, `- In-repo engines: 4 (catalog,
25
+ dfc_provider, order_management, web)`, capped at five names.
26
+
27
+ ### Changed
28
+
29
+ - **`.github/copilot-instructions.md` is built the way CLAUDE.md is.** It was
30
+ the one root file off the shared compact renderer. It now carries the same
31
+ sections (Async, Migrations, Commands, Rules), per-model counts, "Key models
32
+ (N total)" with 15 models where it listed 25, and the `Generated by
33
+ rails-ai-context` line; "Conventions" becomes "Rules". Its tool reference is
34
+ the compact list plus the roster of every tool name, where it had a
35
+ Copilot-only table with one described row per tool and a "detail parameter -
36
+ ALWAYS start with summary" section. The file is capped by `claude_max_lines`
37
+ and ends with the trim note when it runs over. Its docs promised a 500-line
38
+ cap that nothing enforced.
39
+ - **`claude_max_lines` counts content lines, the `BEGIN`/`END` markers
40
+ included, and must be positive.** Blank lines between sections do not spend
41
+ the budget, so a file that fit before still fits and still ends with the
42
+ full MCP tools guide. A file over the budget keeps its Commands, Warnings,
43
+ Rules and MCP tools sections whole and trims the data sections above them,
44
+ with the trim note at the cut and no heading left without its body; it
45
+ trimmed from the end, so the tool list and the last rules were what went. A
46
+ budget too small for those sections alone still trims from the end. Zero or
47
+ a negative number raises `ArgumentError` when set in Ruby, the way
48
+ `cache_ttl` and `max_tool_response_chars` already do; in
49
+ `.rails-ai-context.yml` it warns and keeps the default of 150.
50
+ - **The four compact files print their sections in one order**: Stack, Key
51
+ models, Gems, Architecture, Commands, Warnings, Rules, Tools. AGENTS.md moves
52
+ its MCP tools guide from the middle to the end and its Warnings block above
53
+ Rules, which is where CLAUDE.md already had them.
54
+ - **`views.layout_mapping` is gone from `.ai-context.json`.** It globbed the
55
+ layouts a second time to hold the names `views.layouts` already carries.
56
+ - **`rails_read_logs` and `rails_runtime_info` print sizes through
57
+ ActiveSupport**: binary units and three significant digits, so read_logs
58
+ says "2.29 MB" where it said "2.4 MB", runtime_info says "1 GB" where it said
59
+ "1.0 GB", and a count under a kilobyte reads "512 Bytes" where it read
60
+ "512 B". Both print in English whatever the app's `I18n.locale` is, and in
61
+ the app's own locale only when it offers no English at all. The two tools
62
+ used to disagree on what a megabyte was.
63
+ - **`rails-ai-context tool --list` trims each description at a word boundary
64
+ to at most 79 characters, ellipsis included**, and a cut that lands on
65
+ trailing punctuation drops it before the ellipsis.
66
+ - **`tools/list` is sorted by MCP tool name, and `rails-ai-context tool --list`
67
+ prints its short names in that order.** That is the order v5.29.0 happened
68
+ to print; it no longer depends on which tools a process loaded first.
69
+ - **A failed section is recorded in one place.** The introspection loop turns
70
+ a failed section into its `{ error: }` entry and prints a warning, so a
71
+ section that used to fail without a warning now shows one, and `doctor`
72
+ names the exception class ("database_stats: StandardError: Database not
73
+ found"). The entry itself is unchanged. Only `credentials` still records its
74
+ own failure, to keep the exception message out of the output.
75
+ - **`init` and `version` answer an unexpected error like every other
76
+ command**: `Error: <message>` on stderr and exit 1. One rescue around
77
+ Thor's command invocation replaced eight copies; Thor's own refusals keep
78
+ their wording. With `DEBUG` set, the first 15 backtrace frames follow the
79
+ `Error:` line.
80
+ - **The doctor's view-size hint names only `config.max_view_total_size`**, the
81
+ setting its check compares against, and the generated initializer's comment
82
+ for `max_view_file_size` says no check reads it. The option is still
83
+ accepted.
84
+ - **The gem requires `thor >= 1.2`.** Thor 1.0 and 1.1 raise `NameError` on
85
+ require under every Ruby this gem supports.
86
+ - **The packaged gem leaves out `demo/`**, 525KB of GIFs the README loads from
87
+ GitHub, `gemfiles/`, the CI's own Gemfiles, and `docs/social-preview.html`, a
88
+ one-off screenshot page. The .gem drops from about 1.29MB to about 780KB.
89
+ - **The security policy says only the latest 5.x release gets fixes**, in
90
+ place of a version table that stopped at 5.7.x.
91
+ - **The release runs the same Ruby and Rails matrix as CI**, from one shared
92
+ workflow whose test legs get read-only repository access. It used to skip
93
+ Ruby 3.2 with Rails 8.0 and 8.1, which CI already ran green.
94
+ - **Introspection reads each source once where it used to read it again**:
95
+ auth, Active Storage, turbo views, seeds, conventions, job channels, the
96
+ Gemfile, ActiveRecord and Mongoid models, each migration the schema replays,
97
+ `db/migrate`, and the doctor's view count. Where one walk now answers several
98
+ lists (Active Storage's attachments, validations and variants), a walk that
99
+ fails leaves those lists empty together.
100
+ - **With `DEBUG` set, a skipped scan prints `<label> failed: <message>`**, the
101
+ shape every other debug line in the gem already had.
102
+
103
+ - **A static route count says how many in-repo engine route files it did not
104
+ read**, everywhere a route count is printed: `rails_get_routes`, CLAUDE.md,
105
+ the rule files, `rails_onboard` and the rake summary. An engine draws its
106
+ routes into its own table and the app mounts it at boot, so the static tier
107
+ cannot place those paths.
108
+ - **`rails_get_i18n` and the generated files say what the in-repo engines'
109
+ own locale directories hold that they did not read**: the tool and the
110
+ full file count the files, and the compact files' I18n line ends
111
+ `; 29 in-repo engine locale dirs not read`.
112
+ - **The component catalog reads every `app/components`**, packs and in-repo
113
+ engines included, and its header counts what it could not place, as in `998
114
+ components (862 ViewComponent, 0 Phlex, 136 of no known base class)` on
115
+ OpenProject, which read 559 from its root tree alone, with a base that other
116
+ components inherit from (Consul's two `BaseComponent`s) named on a bases
117
+ line and not counted; the generated files print the same line from the same
118
+ numbers. `component:` takes a name with or without its namespace or
119
+ `Component` suffix, and a short name several components share lists them and
120
+ asks for the full one.
121
+ - **`rails_get_active_support` lists validator classes apart from concerns**,
122
+ so its concern total matches `rails_get_concern`, and a concerns directory
123
+ holding only validators says so.
124
+ - **A condition prints the line the file holds.** A callback, concern
125
+ callback, validation or controller filter whose condition is a lambda, a
126
+ proc or a method call prints that source on one line where it printed
127
+ `[INFERRED]`, and a symbol prints as a symbol (`if: :published?` and
128
+ `on: :create`, where they read `if: published?` and `on: create`), in a
129
+ list as well (`if: [:a?, :b?]`). A
130
+ callback with `if:` or `unless:` now names the condition in
131
+ `rails_get_model_details` and `rails_get_callbacks`, each declaration its
132
+ own when one method is declared twice, and a filter that runs names its
133
+ own condition in every surface that lists filters, where only a skip's
134
+ printed. The same
135
+ goes for any validation option that is not a literal (a regexp, a
136
+ constant) and for an option nested in another one, as in
137
+ `inclusion: { in: proc { ... } }`.
138
+ - **`rails_dependency_graph` draws no node for an association whose
139
+ `class_name` is computed at runtime**, where it drew one named `_INFERRED_`,
140
+ leaves them out of its association count, and names up to ten of them in
141
+ a note that says how many the count leaves out. The same goes for an
142
+ association it cannot name. Its footer says how many edges the page drew
143
+ beside the associations they came from, so the numbers reconcile with the
144
+ diagram.
145
+ - **An association whose name is computed is marked `(computed)` wherever it
146
+ prints** (`` `owner_name` (computed) ``, and
147
+ `` `:"#{name.underscore}_search_data"` (computed) ``), where it printed
148
+ `[INFERRED]` (six times in Discourse's CLAUDE.md). No tool reads it as a
149
+ literal: `rails_dependency_graph` draws no node for it,
150
+ `rails_generate_test` writes no matcher for it, where it wrote a spec that
151
+ did not pass, and the related-model and N+1 checks name no model for it. A
152
+ computed `class_name` or `through:` prints its source too.
153
+ - **A `class_name` written with a leading `::` names the class it names**
154
+ (`class_name: "::Token::API"`), in every tool and generated file.
155
+ - **The generated files rank "Key models" by association count**, most
156
+ connected first and then by name, through the same function `rails_onboard`
157
+ uses, and the heading says so. A subclass ranks by the associations it adds
158
+ to its parent, so one that only repeats them sinks and one with many of its
159
+ own (OpenProject's `User`) stays: Huginn's list was `Agent` and 14 of its
160
+ subclasses, with no `User`, `Event` or `Scenario`, and Discourse's was
161
+ headed by `Chat::NullUser`. They listed the first 15 by name, and
162
+ `rails_onboard` broke ties in file order.
163
+ - **The controller count beside a route count reads "routed controllers"** in
164
+ CLAUDE.md, AGENTS.md, `rails_onboard` and `rake ai:inspect`, since it counts
165
+ the controllers the routes name, not the controller files.
166
+ - **A Stimulus controller is named by its Stimulus identifier**, from its path
167
+ under the `controllers` directory (`users--tools--ajax`), and a lookup takes
168
+ the identifier, the file path with slashes or underscores, or the file name
169
+ (`ajax_controller.js`), a file name several controllers share listing them
170
+ all. Its "Used in" list reads the same files that confirm a name, views
171
+ first. Outside the rails-new `app/javascript/controllers` home the app's own
172
+ loader decides the name, so a name the app registers in its JavaScript
173
+ (`application.register("x", Klass)`, `preregister('x', Klass)`, literal
174
+ strings, the class as the whole argument, a module imported through the
175
+ app's alias table in tsconfig or jsconfig `compilerOptions.paths`, read
176
+ through `extends`, or a literal vite, webpack or rspack `resolve.alias`
177
+ (matched as the bundlers match, an exact key exact), test files not counted)
178
+ is used, and otherwise a derived name is checked against the identifiers the
179
+ app writes in any template or Ruby file under its code roots and `lib`
180
+ (`data-controller`, a target, an action, a `content_controller`-style
181
+ helper, a `data: { controller: }` hash in a component, form object or model)
182
+ and takes the one they use; one nothing confirms carries a note that it is
183
+ inferred. No two controllers share an identifier across code roots: an
184
+ in-repo plugin's `foo_controller.js` beside the app's reads `chat--foo`,
185
+ marked inferred. A controller the app registers from an installed package
186
+ (Errbit's `reveal` from `@stimulus-components/reveal`) is listed as
187
+ registered from that package, where `rails_get_stimulus` said none while
188
+ `rails_get_view` showed it. OpenProject's 161 read the names its code writes
189
+ (`admin--custom-fields`, not the path's `dynamic--admin--custom-fields`),
190
+ and the few nothing confirms are marked inferred, named by their directory's
191
+ rule when the app shows one (a loader importing the directory by a derived
192
+ path, or confirmed neighbours that all drop its segment), so OpenProject's 8
193
+ read `generic-dialog-close`, not `dynamic--generic-dialog-close`. The empty
194
+ answer names the directories searched, and `rails_get_conventions` labels
195
+ the pattern `Stimulus controllers` with no directory after it.
196
+ - **The frontend framework is picked by config files and component counts**, a
197
+ tsconfig read with its comments, trailing commas and `extends` (a `tsc
198
+ --init` file read `TypeScript: disabled`), and a second framework the app
199
+ also uses is named. An Angular app that also depends on React read as React,
200
+ and so did a Preact or Solid app that lists `react` for compatibility. Each
201
+ framework reports its own version, and a frontend root counts the components
202
+ its framework writes (OpenProject's `frontend` has 305 `*.component.ts`; it
203
+ read 3).
204
+ - **`rails_get_test_info` names both frameworks** (`rspec, minitest`) when an
205
+ app runs both.
206
+ - **The conventions' Locales line names the locales each file declares** (a
207
+ `validates_timeliness.en.yml` read as a locale named
208
+ `validates_timeliness`), a class named from the root scope (`class ::Foo`
209
+ inside a module) is the top-level constant, and an unread `database.yml`
210
+ default is never shown as another site's default.
211
+ - **The generated files' Multi-Database section labels each adapter the way
212
+ every other surface does** (`PostgreSQL`, `MySQL by database.yml default`),
213
+ where it printed the raw adapter name, and prints `unknown` for one it
214
+ cannot read instead of nothing.
215
+ - **`rails_get_partial_interface(partial:)` given a bare name that several
216
+ partials share lists them**, where it answered with the first by path, and a
217
+ call to a helper the app defines (in `app/helpers` of any code root, or a
218
+ module those helpers include from any autoload root; a module's class
219
+ methods are not helpers) is not one of a partial's locals: Discourse's
220
+ `layouts/head` listed `canonical_link_tag` and three more.
221
+ - **A context run reads more of the app, for more time where there is more to
222
+ read.** Where a kind of code lives is resolved once per root, each file is
223
+ parsed once per run and answered from its stat after that, a mixin is walked
224
+ once for every model that includes it, and a CLI tool call loads only the
225
+ tool it names; none of it outlives a run, so a directory created while a
226
+ server runs is seen by the next one, and a file's stat stands in for reading
227
+ it only once the file is two seconds older than the read, so a same-size
228
+ edit right after a parse on a coarse Linux clock is read again. The CLI
229
+ picks between a built-in tool and a custom one of the same name as the MCP
230
+ server does: the built-in, unless `skip_tools` names it. On the static tier,
231
+ `context --format all` takes about the same CPU as 5.29.0 on Canvas (17.4s
232
+ against 18.1s), Mastodon (2.8s against 2.7s) and Errbit, a sixth more on
233
+ Huginn (1.1s against 1.0s), a quarter more on a private API app (3.1s
234
+ against 2.5s; booted 5.4s against 4.7s), where the `Services:` line reads
235
+ all its services, and half again on OpenProject (9.4s against 6.4s), whose
236
+ 30 in-repo modules are now read for models, Stimulus identifiers, concerns,
237
+ mailers and model bases.
238
+ - **The generated files' `Async:` line counts Sidekiq workers** beside jobs and
239
+ mailers, and `rails_get_job_pattern` names the directories it read under
240
+ every listing and also searches `app/sidekiq` for what enqueues a job. The
241
+ `Jobs:` line lists what the job scan found, jobs and workers alike, up to 12
242
+ names and a count of the rest, where it globbed `app/jobs/*.rb`.
243
+ - **A worker entry says what a job entry says**: its line count, the
244
+ `retry_on` and `discard_on` it declares and what it calls. Where a base
245
+ class owns `perform` and the worker defines `execute`, as Discourse's jobs
246
+ do, `execute` is read instead.
247
+ - **A class in a job directory that reaches neither ActiveJob nor Sidekiq but
248
+ defines `perform` is listed as `[unknown base]`**, as a Resque job is, and
249
+ counted under an `unknown` queue rather than the default one, in both tiers.
250
+ - **A job's "Enqueued By" list names only the files that enqueue that job, and
251
+ every one of them.** It is read off the call nodes, with one enqueue rule
252
+ shared by the job and service side effects, the controller render map and
253
+ `rails_search_code`'s "Calls internally", so a call in a comment does not
254
+ count and Sidekiq's `perform_in`, `perform_at` and `perform_bulk` and
255
+ ActiveJob's `perform_all_later` do (a private API app listed 3 of the 6
256
+ files that enqueue one of its workers); a call through the app's own
257
+ `enqueue`, `enqueue_in` or `enqueue_at` helper counts, the job named by a
258
+ symbol, a string or the class (Discourse's `Jobs.enqueue(:process_post)`
259
+ lists `app/models/post.rb`, where Discourse jobs had no callers); all of
260
+ `app/` and `lib/` is searched, rake tasks, `bin/` and `script/` programs and
261
+ all of `db/` included, and a list capped at twenty says how many more there
262
+ are; a job named relative to the caller's namespace (`SyncJob` inside
263
+ `module Admin`, in a method or a class-body block such as a callback) is
264
+ credited to the job Ruby resolves it to, a compact `class A::B` nesting only
265
+ itself as Ruby does, not a top-level one of the same short name; and a job
266
+ nothing enqueues says so in one line where the section was left out. A call
267
+ on another job whose name ends the same way (`Admin::SyncJob.perform_async`
268
+ for `SyncJob`) counted, and the job's own file was left out by a substring
269
+ of its name, which dropped any file whose name contained it and kept the
270
+ job's own file under a folder spelled with an acronym; the file is now left
271
+ out by the constant it declares.
272
+ - **A job shows the queue and Sidekiq options it inherits from the app's own
273
+ base classes**, its own settings winning key by key, each inherited one
274
+ named once with its base and its value when short. Diaspora's `Mail::*`
275
+ workers showed no queue where `Mail::NotifierBaseWorker` sets `queue: low`,
276
+ Mastodon's `Fasp::*` workers none where their base sets `fasp`, and
277
+ Whitehall's `PublishingApi*Job`s none where theirs sets `publishing_api`.
278
+ - **A job queue computed by a block or an expression says so, with its
279
+ source**: `queue_as -> { ... }` read `dynamic` in the booted tier and showed
280
+ no queue without boot.
281
+ - **A class is a job base only when its own chain reaches ActiveJob or a
282
+ Sidekiq mixin.** OpenProject's `Meetings::PDF::Common::Styles::Base`, a PDF
283
+ style class under `app/workers`, was listed as a job base.
284
+ - **A mailer kept in `app/services` is a mailer and not also a service**, and
285
+ a service not found is offered the constants the files declare
286
+ (`ActivityPub::...`, where it offered `Activitypub::...`).
287
+ - **A module other classes in the app include, extend or prepend is not a
288
+ service object**, unless it has an entry point of its own (a class method
289
+ other than the `included`, `extended` or `prepended` hook,
290
+ `module_function`, `extend self`). Whitehall's `AssetManager::ServiceHelper`
291
+ was listed as a service, as were 11 mixins in a private API app and 11 in
292
+ OpenProject; the generated `Services:` line follows.
293
+ - **`rails_get_service_pattern` and `rails_get_helper_methods` find a service
294
+ or helper by the name the app writes when its path uses a registered
295
+ acronym.** Mastodon's 20 `ActivityPub::...` services, in `activitypub/`,
296
+ answered not found and suggested `Activitypub::...`.
297
+ - **The generated files' `Services:` line reads the same scan
298
+ `rails_get_service_pattern` lists from**, so a service in a subdirectory, a
299
+ pack or an engine appears; it globbed `app/services/*.rb`, which gave a
300
+ private API app, whose services are all namespaced, no line at all. It and
301
+ the `Jobs:` line show 12 names and say how many more there are, in every
302
+ tier: the static tier dropped both with the conventions it cannot read.
303
+ - **A blank name given to a by-name lookup is answered as blank**, naming what
304
+ the tool looks up ("The concern name is blank"), in the helper, service,
305
+ job, mailer, component and concern tools: Discourse's
306
+ `rails_get_helper_methods(helper: "")` crashed on a file named `helper.rb`.
307
+ A string passed on the command line or in a direct call where a tool takes a
308
+ list (`files`, `include`, `checks`) is a one-item list, or a list when
309
+ comma-separated, where it failed with `undefined method 'any?'`; over MCP
310
+ the schema asks for the list itself.
311
+ - **The generated Commands section and `rails_onboard`'s Getting Started name
312
+ only commands the app has**, from one decision: `bin/dev` where it exists,
313
+ otherwise `bin/rails server`, and `bin/rails db:migrate` where the app has
314
+ `bin/rails` and `db/migrate`, and Getting Started builds the database from
315
+ the app's schema or migrations, never with `db:setup`, which also runs the
316
+ seeds. A private API app was told to run `bin/dev`, which it does not have.
317
+ - **The booted schema names a bigint column `bigint`**, as `db/schema.rb`
318
+ does; a private API app's `filesize` column read `integer`. A PostgreSQL
319
+ array column keeps its array flag in both tiers and in schema hints, its
320
+ default read as `[]` where the booted tier printed `{}`, a booted expression
321
+ index reads as a key list, as the dumps give it (`rails_migration_advisor`
322
+ crashed on any table that has one), and schema hints print a structure.sql
323
+ app's primary key as `id` (a composite one as `a, b`), where Discourse's
324
+ read ``(pk: `["id"]`)``.
325
+ - **Static routes read the route files `config.paths["config/routes.rb"]`
326
+ registers**, in order (an assignment, `<<`, `concat`, `Rails.root.join`, a
327
+ literal `Dir[...]` glob, kept inside the app), and name a mounted app built
328
+ by a call (`Flipper::UI.app`). OpenFoodNetwork read 69 of its 340 app
329
+ routes, and its Rswag, DfcProvider, Flipper UI and Sidekiq mounts were
330
+ missing. A file pulled in by `draw` is read inside the scope its `draw` sits
331
+ in, and again under each scope that draws it: Forem's `draw :api` inside
332
+ `namespace :api` put `/articles/search` under the web `articles` controller
333
+ without `/api`, and its v0 routes were dropped. `match ... via:` is one
334
+ route answering each verb it names (`GET|POST`, `ANY` for `via: :all`, a
335
+ `via:` from an enclosing scope too), where static routes dropped it. Routes
336
+ take the controller from `scope(controller:)` or their own `controller:`
337
+ (Canvas read 297 made-up controllers such as `accounts/:account_id`), routes
338
+ drawn through an app class (`ApiRouteSet::V1.draw(self)`) take the path and
339
+ name prefixes it returns (Canvas's 971 `/api/v1` routes), a path under an
340
+ optional scope is written as Rails normalizes it, and a route name another
341
+ file already took is left off, as Rails does; a singular `resource` names
342
+ its show route and a root route takes its `as:` as Rails does, an engine
343
+ lists what it could not expand and every path it is mounted at, a controller
344
+ filter lists engine rows like the app's own, and every route count says how
345
+ many more routes sit in engine tables.
346
+ - **The auth section's CORS flag is the api section's decision** (a
347
+ commented-out `cors.rb` is not configured in either), so `.ai-context.json`
348
+ no longer says OpenProject has no CORS beside its `rack-cors.rb`.
349
+ - **The locale note counts every in-repo gem, engine or plugin with
350
+ `config/locales`**, those without `app/` too (Discourse: 46 directories,
351
+ where it said 28).
352
+ - **The booted database and connection-pool sections list replicas**
353
+ (`configs_for` with `include_hidden: true`), as the static tier does.
354
+ - **`rails_security_scan` and `doctor` read the app's `Gemfile.lock` before
355
+ saying its bundle lacks brakeman.** `doctor` checks every file the tools
356
+ refuse, names each one it finds, fails on one never committed on purpose
357
+ that is not ignored and warns on the rest.
358
+ - **`rails_validate` and `rails_generate_test` decide whether a controller has
359
+ an action with one reader**, over its ancestors, included modules,
360
+ `define_method` names and templates at every ancestor's view prefix; the
361
+ route-to-action check calls an action missing only when the app's source
362
+ holds every ancestor and mixin, and otherwise names the unread gem or
363
+ ancestor and says the action is unverified; a namespaced controller is
364
+ checked at all, and a route to a method ending in `?` or `!` finds it
365
+ (Mastodon's `requests#merged?` was flagged).
366
+ - **An engine's root, an initializer's source and an autoload directory in the
367
+ context are app-relative, or the gem and version**, the gem's own
368
+ initializers and autoload directories are left out, and is left out rather
369
+ than written as an absolute path from the generating machine.
370
+ - **Routes an app draws into an engine (`Spree::Core::Engine.routes.draw`) are
371
+ listed with that engine**, under the path the app mounts it at and named
372
+ through its route proxy (`spree.admin_orders_path`, after the mount's `as:`
373
+ or the engine's `engine_name`), and are not in the app's route count on
374
+ either tier. Booted, the listing is each mounted engine's whole table, read
375
+ under its mount the way `bin/rails routes` prints it; statically, it is what
376
+ the app's route files draw into the engine. Controller actions, route-helper
377
+ validation and generated tests read these routes too.
378
+ `rails_get_routes(controller: "spree/admin/orders")` answers with OFN's 15
379
+ such routes, a short name such as `orders` lists them beside the app's own
380
+ matches, and the listing names the 219 it draws into Spree apart, PATCH and
381
+ PUT merged as in the app count.
382
+ - **A mounted app is listed once per app and path, with its path** (the
383
+ compact `Mounted:` line names each app once, with its path count, before its
384
+ five-name cap), when the mount sits in a `scope path: nil` or in both
385
+ branches of an `if`: Discourse's "Mounted Apps (4)" listed `Sidekiq::Web`
386
+ and `Logster::Web` twice each with no path.
387
+ - **`rails_get_env` and the context file's `env` section read ENV names from
388
+ one scan**, and a default is redacted by the source rule, so an empty one,
389
+ an address, a URL or a hostname prints as written where it read
390
+ `[FILTERED]`, which includes the ERB in `config/database.yml` and every
391
+ other `config/**/*.yml`, skipping `<%#` comments. Config YAML on
392
+ `sensitive_patterns` is read for the names in its ERB tags only, never a
393
+ value or a default; credentials and keys are still never opened.
394
+ `rails_get_env` gains 19 names on a private API app, 13 on Mastodon
395
+ and 9 on Huginn (its `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_SOCKET`).
396
+ - **A connected app with no tables yet is described as connected**, where the
397
+ schema note said "no DB connection, no schema.rb".
398
+ - **`rails_security_scan` reports brakeman's error message**, where it quoted
399
+ the last backtrace frame.
400
+ - **A commented-out Gemfile line is not a gem** wherever the Gemfile is read:
401
+ `rails_get_env`'s services, `rails_get_helper_methods` and the Mongoid check
402
+ read it through one AST reader.
403
+ - **A loaded engine's model count counts models**, by the rule the models
404
+ section uses, where it counted every `.rb` under the engine's `app/models`:
405
+ Active Storage read 15 and has 3, Solid Queue read 26 and has 11.
406
+
407
+ ### Fixed
408
+
409
+ - **The static tier lists the filters a controller's concerns declare.** An
410
+ `include` adds its concern's `before_action`s where the line stands, and
411
+ each prints the concern it came from (`_(from Localized via
412
+ ApplicationController)_`), in `rails_get_controllers`,
413
+ `rails_analyze_feature` and the generated "Global before_actions".
414
+ Mastodon's `AboutController` listed none of `WebAppControllerConcern`'s
415
+ four filters or the `set_locale` its parent's `Localized` wraps every
416
+ action in, and `FollowerAccountsController` had no `set_account`. An
417
+ included module whose file is not in the app is named as not read.
418
+ - **An included module is found in any `app/*` directory**, as Rails autoloads
419
+ it, for a model as for a controller: Consul's `Budget::Investment` carries
420
+ the `after_save` its `Budget::Reclassification` declares, OpenProject's
421
+ `WorkPackage`, which read 0 validations and 17 unread concerns, reads its
422
+ `app/models/work_package/validations.rb`, and a model that includes a helper
423
+ from `app/helpers` no longer reports it as not read.
424
+ - **A partial index prints its WHERE clause.** The static tier carries the
425
+ `where:` of a schema.rb index, the WHERE of a structure.sql `CREATE INDEX`
426
+ and the `where:` a replayed migration passes, and `rails_get_schema` and
427
+ `.claude/rules` print it: Forem's `canonical_url` reads `[unique where
428
+ (published IS TRUE)]` and Discourse's `unique_index_categories_on_slug` ends
429
+ `where ((slug)::text <> ''::text)`, where both read as unique over the whole
430
+ table; a column two partial unique indexes cover shows each condition.
431
+ - **"Missing FK Indexes" reports a column only when an association or a
432
+ foreign key uses it.** Any `*_id` column whose type matched some table's
433
+ primary key counted, so an app with one string key reported every external
434
+ id: a private API app listed 111, among them `users.stripe_customer_id` and
435
+ integer ids other services assign. A column now counts when an association
436
+ names it (a `belongs_to`, a `has_many` or `has_one` from the other side, or
437
+ a `has_and_belongs_to_many` join table, named the way Rails names it), when
438
+ an `add_foreign_key` names it in schema.rb, structure.sql or the migrations,
439
+ or when `acts_as_tree`, `acts_as_nested_set` or `has_closure_tree` declares
440
+ it as the parent key. The associations are the ones the models section
441
+ lists, so a model's concerns and inherited bases count, and so do models
442
+ that subclass `ActiveRecord::Base`. Against 5.29.0: a private API app lists
443
+ 6 where it listed 111, all six real; Mastodon 16 where it listed 20;
444
+ Diaspora 15 where it listed 13; OpenProject 43 where it listed 36; Discourse
445
+ 166 where it listed 2, because its models subclass `ActiveRecord::Base` and
446
+ none were read before. `rails_performance_check` and the "Performance: N
447
+ issues detected" line in the generated files follow (135 to 30 on a private
448
+ API app). A `has_and_belongs_to_many` record in the models section now
449
+ carries a declared `join_table` and `association_foreign_key` on both tiers,
450
+ so a custom join table is read by name booted as well as static.
451
+ - **The gem's own warnings never reach the JSON-RPC stream.** While the stdio
452
+ transport owns stdout, including while live reload starts, a warning goes to
453
+ stderr rather than `Rails.logger`, which an app may have pointed at stdout.
454
+ - **CLAUDE.md, AGENTS.md and .cursorrules keep the blank lines between
455
+ sections.** The line cap split every line on newlines, and
456
+ `"".split("\n", -1)` is `[]`, so each blank separator vanished.
457
+ - **`rails_validate` no longer pins the whole app's missing foreign-key
458
+ indexes on one file.** At `level: "rails"`, a model file it could not map to
459
+ a model matched every finding of a check that filtered on a key the
460
+ performance introspector never emits, so that check is gone; it never fired
461
+ for a mapped model. The check that reports a model's own missing
462
+ foreign-key index is unchanged.
463
+ - **`rails_get_stimulus` lists HAML and Slim views under "Used in views".** It
464
+ only ever globbed ERB.
465
+ - **`multi_database.replicas` lists the `replica: true` entries of
466
+ `config/database.yml` when the app is not booted**, where it was always
467
+ empty.
468
+ - **A partial is a template.** `views.partials` listed any file or directory
469
+ under `app/views` whose name began with an underscore (an image named
470
+ `_logo.png` was a partial), and `rails_get_partial_interface` offered the
471
+ same names as available partials.
472
+ - **A custom middleware reports the class its file is named for.** A helper
473
+ class nested in it or declared beside it could answer `has_call_method` and
474
+ `initializes_app` in its place.
475
+ - **An escaped symbol reads as the characters it names.** A symbol written
476
+ `:"first\tname"` was reported as its raw source text, backslash and all:
477
+ column, index and table names, enum names, `attribute` names and types,
478
+ callback, association and scope names, keyword option keys and values,
479
+ `params.require` keys, permitted strong-params keys, `action_name ==` filter
480
+ conditions, CORS origins and resources, model constant arrays, route
481
+ `namespace` and `scope` segments, and rake task names and their arguments.
482
+ - **`config/database.yml` is read as YAML in the static tier.** It was scanned
483
+ line by line, and the scan disagreed with the file format on every shape
484
+ that is not the generated one: an entry sharing its adapter through `<<:
485
+ *default` came back with only its name, a database called `replica` (as
486
+ Mastodon names its read replica) was read as a config key of the one before
487
+ it, so the primary was labelled the replica and the app read as
488
+ single-database, and an env given only a `url:` listed no database at all.
489
+ The file is now parsed by `YAML` with aliases on, after its ERB tags are
490
+ replaced without being run, and the databases come out of the env hash by
491
+ the same rule Rails applies: every value a Hash means one database per key,
492
+ anything else is a single primary. An adapter the file computes in ERB is
493
+ reported as the literal the ERB falls back to, labelled so (`MySQL by
494
+ database.yml default`), or as unknown when there is none; it is never picked
495
+ from the Gemfile when the app bundles more than one adapter gem, as Huginn
496
+ does. Without a committed `config/database.yml`, the adapter is read from
497
+ the schema first, a structure.sql dialect and then column types only one
498
+ database has (`jsonb`, arrays, `hstore`; `mediumtext`), then from the app's
499
+ `database.yml.*` example files, then the Gemfile, so Canvas reads PostgreSQL
500
+ where it read `PostgreSQL or SQLite`; `postgis` reads as PostgreSQL, and
501
+ when nothing decides, the reason names only the files the app has. With
502
+ nothing to decide between them, the answer names the candidates (`MySQL or
503
+ SQLite`, and in `rails_get_schema` `MySQL or SQLite, database.yml does not
504
+ say which`). A file that is not valid YAML reports no databases instead of a
505
+ misreading. A boot that fails part way now falls back to the file for the
506
+ database and replica lists, where it reported none. By the same rule, an env
507
+ that mixes plain keys with a nested `secondary:` hash is one database, as
508
+ Rails reads it, so that nested entry is no longer reported as a replica.
509
+ - **An association reads the options of a `with_options` block around it.**
510
+ The static tier ignored the block, so `rails_get_model_details`,
511
+ `rails_dependency_graph` and `.ai-context.json` named classes the app does
512
+ not have (Mastodon's `EmailDomainBlock` parent and child read as `Parent`
513
+ and `Child`, `Appeal`'s reviewers as `ApprovedByAccount`), and
514
+ `rails_validate` reported a `has_many` inside `with_options dependent:
515
+ :destroy` as missing `:dependent`; Mastodon's `Account` drew 42 such
516
+ warnings. It follows ActiveSupport: a block without a parameter passes its
517
+ options to the associations inside it, a block with one (`do |assoc|
518
+ assoc.has_many ... end`) passes them only to the calls on that parameter,
519
+ and the association's own options still win over the block's.
520
+ - **`rails_validate` compares an escaped symbol by its real characters.** A
521
+ `validates`, `permit` or callback symbol written with an escape no longer
522
+ reads as a missing column.
523
+ - **`rails_get_gems` names a config file only when the app has one**, and
524
+ finds an initializer with a load-order prefix
525
+ (`config/initializers/3_omniauth.rb`, `009-omniauth.rb`). It printed
526
+ `config/initializers/omniauth.rb`, `config/initializers/redis.rb` and
527
+ `config/storage.yml` whether or not they existed.
528
+ - **`rails_get_service_pattern` leaves concerns out.** A module under a
529
+ `concerns` directory at any depth of a services root, or one that extends
530
+ `ActiveSupport::Concern`, was counted and listed as a service object,
531
+ while `rails_get_concern` already listed it as a concern; the generated
532
+ `Services:` line follows.
533
+ - **When no services directory is found, the answer names every place the scan
534
+ reads**, in-repo engines, plugins, modules and `extra_app_paths` included;
535
+ it named only `app/services`, packs and engines.
536
+ - **`rails_search_code` hides a generated file the same way on both
537
+ backends.** Without ripgrep, the Ruby fallback matched only the exact paths
538
+ the installer writes, so a nested `AGENTS.md`, `CLAUDE.md`, `.cursorrules`,
539
+ `opencode.json` or `.ai-context.json` that ripgrep hid showed up in results.
540
+ - **A HAML or Ruby-hash `data-controller` names its controllers.** A view
541
+ written `%div{ "data-controller" => "hello" }` carried no Stimulus
542
+ controller in `rails_get_view` or in the cross-controller composition list,
543
+ while the `data-controller="hello"` spelling did.
544
+ - **A view symlinked out of `app/views` stays out of the turbo payload.** The
545
+ frame and stream scans already skipped one; `permanent_elements`,
546
+ `turbo_drive_settings` and the Turbo Native conditionals counted it.
547
+
548
+ - **A `retry_on` or `discard_on` prints as one entry with its options**, in
549
+ the same words wherever a job or worker is listed, asked for by name, or
550
+ read from `.ai-context.json`, even when it is written across lines. The
551
+ payload's job and worker records carry one `retries` list in place of
552
+ `retry_on` and `discard_on`.
553
+ - **`config/sidekiq.yml` is read as YAML**, so a path named in a comment and
554
+ an environment key are no longer listed as queues (OpenFoodNetwork read
555
+ `default, mailers, config`), and a weighted queue (`[critical, 8]`) is
556
+ named by its queue.
557
+ - **A page past the end of a list says only that.** `rails_get_i18n` and
558
+ `rails_get_mailers` asked for such a page said the app had no locale files
559
+ or no mailers, beside the count that proved otherwise.
560
+ - **`rails-ai-context tool env_config --environment production` shows that
561
+ environment.** The binary took the flag as `RAILS_ENV` only, so the tool
562
+ named it as current and listed every environment. After the name of a tool
563
+ that declares an `environment` parameter, the flag is now that tool's filter
564
+ and leaves `RAILS_ENV` alone, so a booted app is not booted into an
565
+ environment it has no database for (`--environment prod` exited 1 with "The
566
+ `prod` database is not configured" and now answers "Did you mean
567
+ 'production'?"); `RAILS_ENV` stays settable through the variable, or with
568
+ `--environment` before the command name.
569
+ - **The route total agrees across surfaces.** `.ai-context.json` and the
570
+ generated files counted one route twice where two controllers answer on
571
+ the same path and action (Forem's two API versions), so they read 769
572
+ beside `rails_get_routes`' 765 plus 3 framework routes.
573
+ - **A share short of the whole never prints as 100%.** Translation coverage,
574
+ the doctor readiness score, connection-pool use and the cache hit rate are
575
+ floored, so a locale missing one key reads `99.9%` where it read `100.0%`
576
+ beside the line naming the missing key.
577
+ - **An authorization library is named only when the app bundles it.** A
578
+ bare `app/policies` directory reads "policies in app/policies" with the
579
+ policy classes listed, and an `Ability` class without cancancan is named
580
+ as that file, where OpenProject, which bundles neither, read
581
+ `Auth: Pundit`.
582
+ - **A Brakeman finding from `rails_validate` prints under the file it
583
+ belongs to**, where every finding landed under the last file listed.
584
+ - **The ActiveStorage stack line counts models and attachments apart** (`14
585
+ models with 26 attachments` on a private API app, which read `26 models with
586
+ attachments`).
587
+ - **`doctor` finds the brakeman `rails_security_scan` runs**, including one
588
+ installed outside the app's bundle, where it said brakeman was missing
589
+ beside a scan that ran.
590
+ - **`rails_get_config`'s middleware section splits the app's own classes
591
+ from what it adds to the stack**, the way the generated files do, where it
592
+ listed every gem middleware as custom.
593
+ - **The static schema reads the migrations it was missing.** The replay
594
+ follows the files a migration requires under its own `db/`, an in-repo
595
+ engine's included (`require`, `require_relative`, `Dir[...]`, with
596
+ `__dir__`, `File.join`, `File.expand_path` and `Rails.root` read as literal
597
+ paths, never outside a `db/` directory) and replays every `db/migrate` the
598
+ repository owns, its in-repo engines' included. A `create_table` that names
599
+ its table through its class takes the file's name when a class declared
600
+ there agrees (an acronym the app inflects, such as `OAuth`, included), a
601
+ call to a `create_table` wrapper the migrations define is read as one, and
602
+ an index the migration leaves unnamed takes the name Rails gives it. A
603
+ reference (`t.references`, `t.belongs_to`, `add_reference`) gets the index
604
+ Rails gives it and a polymorphic one its type column, a column declared with
605
+ `index:` gets its index, an explicit `type:` gives the reference its type,
606
+ and `rename_column` and `rename_table` rename the indexes Rails named for
607
+ them. A reference's index follows the Rails version its migration names (a
608
+ polymorphic pair's name changed in 6.1; a 4.2 migration adds none unless
609
+ asked), and a column whose name is computed is left out and counted. The
610
+ replay reads `remove_reference`, `remove_index`, `remove_foreign_key` and
611
+ `remove_columns` by Rails' own lookup (a dropped column takes its indexes
612
+ and foreign key), applies `change_table` to the table it names, where a
613
+ column it added could land on the last table the file created, replays a
614
+ migration's private methods where `up` or `change` calls them, reads
615
+ `db/post_migrate`, and replays a helper from an included module when its
616
+ body is one schema statement, counting any other helper call (and a class
617
+ method on the app's own constants, such as a column dropper) in the note. A
618
+ positional `change_column_default :t, :col, value` is read as well as the
619
+ `from:`/`to:` form. `create_join_table` called on the migration object a
620
+ table file is given is replayed; `t.timestamps` and `add_timestamps` keep
621
+ `null:` and `default:`, a `create_table` block's `t.timestamps index: true`
622
+ gives Rails' two indexes, and timestamps allow NULL in a migration older
623
+ than 5.0; a proc default reads as its source, as schema.rb and structure.sql
624
+ give it. `rails_get_schema` prints where the answer came from at every
625
+ detail level, including how many names were read from files and how many
626
+ `create_table` calls stayed unnamed. OpenProject goes from 37 tables and 39
627
+ indexes to 208 and 556, and `rails_get_schema(table: "work_packages")`
628
+ answers where it said the table did not exist.
629
+ - **A route target with a leading slash is absolute**, as Rails reads it:
630
+ OpenProject's `controller: "/admin/settings/..."` inside its admin
631
+ namespaces read `admin/settings/admin/settings/...`.
632
+ - **`app/concerns` is read as a concerns directory**, in the root tree, packs
633
+ and in-repo engines, as Rails autoloads it. Huginn keeps its model concerns
634
+ there and read 1 concern where it has 28, and `rails_get_model_details`
635
+ called 8 of `Agent`'s concerns unread; `rails_get_concern(type: "other")`
636
+ lists them. `type:` takes every type the listing prints, in either spelling
637
+ (`service`, `LibStatic`), and only types that hold a concern are listed, and
638
+ a type the app lacks is answered with the ones it has, where the CLI dropped
639
+ such a value and listed every concern. A module a concern declares inside
640
+ itself and mixes in (`DryRunnable::Wrapper`) is looked up from the enclosing
641
+ namespace outward, and a mixin in an autoload root the app declares
642
+ (`autoload_lib`, `autoload_paths`) is read like any concern, so Discourse's
643
+ `RateLimiter::OnCreateRecord` adds its callbacks to `Post` and `Topic`. A
644
+ macro, callback or mixin inside a method of such a module applies only to
645
+ the classes that call that method, in their own body or through a concern's
646
+ `included do` block, as at runtime: every OpenProject model that includes
647
+ the module holding `acts_as_watchable` would otherwise show `watchers` and
648
+ `favorites`. The macros are read with the call's literal arguments (a
649
+ parameter the method reassigns or a block shadows keeps its own value),
650
+ taking the `if`, `unless` or `case` branch they decide (a private API app's
651
+ own macro called with `:picture, has_one: true` gives `has_one :picture`,
652
+ not a `name` pair); a declaration only a condition the literals cannot
653
+ decide holds is not listed or counted, and is named with its condition under
654
+ "Only under a condition the source does not decide" (OpenProject's
655
+ `WorkPackage` showed a `has_many custom_comments` it does not have). A
656
+ called method the reader cannot read costs only itself, named as unread; a
657
+ literal hash argument binds as keywords (`validates_translation :summary,
658
+ presence: true`) and is read through `merge`, `slice`, `except` and
659
+ `reject`, so Consul's translated validations read as `presence` and `length`
660
+ under their condition, an unused `options = {}` no longer swallows the next
661
+ declaration, a parameter reassigned in a block keeps its own value, and what
662
+ a macro method declares inside a block it evaluates on another receiver
663
+ (`translation_class.instance_eval { validates ... }`) is named under
664
+ "Declared on another class" with the condition it runs under, and not
665
+ counted as the model's (Consul's `Proposal` read 20 validation lines, 10 of
666
+ them its translation class's). What a mixin hook declares (`def
667
+ self.included(base)` and the like, in `class_eval`, as `base.has_many`,
668
+ `base.scope` or `base.delegate`, or through a helper it calls) applies to
669
+ every includer, each hook only for its own way of mixing in (`included` on
670
+ include, `prepended` on prepend, `extended` on extend). A module the app
671
+ mixes into `ActiveRecord::Base` or `ApplicationRecord` from an initializer,
672
+ `lib`, `lib_static` or a plugin's `init.rb` (a reopened class,
673
+ `send(:include)`, an `on_load` hook, a required file) is every model's
674
+ mixin, read again on each run so `watch` and the MCP server see one added
675
+ later, a model's own `extend` of an app module is read the same way
676
+ (Canvas's `resolves_root_account` gives `belongs_to :root_account`), a
677
+ module the model's own file declares is not reported unread (OpenProject's
678
+ `WorkPackage` gains `journals`, `attachments` and `custom_values`), a mixin
679
+ in a path gem inside the repo is read like the app's own code (Canvas's
680
+ `gems/broadcast_policy` and `gems/workflow`), and one from a gem installed
681
+ outside the repo is named as unread. One rule answers which classes mix in a
682
+ module, for concerns, services and join tables alike, and a subclass
683
+ inherits what its parent's concerns call into (OpenProject's
684
+ `WorkPackage::InexistentWorkPackage` lacked the `journals` its parent has).
685
+ - **Index hints follow how an index is used**, their clauses parted by
686
+ semicolons so a clause listing several columns reads as one, each clause
687
+ printed once. A column is `[indexed]` only when it leads an index and
688
+ `[unique]` only when a unique index covers it alone; one that trails another
689
+ key reads `[in index after x]` and one in a composite unique index `[unique
690
+ with <partners>]`, each partner set once. Each column of a composite unique
691
+ index read `[unique]` and every column of a composite index `[indexed]`.
692
+ `rails_performance_check` uses the same rule `rails_validate` does, so a
693
+ foreign key that only trails another column is reported as unindexed and a
694
+ polymorphic pair must lead an index together, and `rails_migration_advisor`
695
+ warns of a duplicate index only when the name `add_index` would generate
696
+ exists.
697
+ - **The `structure.sql` reader keeps an expression index key whole** and the
698
+ keys after it, and no longer reads `DESC`, `NULLS LAST` or an operator
699
+ class as a column: Discourse's `(COALESCE(parent_category_id,
700
+ '-1'::integer), name)` read as four columns with `name` lost. It also
701
+ reads each column's default, the way the `schema.rb` reader reports one
702
+ (a literal as its value, a cast string as the string, json and arrays as
703
+ Rails dumps them, an expression as `-> { "CURRENT_TIMESTAMP" }`, a serial
704
+ key's `nextval` as none): Discourse's schema had no default hints at all
705
+ and now has 583.
706
+ - **Both search backends return the same lines.** The Ruby fallback matched
707
+ case-insensitively, searched only a list of extensions and read
708
+ `excluded_paths` as path prefixes, so on a private API app one model name
709
+ gave 6653 lines with ripgrep and 50113 without it. It now searches every
710
+ non-hidden, non-binary file, case-sensitively, and reads `excluded_paths` as
711
+ gitignore patterns; `search_extensions` defaults to unset and, when set,
712
+ still narrows the fallback.
713
+ - **The Ruby search fallback reads what ripgrep reads, and fast.** It reads
714
+ the ignore files of every directory above the app (the git ones as far as
715
+ the nearest `.git`, a worktree's or submodule's `.git` file included),
716
+ matches ignore patterns case-sensitively, leaves symlinks alone, walks the
717
+ tree once without entering an ignored directory, and decides a binary file
718
+ from its first 64 KiB: 14.5 s to 2.5 s on a private API app and 27.5 s to
719
+ 8.6 s on Canvas, with the same lines ripgrep returns. A ripgrep match in a
720
+ file that is not UTF-8 is reported as a match, where it came back as one
721
+ fake `error:0: invalid byte sequence` row.
722
+ - **`rails_search_code` never answers "No matches" while matches exist.** A
723
+ `limit` smaller than one match and its context lines held only context, so
724
+ the page came back empty; it now starts at the next match and says so.
725
+ - **A standalone install lets the app's `Gemfile.lock` win.** An app locking
726
+ an older `json` than the one this gem's dependencies pulled in failed to
727
+ boot and fell back to the static tier. Every gem the binstub activated for
728
+ itself now leaves `$LOAD_PATH` before `Bundler.setup` and comes back behind
729
+ the app's gems but ahead of Ruby's own lib directories, so a gem with a
730
+ default-gem twin (prism, json) never loads half from each; after a boot that
731
+ fails before the app's bundle is set up, the load path goes back to its
732
+ pre-boot order, and after one that fails later, such as an initializer that
733
+ raises, the app's bundle stays in front, so ActiveSupport finishes loading
734
+ and mcp never loads from two versions (every tool and the MCP server
735
+ answered with a load error there). When the app locks one of this gem's
736
+ dependencies at a version outside its requirement, a warning names it. A
737
+ standalone install no longer points at commands it does not have: `doctor`'s
738
+ fixes, the "to regenerate" line of the full-mode context files and the
739
+ `session_context` note name `rails-ai-context context`, `init` and `tool`
740
+ there, and `rails ai:context`, the install generator and `rails ai:tool`
741
+ where the app bundles the gem. `tool --list` honours `--no-boot`, and the
742
+ CLI no longer warns `could not restore gemspec(s): json-schema` on every
743
+ run: it re-registers the gemspecs it activated for itself rather than a
744
+ hand-kept list.
745
+ - **The test framework is read from the suite the app has**: Ruby spec and
746
+ test files, their helpers and `spec/factories`, not from whether a `spec/`
747
+ directory exists. With no tests anywhere, the answer says so and names the
748
+ framework the bundle holds (`no tests yet (rspec-rails in the bundle)`),
749
+ where it said the app ran rspec or minitest. Whitehall's and Plots2's
750
+ `spec/` hold only JavaScript tests, so minitest apps read as RSpec and
751
+ `rails_generate_test` wrote them an RSpec spec. `doctor` reads the same
752
+ answer (`minitest test suite found`, or `No test suite found` where it said
753
+ `No test directory found`), and an app that runs both is told to run `bundle
754
+ exec rspec` where it was told `rails test`.
755
+ - **Tests are found and written where the app keeps them.**
756
+ `rails_get_test_info(model:)` and `(controller:)` look in the directories
757
+ the app's tests live in, so Whitehall's `test/functional` and
758
+ `test/unit/app/models` tests are found, where the tool said "No test file
759
+ found" for files its own no-argument answer counted, and
760
+ `rails_generate_test` writes into the directory most of the app's model or
761
+ controller tests share, under the file-name suffix most of them use, falling
762
+ back to the convention when there are none, and names the subject's existing
763
+ test rather than writing a second one. A controller's lookup also lists the
764
+ system, feature, request and integration tests named for it or reaching its
765
+ routes, each labelled by kind (Mastodon's `AboutController` said "No test
766
+ file found" beside `spec/system/about_spec.rb`). A model's tests are looked
767
+ for only where the app keeps model tests: a private API app's
768
+ `rails_get_test_info(model: "User")` answered with a serializer spec sharing
769
+ the basename, where its model spec is `spec/models/user_model_spec.rb`. A
770
+ generated controller test asserts what each action answers, `create`,
771
+ `update` and `destroy` included, `respond_with` read by format and verb as
772
+ the responder answers and an early bare `return` counted as an answer (an
773
+ API `create` rendering `status: :created` or a `head :no_content` destroy no
774
+ longer gets the scaffold's redirect), read from its body and its
775
+ `respond_to` block for the format the test sends, the branch valid params
776
+ take where a save splits it (a redirect as `:redirect` with its target when
777
+ the test can name it, `head` as its status, a JSON or plain render with its
778
+ media type, a conditional or unreadable action only as not a server error,
779
+ with a comment saying why), where it asserted `:success` for Plots2's
780
+ redirecting `verify_email`. A controller under a gem's controller (Devise)
781
+ keeps its routed actions, unverified; RSpec controller specs include
782
+ `Devise::Test::ControllerHelpers` (Devise setup in `spec/support` counts), a
783
+ test of a Devise controller sets `devise.mapping` from Devise's route names,
784
+ a `Post` model gets `let(:post_record)` so the `post` request method is not
785
+ shadowed, a destroy test builds its record before reading its id, a redirect
786
+ target is asserted only when it names a route, and `rails_generate_test`
787
+ looks for an existing test where `rails_get_test_info` does. A namespaced
788
+ model is built from the controller's namespace outward (OpenFoodNetwork's
789
+ `LineItemsController` got a skip), an `on:` validation is checked in its
790
+ context and a conditioned one is skipped with its condition named, and a
791
+ glob segment is passed. It covers only the actions the controller has (its
792
+ own and inherited public actions, template-backed ones, and names a
793
+ `define_method` or a concern may build), naming each routed action it leaves
794
+ out, so a `resources` route no longer gets a `show` test for a controller
795
+ with no `show`. It passes every required path segment and builds its record
796
+ under its route parent (Whitehall's contacts under organisations raised
797
+ `UrlGenerationError`), leaves optional segments out, fills a custom segment
798
+ from the column of the same name on the record or its parent (Plots2's
799
+ `graph/file/:uid/:id` gets `uid: @csvfile.uid`), and skips with the segment
800
+ named when it cannot fill one; one written among an app's controller specs
801
+ is a `type: :controller` spec. A generated minitest controller test uses the
802
+ test class and data style (factories or fixtures) most of the app's
803
+ controller tests use, each test's superclass read through Prism and followed
804
+ through an app base class in the test tree (`::ActionController::TestCase`
805
+ counts), and a model's fixtures are found under its pluralized class name or
806
+ its `set_fixture_class` mapping (read from the test and spec helpers and
807
+ support files), a namespaced model's under the names Rails gives them
808
+ (`admin_users(:one)`, where it wrote `admin/users(:one)`): Plots2's `Node`,
809
+ whose table is `node`, got a TODO beside a `nodes.yml`.
810
+ - **`rails_get_test_info` counts the examples a test file runs, and lists
811
+ exactly those** (`it`, `specify`, `scenario`, `test`, `def test_*`,
812
+ one-liners too, never a `describe` or `context` line: a private API app's
813
+ `User` spec read 283 tests and runs 136), `rails_onboard` counts factory
814
+ definitions and the files they sit in ("203 factories in 134 files", a floor
815
+ marked "+" where a name is computed in a loop), and a fixtures directory
816
+ that also holds `file_fixture` files reads "1 YAML fixture set, 485 other
817
+ files" where it read "1 file".
818
+ - **`rails_get_test_info` renders each factory file's traits as a list item**
819
+ (`- **users.rb:** admin, with_posts`), where it printed a Ruby array.
820
+ - **The declared Ruby version falls back to `.ruby-version` and the `ruby`
821
+ line of `.tool-versions`** when neither the lockfile nor the Gemfile names
822
+ one, and the gems payload says which source answered
823
+ (`declared_ruby_version_source`).
824
+ - **The Rails version is read from `railties`** when an app bundles it without
825
+ the `rails` gem, as Discourse does.
826
+ - **`--no-boot` reads the `config/database.yml` section for `RAILS_ENV` or
827
+ `RACK_ENV`**, where it always read `development`.
828
+ - **Code in an in-repo engine, plugin or module is read like `app/`**:
829
+ models, controllers, views, concerns, services, jobs, mailers, helpers and
830
+ serializers under `plugins/*`, `modules/*` and `gems/plugins/*`. A view
831
+ there reaches `rails_get_view`, `rails_get_partial_interface`,
832
+ `rails_get_turbo_map` and the view lookups, and can be read by path
833
+ (OpenProject: 318 to 492 templates). A directory
834
+ counts when it has its own `app/` with a Rails directory in it and a
835
+ gemspec, a `plugin.rb` or an `engine.rb` says Ruby loads it. Discourse goes
836
+ from 223 to 359 models and OpenProject from 169 to 274, and
837
+ `rails_get_schema` stops calling a plugin's table model-less and pointing at
838
+ the Gemfile.
839
+ - **A model reads the table prefix its namespace gives it**, from a `def
840
+ self.table_name_prefix` on the namespace or an engine's `isolate_namespace`,
841
+ as Rails' `full_table_name_prefix` does; an explicit `self.table_name =`
842
+ still wins, read as the literal when its only interpolations are
843
+ `table_name_prefix` and `table_name_suffix` (the class's own, else
844
+ `config.active_record`'s), and a derived name takes
845
+ `config.active_record.table_name_prefix` and `_suffix` when nothing nearer
846
+ sets one. A class nested in a concrete model takes that model's singular
847
+ table as a prefix, as Rails' `compute_table_name` does: OpenProject's
848
+ `Project::Phase` reads `project_phases` where it read `phases`, and its
849
+ `Principal`, `User`, `Group` and four more read `users` where they read
850
+ `principals`. 51 of OpenFoodNetwork's models (its Spree models and their
851
+ calculators) read `orders` where the table is `spree_orders`, and
852
+ Discourse's `DiscourseRssPolling::RssFeed` read `rss_feeds`, so
853
+ `rails_get_schema` called their tables model-less.
854
+ - **`rails_get_schema` names the gem that owns a table with no model file**,
855
+ one line per gem the lockfile or Gemfile has (good_job, doorkeeper,
856
+ friendly_id, closure_tree, paper_trail, acts-as-taggable-on, pghero, the
857
+ Solid gems, Active Storage, Action Text, Action Mailbox, noticed,
858
+ delayed_job), where it listed the table as model-less. Both lists, and the
859
+ warning for a table with no model file, cover the whole schema at every
860
+ detail level, where they covered only the current page of the standard view
861
+ (a private API app's standard view named only one table), and `format:"json"`
862
+ returns them in `tables_without_model_file` and `gem_owned_tables`. A
863
+ `has_and_belongs_to_many` `join_table:` built from the table name prefix and
864
+ suffix reads as the table it names, so a join table is not called model-less
865
+ either.
866
+ - **A migration's literal `execute("DROP TABLE ...")` drops the table from the
867
+ replayed schema**, MySQL's backtick-quoted names included. OpenProject's
868
+ `meeting_contents` and `meeting_content_journals` were listed, and four
869
+ missing-index suggestions came with them.
870
+ - **Jobs are read from `app/jobs`, `app/workers` and `app/sidekiq` alike.** An
871
+ app with its workers in the Sidekiq 7 generator directory, or its ActiveJob
872
+ classes in `app/workers`, answered "No jobs found" (Whitehall has 31,
873
+ OpenProject 103 with its modules). A class is a worker when a Sidekiq mixin
874
+ is anywhere up its chain and a job when its chain reaches ActiveJob,
875
+ wherever it lives, so a Sidekiq job under `app/jobs` shows the queue its
876
+ `sidekiq_options` names rather than the default. An abstract base, one
877
+ another job inherits from and whose name says it is a base (`Jobs::Base`,
878
+ `ApplicationWorker`), is no longer counted as a job, in either tier. A job
879
+ is named by the constant its file declares
880
+ when that names its own namespace: Discourse's read `Regular::AnonymizeUser`
881
+ for `Jobs::AnonymizeUser`.
882
+ - **A mailer is listed wherever it lives and whatever it declares.** One
883
+ outside `app/mailers` is found by its parent chain anywhere under `app/`
884
+ (Canvas keeps `Mailer < ActionMailer::Base` in `app/models` and got "_No
885
+ mailers found._"), a chain is followed through the app's autoload roots to
886
+ `ActionMailer::Base` or a gem mailer, a class under `app/mailers` whose
887
+ chain reaches no mailer is not one (Diaspora listed twelve header builders
888
+ with `Actions: set_headers`), and one with no action of its own is listed
889
+ with the parent it takes its actions from and any class methods it has
890
+ (Whitehall's `MultiNotifications`, Consul's `DeviseMailer` and Diaspora's
891
+ `DiasporaDeviseMailer`). A mailer a gem defines is not listed as the app's,
892
+ and an ActionMailer setting such as `mailer_name` is not a mailer's
893
+ interface.
894
+ - **A by-name lookup answers the name it was given, or says it has no such
895
+ thing** and suggests the closest, by Ruby's own spell checker over full
896
+ names and last segments, where it offered whatever shared the first three
897
+ characters. `rails_get_mailers(mailer:)`,
898
+ `rails_get_i18n(locale:)` and `rails_get_env_config(environment:)` matched
899
+ substrings and answered with another thing's data under the name asked for:
900
+ `Mailer` got `UserMailer`'s actions, `prod` got production, and `e` got the
901
+ first locale by name. `rails_get_routes(controller:)` answers the
902
+ controller whose trailing segments the name spells, where `users` swept in
903
+ `admin/users/roles`, and its miss message lists at most 20 controllers.
904
+ `rails_get_turbo_map(controller:)` matches whole path segments, so
905
+ `blog_posts/` views no longer count as `posts`, and
906
+ `rails_get_test_info(controller:)` no longer offers a top-level request spec
907
+ as a namespaced controller's test. A short name two namespaces share
908
+ (`ReportMailer` for `Admin::ReportMailer` and `Staff::ReportMailer`) names
909
+ both and asks for the full one.
910
+ - **The booted tier leaves out a model a gem defines**, wherever the bundle is
911
+ installed (a private app counted `PaperTrail::Version`, 139 where the static
912
+ tier said 138), its Concerns list is what the model's source includes and
913
+ what a gem macro includes into that model alone, labelled by the macro
914
+ (`devise :a, :b` read statically as Devise's modules), without the modules
915
+ gems put into every model, a booted mailer lists only the actions a mailer
916
+ class defines, conventions claim model concerns only from a concerns file
917
+ that is a module, and `rails_onboard` names Mongoid for a Mongoid app, where
918
+ Errbit read "on unknown".
919
+ - **Neither tier lists a mailer's or a controller's action callbacks,
920
+ predicates or bang methods as actions**, nor a controller method that needs
921
+ an argument, which Rails could not dispatch. The booted tier listed what
922
+ `action_methods` returns; a private API app's `BaseController` listed its own
923
+ `before_action` target and a predicate. A controller lists the actions it
924
+ inherits from every app base, whatever the base is called and however its
925
+ name is qualified (OpenProject's
926
+ `CustomFields::Hierarchy::ItemsBaseController`, written inside
927
+ `Admin::Settings`), in its actions and its filter chain; an inherited public
928
+ method that no route names for the controller is a shared helper and not an
929
+ action (paper_trail's `info_for_paper_trail` read as an action of every EF
930
+ api controller), and an unrouted base controller lists only the actions its
931
+ subclasses are routed for, a controller under `app/controllers` no route
932
+ names lists only what it defines itself, a controller lists the public
933
+ actions an included app concern offers (Whitehall's
934
+ `Admin::ContactTranslationsController` lacked `create`, `update` and
935
+ `destroy`), and the booted tier drops a method that needs an argument and
936
+ settles inherited actions by the routes, as the static tier does; a routed
937
+ action with a template and no method is an action (OpenFoodNetwork's
938
+ `Admin::OrderCyclesController` lacked `incoming`, `outgoing` and
939
+ `checkout_options`); and a file under `app/controllers` not named
940
+ `*_controller.rb` is a controller only when it subclasses one, so
941
+ OpenProject's Grape API classes stay out.
942
+ - **A Stimulus controller outside `app/javascript/controllers` is found**:
943
+ under `app/webpacker`, `app/frontend`, `frontend`, `client`, component
944
+ sidecars in `app/components`, and the same homes in packs and in-repo
945
+ engines and plugins, named `*_controller` or `*.controller`, in JS or TS.
946
+ Outside the rails-new home a file counts only when it imports from
947
+ `@hotwired/stimulus` or `stimulus`, or extends a class that does (an app
948
+ file reached relatively or through the app's alias table, or an installed
949
+ Stimulus package), so an AngularJS `*.controller.js`, OpenProject's Angular
950
+ `activity-base.controller.ts` or a React `*_controller.tsx` does not. Forem
951
+ read 1 of its 23, OpenFoodNetwork 0 of 73, OpenProject 0 of 161.
952
+ - **Editing a Stimulus controller outside `app/javascript/controllers`, or a
953
+ job in `app/workers` or `app/sidekiq`, invalidates the cached context**, so
954
+ tools stop answering from stale data.
955
+ - **The app's architecture lists `stimulus` and `hotwire` wherever it keeps its
956
+ Stimulus controllers.** OpenFoodNetwork and OpenProject read as having none.
957
+ A React component named `*_controller.tsx` still does not count.
958
+ - **`rails_get_stimulus` counts a view as a use only when its markup names the
959
+ controller** in a `data-controller` attribute, a target, an action or a
960
+ `content_controller`-style helper. An i18n key or a word in prose with the
961
+ same text counted.
962
+ - **`rails_get_turbo_map` says how many Turbo Stream responses it showed of
963
+ how many**, and points at `detail:"full"` for the rest, where entries
964
+ dropped off after 15 with no sign; one declared in a controller concern is
965
+ named without the `Concerns::` segment the autoload root does not carry.
966
+ - **The generated files promise Stimulus auto-registration only where
967
+ stimulus-loading does it**, for controllers in `app/javascript/controllers`;
968
+ anywhere else they say a new controller is registered the way the
969
+ existing ones are.
970
+ - **A markup comment is not markup.** A HAML `-#` comment, with the lines
971
+ indented under it, and an ERB `<%# %>` comment are no longer read for
972
+ partials, instance variables, Stimulus identifiers, slots, fields or helper
973
+ calls: OpenFoodNetwork's `create_linked_variant.turbo_stream.haml` listed a
974
+ partial named `the`, from `-# Pre-render the form`.
975
+ - **A `render` call's partial is read with or without parentheses**, wherever
976
+ its keyword sits: `render(partial: 'x')`, `render 'x'`, `render(template:
977
+ 'x')`, `render collection: @items, partial: 'x'`, the `:partial =>` form and
978
+ calls over several lines, in ERB, HAML and Slim; a JavaScript `.render(...)`
979
+ in an inline script and a partial name built at runtime are not read. A
980
+ `partial:` inside `locals:` or a component's constructor is not the call's
981
+ partial, and an escaped quote in an earlier argument no longer hides it; a
982
+ turbo frame id or stream name holding a comma inside a string is read whole,
983
+ and a Stimulus `static values` default holding a brace, a paren or a regex
984
+ literal is read whole, in source order. OpenFoodNetwork's views gained 51
985
+ partial references and Consul's 23.
986
+ - **A layout's yields are read in ERB, HAML and Slim**: the unnamed one as
987
+ `(main)`, `yield(:x)`, `(yield :x).presence` and `content_for?(:x)`, read
988
+ only where Ruby runs (ERB tags, HAML code lines and attribute hashes, Slim
989
+ code and attribute values), and a `content_for` that sets content, with a
990
+ block or a value, parenthesised or not, is not a yield. Canvas's six layouts
991
+ reported none.
992
+ - **A Stimulus action descriptor is read with its options, event filters and
993
+ neighbours** (`click->x#open:prevent`, `keydown.enter->`, `resize@window->`,
994
+ several descriptors in one `data-action`), read only from an action value (a
995
+ `data-action` attribute, an `action:` in a data hash, an action-named key or
996
+ Ruby variable such as `confirm_actions:` or `hiddenFieldAction:`), so an
997
+ apostrophe in page text no longer hides the actions after it, and prose, a
998
+ Ruby comment or a Rails route list (`%w[posts#create]`) confirms nothing,
999
+ where OpenFoodNetwork's `bulk-form` and `modal` uses were missed.
1000
+ - **`url_for(controller: "x")` is not a Stimulus controller** in view answers.
1001
+ - **`data-turbo` counts read the HAML and Ruby-hash spellings**
1002
+ (`"data-turbo": "false"`, `'data-turbo' => 'false'`), not only
1003
+ `data-turbo="false"`.
1004
+ - **A model whose `inclusion: { in: [...] }` list mixes strings with `nil` no
1005
+ longer fails** `rails_get_model_details` and `rails_context` with
1006
+ `ArgumentError: comparison of String with nil failed`. Whitehall's `Edition`
1007
+ and its 13 STI subclasses failed.
1008
+ - **A validation or callback inside `with_options` carries the block's
1009
+ options**, so a validation scoped `on: :publish` no longer reads as always
1010
+ required, and a macro sent to the block's parameter is kept.
1011
+ - **A scope written `scope :x, lambda { ... }` or with a `do ... end` block
1012
+ shows its body** and is marked verified where it was empty and `[INFERRED]`.
1013
+ Every scope body prints on one line, folded the way Ruby's lexer reads its
1014
+ newlines, a string literal written across lines respelt as the same string
1015
+ on one line: statements are separated by `;`, a call split at the dot joins
1016
+ back, and a comment inside it is left out. A body carrying a heredoc prints
1017
+ as the file wrote it.
1018
+ - **A Mongoid app counts `embeds_many` and `embeds_one` as associations
1019
+ everywhere, and lists only documents as models.** `rails_dependency_graph`
1020
+ draws their edges, and Errbit's `App` read "1 association" beside 5 edges; a
1021
+ plain Ruby class under `app/models` (Errbit's `ErrorReport` and `Issue`) is
1022
+ not a model.
1023
+ - **Both tiers list every declared validation the same way, with its
1024
+ conditions**: `validates_with`, `validates_date`, `validates_translation`
1025
+ and the rest, each with its `if:`, `unless:` and `on:`, and the presence
1026
+ validation Rails adds for a required `belongs_to`, marked `_(implicit from
1027
+ belongs_to)_` by `optional:`, `required:`, `belongs_to_required_by_default`
1028
+ (read from `config/application.rb` and every initializer) and
1029
+ `load_defaults`; an `optional:` written as an expression prints it and makes
1030
+ the implicit presence conditional on it, and a hand-written presence rule on
1031
+ the same name shows in its place. A validator no source line declares is
1032
+ listed too: the booted tier adds each reflection validator no line covers,
1033
+ marked by what added it (`has_secure_password`, `devise :validatable`, a gem
1034
+ module), and the static tier reads `has_secure_password` and `devise
1035
+ :validatable` as the validators they add, `has_secure_password`'s by the
1036
+ Rails version `Gemfile.lock` names (`length` and `confirmation` before 7.1,
1037
+ `confirmation` from 7.1, and an assumption named when the lockfile says
1038
+ none), so a private API app's `User` shows its password `confirmation`. A
1039
+ validation whose attribute is computed, as in a loop, says so, and a custom
1040
+ `validate :method` prints its `on:`, `if:` or `unless:` condition in both
1041
+ tiers. A private API app's booted `Document` dropped its `if:` conditions
1042
+ and printed its `validates_with` line as `document on`, the static tier
1043
+ left `validates_with` out (Plots2's `User` read 0 validations), and a
1044
+ hand-written presence rule on a `belongs_to` name was labelled implicit.
1045
+ - **An inheritance column set to `nil`, `false` or `:_type_disabled` reads as
1046
+ STI off**, where the conventions claimed STI. A validator option set to
1047
+ `false` (`presence: false`) is no validation, in both tiers; a class's own
1048
+ `belongs_to_required_by_default` beats the app's and passes to its
1049
+ subclasses (OpenFoodNetwork's `Subscription` showed 7 implicit presence
1050
+ lines); a computed enum prints its source marked computed, and a frozen
1051
+ literal one its values; and one `after_commit` declared for several events
1052
+ is one callback showing its events.
1053
+ - **`rails_validate` judges only the `has_many` associations the model's file
1054
+ declares**, and treats encrypted attributes (`has_encrypted`, `encrypts`,
1055
+ `attr_encrypted`) and store keys as attributes with no column of their own,
1056
+ so it no longer asks for `dependent:` on paper_trail's `has_many :versions`.
1057
+ - **`rails_dependency_graph` resolves an association's class from the owner's
1058
+ namespace outward, as Rails' `compute_type` does**, a leading `::` meaning
1059
+ top level (kept as written in both tiers), and the missing FK index check,
1060
+ both habtm join-table readers and the counter-cache check resolve it the
1061
+ same way; every tool that derives a model from a name spells it as the app
1062
+ does, acronyms included (`analyze_feature` named models that do not exist);
1063
+ two associations of one kind to one class each get an arrow labelled by
1064
+ name; and a through association that names no association of its model, or
1065
+ whose name is computed at run time, draws no node and is named in a note
1066
+ saying which: Consul's `Budget` pointed at a top-level `Investment`,
1067
+ `Ballot`, `Group` and `Heading`, and OpenFoodNetwork's `Spree::Order` at
1068
+ `Payment`, `Shipment` and `Adjustment`. It draws a through edge per join
1069
+ model, where Mastodon's `Account` lost its through `Follow`, `Block` and
1070
+ `Mute` edges behind the first one; a through association with `source_type:`
1071
+ points at the class `source_type` names; and the "pass model:" hint is left
1072
+ out when a model was passed.
1073
+ - **A block callback a concern declares is kept beside the model's own block
1074
+ callback of the same kind**: Discourse's `Topic` lost the `after_create do`
1075
+ of `RateLimiter::OnCreateRecord`.
1076
+ - **A static route honours the action its `action:` option names** (`get
1077
+ :delete, action: :deletion_dialog` read as `delete`).
1078
+ - **A component is typed by following its superclass chain through the app's
1079
+ own base classes**, so one two or more steps from `ViewComponent::Base` or a
1080
+ Phlex base is no longer "unknown", and a sidecar file that reopens its parent
1081
+ namespace is named for the class the file is named for. A component nested
1082
+ in a module is listed by its full constant (`Admin::RowComponent`, where
1083
+ three different classes all read `RowComponent`), which renames most of
1084
+ OpenProject's catalog.
1085
+ - **A concern that nests its own `ActiveModel::Validator` is a concern**, not
1086
+ a validator, and every surface that lists concerns or helpers names each one
1087
+ by the constant its file declares, in-repo plugins included (Discourse's
1088
+ `DiscourseSubscriptions::Group` read `Group`, and
1089
+ `rails_get_helper_methods(helper: "DiscourseChatIntegration::Helper")`
1090
+ answered not found): `rails_get_active_support` read `Featurable` for
1091
+ `Edition::Featurable`, and an inflected namespace (`SDG::TagList` in
1092
+ `concerns/sdg/tag_list.rb`) is no longer spelled `Sdg::TagList`.
1093
+ - **A module that extends `ActiveSupport::Concern` is a concern wherever the
1094
+ app autoloads it from**, and the default `excluded_concerns` patterns each
1095
+ match a whole namespace, so an app module named only like a framework's
1096
+ (Whitehall's `ActiveRecordLikeInterface`) is listed, not hidden; listed with
1097
+ its path under a type naming its root (`Service Concerns`), in
1098
+ `rails_get_concern` and `rails_get_active_support` alike: OpenProject read
1099
+ 82 concerns and has 329, Canvas 9 and 26, a private API app none and 2.
1100
+ - **A concern's includes and methods are its own body's**, not those of
1101
+ classes nested in its file: Huginn's `LiquidDroppable` read `Includes:
1102
+ Enumerable` and its nested `Drop` classes' `to_s`, `each` and `as_json`, and
1103
+ `Agents::ImapFolderAgent` was credited with `Scrubbed`. A concern whose
1104
+ methods are all private reads `0 public methods (N private)` and lists them,
1105
+ where it read `0 methods`.
1106
+ - **A concern's "Included By" lists the classes whose `include` resolves to
1107
+ it** from their own namespace, each once, named by its constant, searching
1108
+ every autoload root and naming them when it finds none: Mastodon's
1109
+ `Admin::ExportControllerConcern` listed 9 includers, 7 of them including
1110
+ `Settings::ExportControllerConcern`, and Discourse listed
1111
+ `SubscriptionsController` twice.
1112
+ - **Component previews are read from the directories the config sets**
1113
+ (`view_component.previews.paths`, `preview_paths`, `preview_path`) as well
1114
+ as the defaults, and a preview links to its component by path or by the one
1115
+ component it renders: OpenProject read no component with a preview, and 39
1116
+ have one.
1117
+ - **A boot error no longer repeats in full under every answer.** The footer
1118
+ and each `[UNAVAILABLE]` note keep its first 140 characters, and the whole
1119
+ error stays in the startup banner and `doctor`. On Discourse it was 61% of
1120
+ the output.
1121
+ - **`rails_get_view` does not count a partial under `app/views/layouts` as a
1122
+ layout.** Forem read 19 layouts where it has 4.
1123
+ - **An abstract base is not counted as a service, a job or a mailer.** A class
1124
+ other classes inherit from and whose name says it is a base
1125
+ (`ApplicationService`, `BaseService`, `Jobs::Base`, `ApplicationMailer`)
1126
+ leaves `rails_get_service_pattern`, `rails_get_job_pattern`,
1127
+ `rails_get_mailers` and the generated files by one rule in both tiers, and
1128
+ one sentence above each listing names what it left out; a base answers when
1129
+ asked for by name with what it hands down, printed without the comments
1130
+ written inside a call and never counting a call inside a method as a
1131
+ declaration: a job base its queue, options, retry policy, mixins, throttle
1132
+ and callbacks, a mailer base its layout, helpers, defaults, callbacks and
1133
+ mixins as the file writes them, and either one the methods it defines and
1134
+ the classes that inherit it. Mastodon's generated files listed `BaseService`
1135
+ as a service, and Consul's and OpenProject's `ApplicationMailer` helpers
1136
+ read as emails.
1137
+ - **A class file is named by the constant it declares.** A model, job or
1138
+ service whose file declares a namespaced constant at a path that does not
1139
+ spell it is listed under that constant, where it was dropped or named from
1140
+ the path: Discourse's `DiscourseGithubPlugin::GithubCommit` and six other
1141
+ plugin models were missing, and `Jobs::DiscourseChatAddTypeField` read
1142
+ `Onceoff::AddTypeField`.
1143
+ - **A bare superclass resolves from the enclosing namespace outward**, as
1144
+ Ruby resolves it, so `module Bim::Bcf; class NonExistentComment < Comment`
1145
+ inherits `Bim::Bcf::Comment`, and a class that shares a name with a model in
1146
+ another namespace is no longer counted as a model.
1147
+ - **What a job or service hands work to is read from the call nodes**, so
1148
+ `.run`, `.perform_now`, any bang method (`GithubBadges.grant!`), a receiver
1149
+ written `::Fully::Qualified` and a call split across lines are listed under
1150
+ its calls, and a call written in a comment or a string is not.
1151
+ - **`rails_get_api` reports a CORS initializer whose origins come from a block
1152
+ or an expression**, where it said "not detected", and says when the block is
1153
+ exactly the request `Origin` header echoed back, which allows any origin. A
1154
+ `resource` path that is not a literal prints as its source. A CORS
1155
+ initializer with no `allow` block is reported with the middleware it
1156
+ inserts, or as a file whose `Rack::Cors` or `allow` block is commented out;
1157
+ a file that only has `cors` in its name is not a CORS initializer. An `else`
1158
+ branch reads "otherwise" and each `elsif` keeps its own condition, where a
1159
+ private API app's second `allow` block read `* if else Rails.env.staging?`.
1160
+ - **CORS and Rack::Attack are found under the initializer name the app gave
1161
+ them** (`rack-attack.rb`, `rack-cors.rb`, `008-rack-cors.rb`), where only
1162
+ `cors.rb` and `rack_attack.rb` were read.
1163
+ - **A notable gem's note names a config file only when the app has it.** It
1164
+ points at the file the app does have, or drops the clause naming the path
1165
+ and keeps the description; it named `config/initializers/cors.rb`,
1166
+ `app/assets/config/manifest.js` and `app/javascript/controllers/` on apps
1167
+ without them.
1168
+ - **`rails_get_api` reads Grape versions under `app/api/v*` and `lib/api/v*`**
1169
+ and names the directories each version came from.
1170
+ - **An ActiveRecord attribute coder in a `serializers/` directory is not
1171
+ reported as the app's serialization layer.**
1172
+ - **GraphQL counts leave out base classes**, judged from the class the file
1173
+ is named for rather than from a `base_` file name: a class that declares
1174
+ `graphql_name` is concrete, and a base is one others inherit from, or one
1175
+ named like a base that subclasses graphql-ruby and adds nothing to the
1176
+ schema. The enums, inputs, scalars and connections that subclass
1177
+ graphql-ruby directly still count (Canvas: 176 types, 102 mutations). An
1178
+ app with no `queries/` directory is pointed at its query root file rather
1179
+ than told "0 queries".
1180
+ - **Middleware changes are read wherever the app makes them**: through
1181
+ `Rails.configuration.middleware`, `Rails.application.middleware` and
1182
+ `app.middleware`, the app's own `MyApp::Application.config.middleware` and
1183
+ `config.middleware` (an engine's own stack, `MyEngine.config.middleware`, is
1184
+ left alone), with `insert`, `insert_before`, `insert_after`, `unshift`,
1185
+ `swap`, `move_before`, `move_after` and `delete`, in `config/application.rb`
1186
+ and `config/environments/*.rb` as well as the initializers. A class the app
1187
+ owns is listed with its file, under the name its file declares, wherever the
1188
+ app autoloads it from (`lib/middleware`, `app/lib/middlewares`) or wherever
1189
+ its config or initializers declare it with a `call` method (Mastodon's
1190
+ `TelemetryLoggingMiddleware`, Discourse's `Discourse::Cors`);
1191
+ `config.exceptions_app` is named apart from the stack; one it does not own
1192
+ is listed under "Stack changes from the app's config" as inserted, moved,
1193
+ swapped in or removed, each with the file that does it. Discourse read no
1194
+ custom middleware and 1 insertion; it has 16 classes and 27 stack changes
1195
+ (19 inserted, 2 swapped in, 1 moved, 5 removed).
1196
+ - **The autoload roots include the ones `config/application.rb` adds**
1197
+ (`autoload_paths`, `eager_load_paths`, `autoload_lib`, `config.paths.add`),
1198
+ when they are written as literal paths inside the app, so a class there is
1199
+ found as the app's own. OpenProject keeps middleware in `lib_static`.
1200
+
1201
+ ### Removed
1202
+
1203
+ - **Internal helpers nothing in the gem called**: `DetailLevel.at_least?`,
1204
+ `SchemaHint#verified?`, `#column_names` and `#association_names`,
1205
+ `Fingerprinter.watched_files`, `Payload.mailer_file`,
1206
+ `Payload.rails_engines` and `Payload.databases`, `SafePath::REFUSALS`,
1207
+ `ViewFile::Result` (a view lookup returns `SafePath::Resolution`, which has
1208
+ the same fields), `Introspectors::ListenerRegistration.register`,
1209
+ `Listeners::MongoidFieldsListener` (the generic macro listener reads
1210
+ Mongoid's macros), and `StackOverviewHelper#database_adapter_label`. None
1211
+ was documented.
1212
+
1213
+ ### Security
1214
+
1215
+ - **Secret files a Figaro, direnv or config-gem app keeps are refused like the
1216
+ rest.** `config/application.yml`, `.envrc`, `config/settings.local.yml`,
1217
+ `config/settings/*.local.yml`, `config/secrets*.yml`,
1218
+ `config/secrets*.yml.enc`, `*.p8` and `*.env` (a compose `env_file:` such as
1219
+ `docker/secrets.env`) join the sensitive-file list, and `rails_search_code`
1220
+ refuses one given as its `path` (a file path is now taken, as ripgrep takes
1221
+ it), so `rails_get_edit_context` and `rails_search_code` no longer return
1222
+ the values in them. On a private API app both returned JWT signing secrets
1223
+ from `config/application.yml`.
1224
+ - **The Ruby search fallback skips what ripgrep skips**: sensitive files, and
1225
+ whatever `.ignore` and `.rgignore` files ignore, and inside a git repository
1226
+ whatever the root and nested `.gitignore` files, `.git/info/exclude` and the
1227
+ global excludes file ignore, with ripgrep's precedence: any `.rgignore` over
1228
+ any `.ignore` over any `.gitignore`, then `.git/info/exclude`, then the
1229
+ global excludes file, the deepest file deciding only within one type, and a
1230
+ directory they ignore never entered. Without ripgrep on the PATH the
1231
+ fallback returned lines from gitignored secret files; it now returns
1232
+ ripgrep's rows, context lines and line cap, matching each line without its
1233
+ newline.
1234
+ - **Every line `rails_search_code` and `rails_get_edit_context` return passes
1235
+ through redaction, and redaction filters secrets, never code.** A string
1236
+ literal of eight or more characters under a secret-named key (Ruby, YAML,
1237
+ JSON written `"key":"value"`, or a setter on any receiver such as
1238
+ `config.secret_key_base = '...'`), four or more under a password-type key
1239
+ (`password`, `passwd`, `pass`, `pin`, `passphrase`), letters-only and
1240
+ digits-only ones included, and the unquoted value of a secret-named
1241
+ `NAME=value` in any file (shell, Dockerfile, compose, env or code; inside a
1242
+ string or URL only the value is replaced, and a setter symbol, a regex
1243
+ lookbehind or an interpolation is left alone), a value in an example file
1244
+ (`*.example`, `*.sample`, `*.template`, and YAML ones like `*.yml.example`)
1245
+ unless it is plainly a placeholder, a cipher key name (`cbc_key`,
1246
+ `hmac_key`) counted as secret-named, a credential-looking value under any
1247
+ other `*_KEY` name (a 16+ character run mixing letters and digits, or a
1248
+ known token format, never a cache, i18n or storage key), a literal assigned
1249
+ through `ENV["NAME"] =`, `||=` or another op-assign or held by an instance
1250
+ variable or constant, a literal passed through a call or block that names a
1251
+ secret (`ENV.fetch` default, `let(:api_key) { }`, `default:` under a secret
1252
+ option), never a ternary's branches, a PGP private key block, Anthropic and
1253
+ OpenAI keys, unless it is plainly not a credential (a message, URL, path,
1254
+ placeholder, version, date, header, env or parameter name, or a
1255
+ translation), and a value in a known credential format (AWS keys, JWTs,
1256
+ vendor tokens) read `[FILTERED]` in any file; a PEM private key is filtered
1257
+ whole, across lines too and with armor headers before its body, with the
1258
+ line count kept, while a BEGIN marker named in a regex or string with no key
1259
+ body after it is code. `rails_search_code` shows a match that falls only
1260
+ inside a filtered value as the context row a wrong guess would show, so a
1261
+ pattern cannot confirm a secret's prefix. Log lines read `NAME=value` with
1262
+ the source pattern and decide each name and value pair once; they also
1263
+ filter a connection URL's password and a PEM body on the lines after its
1264
+ marker. Both tools redact the whole file before they cut a window or a row,
1265
+ so a window or a context line inside a key still hides it. Code is returned
1266
+ as written: a call that passes a token along (`issue(token:
1267
+ TokenService.run(user: @user).result)`) or reads one (`password:
1268
+ params[:password]`) is never rewritten. A `password_confirmation` literal is
1269
+ filtered like the password, in logs and config too, and log redaction keeps
1270
+ the quotes and the variable name around `[FILTERED]`.
1271
+
1272
+ ## [5.29.0] - 2026-09-23
1273
+
1274
+ Twenty-three QA reports against v5.27.0, each one a wrong answer a reader
1275
+ could act on, plus what eight review rounds found in the fixes themselves.
1276
+
1277
+ ### Added
1278
+
1279
+ - **Model payloads carry method counts and the model's own methods
1280
+ uncapped.** `instance_method_count`, `class_method_count` and
1281
+ `source_instance_methods` sit beside the capped `instance_methods` and
1282
+ `class_methods` lists, on both tiers, so a reader can tell a cut list from
1283
+ a whole one.
1284
+ - **The schema payload carries `declared_tables`**, what `db/schema.rb`
1285
+ declares, on both tiers: nil for an app whose tables come from
1286
+ `structure.sql` or the migrations.
1287
+
1288
+ ### Changed
1289
+
1290
+ - **"Mounted Engines" is "Mounted Apps" everywhere it is printed**: the
1291
+ `engines`, `routes` and `onboard` tools, the generated context files, and
1292
+ the MCP `rails://engines` resource name. The stack overview line reads
1293
+ `Mounted:` rather than `Engines:`. Half of what the list holds are plain
1294
+ Rack apps. The payload key stays `mounted_engines`.
1295
+ - **A mount whose path the source does not spell out carries `path: nil`**
1296
+ rather than the string `"unknown"`, and is printed without a path.
1297
+ - **`routes` reads a blank `controller` as no filter**, and a name that
1298
+ normalizes to nothing (`_controller`) as matching nothing, where the empty
1299
+ string used to match every route key.
1300
+
1301
+ ### Fixed
1302
+
1303
+ - **A Sidekiq worker answers to its own name, and the bracket carries the
1304
+ limit that governs it.** `job:"Billing::Invoices::CreateWorker"` answered
1305
+ "No jobs found" for a worker the same tool had just listed, because the
1306
+ single-job lookup read the ActiveJob list only. It reads both lists now, and
1307
+ a name in neither is a not-found that names what exists. The listing prints
1308
+ its "workers the introspector did not see" caveat whenever it prints
1309
+ workers, rather than only when `config/sidekiq.yml` happens to exist, and a
1310
+ `sidekiq_throttle` prints under the worker it throttles - 438 of 522 workers
1311
+ on one app declared one and none of them showed it.
1312
+ - **onboard's async section reads the workers out of the hash it already
1313
+ had.** The section counted jobs, mailers and channels, so an app whose
1314
+ background work is 522 Sidekiq workers read as "4 mailers." and the section
1315
+ disappeared entirely when workers were the only async code. The "not
1316
+ covered" line no longer prints next to a worker list it contradicts.
1317
+ - **What an ActiveInteraction declares is read in one place.** A filter
1318
+ declared inside another filter's block (`string :title` inside `hash
1319
+ :order_params do`) is a key of that hash, not an input of the class:
1320
+ `service_pattern` showed four inputs where `.filters` has two, and
1321
+ `generate_test` passed the other two to `.run`, which drops them silently. A
1322
+ subclass of the app's own base interaction is an interaction too, with its
1323
+ parent's filters first, so `generate_test` stops emitting `.call`, which
1324
+ `ActiveInteraction::Base` does not define. `GenericMacroListener` records
1325
+ each call's own offset and its enclosing call's, which is what both tools
1326
+ pair filters by.
1327
+ - **generate_test names the constant the file declares.** A path camelizes
1328
+ through Ruby's inflector, which has not read the app's
1329
+ `config/initializers/inflections.rb` on the static tier, so
1330
+ `ai_reports/build.rb` gave `AiReports::Build` where the app defines
1331
+ `AIReports::Build` - a constant nothing defines, in a spec that dies on
1332
+ load.
1333
+ - **service_pattern looks for callers where the app keeps code.** The scan
1334
+ named six `app/` directories, so a caller in `app/tools` or under `lib/` was
1335
+ invisible. It reads every `app/` and `lib/` tree, plus whatever else a
1336
+ booted app autoloads from, and says when the twenty-entry cap or its own
1337
+ file ceiling left the list partial.
1338
+ - **schema tells a missing migration from a typo.** Booted, a table declared
1339
+ in `db/schema.rb` and absent from the connected database answered "Did you
1340
+ mean 'comments'?". The payload carries the declared tables beside the live
1341
+ ones, so the answer names the migration that has not run, and the listing
1342
+ header says the two counts disagree instead of pairing a live table count
1343
+ with the file's version stamp.
1344
+ - **A validator under app/models/concerns is not a concern.** The type came
1345
+ from the directory alone, so 37 `ActiveModel::Validator` subclasses on one
1346
+ app were listed as model concerns used by nothing. They are listed as
1347
+ validators - following the app's own validator base class, not one level of
1348
+ compare - and looked up by the `validates_with`, or the validation option,
1349
+ that wires them, in a model or in a concern's `included` block. The keys
1350
+ `validates` reads for itself (`on:`, `if:` and the like) name no
1351
+ validator.
1352
+ - **dependency_graph counts both header numbers over the same models.** The
1353
+ model count was app-wide and the association count covered the fifty nodes
1354
+ that survived the cap. Both are app-wide now, and the truncation note says
1355
+ how many of the associations the cut graph draws.
1356
+ - **get_context reads the views Rails would resolve.** It handed `GetView`
1357
+ the last segment of the controller path, so `Api::V1::Admin::OrdersController`
1358
+ picked up `app/views/orders`, a directory of templates a background service
1359
+ renders. The answer names the directory its views came from, and a
1360
+ flat-directory fallback is labelled as one.
1361
+ - **analyze_feature finds a test by its path.** A spec whose feature word is a
1362
+ directory (`spec/services/billing/invoices/create_spec.rb`) was dropped,
1363
+ while the gap checker beside it already matched on the path. The suite's
1364
+ own words stay out of the match - the `spec/` root, the type directory it
1365
+ files a test under (`models/`, `requests/`), and the `_spec` suffix - so
1366
+ `--feature models` is not every model spec.
1367
+ - **routes answers an exact controller key with its own routes.** A substring
1368
+ filter returned a nested sibling's routes too (`api/v1/admin/orders` swept
1369
+ in `api/v1/admin/orders/ai_data`), and `get_context` inherited it. A short
1370
+ name still matches every controller that carries it.
1371
+ - **A template at the root of app/views is listed.** Its filename became a
1372
+ directory group that matched nothing, so the header counted a file the body
1373
+ never printed, and the controller-miss hint suggested a directory that does
1374
+ not exist.
1375
+ - **A word in a quoted string is not an instance variable.** `view` reported
1376
+ a chat handle inside a Ruby string literal as a template's ivar; the reader
1377
+ strips string literals and keeps interpolation, in ERB tags and in the
1378
+ Ruby template handlers (Jbuilder, Builder, `.ruby`), and `get_view`'s
1379
+ hydrator reads through the same method. Whichever quote opens first owns
1380
+ the literal, so an apostrophe inside `"Don't"` does not swallow the ivar
1381
+ beside it.
1382
+ - **env_config tells a re-assignment from a tuple.** Two unconditional
1383
+ assignments of one key rendered as `:file, :test`, which reads exactly like
1384
+ `:mem_cache_store, { pool_size: 5 }`. The winner is named, with what it
1385
+ overrode.
1386
+ - **env keeps one default per call site.** One label for every site said a
1387
+ variable was optional while one of its reads was `ENV.fetch` with no
1388
+ default, which raises `KeyError`. A fetch whose fallback is an expression
1389
+ says its default is computed at runtime rather than claiming it has none,
1390
+ and an `ENV["X"]` read says it is nil when unset, apart from the fetch
1391
+ that raises.
1392
+ - **A Rack app attached with `match ... to:` is found.** `mount` is that call
1393
+ with a name derived, and the exact-path form is what an app writes when an
1394
+ unanchored mount would swallow a sibling path. It reaches `engines` and
1395
+ `routes`, which names the mounted apps it counts instead of calling them
1396
+ engine mounts, and the booted tier lists every Rack endpoint rather than
1397
+ `Rails::Engine` subclasses alone. A mount inside a `namespace` or `scope`
1398
+ carries that prefix; one whose enclosing scope, or its `path:`, is an expression is listed
1399
+ with no path rather than an unprefixed one, and every list prints it
1400
+ without one. `engines` follows every file `config/routes.rb` draws,
1401
+ through the walk the static `routes` answer uses, so on the static tier
1402
+ the two name one set of mounted apps; booted, `routes` reads the live
1403
+ route table. `engines`, `onboard`, the MCP resource and the
1404
+ generated context files head the same list "Mounted Apps", because half of
1405
+ what it holds are not engines. On the static tier a `match ... to: SomeApp`
1406
+ is counted once, as the mount it is, rather than also as a construct the
1407
+ walk could not expand, and so are `get "/status" => StatusApp` and
1408
+ `mount ActionCable.server => "/cable"`, which the walk did not see at all.
1409
+ - **config calls a zero-byte initializer empty** rather than "all commented
1410
+ out".
1411
+ - **Smaller corrections in the same pass.** `validate_semantics` asks the
1412
+ loaded model class before calling a callback method missing, and on the
1413
+ static tier makes no claim once the payload's method list was cut at its
1414
+ cap, so an inherited method past the cap is no longer reported as missing.
1415
+ `get_controllers` points
1416
+ `rails_get_view` at the controller's full path rather than its last segment,
1417
+ which is the directory Rails resolves. The stack overview's `Engines:` line
1418
+ is `Mounted:`, because half of what it lists are plain Rack apps.
1419
+ - **diagnose stops reading a display cap as a model's whole interface.** A
1420
+ method past the thirtieth was reported as not existing, in the same answer
1421
+ whose Method Trace printed its definition. Booted, diagnose asks the loaded
1422
+ model class, which knows a concern's methods and a gem's as well as the
1423
+ model's own. Statically, the model's own methods travel uncapped beside the
1424
+ capped display list, and where a concern or a parent could define the
1425
+ method the answer declines rather than guesses. `model_details` says how
1426
+ many of the model's methods it is showing.
1427
+ - **A CamelCase controller name resolves everywhere.** The needle was
1428
+ downcased without being underscored, so "GiftCards" never equalled the
1429
+ route key's own "gift_cards": `Payload.find_controller` missed it, and every
1430
+ tool that resolves a controller through it missed it too. One normalization
1431
+ now serves the payload, the routes tool and the MCP resource, and the
1432
+ resource answers a name that resolves to nothing with an error naming what
1433
+ exists rather than a zero-route success document.
1434
+ - **security_scan runs the brakeman the machine has.** The app's bundle
1435
+ narrows the load path, so a machine with brakeman installed was told to add
1436
+ it to the Gemfile while the other tier scanned the same app. When the
1437
+ in-process require fails and the gem is installed, the scan runs it as its
1438
+ own process outside the bundle - reading the report from a file of its own,
1439
+ since a gem manager's binstub can print to stdout first - and renders the
1440
+ result the same way, with a line saying which brakeman answered and from
1441
+ where. When the outside run writes no report, the answer carries the last
1442
+ line brakeman printed about why. With no brakeman anywhere, the message
1443
+ says that instead of guessing, and the availability answer is keyed by
1444
+ tier rather than decided once per process.
1445
+ - **An exact search with a space keeps its context lines on ripgrep 13.**
1446
+ The literal was escaped with Ruby's `\ `, which ripgrep 13 (Ubuntu 22.04,
1447
+ Debian 12) rejects, so the search fell back to the Ruby scan and dropped
1448
+ context lines and files with no listed extension. The space goes through
1449
+ unescaped now, which both engines read the same way.
1450
+
8
1451
  ## [5.28.0] - 2026-09-22
9
1452
 
10
1453
  Thirty-six changes an architecture survey of v5.27.0 asked for, nine of them