rails-ai-context 5.30.2 → 5.32.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 (222) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +868 -0
  3. data/CONTEXT.md +7 -3
  4. data/README.md +1 -1
  5. data/docs/ARCHITECTURE.md +3 -3
  6. data/docs/CLI.md +1 -1
  7. data/docs/COMPATIBILITY.md +50 -14
  8. data/docs/CONFIGURATION.md +4 -1
  9. data/docs/FAQ.md +1 -1
  10. data/docs/GUIDE.md +14 -8
  11. data/docs/INTROSPECTORS.md +50 -23
  12. data/docs/SECURITY.md +56 -1
  13. data/docs/TOOLS.md +58 -12
  14. data/lib/generators/rails_ai_context/install/install_generator.rb +1 -0
  15. data/lib/rails_ai_context/action_filters.rb +121 -48
  16. data/lib/rails_ai_context/app_commands.rb +2 -2
  17. data/lib/rails_ai_context/app_kind.rb +65 -8
  18. data/lib/rails_ai_context/ast_cache.rb +53 -18
  19. data/lib/rails_ai_context/base_mixins.rb +46 -16
  20. data/lib/rails_ai_context/boot_manager.rb +5 -1
  21. data/lib/rails_ai_context/cli/entry_boot.rb +17 -2
  22. data/lib/rails_ai_context/cli/tool_runner.rb +15 -1
  23. data/lib/rails_ai_context/concern_macros.rb +791 -111
  24. data/lib/rails_ai_context/concern_membership.rb +25 -5
  25. data/lib/rails_ai_context/concern_paths.rb +63 -17
  26. data/lib/rails_ai_context/config_yaml.rb +40 -0
  27. data/lib/rails_ai_context/configuration.rb +3 -3
  28. data/lib/rails_ai_context/data/docs/index.json +324 -0
  29. data/lib/rails_ai_context/database_yml.rb +136 -0
  30. data/lib/rails_ai_context/doctor.rb +30 -37
  31. data/lib/rails_ai_context/erb_source.rb +40 -0
  32. data/lib/rails_ai_context/fingerprinter.rb +48 -22
  33. data/lib/rails_ai_context/fixture_keys.rb +42 -1
  34. data/lib/rails_ai_context/gem_lock.rb +275 -47
  35. data/lib/rails_ai_context/git_ignore.rb +13 -11
  36. data/lib/rails_ai_context/hydrators/hydration_formatter.rb +8 -2
  37. data/lib/rails_ai_context/hydrators/schema_hint_builder.rb +10 -5
  38. data/lib/rails_ai_context/introspector.rb +10 -1
  39. data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +27 -19
  40. data/lib/rails_ai_context/introspectors/action_presence.rb +11 -8
  41. data/lib/rails_ai_context/introspectors/action_resolver.rb +38 -26
  42. data/lib/rails_ai_context/introspectors/action_text_introspector.rb +2 -2
  43. data/lib/rails_ai_context/introspectors/active_record_settings.rb +56 -0
  44. data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +1 -1
  45. data/lib/rails_ai_context/introspectors/active_support_introspector.rb +98 -22
  46. data/lib/rails_ai_context/introspectors/admin_resources.rb +101 -0
  47. data/lib/rails_ai_context/introspectors/anyway_configs.rb +64 -0
  48. data/lib/rails_ai_context/introspectors/apartment_config.rb +47 -0
  49. data/lib/rails_ai_context/introspectors/api_introspector.rb +125 -26
  50. data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +24 -19
  51. data/lib/rails_ai_context/introspectors/ast_walk.rb +18 -0
  52. data/lib/rails_ai_context/introspectors/auth_introspector.rb +71 -18
  53. data/lib/rails_ai_context/introspectors/autoload_introspector.rb +62 -15
  54. data/lib/rails_ai_context/introspectors/base.rb +38 -0
  55. data/lib/rails_ai_context/introspectors/call_site_expansion.rb +349 -39
  56. data/lib/rails_ai_context/introspectors/component_introspector.rb +26 -14
  57. data/lib/rails_ai_context/introspectors/config_introspector.rb +22 -9
  58. data/lib/rails_ai_context/introspectors/connection_pool_introspector.rb +1 -4
  59. data/lib/rails_ai_context/introspectors/controller_filters.rb +335 -31
  60. data/lib/rails_ai_context/introspectors/controller_introspector.rb +315 -129
  61. data/lib/rails_ai_context/introspectors/controller_settings.rb +182 -0
  62. data/lib/rails_ai_context/introspectors/convention_introspector.rb +18 -15
  63. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +1 -1
  64. data/lib/rails_ai_context/introspectors/declared_constant.rb +108 -23
  65. data/lib/rails_ai_context/introspectors/eager_load.rb +12 -7
  66. data/lib/rails_ai_context/introspectors/engine_introspector.rb +13 -5
  67. data/lib/rails_ai_context/introspectors/env_config_introspector.rb +62 -17
  68. data/lib/rails_ai_context/introspectors/env_introspector.rb +8 -6
  69. data/lib/rails_ai_context/introspectors/env_references.rb +21 -4
  70. data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +37 -22
  71. data/lib/rails_ai_context/introspectors/gem_introspector.rb +3 -1
  72. data/lib/rails_ai_context/introspectors/gemfile_gems.rb +53 -5
  73. data/lib/rails_ai_context/introspectors/grape_endpoints.rb +109 -0
  74. data/lib/rails_ai_context/introspectors/helper_names.rb +12 -1
  75. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +101 -46
  76. data/lib/rails_ai_context/introspectors/includers.rb +6 -1
  77. data/lib/rails_ai_context/introspectors/initializer_introspector.rb +2 -5
  78. data/lib/rails_ai_context/introspectors/job_introspector.rb +548 -103
  79. data/lib/rails_ai_context/introspectors/listener_registration.rb +11 -1
  80. data/lib/rails_ai_context/introspectors/listeners/associations_listener.rb +51 -4
  81. data/lib/rails_ai_context/introspectors/listeners/autoload_ignore_listener.rb +31 -0
  82. data/lib/rails_ai_context/introspectors/listeners/autoload_paths_listener.rb +4 -47
  83. data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +80 -14
  84. data/lib/rails_ai_context/introspectors/listeners/branch_conditions.rb +156 -0
  85. data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +85 -23
  86. data/lib/rails_ai_context/introspectors/listeners/class_definition_listener.rb +35 -6
  87. data/lib/rails_ai_context/introspectors/listeners/component_structure_listener.rb +30 -2
  88. data/lib/rails_ai_context/introspectors/listeners/conditional_macro_listener.rb +19 -0
  89. data/lib/rails_ai_context/introspectors/listeners/config_assignment_listener.rb +255 -10
  90. data/lib/rails_ai_context/introspectors/listeners/constant_reference_listener.rb +34 -0
  91. data/lib/rails_ai_context/introspectors/listeners/constructor_macro_listener.rb +123 -0
  92. data/lib/rails_ai_context/introspectors/listeners/definition_file_paths_listener.rb +66 -0
  93. data/lib/rails_ai_context/introspectors/listeners/enums_listener.rb +2 -2
  94. data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +31 -0
  95. data/lib/rails_ai_context/introspectors/listeners/filter_macro_listener.rb +54 -0
  96. data/lib/rails_ai_context/introspectors/listeners/fixture_paths_listener.rb +62 -0
  97. data/lib/rails_ai_context/introspectors/listeners/gemfile_dsl_listener.rb +42 -1
  98. data/lib/rails_ai_context/introspectors/listeners/generic_macro_listener.rb +28 -2
  99. data/lib/rails_ai_context/introspectors/listeners/grape_api_listener.rb +137 -0
  100. data/lib/rails_ai_context/introspectors/listeners/http_client_call_listener.rb +50 -0
  101. data/lib/rails_ai_context/introspectors/listeners/i18n_load_path_listener.rb +54 -0
  102. data/lib/rails_ai_context/introspectors/listeners/literal_paths.rb +123 -0
  103. data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +248 -4
  104. data/lib/rails_ai_context/introspectors/listeners/mailbox_routing_listener.rb +2 -1
  105. data/lib/rails_ai_context/introspectors/listeners/method_call_listener.rb +7 -1
  106. data/lib/rails_ai_context/introspectors/listeners/methods_listener.rb +352 -69
  107. data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +1 -1
  108. data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +35 -26
  109. data/lib/rails_ai_context/introspectors/listeners/mixins_listener.rb +62 -29
  110. data/lib/rails_ai_context/introspectors/listeners/mount_listener.rb +8 -0
  111. data/lib/rails_ai_context/introspectors/listeners/namespaced_roots_listener.rb +35 -0
  112. data/lib/rails_ai_context/introspectors/listeners/nested_constants_listener.rb +32 -0
  113. data/lib/rails_ai_context/introspectors/listeners/owner_scope.rb +67 -0
  114. data/lib/rails_ai_context/introspectors/listeners/preview_paths_listener.rb +16 -7
  115. data/lib/rails_ai_context/introspectors/listeners/proc_literal_listener.rb +42 -0
  116. data/lib/rails_ai_context/introspectors/listeners/queue_assignment_listener.rb +54 -0
  117. data/lib/rails_ai_context/introspectors/listeners/rake_task_dsl_listener.rb +21 -8
  118. data/lib/rails_ai_context/introspectors/listeners/route_files_listener.rb +4 -11
  119. data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +461 -65
  120. data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +76 -14
  121. data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +40 -1
  122. data/lib/rails_ai_context/introspectors/listeners/validations_listener.rb +10 -1
  123. data/lib/rails_ai_context/introspectors/listeners/view_paths_listener.rb +37 -0
  124. data/lib/rails_ai_context/introspectors/middleware_introspector.rb +47 -3
  125. data/lib/rails_ai_context/introspectors/migration_introspector.rb +3 -3
  126. data/lib/rails_ai_context/introspectors/migration_replay/helpers.rb +4 -3
  127. data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +72 -12
  128. data/lib/rails_ai_context/introspectors/migration_replay.rb +110 -14
  129. data/lib/rails_ai_context/introspectors/model_introspector.rb +708 -214
  130. data/lib/rails_ai_context/introspectors/module_aliases.rb +11 -3
  131. data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +9 -57
  132. data/lib/rails_ai_context/introspectors/observability_introspector.rb +5 -8
  133. data/lib/rails_ai_context/introspectors/performance_introspector.rb +28 -8
  134. data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +118 -51
  135. data/lib/rails_ai_context/introspectors/recurring_schedules.rb +150 -0
  136. data/lib/rails_ai_context/introspectors/rendered_record.rb +87 -0
  137. data/lib/rails_ai_context/introspectors/retry_policy.rb +8 -5
  138. data/lib/rails_ai_context/introspectors/route_introspector.rb +153 -42
  139. data/lib/rails_ai_context/introspectors/schema_conventions.rb +127 -40
  140. data/lib/rails_ai_context/introspectors/schema_dump_path.rb +80 -0
  141. data/lib/rails_ai_context/introspectors/schema_introspector.rb +346 -112
  142. data/lib/rails_ai_context/introspectors/schema_reader.rb +69 -29
  143. data/lib/rails_ai_context/introspectors/service_classes.rb +21 -15
  144. data/lib/rails_ai_context/introspectors/source_calls.rb +1 -1
  145. data/lib/rails_ai_context/introspectors/source_introspector.rb +17 -2
  146. data/lib/rails_ai_context/introspectors/source_scan.rb +174 -26
  147. data/lib/rails_ai_context/introspectors/sqlite_virtual_tables.rb +28 -0
  148. data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +16 -5
  149. data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +372 -98
  150. data/lib/rails_ai_context/introspectors/superclass_chain.rb +14 -15
  151. data/lib/rails_ai_context/introspectors/table_name.rb +46 -45
  152. data/lib/rails_ai_context/introspectors/test_introspector.rb +289 -78
  153. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +10 -5
  154. data/lib/rails_ai_context/introspectors/view_introspector.rb +5 -40
  155. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +12 -10
  156. data/lib/rails_ai_context/method_name.rb +31 -0
  157. data/lib/rails_ai_context/migration_status.rb +5 -5
  158. data/lib/rails_ai_context/output_guard.rb +6 -4
  159. data/lib/rails_ai_context/package_json.rb +96 -5
  160. data/lib/rails_ai_context/path_resolver.rb +170 -15
  161. data/lib/rails_ai_context/payload.rb +48 -0
  162. data/lib/rails_ai_context/pending_migrations.rb +15 -18
  163. data/lib/rails_ai_context/portable_path.rb +76 -4
  164. data/lib/rails_ai_context/redaction.rb +8 -2
  165. data/lib/rails_ai_context/route_coverage.rb +5 -1
  166. data/lib/rails_ai_context/safe_file.rb +4 -1
  167. data/lib/rails_ai_context/safe_path.rb +61 -10
  168. data/lib/rails_ai_context/schema_hint.rb +8 -2
  169. data/lib/rails_ai_context/schema_version.rb +7 -9
  170. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +1 -1
  171. data/lib/rails_ai_context/serializers/markdown_serializer.rb +13 -4
  172. data/lib/rails_ai_context/serializers/section_facts.rb +5 -1
  173. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +2 -2
  174. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +7 -4
  175. data/lib/rails_ai_context/server.rb +26 -7
  176. data/lib/rails_ai_context/test_framework.rb +32 -5
  177. data/lib/rails_ai_context/tools/analyze_feature.rb +22 -10
  178. data/lib/rails_ai_context/tools/base_tool.rb +69 -30
  179. data/lib/rails_ai_context/tools/diagnose.rb +1 -1
  180. data/lib/rails_ai_context/tools/generate_test.rb +8 -9
  181. data/lib/rails_ai_context/tools/get_active_support.rb +19 -4
  182. data/lib/rails_ai_context/tools/get_api.rb +17 -11
  183. data/lib/rails_ai_context/tools/get_autoload.rb +10 -5
  184. data/lib/rails_ai_context/tools/get_callbacks.rb +65 -14
  185. data/lib/rails_ai_context/tools/get_component_catalog.rb +7 -4
  186. data/lib/rails_ai_context/tools/get_concern.rb +31 -18
  187. data/lib/rails_ai_context/tools/get_config.rb +52 -18
  188. data/lib/rails_ai_context/tools/get_controllers.rb +57 -13
  189. data/lib/rails_ai_context/tools/get_conventions.rb +44 -22
  190. data/lib/rails_ai_context/tools/get_engines.rb +2 -0
  191. data/lib/rails_ai_context/tools/get_env.rb +209 -52
  192. data/lib/rails_ai_context/tools/get_env_config.rb +38 -7
  193. data/lib/rails_ai_context/tools/get_frontend_stack.rb +56 -7
  194. data/lib/rails_ai_context/tools/get_helper_methods.rb +136 -26
  195. data/lib/rails_ai_context/tools/get_job_pattern.rb +143 -87
  196. data/lib/rails_ai_context/tools/get_mailers.rb +59 -2
  197. data/lib/rails_ai_context/tools/get_model_details.rb +178 -28
  198. data/lib/rails_ai_context/tools/get_partial_interface.rb +101 -62
  199. data/lib/rails_ai_context/tools/get_routes.rb +35 -9
  200. data/lib/rails_ai_context/tools/get_schema.rb +160 -61
  201. data/lib/rails_ai_context/tools/get_service_pattern.rb +121 -25
  202. data/lib/rails_ai_context/tools/get_stimulus.rb +1 -1
  203. data/lib/rails_ai_context/tools/get_test_info.rb +152 -82
  204. data/lib/rails_ai_context/tools/get_view.rb +37 -20
  205. data/lib/rails_ai_context/tools/migration_advisor.rb +64 -24
  206. data/lib/rails_ai_context/tools/onboard.rb +174 -88
  207. data/lib/rails_ai_context/tools/performance_check.rb +5 -13
  208. data/lib/rails_ai_context/tools/query.rb +122 -28
  209. data/lib/rails_ai_context/tools/read_logs.rb +45 -17
  210. data/lib/rails_ai_context/tools/runtime_info.rb +1 -1
  211. data/lib/rails_ai_context/tools/safe_call.rb +20 -15
  212. data/lib/rails_ai_context/tools/search_code.rb +114 -40
  213. data/lib/rails_ai_context/tools/search_docs.rb +22 -11
  214. data/lib/rails_ai_context/tools/security_scan.rb +51 -4
  215. data/lib/rails_ai_context/tools/validate.rb +21 -4
  216. data/lib/rails_ai_context/tools/validate_semantics.rb +18 -6
  217. data/lib/rails_ai_context/version.rb +1 -1
  218. data/lib/rails_ai_context/vfs.rb +2 -2
  219. data/lib/rails_ai_context/view_file.rb +53 -3
  220. data/lib/rails_ai_context.rb +10 -2
  221. data/server.json +1 -1
  222. metadata +34 -3
