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
@@ -40,11 +40,12 @@ module RailsAiContext
40
40
  # @param lookup [#call, nil] constant name -> that class's source
41
41
  # @param only [String, nil] answer for this class name alone, ignoring the file's other
42
42
  # declarations (a concern that nests a validator class declares both)
43
+ # @param path_name [String, nil] the constant the source's path spells, so its Class.new class counts
43
44
  # @return [Array<Link>]
44
- def to(source, bases:, lookup: nil, seen: [], only: nil)
45
+ def to(source, bases:, lookup: nil, seen: [], only: nil, path_name: only)
45
46
  return [] if source.nil? || seen.size >= MAX_DEPTH
46
47
 
47
- declarations = DeclaredConstant.declarations(source)
48
+ declarations = DeclaredConstant.declarations(source, path_name: path_name)
48
49
  declarations = [ DeclaredConstant.declaration_named(declarations, only) ].compact if only
49
50
  return [] if declarations.empty?
50
51
 
@@ -58,7 +59,7 @@ module RailsAiContext
58
59
  parent_source = lookup&.call(parent)
59
60
  next if parent_source.nil?
60
61
 
61
- rest = to(parent_source, bases: bases, lookup: lookup, seen: seen + [ parent ])
62
+ rest = to(parent_source, bases: bases, lookup: lookup, seen: seen + [ parent ], path_name: parent)
62
63
  return [ Link.new(name: candidate.name, superclass: parent, source: source) ] + rest if rest.any?
63
64
  end
64
65
 
@@ -95,21 +96,13 @@ module RailsAiContext
95
96
  name == base || name.end_with?("::#{base}")
96
97
  end
97
98
 
98
- # Ruby resolves a bare superclass from the enclosing namespace outward,
99
- # so `Fasp::BackfillWorker < BaseWorker` means Fasp::BaseWorker where
100
- # that exists and ::BaseWorker where it does not.
101
- #
102
- # @param declared [String] the subclass's fully qualified name
103
- # @param parent [String, nil] the superclass as the source writes it
104
- # @yieldparam candidate [String] a name to try, nearest scope first
105
- # @return [Object, nil] the block's first truthy answer
106
- def resolve_in_scope(declared, parent)
99
+ # A bare superclass resolves through Module.nesting: `module Fasp; class W < BaseWorker` tries Fasp::BaseWorker first.
100
+ # A nil `nesting` is read off the declared name, which is right for the nested form only.
101
+ def resolve_in_scope(declared, parent, nesting: nil)
107
102
  parent = parent.to_s.delete_prefix("::")
108
103
  return nil if parent.empty?
109
104
 
110
- scope = declared.to_s.split("::")[0..-2]
111
- scope.size.downto(0) do |i|
112
- candidate = (scope.first(i) + [ parent ]).join("::")
105
+ ((nesting || nesting_of(declared)).map { |scope| "#{scope}::#{parent}" } + [ parent ]).each do |candidate|
113
106
  # `class CostQuery::Export < Export` names the top-level Export; the
114
107
  # nearest candidate is the class itself, which is nobody's parent.
115
108
  next if candidate == declared.to_s
@@ -120,6 +113,12 @@ module RailsAiContext
120
113
  nil
121
114
  end
122
115
 
116
+ # The nesting the nested form gives a class: every enclosing name, innermost first.
117
+ def nesting_of(declared)
118
+ scope = declared.to_s.split("::")[0..-2]
119
+ scope.size.downto(1).map { |i| scope.first(i).join("::") }
120
+ end
121
+
123
122
  # A callable from a constant name to the source of the file declaring
124
123
  # it, probed against the app's autoload roots rather than a walk over
125
124
  # the tree: Zeitwerk resolves a constant to one path under one root, and
@@ -13,23 +13,18 @@ module RailsAiContext
13
13
  # namespace and the superclass chain is the caller's business, because only
14
14
  # it knows the other files.
15
15
  #
16
- # It reads table_name, table_name_prefix and table_name_suffix and nothing
17
- # else: a class-level `self.primary_key =` is just as invisible to the
18
- # static tier, and answering one of those and not the other would be worse
19
- # than answering neither.
16
+ # It reads table_name, table_name_prefix, table_name_suffix,
17
+ # pluralize_table_names and a literal `self.primary_key =`.
20
18
  module TableName
