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
@@ -14,6 +14,8 @@ module RailsAiContext
14
14
  include: %w[included append_features], prepend: %w[prepended prepend_features], extend: %w[extended]
15
15
  }.freeze
16
16
  MIXIN_HOOKS = HOOKS_BY_MACRO.values.flatten.freeze
17
+ # The macros that give the class a module's methods as class methods.
18
+ SINGLETON_MACROS = %i[extend singleton_include singleton_prepend].freeze
17
19
  # The block ActiveSupport::Concern runs for each.
18
20
  CONCERN_BLOCKS = { include: :included, prepend: :prepended }.freeze
19
21
  FRAMEWORK_PREFIXES = %w[
@@ -60,12 +62,30 @@ module RailsAiContext
60
62
 
61
63
  # Matched on the last segment: `module Edition; module Featurable` and
62
64
  # `module Edition::Featurable` both write one constant.
63
- def own_mixins(mixins, name)
64
- return Array(mixins) if Array(mixins).none? { |mixin| mixin.key?(:owner) }
65
+ # An edition module (`prepend_mod_with`) counts only where `root` holds its file.
66
+ def owned_by(records, name, root: nil)
67
+ return Array(records) if Array(records).none? { |record| record.key?(:owner) }
65
68
 
66
- own = own_owner(mixins, name)
67
- # `Other.include X` written in the body mixes into Other.
68
- Array(mixins).select { |mixin| own && !mixin[:receiver] && Array(mixin[:owner]).join("::") == own }
69
+ own = own_owner(records, name)
70
+ Array(records).filter_map do |record|
71
+ next if record[:edition] && !(root && ConcernPaths.find_file(root.to_s, record[:name]))
72
+
73
+ if record[:receiver]
74
+ mixed_into(record, name)
75
+ elsif own && Array(record[:owner]).join("::") == own
76
+ record
77
+ end
78
+ end
79
+ end
80
+
81
+ # `Note.prepend X` mixes into Note as `prepend X` in its body would; `Other.include X` is Other's.
82
+ def mixed_into(record, name)
83
+ receiver = record[:receiver].to_s
84
+ target = receiver.start_with?("::") ? receiver.delete_prefix("::") : [ *Array(record[:owner]), receiver ].join("::")
85
+ return unless target == name.to_s
86
+
87
+ own = record.except(:receiver)
88
+ record.key?(:ancestor) ? own.merge(ancestor: CONCERN_BLOCKS.key?(record[:macro])) : own
69
89
  end
70
90
 
71
91
  def own_owner(records, name)
@@ -133,22 +133,48 @@ module RailsAiContext
133
133
  # bare `include DebugConcern` inside Fasp::Provider resolves at runtime
134
134
  # to Fasp::Provider::DebugConcern, which the literal spelling misses.
135
135
  # @param dirs [Array<String>, nil] pre-resolved concern directories
136
- def find_file(root, concern_name, prefer: nil, within: nil, dirs: nil)
137
- # Wherever Zeitwerk would look, plus an in-repo path gem's lib. A lib the app does
138
- # not add to its autoload paths holds nothing it autoloads, so it is skipped.
139
- dirs = ordered_dirs(root, prefer, dirs) + PathResolver.app_roots(root) + PathResolver.declared_roots(root) +
140
- PathResolver.path_gem_libs(root)
141
-
142
- candidate_names(concern_name, within).each do |name|
143
- underscore = name.underscore
144
- next if underscore.empty? || underscore.include?("..")
145
-
146
- found = dirs.find { |dir| file_exist?(dir, "#{underscore}.rb") }
147
- path = found && File.join(found, "#{underscore}.rb")
148
- return path if path
136
+ # @param outer [Boolean] whether a module with no file of its own is found in its outer constant's file
137
+ def find_file(root, concern_name, prefer: nil, within: nil, dirs: nil, outer: true)
138
+ found = find_named(root, concern_name, prefer: prefer, within: within, dirs: dirs)
139
+ (found || (outer_named(root, concern_name, within: within, dirs: dirs) if outer))&.last
140
+ end
141
+
142
+ # The source of the module a reference resolves to: its file, or its own node in its outer constant's file.
143
+ def module_source(root, concern_name, prefer: nil, within: nil)
144
+ path = find_file(root, concern_name, prefer: prefer, within: within, outer: false)
145
+ return SafeFile.read(path) if path
146
+
147
+ name, path = outer_named(root, concern_name, within: within)
148
+ path && Introspectors::DeclaredConstant.module_node(AstCache.parse(path).value, name)&.slice
149
+ end
150
+
151
+ # [`Outer::Inner`, Outer's file] for a module with no file of its own, which Zeitwerk loads with Outer.
152
+ def outer_named(root, concern_name, within: nil, dirs: nil)
153
+ RunCache.fetch([ :outer_named, root, concern_name, within, dirs ]) do
154
+ candidate_names(concern_name, within).lazy.filter_map do |name|
155
+ outer = name.rpartition("::").first
156
+ path = !outer.empty? && find_file(root, outer, dirs: dirs)
157
+ [ name, path ] if path && Introspectors::DeclaredConstant.module_node(AstCache.parse(path).value, name)
158
+ end.first
149
159
  end
160
+ end
161
+
162
+ # [the constant the reference resolves to, its file] for `find_file`'s file, or nil.
163
+ def find_named(root, concern_name, prefer: nil, within: nil, dirs: nil)
164
+ # Every model asks for the same few names, each across hundreds of directories.
165
+ RunCache.fetch([ :find_named, root, concern_name, prefer, within, dirs ]) do
166
+ # Wherever Zeitwerk would look, plus an in-repo path gem's lib. A lib the app does
167
+ # not add to its autoload paths holds nothing it autoloads, so it is skipped.
168
+ searched = ordered_dirs(root, prefer, dirs) + PathResolver.app_roots(root) + PathResolver.declared_roots(root) +
169
+ PathResolver.path_gem_libs(root)
170
+ candidate_names(concern_name, within).lazy.filter_map do |name|
171
+ underscore = name.underscore
172
+ next if underscore.empty? || underscore.include?("..")
150
173
 
151
- nil
174
+ found = searched.find { |dir| file_exist?(dir, "#{underscore}.rb") }
175
+ [ name, File.join(found, "#{underscore}.rb") ] if found
176
+ end.first
177
+ end
152
178
  end
153
179
 
154
180
  # Each directory is listed once per run and a candidate is walked down
@@ -166,9 +192,28 @@ module RailsAiContext
166
192
  end
167
193
  RunCache.fetch([ :exist, current ]) { File.exist?(current) }
168
194
  end
195
+ private_class_method :file_exist?
196
+
197
+ # `relative` as the disk spells it: an app acronym underscores `ActivityPub` to `activitypub`,
198
+ # where this process, which has none of the app's acronyms, writes `activity_pub`. Nil when absent.
199
+ def spelled(dir, relative)
200
+ return relative if file_exist?(dir, relative)
201
+
202
+ current = dir
203
+ segments = relative.split("/").map do |segment|
204
+ found = listing(current).squashed[segment.delete("_")] or return nil
205
+ current = File.join(current, found)
206
+ found
207
+ end
208
+ RunCache.fetch([ :exist, current ]) { File.exist?(current) } ? segments.join("/") : nil
209
+ end
169
210
 
170
211
  Listing = Struct.new(:names, :folded) do
171
212
  def include?(name) = names.include?(name)
213
+
214
+ def squashed
215
+ @squashed ||= names.each_with_object({}) { |name, found| found[name.delete("_")] ||= name }
216
+ end
172
217
  end
173
218
 
174
219
  def listing(dir)
@@ -189,7 +234,6 @@ module RailsAiContext
189
234
  File.identical?(File.join(dir, name), File.join(dir, name.swapcase))
190
235
  end
191
236
  private_class_method :case_insensitive?
192
- private_class_method :file_exist?
193
237
 
194
238
  def ordered_dirs(root, prefer, dirs = nil)
195
239
  dirs ||= resolve(root)
@@ -201,10 +245,12 @@ module RailsAiContext
201
245
  # The enclosing namespaces from the innermost outward, then the reference
202
246
  # itself - Ruby's own constant lookup order, which reaches the top level
203
247
  # last. Bare-name-first would bind Fasp::Provider's `include DebugConcern`
204
- # to a top-level DebugConcern the runtime never sees.
248
+ # to a top-level DebugConcern the runtime never sees. A qualified name's
249
+ # first segment is looked up the same way; only a leading `::` skips it.
205
250
  def candidate_names(concern_name, within)
206
251
  name = concern_name.to_s
207
- return [ name ] if within.nil? || name.include?("::")
252
+ return [ name.delete_prefix("::") ] if name.start_with?("::")
253
+ return [ name ] if within.nil?
208
254
 
209
255
  scopes = within.to_s.split("::")
210
256
  scopes.size.downto(1).map { |n| "#{scopes.first(n).join('::')}::#{name}" } + [ name ]
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "yaml"
5
+
6
+ module RailsAiContext
7
+ # An app's config YAML read without running its ERB: string keys, and a failure
8
+ # logged under the caller's label and answered as nil.
9
+ module ConfigYaml
10
+ ERB_OUTPUT = "RAC_ERB_OUTPUT"
11
+
12
+ module_function
13
+
14
+ # With `marker` an output tag reads as that marker, which marked? finds; without it, as empty.
15
+ def read(root, file, label:, marker: nil)
16
+ content = SafePath.read(file, under: root.to_s).first or return nil
17
+ content = marker ? ErbSource.with_output_marked(content, marker) : ErbSource.without_tags(content)
18
+ stringify_keys(YAML.safe_load(content, aliases: true, permitted_classes: [ Symbol, Date, Time ]))
19
+ rescue StandardError, ScriptError => e
20
+ RailsAiContext.debug_fail(e, nil, label: "#{label} #{file}")
21
+ end
22
+
23
+ def marked?(value)
24
+ value.is_a?(String) && value.include?(ERB_OUTPUT)
25
+ end
26
+
27
+ # Each output tag reads as a numbered marker; shown, every one is `<%= ... %>` again.
28
+ def shown(text)
29
+ text.to_s.gsub(/#{ERB_OUTPUT}(?:_\d+_)?/o, "<%= ... %>")
30
+ end
31
+
32
+ def stringify_keys(value)
33
+ case value
34
+ when Hash then value.to_h { |key, inner| [ key.to_s.delete_prefix(":"), stringify_keys(inner) ] }
35
+ when Array then value.map { |inner| stringify_keys(inner) }
36
+ else value
37
+ end
38
+ end
39
+ end
40
+ end
@@ -362,7 +362,7 @@ module RailsAiContext
362
362
  attr_accessor :concern_paths # Where to look for concern source files (default: nil, discovers app/*/concerns)
363
363
 
364
364
  # Frontend framework detection (optional overrides - auto-detected if nil)
365
- attr_accessor :frontend_paths # User-declared frontend dirs (e.g. ["app/frontend", "../web-client"])
365
+ attr_accessor :frontend_paths # User-declared frontend dirs (e.g. ["app/frontend", "../web-client"]; one outside the app root is read for manifests only)
366
366
 
367
367
  # Additional app-root-relative directories that also contain Rails app
368
368
  # code (e.g. ["src", "vendor/internal"]). Code discovery scans each
@@ -387,8 +387,8 @@ module RailsAiContext
387
387
  @server_name = "rails-ai-context"
388
388
  @introspectors = PRESETS[:full].dup
389
389
  @excluded_paths = %w[node_modules tmp log vendor .git doc docs]
390
- # Files each tool's own docs say to gitignore. The patterns leave the placeholder
391
- # beside one (config/application.example.yml, .env.example) readable.
390
+ # Files each tool's own docs say to gitignore. A placeholder beside one
391
+ # (config/application.example.yml, .env.example) stays readable: see SafePath.sensitive?.
392
392
  @sensitive_patterns = %w[
393
393
  .env .env.* *.env .envrc
394
394
  config/master.key
@@ -119,6 +119,7 @@
119
119
  "id": "active_record_encryption",
120
120
  "title": "Active Record Encryption",
121
121
  "source": "rails",
122
+ "since": "7.0",
122
123
  "summary": "Encrypt model attributes at rest with deterministic and non-deterministic encryption",
123
124
  "keywords": [
124
125
  "encryption",
@@ -502,6 +503,329 @@
502
503
  ],
503
504
  "category": "api"
504
505
  },
506
+ {
507
+ "id": "action_controller_advanced_topics",
508
+ "title": "Action Controller Advanced Topics",
509
+ "source": "rails",
510
+ "since": "8.0",
511
+ "summary": "CSRF protection, HTTP authentication, streaming, log parameter filtering, rescue_from, health check endpoint",
512
+ "keywords": [
513
+ "csrf",
514
+ "http authentication",
515
+ "streaming",
516
+ "send_data",
517
+ "filter parameters",
518
+ "rescue_from",
519
+ "health check"
520
+ ],
521
+ "category": "controllers"
522
+ },
523
+ {
524
+ "id": "active_model_basics",
525
+ "title": "Active Model Basics",
526
+ "source": "rails",
527
+ "summary": "Active Model modules for plain Ruby objects: attributes, validations, callbacks, serialization, naming",
528
+ "keywords": [
529
+ "active model",
530
+ "activemodel",
531
+ "plain ruby object",
532
+ "form object",
533
+ "attributes",
534
+ "serialization"
535
+ ],
536
+ "category": "models"
537
+ },
538
+ {
539
+ "id": "active_record_composite_primary_keys",
540
+ "title": "Composite Primary Keys",
541
+ "source": "rails",
542
+ "since": "7.1",
543
+ "summary": "Tables, queries, associations, forms and fixtures with composite primary keys",
544
+ "keywords": [
545
+ "composite primary key",
546
+ "primary_key",
547
+ "query_constraints",
548
+ "multi-column key"
549
+ ],
550
+ "category": "models"
551
+ },
552
+ {
553
+ "id": "active_record_postgresql",
554
+ "title": "Active Record and PostgreSQL",
555
+ "source": "rails",
556
+ "summary": "PostgreSQL datatypes, UUID keys, deferrable foreign keys, exclusion constraints, full text search, views",
557
+ "keywords": [
558
+ "postgresql",
559
+ "postgres",
560
+ "jsonb",
561
+ "uuid",
562
+ "array",
563
+ "full text search",
564
+ "exclusion constraint",
565
+ "database views"
566
+ ],
567
+ "category": "models"
568
+ },
569
+ {
570
+ "id": "active_support_core_extensions",
571
+ "title": "Active Support Core Extensions",
572
+ "source": "rails",
573
+ "summary": "Ruby language extensions Active Support adds: blank?, present?, try, String, Hash, Array, Date and Time helpers",
574
+ "keywords": [
575
+ "core extensions",
576
+ "core_ext",
577
+ "blank?",
578
+ "present?",
579
+ "try",
580
+ "string",
581
+ "hash",
582
+ "time"
583
+ ],
584
+ "category": "core"
585
+ },
586
+ {
587
+ "id": "active_support_instrumentation",
588
+ "title": "Active Support Instrumentation",
589
+ "source": "rails",
590
+ "summary": "ActiveSupport::Notifications: subscribing to hooks, the framework's instrumentation events, custom instrumentation",
591
+ "keywords": [
592
+ "instrumentation",
593
+ "notifications",
594
+ "subscribe",
595
+ "instrument",
596
+ "events",
597
+ "activesupport::notifications"
598
+ ],
599
+ "category": "core"
600
+ },
601
+ {
602
+ "id": "autoloading_and_reloading_constants",
603
+ "title": "Autoloading and Reloading Constants",
604
+ "source": "rails",
605
+ "summary": "Zeitwerk autoloading, reloading and eager loading, autoload paths, project structure, STI",
606
+ "keywords": [
607
+ "autoloading",
608
+ "autoload",
609
+ "zeitwerk",
610
+ "reloading",
611
+ "eager loading",
612
+ "autoload_paths",
613
+ "constants"
614
+ ],
615
+ "category": "core"
616
+ },
617
+ {
618
+ "id": "debugging_rails_applications",
619
+ "title": "Debugging Rails Applications",
620
+ "source": "rails",
621
+ "summary": "Debug helpers, the logger, the debug gem, breakpoints, stack traces, SQL query sources",
622
+ "keywords": [
623
+ "debugging",
624
+ "debug",
625
+ "debugger",
626
+ "logger",
627
+ "breakpoint",
628
+ "stack trace",
629
+ "byebug"
630
+ ],
631
+ "category": "core"
632
+ },
633
+ {
634
+ "id": "engines",
635
+ "title": "Getting Started with Engines",
636
+ "source": "rails",
637
+ "summary": "Generating and mounting engines, isolated namespaces, overriding engine code, load hooks",
638
+ "keywords": [
639
+ "engine",
640
+ "engines",
641
+ "mountable",
642
+ "isolate_namespace",
643
+ "mount",
644
+ "railtie"
645
+ ],
646
+ "category": "core"
647
+ },
648
+ {
649
+ "id": "error_reporting",
650
+ "title": "Error Reporting in Rails Applications",
651
+ "source": "rails",
652
+ "since": "7.1",
653
+ "summary": "Rails.error reporter: handle, record, report, and custom error subscribers",
654
+ "keywords": [
655
+ "error reporting",
656
+ "error reporter",
657
+ "rails.error",
658
+ "exceptions",
659
+ "subscriber",
660
+ "error tracking"
661
+ ],
662
+ "category": "core"
663
+ },
664
+ {
665
+ "id": "generators",
666
+ "title": "Rails Generators & Templates",
667
+ "source": "rails",
668
+ "summary": "Listing, creating and customizing generators, overriding scaffold templates, application templates",
669
+ "keywords": [
670
+ "generators",
671
+ "generator",
672
+ "rails generate",
673
+ "templates",
674
+ "scaffold templates",
675
+ "application template"
676
+ ],
677
+ "category": "core"
678
+ },
679
+ {
680
+ "id": "getting_started_with_devcontainer",
681
+ "title": "Getting Started with Dev Containers",
682
+ "source": "rails",
683
+ "since": "7.2",
684
+ "summary": "rails-new and running a Rails app inside a development container",
685
+ "keywords": [
686
+ "devcontainer",
687
+ "dev container",
688
+ "docker",
689
+ "rails-new",
690
+ "development environment"
691
+ ],
692
+ "category": "core"
693
+ },
694
+ {
695
+ "id": "initialization",
696
+ "title": "The Rails Initialization Process",
697
+ "source": "rails",
698
+ "summary": "The boot sequence: bin/rails, config/boot.rb, config/application.rb, Rails::Server",
699
+ "keywords": [
700
+ "initialization",
701
+ "boot",
702
+ "boot process",
703
+ "startup",
704
+ "rails server",
705
+ "config/application.rb"
706
+ ],
707
+ "category": "core"
708
+ },
709
+ {
710
+ "id": "plugins",
711
+ "title": "The Basics of Creating Rails Plugins",
712
+ "source": "rails",
713
+ "summary": "Creating a plugin gem, extending core classes, adding methods to ApplicationRecord, publishing",
714
+ "keywords": [
715
+ "plugins",
716
+ "plugin",
717
+ "gem",
718
+ "extend",
719
+ "acts_as"
720
+ ],
721
+ "category": "core"
722
+ },
723
+ {
724
+ "id": "product_reviews",
725
+ "title": "Product Reviews",
726
+ "source": "rails",
727
+ "since": "8.1",
728
+ "summary": "Getting Started tutorial series: collecting reviews, average ratings, filtering reviews",
729
+ "keywords": [
730
+ "reviews",
731
+ "ratings",
732
+ "tutorial",
733
+ "e-commerce"
734
+ ],
735
+ "category": "core"
736
+ },
737
+ {
738
+ "id": "rails_on_rack",
739
+ "title": "Rails on Rack",
740
+ "source": "rails",
741
+ "summary": "Rack, the middleware stack, configuring middleware, Rails as a Rack application",
742
+ "keywords": [
743
+ "rack",
744
+ "middleware",
745
+ "middleware stack",
746
+ "config.ru",
747
+ "rack app"
748
+ ],
749
+ "category": "core"
750
+ },
751
+ {
752
+ "id": "sign_up_and_settings",
753
+ "title": "Sign Up and Settings",
754
+ "source": "rails",
755
+ "since": "8.1",
756
+ "summary": "Getting Started tutorial series: user sign up, rate limiting, nested layouts, admin and user controllers",
757
+ "keywords": [
758
+ "sign up",
759
+ "signup",
760
+ "registration",
761
+ "settings",
762
+ "rate_limit",
763
+ "tutorial"
764
+ ],
765
+ "category": "core"
766
+ },
767
+ {
768
+ "id": "threading_and_code_execution",
769
+ "title": "Threading and Code Execution in Rails",
770
+ "source": "rails",
771
+ "summary": "Concurrency in Rails, the executor and reloader, wrapping application code, load interlock",
772
+ "keywords": [
773
+ "threading",
774
+ "threads",
775
+ "concurrency",
776
+ "executor",
777
+ "reloader",
778
+ "load interlock"
779
+ ],
780
+ "category": "core"
781
+ },
782
+ {
783
+ "id": "tuning_performance_for_deployment",
784
+ "title": "Tuning Performance for Deployment",
785
+ "source": "rails",
786
+ "since": "7.2",
787
+ "summary": "Puma workers and threads, YJIT, memory allocators, performance testing for production",
788
+ "keywords": [
789
+ "performance",
790
+ "puma",
791
+ "tuning",
792
+ "workers",
793
+ "threads",
794
+ "yjit",
795
+ "deployment"
796
+ ],
797
+ "category": "core"
798
+ },
799
+ {
800
+ "id": "upgrading_ruby_on_rails",
801
+ "title": "Upgrading Ruby on Rails",
802
+ "source": "rails",
803
+ "summary": "Steps and per-version changes when upgrading an app to a newer Rails",
804
+ "keywords": [
805
+ "upgrading",
806
+ "upgrade",
807
+ "rails update",
808
+ "new framework defaults",
809
+ "deprecations",
810
+ "migration path"
811
+ ],
812
+ "category": "core"
813
+ },
814
+ {
815
+ "id": "wishlists",
816
+ "title": "Wishlists",
817
+ "source": "rails",
818
+ "since": "8.1",
819
+ "summary": "Getting Started tutorial series: wishlists, counter caches, friendly URLs, filtering records",
820
+ "keywords": [
821
+ "wishlists",
822
+ "counter cache",
823
+ "friendly urls",
824
+ "to_param",
825
+ "tutorial"
826
+ ],
827
+ "category": "core"
828
+ },
505
829
  {
506
830
  "id": "turbo_handbook_introduction",
507
831
  "title": "Turbo Handbook: Introduction",