data/docs/TOOLS.md CHANGED
@@ -111,7 +111,7 @@ Method-aware code extraction with surrounding class context. Every line returned
111
111
 
112
112
  ### `rails_analyze_feature`
113
113
 
114
- Full-stack feature analysis: models + controllers + routes + services + jobs + views + tests in one call.
114
+ Full-stack feature analysis: models + controllers + routes + services + admin resources + jobs + views + tests in one call.
115
115
 
116
116
  | Parameter | Type | Default | Description |
117
117
  |:----------|:-----|:--------|:------------|
@@ -130,7 +130,12 @@ Composite context: schema + model + controller + routes + views for a resource.
130
130
 
131
131
  ### `rails_onboard`
132
132
 
133
- Narrative app walkthrough for getting up to speed.
133
+ Narrative app walkthrough for getting up to speed. It ends with the app's custom
134
+ rake tasks (Rakefile, `lib/tasks`, `rakelib`), each with its arguments,
135
+ description and file: the first 15 at `standard`, every one at `full`.
136
+ Then the app's own generators under `lib/generators` (the `bin/rails generate`
137
+ command and its USAGE line), the `lib/templates` files that replace a built-in
138
+ generator's template, and the Railties under `lib/` with their initializers.
134
139
 
135
140
  | Parameter | Type | Default | Description |