21
19
  module_function
22
20
 
23
- NONE = { table_name: nil, table_name_prefix: nil, table_name_suffix: nil }.freeze
24
-
25
- PREFIX_INDEX = Concurrent::Map.new
26
- APP_AFFIXES = Concurrent::Map.new
21
+ NONE = { table_name: nil, table_name_prefix: nil, table_name_suffix: nil, pluralize_table_names: nil, primary_key: nil }.freeze
27
22
 
28
23
  # {"RssPolling" => "rss_polling_"}: the engine prefix no model file says.
29
24
  # ponytail: reads `lib/**/engine.rb` only; walk lib wholesale if an app needs more.
30
25
  def namespace_prefixes(root)
31
26
  root = File.expand_path(root.to_s)
32
- PREFIX_INDEX.compute_if_absent(root) { read_namespace_prefixes(root) }
27
+ RunCache.fetch([ :namespace_prefixes, root ]) { read_namespace_prefixes(root) }
33
28
  end
34
29
 
35
30
  # A table an option writes as that source (a habtm join_table), read the
@@ -39,31 +34,18 @@ module RailsAiContext
39
34
  statements&.size == 1 ? affixed_node(statements.first, own, root) : nil
40
35
  end
41
36
 
42
- def clear_namespace_prefixes
43
- PREFIX_INDEX.clear
44
- APP_AFFIXES.clear
45
- end
46
-
47
- # {table_name_prefix: "op_"}: what config/application.rb sets on
48
- # config.active_record, the class attribute's value in every model.
49
- def app_affixes(root)
50
- return {} unless root
51
-
52
- root = File.expand_path(root.to_s)
53
- APP_AFFIXES.compute_if_absent(root) { read_app_affixes(root) }
54
- end
55
-
56
- # All three declarations of one class body, read in one walk.
37
+ # Every declaration of one class body, read in one walk.
57
38
  #
58
39
  # @param source [String] the file's source
59
40
  # @param name [String] the qualified name of the class or module
60
- # @return [Hash] the three, each nil when this scope declares none
41
+ # @return [Hash] the five, each nil when this scope declares none
61
42
  # @param root [String, nil] the app, whose configured affixes a table
62
43
  # name interpolating table_name_prefix or table_name_suffix reads
63
44
  def declarations(source, name, root = nil)
64
45
  read(source, name) do |body|
65
46
  own = { table_name_prefix: affix(body, :table_name_prefix), table_name_suffix: affix(body, :table_name_suffix) }
66
47
  { table_name: assigned(body, :table_name=) || interpolated(body, own, root) }.merge(own)
48
+ .merge(pluralize_table_names: boolean_assigned(body, :pluralize_table_names=), primary_key: primary_key_assigned(body))
67
49
  end || NONE
68
50
  end
69
51
 
@@ -77,8 +59,9 @@ module RailsAiContext
77
59
  # name already carries that inflection - Zeitwerk resolved the constant
78
60
  # from it. Underscoring the constant instead turns OAuthClientConfig into
79
61
  # o_auth_client_configs, a table no app has.
80
- def stem(path)
81
- File.basename(path.to_s, ".rb").pluralize
62
+ def stem(path, pluralize = true)
63
+ base = File.basename(path.to_s, ".rb")
64
+ pluralize ? base.pluralize : base
82
65
  end
83
66
 
84
67
  # The table a model name alone implies. The namespace is not part of it:
@@ -141,21 +124,6 @@ module RailsAiContext
141
124
  RailsAiContext.debug_fail(e, {}, label: "namespace_prefixes")
142
125
  end
143
126
 