136
141
  |:----------|:-----|:--------|:------------|
@@ -179,8 +184,17 @@ Concerns as `[UNAVAILABLE]` rather than dropped.
179
184
  Callbacks grouped by type, in Rails event order, with source code. The list
180
185
  covers concern-declared callbacks too, with the body read from the concern
181
186
  file, and the "From Concerns" section says which concern declared each one.
182
- Within one type the order is declaration order (the model file first, then
183
- its concerns), not the order Rails registered them in.
187
+ Within one type the order is the order Rails runs them: base classes first, a
188
+ concern's callbacks where its `include` line stands, then the model's own, and
189
+ a `before_` or `around_` callback with `prepend: true` first. `after_commit`
190
+ and `after_rollback` are last declared first unless the app turns on
191
+ `run_after_transaction_callbacks_in_order_defined` (`load_defaults 7.1`). A
192
+ method declared again for one type shows once, with the later declaration's
193
+ conditions, as Rails keeps it. The order is read from source in both tiers,
194
+ and dynamic dispatch is not evaluated: a callback registered through
195
+ `send(:before_save, ...)`, through a method called with `send` or defined with
196
+ `define_singleton_method`, or through a module included in `class << self` is
197
+ not read, so the list can miss it or show the definition it replaces.
184
198
 
185
199
  | Parameter | Type | Default | Description |
186
200
  |:----------|:-----|:--------|:------------|
@@ -226,7 +240,9 @@ fully qualified controller key answers with its own routes only; a short name
226
240
  reaches the controller whose trailing segments it spells, and not the ones that
227
241
  merely start with it. Rack apps attached with `mount`