144
- def read_app_affixes(root)
145
- path = File.join(root, "config", "application.rb")
146
- return {} unless File.file?(path)
147
-
148
- Array(SourceIntrospector.walk(path, { config: Listeners::ConfigAssignmentListener })[:config])
149
- .each_with_object({}) do |entry, found|
150
- path = entry[:path]
151
- next unless path.size == 2 && path.first == :active_record && entry[:value].is_a?(String)
152
-
153
- found[path.last] = entry[:value] if %i[table_name_prefix table_name_suffix].include?(path.last)
154
- end
155
- rescue StandardError, ScriptError => e
156
- RailsAiContext.debug_fail(e, {}, label: "app_affixes")
157
- end
158
-
159
127
  def engine_files(root)
160
128
  roots = [ root ] + PathResolver.code_roots(root)
161
129
  roots.flat_map { |dir| Dir.glob(File.join(dir, "lib", "**", "engine.rb")) }.uniq.sort
@@ -207,11 +175,24 @@ module RailsAiContext
207
175
  return statements(node) if qualified == name
208
176
 
209
177
  descend(node, scope + [ segment(node) ], name)
178
+ when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
179
+ written = node.is_a?(Prism::ConstantWriteNode) ? node.name.to_s : node.target.slice.delete_prefix("::")
180
+ return built_body(node.value) if (scope + [ written ]).join("::") == name
181
+
182
+ descend(node, scope, name)
210
183
  else
211
184
  descend(node, scope, name)
212
185
  end
213
186
  end
214
187
 
188
+ # `Name = Class.new(Base) do ... end` runs the block as Name's body.
189
+ def built_body(value)
190
+ return [] unless value.is_a?(Prism::CallNode) && value.name == :new && value.receiver&.slice == "Class"
191
+
192
+ block = value.block
193
+ block.is_a?(Prism::BlockNode) && block.body.is_a?(Prism::StatementsNode) ? block.body.body : []
194
+ end
195
+
215
196
  def descend(node, scope, name)
216
197
  node.child_nodes.compact.each do |child|
217
198
  found = body_of(child, scope, name)
@@ -236,6 +217,26 @@ module RailsAiContext
236
217
  call && literal(call.arguments&.arguments)
237
218
  end
238
219
 
220
+ # A name, or a composite key's names; nil for anything computed.
221
+ def primary_key_assigned(body)
222
+ call = body.find { |node| node.is_a?(Prism::CallNode) && node.name == :primary_key= && node.receiver.is_a?(Prism::SelfNode) }
223
+ args = call&.arguments&.arguments
224
+ return literal(args) unless args&.size == 1 && args.first.is_a?(Prism::ArrayNode)
225
+
226
+ names = args.first.elements.map { |element| literal([ element ]) }
227
+ names if names.all?
228
+ end
229
+
230
+ def boolean_assigned(body, name)
231
+ call = body.find do |node|
232
+ node.is_a?(Prism::CallNode) && node.name == name && node.receiver.is_a?(Prism::SelfNode)
233
+ end
234
+ args = call&.arguments&.arguments
235
+ return nil unless args&.size == 1
236
+
237
+ { Prism::TrueNode => true, Prism::FalseNode => false }[args.first.class]
238
+ end
239
+
239
240
  # `self.table_name = "#{table_name_prefix}users#{table_name_suffix}"`: the class's
240
241
  # own affix, else the app's configured one.
241
242
  def interpolated(body, own, root)
@@ -253,7 +254,7 @@ module RailsAiContext
253
254
  next part.unescaped if part.is_a?(Prism::StringNode)
254
255
 
255
256
  key = affix_read(part) or return nil
256
- own[key] || app_affixes(root)[key] || ""
257
+ own[key] || ActiveRecordSettings.for(root)[key] || ""
257
258
  end.join
258
259
  end
259
260
 
@@ -284,8 +285,8 @@ module RailsAiContext
284
285
  end
285
286
  end
286
287
 
287
- private_class_method :affix, :read, :body_of, :descend, :statements, :segment,
288
- :assigned, :returned, :literal, :read_namespace_prefixes, :read_app_affixes,
288
+ private_class_method :affix, :read, :body_of, :built_body, :descend, :statements, :segment,
289
+ :assigned, :boolean_assigned, :primary_key_assigned, :returned, :literal, :read_namespace_prefixes,
289
290
  :interpolated, :affixed_node, :affix_read,
290
291
  :engine_files, :collect_isolate_calls
291
292
  end