228
242
  or `match ... to:` are named with the path they answer on, when the source
229
- spells one out.
243
+ spells one out. A mounted Grape API lists its endpoints under the mount (verb,
244
+ full path, declared params), read from its classes in `app/api` and `lib/api`;
245
+ the summary counts them.
230
246
 
231
247
  | Parameter | Type | Default | Description |
232
248
  |:----------|:-----|:--------|:------------|
@@ -367,7 +383,7 @@ API layer: api_only mode, serialization strategy (Jbuilder, serializers), GraphQ
367
383
 
368
384
  ### `rails_get_conventions`
369
385
 
370
- Auth checks, flash messages, create action template, test patterns.
386
+ Auth checks, flash messages, create action template, test patterns, and the admin resources ActiveAdmin, Administrate, Avo, Madmin and Trestle register (with ActiveAdmin's permitted params).
371
387
 
372
388
  | Parameter | Type | Default | Description |
373
389
  |:----------|:-----|:--------|:------------|
@@ -375,7 +391,7 @@ Auth checks, flash messages, create action template, test patterns.
375
391
 
376
392
  ### `rails_get_config`
377
393
 
378
- Database config, auth framework, assets, cache, queue, Action Cable.
394
+ Database config, auth framework, assets, cache, queue, Action Cable. The `use` and `map` calls in `config.ru` are listed apart from the stack, since they run before Rails.
379
395
 
380
396
  | Parameter | Type | Default | Description |
381
397
  |:----------|:-----|:--------|:------------|
@@ -393,7 +409,7 @@ found under a load-order prefix too (`config/initializers/3_omniauth.rb`).
393
409
 
394
410
  ### `rails_get_env`
395
411
 
396
- Environment variables + credentials keys (values are never exposed). Scans `.rb`, `.rake`, ERB views and config YAML under `app`, `config` and `lib`; files matching `sensitive_patterns` (`config/database.yml`, credentials, keys) are never read, and the answer says so. A variable whose call sites pass different defaults is labelled as such rather than with one site's default; `detail:"full"` names each site's. A default is redacted by the rule a source literal gets: a credential format under any name, a URL's password, and under a secret-named variable a value that is not clearly something else, so an address, URL or hostname default prints as written.
412
+ Environment variables + credentials keys (values are never exposed). Scans `.rb`, `.rake`, ERB views and config YAML under `app`, `config` and `lib`, plus `config.ru`, `db/seeds.rb`, `db/seeds/` and the Ruby scripts in `bin/`; files matching `sensitive_patterns` (`config/database.yml`, credentials, keys) are never read, and the answer says so. The env Kamal's `config/deploy.yml` gives the app container is listed too: secret names only, and clear values, except one holding a URL or a key-like token, or under a secret-named variable, which shows as hidden, and one an ERB tag sets, which says so. So are the setting keys in the config gem's `config/settings.yml` and `config/settings/<env>.yml` when the bundle has the config gem, and the attributes of each `Anyway::Config` class in `config/configs` or `app/configs` with the env name it reads (`PAYMENT_API_KEY`), never their values. A variable whose call sites pass different defaults is labelled as such rather than with one site's default; `detail:"full"` names each site's. A default is redacted by the rule a source literal gets: a credential format under any name, a URL's password, and under a secret-named variable a value that is not clearly something else, so an address, URL or hostname default prints as written.
397
413
 
398
414
  | Parameter | Type | Default | Description |
399
415
  |:----------|:-----|:--------|:------------|
@@ -409,7 +425,9 @@ Application and framework helpers with view cross-references.
409
425
 
410
426
  ### `rails_get_service_pattern`
411
427
 
412
- Service object interface, dependencies, side effects, callers. An
428
+ Service object interface, dependencies, side effects, callers, read from
429
+ `app/services/`, `app/interactions/` and `app/interactors/`. An interactor
430
+ organizer lists the steps it runs, in order. An
413
431
  ActiveInteraction's inputs include the ones it inherits, with the filters
414
432
  nested inside a `hash` filter shown under it. Callers are read from every
415
433
  `app/` and `lib/` tree, and on a booted app from any other directory it
@@ -437,7 +455,18 @@ job inherits from is not counted as a job; the listing names the ones it left
437
455
  out, in the sentence the service and mailer listings use, and `job:` answers
438
456
  for one of them with what every job below it inherits: its queue, options,
439
457
  retries, mixins, throttle and callbacks, and which jobs inherit it. `job:`
440
- answers a worker name as well as a job name.
458
+ answers a worker name as well as a job name. A job's own page shows its
459
+ `queue_with_priority`, `enqueue_after_transaction_commit` and Solid Queue
460
+ `limits_concurrency` (its own or the nearest base's), every `retry_on` option,
461
+ Sidekiq's `sidekiq_retry_in` and `sidekiq_retries_exhausted` blocks, and its
462
+ enqueue, perform and discard callbacks as written. A job that includes
463
+ `ActiveJob::Continuable` (itself or through a base) is marked continuable, with
464
+ the steps its `perform` runs in order, each a block or a method. A model method
465
+ delayed_job's `handle_asynchronously` wraps is listed as a background method with
466
+ its options, here and on the model's `rails_get_model_details` page. The listing names the queues
467
+ `config/sidekiq.yml` declares and the queues Solid Queue's workers poll in
468
+ `config/queue.yml` (this environment's section), and names each job queue no
469
+ worker polls; that job's page says so beside its queue.
441
470
 
442
471
  | Parameter | Type | Default | Description |
443
472
  |:----------|:-----|:--------|:------------|
@@ -467,6 +496,19 @@ I18n setup: default/available locales, backend, locale files with key counts, pe
467
496
 
468
497
  ActionMailer mailers: every mailer class with its delivery actions and delivery method. A mailer that declares no action of its own - one taking them from a gem base or a mixin, or one called through class methods - is listed with where its actions come from rather than left out. A base other mailers inherit from is not one: it leaves the listing, and a line above names the ones left out. Asked for by name, a base answers with what every mailer below it inherits - its layout, helpers, defaults, callbacks and mixins as the app wrote them, the methods it defines - and which mailers inherit it.
469
498
 
499
+ Each mailer also shows its own `default`, `layout`, `helper` and action and
500
+ deliver callbacks as written, the template formats of each action under
501
+ `app/views/<mailer>`, and its preview class with the emails it previews (read
502
+ from `test/mailers/previews`, `spec/mailers/previews` and the
503
+ `action_mailer.preview_paths` the config adds). The full listing opens with the
504
+ queue `deliver_later` uses (ActionMailer's setting booted; the config's
505
+ statically, ActiveJob's default queue from `load_defaults` 6.1 on), the
506
+ interceptors and observers the config registers, and the preview paths.
507
+
508
+ The full listing ends with the Action Mailbox mailboxes: the `routing` rules in
509
+ the order Rails tries them, the mailbox each sends to (and whether `app/mailboxes`
510
+ defines it), and each mailbox's processing callbacks.
511
+
470
512
  | Parameter | Type | Default | Description |
471
513
  |:----------|:-----|:--------|:------------|
472
514
  | `mailer` | string | - | One mailer, by exact name |
@@ -487,13 +529,17 @@ Autoloading setup: Zeitwerk vs Classic mode, autoloaders with collapsed/ignored
487
529
 
488
530
  ### `rails_get_active_support`
489
531
 
490
- ActiveSupport surface: concerns registry, deprecators, MessageVerifier/MessageEncryptor usage, tagged logging, subscribed `on_load` hooks, cache store. Validator classes living among the concerns are counted and listed apart from them, as `rails_get_concern` does.
532
+ ActiveSupport surface: concerns registry, deprecators, MessageVerifier/MessageEncryptor usage, the `ActiveSupport::Notifications` events the app subscribes to (`subscribe`, `monotonic_subscribe`, and a Subscriber's `attach_to`) with file and line, read from source in both tiers, tagged logging, subscribed `on_load` hooks, cache store. Validator classes living among the concerns are counted and listed apart from them, as `rails_get_concern` does.
491
533
 
492
534
  *No parameters.*
493
535
 
494
536
  ### `rails_get_env_config`
495
537
 
496
- Per-environment configuration from `config/environments/*.rb`: notable toggles (`force_ssl`, `eager_load`, caching, log level, queue adapter, mailer delivery) and every config key each environment sets. A key assigned in more than one branch reports every value with its condition; booted, the running environment reports the value the app resolved.
538
+ Per-environment configuration from `config/environments/*.rb`: notable toggles (`force_ssl`, `eager_load`, caching, log level, queue adapter, mailer delivery) and every config key each environment sets. A key assigned in more than one branch reports every value with its condition; booted, the running environment reports the value the app resolved. An
539
+ "Every environment" block above them lists the keys `config/application.rb`
540
+ sets (`config.x` included) and, for each `config_for(:name)`, the keys
541
+ `config/name.yml` gives the running environment, `shared` merged in, names
542
+ only.
497
543
 
498
544
  | Parameter | Type | Default | Description |
499
545
  |:----------|:-----|:--------|:------------|
@@ -229,6 +229,7 @@ module RailsAiContext
229
229
  "Frontend" => <<~SECTION
230
230
  # ── Frontend Framework Detection ─────────────────────────────────
231
231
  # Auto-detected from package.json, config/vite.json, etc. Override only if needed.
232
+ # A path outside the app root is read for package.json, lockfiles, bundler config and its tsconfig.json.
232
233
  # config.frontend_paths = ["app/frontend", "../web-client"]
233
234
  SECTION
234
235
  }.freeze
@@ -10,11 +10,14 @@ module RailsAiContext
10
10
  # excludes what the class skipped, so the parent's filters are separated by
11
11
  # name rather than added.
12
12
  module ActionFilters
13
- SKIP_MACROS = %i[skip_before_action skip_after_action skip_around_action].freeze
13
+ # The ancestors' filters, the names they dropped, the conditions they set, every name they
14
+ # declare, and whether every class up to ActionController was read.
15
+ ParentWalk = Struct.new(:filters, :dropped, :conditions, :declares, :whole_chain)
14
16
 
15
17
  module_function
16
18
 
17
- # { own: [filter], inherited: [filter], skipped: [name] } for one action.
19
+ # { own: [filter], inherited: [filter], chain: [filter], skipped: [name] } for one action:
20
+ # `chain` is own and inherited together, in the order Rails adds them; after filters run in reverse.
18
21
  # `source:` is the controller's source when the caller already has it.
19
22
  def for(ctx, controller_name, action, source: nil, root: nil)
20
23
  split(ctx, controller_name, action.to_s, source: source, root: root)
@@ -37,15 +40,17 @@ module RailsAiContext
37
40
 
38
41
  def split(ctx, controller_name, action, source:, root:)
39
42
  info = Payload.controllers(ctx)[controller_name.to_s]
40
- return { own: [], inherited: [], skipped: [] } unless info.is_a?(Hash)
43
+ return { own: [], inherited: [], chain: [], skipped: [] } unless info.is_a?(Hash)
41
44
 
42
45
  skips = own_skips(ctx, controller_name, info, action, root: root, source: source)
43
46
  skipped = absolute_names(skips, action)
44
47
  # A skip record states what does not run, so it is never a filter.
45
- declared = Array(info[:filters]).grep(Hash).reject { |f| f[:skipped] }
46
- parent, dropped, inherited_conditions, declares = parent_filters(ctx, info[:parent_class], action, skipped,
47
- root: root, within: controller_name.to_s)
48
- conditions = merge_conditions(inherited_conditions, conditions_by_name(skips, action))
48
+ offsets = Hash.new(0)
49
+ declared = number_blocks(Array(info[:filters]).grep(Hash).reject { |f| f[:skipped] }, offsets)
50
+ walk = parent_filters(ctx, info[:parent_class], action, skipped, root: root, within: controller_name.to_s, offsets: offsets)
51
+ parent = walk.filters
52
+ dropped = walk.dropped
53
+ conditions = merge_conditions(walk.conditions, conditions_by_name(skips, action))
49
54
  # The runtime tier's list is the whole chain, so an ancestor's skip
50
55
  # applies here too - except to what this body declares again, which
51
56
  # Rails re-adds.
@@ -73,16 +78,71 @@ module RailsAiContext
73
78
  runs_once?(f[:from_concern], root, controller_name))
74
79
  end
75
80
  own = mark_conditional_skips(applicable.reject(&inherited_here), conditions, action)
76
- inherited = mark_conditional_skips(
77
- parent.reject { |f| declared_names.include?(entry_key(f)) } +
78
- applicable.select(&inherited_here)
79
- .map { |f| f.except(:from, :from_concern, :provenance).merge(attribution_of[entry_key(f)]) }, conditions, action
80
- )
81
+ # The runtime tier's list is the whole chain: once the body, every ancestor and every module
82
+ # they mix in were read, a name none of them declares was installed from outside it (a gem's on_load).
83
+ outside = []
84
+ whole_chain = walk.whole_chain && info[:concerns_unread].blank?
85
+ if whole_chain && Array(info[:filters]).grep(Hash).any? { |f| f[:declared] || f[:skipped] }
86
+ outside, own = own.partition { |f| !f[:declared] && !f[:from_concern] }
87
+ outside = outside.map { |f| f.merge(provenance: "not declared in the controller chain") }
88
+ end
89
+ moved = applicable.select(&inherited_here)
90
+ .map { |f| f.except(:from, :from_concern, :provenance).merge(attribution_of[entry_key(f)]) }
91
+ .group_by { |f| entry_key(f) }
92
+ # Each moved entry takes its ancestor's slot, so one the runtime list does not carry keeps its place among them.
93
+ kept = parent.flat_map { |f| declared_names.include?(entry_key(f)) ? Array(moved.delete(entry_key(f))) : [ f ] }
94
+ inherited = mark_conditional_skips(kept + moved.values.flatten, conditions, action) + outside
95
+ own = unnumbered(own)
96
+ inherited = unnumbered(inherited)
97
+ inherited += unplaced_conditional_skips(own + inherited, conditions, action,
98
+ walk.declares | declared.map { |f| f[:name].to_s }.to_set)
99
+ chain = run_sequence(inherited + own, info)
100
+ mine = own.to_set.compare_by_identity
101
+
102
+ { own: chain.select { |f| mine.include?(f) }, inherited: chain.reject { |f| mine.include?(f) },
103
+ chain: chain, skipped: skipped }
104
+ end
105
+
106
+ # The runtime tier's list is the reflection list, already in run order; a filter it
107
+ # does not carry keeps its declared place, just after the carried filter declared before it.
108
+ # The static tier's lists are declarations, which Rails appends in turn and a prepend_* macro unshifts.
109
+ def run_sequence(filters, info)
110
+ declared = filters.each_with_object([]) { |f, list| f[:prepend] ? list.unshift(f) : list.push(f) }
111
+ return declared if RailsAiContext.static_tier?
112
+
113
+ rank = {}
114
+ Array(info[:filters]).grep(Hash).reject { |f| f[:skipped] }.each_with_index { |f, i| rank[entry_key(f)] ||= i }
115
+ leading = []
116
+ behind = {}.compare_by_identity
117
+ last = nil
118
+ declared.each do |f|
119
+ next (last ? behind[last] : leading) << f unless rank.key?(entry_key(f))
120
+
121
+ behind[f] = []
122
+ last = f
123
+ end
124
+ leading + behind.keys.each_with_index.sort_by { |f, i| [ rank[entry_key(f)], i ] }.flat_map { |f, _| [ f ] + behind[f] }
125
+ end
126
+
127
+ BLOCK_NUMBER = /#\d+\z/
128
+
129
+ # Numbered from the chain's end so one block reads the same in a class's list and every descendant's.
130
+ def number_blocks(filters, offsets)
131
+ seen = Hash.new(0)
132
+ numbered = filters.reverse.map do |filter|
133
+ next filter unless Introspectors::ControllerFilters.block?(filter[:name])
134
+
135
+ key = entry_key(filter)
136
+ number = offsets[key] + seen[key]
137
+ seen[key] += 1
138
+ filter.merge(name: "#{filter[:name]}##{number}")
139
+ end.reverse
140
+ filters.each { |filter| offsets[entry_key(filter)] += 1 if filter[:declared] && Introspectors::ControllerFilters.block?(filter[:name]) }
141
+ numbered
142
+ end
81
143
 
82
- { own: own,
83
- inherited: inherited + unplaced_conditional_skips(own + inherited, conditions, action,
84
- declares | declared.map { |f| f[:name].to_s }.to_set),
85
- skipped: skipped }
144
+ def unnumbered(filters)
145
+ filters.map { |filter| filter[:name].to_s.match?(BLOCK_NUMBER) ? filter.merge(name: filter[:name].to_s.sub(BLOCK_NUMBER, "")) : filter }
86
146
  end
87
147
 
88
148
  # A skip of a name no ancestor in the payload declares is the only
@@ -98,7 +158,7 @@ module RailsAiContext
98
158
  # A conditional skip never removes the filter; the record carries the
99
159
  # condition. See CONTEXT.md, "Filter chain".
100
160
  def conditional?(skip, action)
101
- !skip[:if].nil? || !skip[:unless].nil? || partial?(skip, action)
161
+ !skip[:if].nil? || !skip[:unless].nil? || !skip[:condition].nil? || partial?(skip, action)
102
162
  end
103
163
 
104
164
  # Partial only where the answer covers every action: a per-action answer
@@ -139,6 +199,7 @@ module RailsAiContext
139
199
 
140
200
  tail[:skipped_if] = condition_text(skip[:if]) if skip[:if]
141
201
  tail[:skipped_unless] = condition_text(skip[:unless]) if skip[:unless]
202
+ tail[:skipped_condition] = skip[:condition] if skip[:condition]
142
203
  return tail unless partial?(skip, action)
143
204
 
144
205
  tail[:skipped_on] = action_names(skip[:only]) if Array(skip[:only]).any?
@@ -181,7 +242,7 @@ module RailsAiContext
181
242
  # ends it: reconstructing a path from a class name breaks on every app
182
243
  # inflection. A bare superclass is resolved against the enclosing namespace
183
244
  # first, the way Ruby does.
184
- def parent_filters(ctx, parent_class, action, skipped, root:, within: nil)
245
+ def parent_filters(ctx, parent_class, action, skipped, root:, within: nil, offsets: Hash.new(0))
185
246
  controllers = Payload.controllers(ctx)
186
247
  seen = Set.new
187
248
  found = {}
@@ -194,18 +255,30 @@ module RailsAiContext
194
255
  evidence = {}
195
256
  depth = 0
196
257
  dropped = skipped.map(&:to_s).to_set
258
+ # Whole only when every class up to ActionController was read: a walk that
259
+ # stops early may have missed the class that declares a name.
260
+ whole = true
261
+ reached = false
197
262
  name = Introspectors::ActionResolver.resolve_entry_name(controllers, parent_class, within)
198
263
 
199
264
  while name && !seen.include?(name)
200
265
  seen << name
201
266
  depth += 1
202
267
  info = controllers[name]
203
- source = info.is_a?(Hash) ? nil : base_controller_source(name, root)
204
- info ||= { filters: base_filters(source, name, root) } if source
268
+ path = info.is_a?(Hash) ? nil : base_controller_path(name, root)
269
+ source = path && SafeFile.read(path)
270
+ if source
271
+ info ||= { filters: base_filters(source, name, root), file: PortablePath.relativize(path, (root || default_root).to_s),
272
+ parent_class: Introspectors::DeclaredConstant.parent_declaration(source, name.to_s)&.superclass }
273
+ end
205
274
  unless info.is_a?(Hash)
275
+ framework = name.to_s.delete_prefix("::").start_with?("ActionController::")
276
+ reached ||= framework
277
+ whole &&= framework
206
278
  name = gem_controller_base(name, root)
207
279
  next
208
280
  end
281
+ whole = false if info[:error]
209
282
 
210
283
  # The class that skips a filter must not contribute it either: in the
211
284
  # booted tier its own list is the reflection list, which carries every
@@ -215,7 +288,9 @@ module RailsAiContext
215
288
  # The walk runs closest ancestor first, so a nearer class's condition
216
289
  # is the one the child inherits.
217
290
  conditions = merge_conditions(conditions_by_name(skips, action), conditions)
218
- carried = Array(info[:filters]).grep(Hash).reject { |f| f[:skipped] }
291
+ # The child's chain names this class's own blocks by the file they are in.
292
+ carried = number_blocks(Introspectors::ControllerFilters.in_file(Array(info[:filters]).grep(Hash).reject { |f| f[:skipped] }, info[:file]),
293
+ offsets)
219
294
  # Counted before the rejects below, so a conditional skip of a name
220
295
  # the walk did see is never mistaken for a skip of a declaration it
221
296
  # could not. A skip record is not a sighting, hence the reject above.
@@ -233,7 +308,7 @@ module RailsAiContext
233
308
  name = Introspectors::ActionResolver.resolve_entry_name(controllers, info[:parent_class], name)
234
309
  end
235
310
 
236
- [ run_order(found, attributed, positions, evidence), dropped, conditions, declares ]
311
+ ParentWalk.new(run_order(found, attributed, positions, evidence), dropped, conditions, declares, whole && reached)
237
312
  end
238
313
 
239
314
  # The closest ancestor carrying a filter keeps its constraints, but a
@@ -266,7 +341,7 @@ module RailsAiContext
266
341
  .sort_by { |key, index| [ -positions.fetch(key, 0), index ] }
267
342
  .map do |key, _|
268
343
  entry = found[key]
269
- next entry if attributed.include?(key) || !evidence[key]
344
+ next entry if attributed.include?(key) || !evidence[key] || entry[:from_concern]
270
345
 
271
346
  entry.merge(from: nil, provenance: "not declared in the controller chain").compact
272
347
  end
@@ -313,7 +388,8 @@ module RailsAiContext
313
388
  def skip_flag_records(info, action)
314
389
  last_records(info, action).select { |_, f| f[:skipped] }
315
390
  .map do |name, f|
316
- { name: name, kind: f[:kind], if: f[:if], unless: f[:unless], only: f[:only], except: f[:except] }
391
+ { name: name, kind: f[:kind], if: f[:if], unless: f[:unless], only: f[:only], except: f[:except],
392
+ condition: f[:condition] }
317
393
  end
318
394
  end
319
395
 
@@ -344,17 +420,9 @@ module RailsAiContext
344
420
  source ||= carried_source(ctx, controller_name, root)
345
421
  return [] unless source
346
422
 
347
- skip_calls(source).flat_map do |call|
348
- options = call[:options] || {}
349
- next [] unless applies?({ only: options[:only], except: options[:except] }, action)
350
-
351
- kind = call[:name].to_s.sub(/\Askip_/, "").sub(/_action\z/, "")
352
- Array(call[:arguments]).select { |a| a.is_a?(Symbol) || a.is_a?(String) }
353
- .map do |a|
354
- { name: a.to_s, kind: kind, if: options[:if], unless: options[:unless],
355
- only: options[:only], except: options[:except] }
356
- end
357
- end.uniq { |skip| skip[:name] }
423
+ skip_records(source).select { |skip| applies?(skip, action) }
424
+ .map { |skip| skip.slice(:name, :kind, :if, :unless, :only, :except, :condition) }
425
+ .uniq { |skip| skip[:name] }
358
426
  rescue => e
359
427
  RailsAiContext.debug_fail(e, [], label: "ActionFilters skip_source_records")
360
428
  end
@@ -365,13 +433,16 @@ module RailsAiContext
365
433
  # printed it. Only a name the app has a file for is read,
366
434
  # so a gem-owned parent, or one an inflection renames, still ends the walk.
367
435
  def base_controller_source(name, root)
436
+ path = base_controller_path(name, root)
437
+ path && SafeFile.read(path)
438
+ end
439
+
440
+ def base_controller_path(name, root)
368
441
  root ||= default_root
369
442
  return nil unless root
370
443
 
371
444
  relative = "#{name.to_s.underscore}.rb"
372
- path = PathResolver.controller_dirs(root.to_s).map { |dir| File.join(dir, relative) }
373
- .find { |candidate| File.exist?(candidate) }
374
- path && SafeFile.read(path)
445
+ PathResolver.controller_dirs(root.to_s).map { |dir| File.join(dir, relative) }.find { |candidate| File.exist?(candidate) }
375
446
  end
376
447
 
377
448
  # Gem controllers whose base class is set in the app's initializers:
@@ -401,11 +472,10 @@ module RailsAiContext
401
472
  # A module whose file is not found is taken for a concern.
402
473
  def runs_once?(name, root, within)
403
474
  root ||= default_root
404
- path = root && ConcernPaths.find_file(root.to_s, name, prefer: "controller", within: within.to_s)
405
- source = path && SafeFile.read(path)
475
+ source = root && ConcernPaths.module_source(root.to_s, name, prefer: "controller", within: within.to_s)
406
476
  return true unless source
407
477
 
408
- RunCache.fetch([ :concern_module, path ]) do
478
+ RunCache.fetch([ :concern_module, source ]) do
409
479
  Introspectors::SourceIntrospector.walk_source(source, { mixins: Introspectors::Listeners::MixinsListener })[:mixins]
410
480
  .any? { |mixin| mixin[:macro] == :extend && mixin[:name] == "ActiveSupport::Concern" }
411
481
  end
@@ -413,7 +483,7 @@ module RailsAiContext
413
483
 
414
484
  # The last value the initializers set; :computed when it is not a string literal.
415
485
  def configured_base(root, setting)
416
- Dir.glob(File.join(root.to_s, "config", "initializers", "**", "*.rb")).sort.filter_map do |path|
486
+ PathResolver.initializer_paths(root).filter_map do |path|
417
487
  source = SafeFile.read(path)
418
488
  next unless source&.include?(setting.delete_suffix("="))
419
489
 
@@ -443,18 +513,21 @@ module RailsAiContext
443
513
  SafeFile.read(File.join(root.to_s, file))
444
514
  end
445
515
 
446
- def skip_calls(source)
447
- Introspectors::SourceIntrospector.walk_source(source, {
448
- skips: -> { Introspectors::Listeners::MethodCallListener.new(names: SKIP_MACROS) }
449
- })[:skips] || []
516
+ # The reader the listing uses, so a skip carries the same only:/except:/if:/unless: and the
517
+ # if/unless around it; one inside a `def` runs only when called, which ControllerFilters reads.
518
+ def skip_records(source)
519
+ RunCache.fetch([ :skip_records, source ]) do
520
+ Introspectors::ControllerFilters.from_source(source).select { |f| f[:skipped] }
521
+ end
450
522
  end
451
523
 
452
524
  private_class_method :default_root, :split, :applies?, :parent_filters, :skip_source_records, :carried_source,
453
525
  :entry_key,
454
526
 
455
- :skip_calls, :base_filters, :skip_flag_records, :redeclared_names, :last_records, :own_skips,
527
+ :skip_records, :base_filters, :skip_flag_records, :redeclared_names, :last_records, :own_skips,
456
528
  :record_attribution, :conditional?, :partial?, :absolute_names, :conditions_by_name,
457
529
  :merge_conditions, :mark_conditional_skips, :skip_tail, :action_names, :condition_text,
458
- :unplaced_conditional_skips, :evidence_skips, :gem_controller_base, :configured_base, :runs_once?
530
+ :unplaced_conditional_skips, :evidence_skips, :configured_base, :runs_once?, :run_sequence,
531
+ :base_controller_path, :number_blocks, :unnumbered
459
532
  end
460
533
  end
@@ -13,14 +13,14 @@ module RailsAiContext
13
13
  "bin/rails server" if File.exist?(File.join(root.to_s, "bin", "rails"))
14
14
  end
15
15
 
16
- # Builds the database from the schema the app keeps, else its migrations.
16
+ # Builds the database from the dump the app keeps, under whatever name schema_dump gives it, else its migrations.
17
17
  # db:setup is not named: it also runs the seeds.
18
18
  #
19
19
  # @return [String, nil] nil for an app with no database config or bin/rails
20
20
  def setup(root)
21
21
  root = root.to_s
22
22
  return nil unless File.exist?(File.join(root, "bin", "rails")) && File.exist?(File.join(root, "config", "database.yml"))
23
- return "bin/rails db:create db:schema:load" if %w[schema.rb structure.sql].any? { |f| File.exist?(File.join(root, "db", f)) }
23
+ return "bin/rails db:create db:schema:load" if Introspectors::SchemaDumpPath.present(root)
24
24
  return "bin/rails db:create db:migrate" if Dir.exist?(File.join(root, "db", "migrate"))
25
25
 
26
26
  "bin/rails db:create"
@@ -7,16 +7,73 @@ module RailsAiContext
7
7
  module_function
8
8
 
9
9
  def mongoid?(root)
10
- root = root.to_s
11
- return true if File.exist?(File.join(root, "config", "mongoid.yml"))
10
+ File.exist?(File.join(root.to_s, "config", "mongoid.yml")) || uses_gem?(root, "mongoid")
11
+ end
12
+
13
+ def sequel?(root)
14
+ uses_gem?(root, "sequel-rails", "sequel")
15
+ end
16
+
17
+ def uses_gem?(root, *names)
18
+ lock = RailsAiContext::GemLock.for(root.to_s)
19
+ return names.any? { |name| lock.present?(name) } unless lock.missing?
20
+
21
+ # No lockfile yet (fresh checkout, bare directory): the Gemfile's own declaration.
22
+ RailsAiContext::Introspectors::GemfileGems.names(root.to_s).intersect?(names)
23
+ end
24
+
25
+ ACTIVE_RECORD_REQUIRES = %w[rails/all active_record/railtie active_record].freeze
26
+
27
+ # False only when config/application.rb picks its frameworks railtie by
28
+ # railtie and leaves Active Record's out (a require, or a list it requires
29
+ # in a loop) and never configures it; a file it cannot read is taken to load it.
30
+ def active_record?(root)
31
+ source = RailsAiContext::SafeFile.read(File.join(root.to_s, "config", "application.rb"))
32
+ tree = source && RailsAiContext::AstCache.parse_string(source)
33
+ return true unless tree && tree.errors.empty?
34
+
35
+ nodes = Introspectors::AstWalk.each(tree.value).to_a
36
+ strings = nodes.grep(Prism::StringNode).map(&:unescaped)
37
+ return true if strings.intersect?(ACTIVE_RECORD_REQUIRES) || nodes.grep(Prism::CallNode).any? { |n| n.name == :active_record }
38
+
39
+ strings.none? { |s| s.end_with?("/railtie", "/engine") }
40
+ rescue StandardError, ScriptError
41
+ true
42
+ end
43
+
44
+ # Why Active Record introspection does not apply here, or nil when it does.
45
+ def without_active_record(root)
46
+ return nil if active_record?(root)
47
+
48
+ return "this app uses Mongoid" if mongoid?(root)
49
+ return "this app uses Sequel" if sequel?(root)
50
+
51
+ "this app does not load Active Record"
52
+ end
53
+
54
+ # Why the schema files Active Record would read are not its own: sequel-rails keeps its
55
+ # schema.rb and migrations in the same places, written in Sequel's DSL. Nil when they are.
56
+ def sequel_schema(root)
57
+ return nil unless sequel?(root)
58
+
59
+ format, dump = Introspectors::SchemaDumpPath.present(root)
60
+ return nil if format == :sql
61
+
62
+ file = dump || Introspectors::MigrationReplay.migration_files(Introspectors::MigrationReplay.migration_dirs(root), root: root).first
63
+ return nil unless file && sequel_migration?(RailsAiContext::SafeFile.read(file))
64
+
65
+ "#{file.delete_prefix("#{root.to_s.chomp('/')}/")} is a Sequel migration"
66
+ end
12
67
 
13
- lock = RailsAiContext::GemLock.for(root)
14
- return lock.present?("mongoid") unless lock.missing?
68
+ def sequel_migration?(source)
69
+ tree = source && RailsAiContext::AstCache.parse_string(source)
70
+ return false unless tree&.value
15
71
 
16
- # No lockfile yet (fresh checkout, bare directory): fall back to the
17
- # Gemfile's own declaration so the app still gets Mongoid treatment
18
- # instead of misleading ActiveRecord answers.
19
- RailsAiContext::Introspectors::GemfileGems.names(root).include?("mongoid")
72
+ tree.value.statements.body.any? do |node|
73
+ node.is_a?(Prism::CallNode) && node.name == :migration &&
74
+ (node.receiver.is_a?(Prism::ConstantReadNode) || node.receiver.is_a?(Prism::ConstantPathNode)) &&
75
+ node.receiver.slice.delete_prefix("::") == "Sequel"
76
+ end
20
77
  end
21
78
 
22
79
  # The development database config/mongoid.yml names, by `database:` or the