rails-ai-context 5.32.2 → 5.34.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 (203) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1491 -0
  3. data/CONTEXT.md +27 -3
  4. data/README.md +16 -12
  5. data/Rakefile +6 -0
  6. data/app/controllers/rails_ai_context/mcp_controller.rb +112 -84
  7. data/config/routes.rb +2 -3
  8. data/docs/ARCHITECTURE.md +7 -5
  9. data/docs/CLI.md +79 -6
  10. data/docs/COMPATIBILITY.md +75 -1
  11. data/docs/CONFIGURATION.md +30 -11
  12. data/docs/CUSTOM_TOOLS.md +2 -1
  13. data/docs/FAQ.md +16 -6
  14. data/docs/GUIDE.md +117 -71
  15. data/docs/INTROSPECTORS.md +10 -8
  16. data/docs/QUICKSTART.md +3 -3
  17. data/docs/RECIPES.md +1 -1
  18. data/docs/SECURITY.md +109 -25
  19. data/docs/SETUP.md +30 -6
  20. data/docs/STANDALONE.md +10 -3
  21. data/docs/TOOLS.md +152 -34
  22. data/docs/TROUBLESHOOTING.md +90 -12
  23. data/docs/adr/0003-shared-tool-cache-semantics.md +8 -0
  24. data/docs/adr/0005-workspace-mcp-entries.md +39 -0
  25. data/exe/rails-ai-context +733 -104
  26. data/lib/generators/rails_ai_context/install/install_generator.rb +341 -125
  27. data/lib/rails_ai_context/app_kind.rb +18 -5
  28. data/lib/rails_ai_context/ast_cache.rb +7 -0
  29. data/lib/rails_ai_context/base_mixins.rb +8 -8
  30. data/lib/rails_ai_context/boot_manager.rb +22 -3
  31. data/lib/rails_ai_context/change_watch.rb +14 -11
  32. data/lib/rails_ai_context/cli/app_root.rb +401 -0
  33. data/lib/rails_ai_context/cli/entry_boot.rb +58 -22
  34. data/lib/rails_ai_context/cli/tool_runner.rb +47 -21
  35. data/lib/rails_ai_context/code_reloader.rb +115 -1
  36. data/lib/rails_ai_context/concern_membership.rb +3 -1
  37. data/lib/rails_ai_context/concern_paths.rb +8 -3
  38. data/lib/rails_ai_context/configuration.rb +41 -9
  39. data/lib/rails_ai_context/context_file_report.rb +19 -2
  40. data/lib/rails_ai_context/database_file.rb +26 -0
  41. data/lib/rails_ai_context/doctor.rb +1122 -184
  42. data/lib/rails_ai_context/engine.rb +18 -1
  43. data/lib/rails_ai_context/erb_source.rb +19 -0
  44. data/lib/rails_ai_context/executable.rb +21 -0
  45. data/lib/rails_ai_context/facts_formatter.rb +5 -2
  46. data/lib/rails_ai_context/file_walk.rb +61 -10
  47. data/lib/rails_ai_context/fingerprinter/snapshot.rb +234 -0
  48. data/lib/rails_ai_context/fingerprinter.rb +40 -12
  49. data/lib/rails_ai_context/gem_lock.rb +186 -27
  50. data/lib/rails_ai_context/git_ignore.rb +22 -0
  51. data/lib/rails_ai_context/install/ai_tool.rb +31 -5
  52. data/lib/rails_ai_context/install/cleanup.rb +21 -0
  53. data/lib/rails_ai_context/install/program.rb +209 -35
  54. data/lib/rails_ai_context/install/selection_record.rb +258 -97
  55. data/lib/rails_ai_context/install/surface.rb +6 -1
  56. data/lib/rails_ai_context/install/validation_hook.rb +282 -0
  57. data/lib/rails_ai_context/install/workspace.rb +166 -0
  58. data/lib/rails_ai_context/install_mode.rb +71 -13
  59. data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +1 -1
  60. data/lib/rails_ai_context/introspectors/action_presence.rb +11 -6
  61. data/lib/rails_ai_context/introspectors/action_source.rb +182 -0
  62. data/lib/rails_ai_context/introspectors/action_text_introspector.rb +1 -2
  63. data/lib/rails_ai_context/introspectors/active_record_settings.rb +1 -1
  64. data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +1 -3
  65. data/lib/rails_ai_context/introspectors/active_support_introspector.rb +3 -3
  66. data/lib/rails_ai_context/introspectors/admin_resources.rb +1 -1
  67. data/lib/rails_ai_context/introspectors/anyway_configs.rb +1 -1
  68. data/lib/rails_ai_context/introspectors/api_introspector.rb +3 -3
  69. data/lib/rails_ai_context/introspectors/auth_introspector.rb +15 -10
  70. data/lib/rails_ai_context/introspectors/call_site_expansion.rb +24 -1
  71. data/lib/rails_ai_context/introspectors/component_introspector.rb +3 -2
  72. data/lib/rails_ai_context/introspectors/controller_introspector.rb +387 -60
  73. data/lib/rails_ai_context/introspectors/convention_introspector.rb +1 -1
  74. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +15 -3
  75. data/lib/rails_ai_context/introspectors/devops_introspector.rb +3 -1
  76. data/lib/rails_ai_context/introspectors/eager_load.rb +201 -9
  77. data/lib/rails_ai_context/introspectors/engine_introspector.rb +1 -1
  78. data/lib/rails_ai_context/introspectors/env_config_introspector.rb +1 -1
  79. data/lib/rails_ai_context/introspectors/env_references.rb +10 -1
  80. data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +1 -1
  81. data/lib/rails_ai_context/introspectors/gem_introspector.rb +23 -10
  82. data/lib/rails_ai_context/introspectors/grape_endpoints.rb +1 -1
  83. data/lib/rails_ai_context/introspectors/habtm_join_tables.rb +1 -1
  84. data/lib/rails_ai_context/introspectors/helper_names.rb +1 -1
  85. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +3 -3
  86. data/lib/rails_ai_context/introspectors/job_introspector.rb +8 -6
  87. data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +12 -8
  88. data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +78 -4
  89. data/lib/rails_ai_context/introspectors/listeners/config_assignment_listener.rb +1 -0
  90. data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +10 -1
  91. data/lib/rails_ai_context/introspectors/listeners/foreign_evals_listener.rb +61 -0
  92. data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +13 -4
  93. data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +2 -1
  94. data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +3 -2
  95. data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +29 -8
  96. data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +2 -2
  97. data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +88 -12
  98. data/lib/rails_ai_context/introspectors/middleware_introspector.rb +2 -2
  99. data/lib/rails_ai_context/introspectors/migration_introspector.rb +27 -0
  100. data/lib/rails_ai_context/introspectors/migration_replay/helpers.rb +1 -1
  101. data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +83 -27
  102. data/lib/rails_ai_context/introspectors/migration_replay.rb +15 -14
  103. data/lib/rails_ai_context/introspectors/model_introspector.rb +231 -23
  104. data/lib/rails_ai_context/introspectors/n_plus_one_scan.rb +620 -0
  105. data/lib/rails_ai_context/introspectors/performance_introspector.rb +11 -174
  106. data/lib/rails_ai_context/introspectors/route_introspector.rb +10 -1
  107. data/lib/rails_ai_context/introspectors/schema_conventions.rb +23 -3
  108. data/lib/rails_ai_context/introspectors/schema_introspector.rb +66 -16
  109. data/lib/rails_ai_context/introspectors/schema_reader.rb +4 -3
  110. data/lib/rails_ai_context/introspectors/seeds_introspector.rb +1 -1
  111. data/lib/rails_ai_context/introspectors/source_scan.rb +39 -9
  112. data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +15 -6
  113. data/lib/rails_ai_context/introspectors/test_introspector.rb +1 -1
  114. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +131 -12
  115. data/lib/rails_ai_context/introspectors/view_introspector.rb +4 -4
  116. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +15 -1
  117. data/lib/rails_ai_context/legacy_cleanup.rb +17 -4
  118. data/lib/rails_ai_context/live_reload.rb +20 -20
  119. data/lib/rails_ai_context/mcp_config_generator.rb +916 -109
  120. data/lib/rails_ai_context/mcp_edge.rb +182 -32
  121. data/lib/rails_ai_context/middleware.rb +5 -1
  122. data/lib/rails_ai_context/migration_status.rb +88 -25
  123. data/lib/rails_ai_context/output_guard.rb +48 -2
  124. data/lib/rails_ai_context/package_json.rb +1 -1
  125. data/lib/rails_ai_context/path_resolver.rb +46 -13
  126. data/lib/rails_ai_context/payload.rb +92 -8
  127. data/lib/rails_ai_context/pending_migrations.rb +5 -1
  128. data/lib/rails_ai_context/portable_path.rb +3 -2
  129. data/lib/rails_ai_context/presets.rb +15 -11
  130. data/lib/rails_ai_context/redaction.rb +262 -51
  131. data/lib/rails_ai_context/resources.rb +55 -20
  132. data/lib/rails_ai_context/route_coverage.rb +8 -0
  133. data/lib/rails_ai_context/safe_file.rb +14 -1
  134. data/lib/rails_ai_context/safe_path.rb +101 -4
  135. data/lib/rails_ai_context/schema_adapter.rb +8 -0
  136. data/lib/rails_ai_context/schema_version.rb +9 -0
  137. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +88 -33
  138. data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +36 -34
  139. data/lib/rails_ai_context/serializers/context_file_serializer.rb +21 -29
  140. data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +8 -6
  141. data/lib/rails_ai_context/serializers/copilot_serializer.rb +2 -7
  142. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +19 -16
  143. data/lib/rails_ai_context/serializers/guide_examples.rb +142 -0
  144. data/lib/rails_ai_context/serializers/markdown_serializer.rb +146 -30
  145. data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +38 -15
  146. data/lib/rails_ai_context/serializers/section_facts.rb +44 -1
  147. data/lib/rails_ai_context/serializers/section_marker_writer.rb +94 -12
  148. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +24 -12
  149. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +244 -129
  150. data/lib/rails_ai_context/server.rb +304 -71
  151. data/lib/rails_ai_context/tasks/rails_ai_context.rake +447 -372
  152. data/lib/rails_ai_context/tools/analyze_feature.rb +72 -18
  153. data/lib/rails_ai_context/tools/argument_schema.rb +49 -0
  154. data/lib/rails_ai_context/tools/base_tool.rb +367 -46
  155. data/lib/rails_ai_context/tools/brakeman_guard.rb +24 -0
  156. data/lib/rails_ai_context/tools/dependency_graph.rb +63 -28
  157. data/lib/rails_ai_context/tools/diagnose.rb +218 -11
  158. data/lib/rails_ai_context/tools/generate_test.rb +339 -39
  159. data/lib/rails_ai_context/tools/get_callbacks.rb +24 -13
  160. data/lib/rails_ai_context/tools/get_component_catalog.rb +1 -1
  161. data/lib/rails_ai_context/tools/get_concern.rb +91 -27
  162. data/lib/rails_ai_context/tools/get_config.rb +13 -4
  163. data/lib/rails_ai_context/tools/get_context.rb +71 -14
  164. data/lib/rails_ai_context/tools/get_controllers.rb +179 -44
  165. data/lib/rails_ai_context/tools/get_conventions.rb +28 -6
  166. data/lib/rails_ai_context/tools/get_edit_context.rb +27 -14
  167. data/lib/rails_ai_context/tools/get_env.rb +18 -9
  168. data/lib/rails_ai_context/tools/get_frontend_stack.rb +3 -1
  169. data/lib/rails_ai_context/tools/get_gems.rb +1 -1
  170. data/lib/rails_ai_context/tools/get_helper_methods.rb +10 -6
  171. data/lib/rails_ai_context/tools/get_i18n.rb +1 -1
  172. data/lib/rails_ai_context/tools/get_job_pattern.rb +3 -2
  173. data/lib/rails_ai_context/tools/get_mailers.rb +3 -3
  174. data/lib/rails_ai_context/tools/get_model_details.rb +30 -10
  175. data/lib/rails_ai_context/tools/get_partial_interface.rb +242 -23
  176. data/lib/rails_ai_context/tools/get_routes.rb +29 -8
  177. data/lib/rails_ai_context/tools/get_schema.rb +52 -19
  178. data/lib/rails_ai_context/tools/get_service_pattern.rb +24 -10
  179. data/lib/rails_ai_context/tools/get_stimulus.rb +37 -1
  180. data/lib/rails_ai_context/tools/get_test_info.rb +18 -4
  181. data/lib/rails_ai_context/tools/get_turbo_map.rb +221 -58
  182. data/lib/rails_ai_context/tools/get_view.rb +14 -14
  183. data/lib/rails_ai_context/tools/migration_advisor.rb +355 -48
  184. data/lib/rails_ai_context/tools/onboard.rb +52 -8
  185. data/lib/rails_ai_context/tools/performance_check.rb +5 -3
  186. data/lib/rails_ai_context/tools/query.rb +914 -86
  187. data/lib/rails_ai_context/tools/read_logs.rb +69 -15
  188. data/lib/rails_ai_context/tools/review_changes.rb +215 -53
  189. data/lib/rails_ai_context/tools/runtime_info.rb +51 -1
  190. data/lib/rails_ai_context/tools/safe_call.rb +35 -14
  191. data/lib/rails_ai_context/tools/search_code.rb +243 -67
  192. data/lib/rails_ai_context/tools/search_docs.rb +4 -4
  193. data/lib/rails_ai_context/tools/security_scan.rb +143 -33
  194. data/lib/rails_ai_context/tools/session_context.rb +39 -5
  195. data/lib/rails_ai_context/tools/validate.rb +259 -40
  196. data/lib/rails_ai_context/tools/validate_semantics.rb +130 -56
  197. data/lib/rails_ai_context/version.rb +1 -1
  198. data/lib/rails_ai_context/vfs.rb +67 -40
  199. data/lib/rails_ai_context/view_file.rb +41 -3
  200. data/lib/rails_ai_context/watcher.rb +51 -13
  201. data/lib/rails_ai_context.rb +21 -0
  202. data/server.json +1 -1
  203. metadata +14 -1
@@ -66,6 +66,10 @@ module RailsAiContext
66
66
 
67
67
  stat = ActiveRecord::Base.connection_pool.stat
68
68
  lines = [ "## Connection Pool", "" ]
69
+ # Read in-process, so the counts are the MCP server's own. 0 connections
70
+ # said nothing about an app server running beside it.
71
+ lines << "_This MCP server process's pool: the counts are its own connections. Each app server process keeps a pool of its own, of the size configured here._"
72
+ lines << ""
69
73
  lines << "| Metric | Value |"
70
74
  lines << "|--------|-------|"
71
75
  lines << "| Pool size | #{stat[:size]} |"
@@ -233,10 +237,49 @@ module RailsAiContext
233
237
  value.is_a?(Hash) ? value.map { |k, v| "#{k}: #{v}" }.join(", ") : value.to_s
234
238
  end
235
239
 
240
+ # Whose numbers these are depends on where the store keeps its entries.
241
+ def cache_scope(cache)
242
+ case cache.class.name.demodulize
243
+ when "MemoryStore"
244
+ "_A MemoryStore lives inside one process, so these numbers are this MCP server's own. " \
245
+ "Each app server process holds entries of its own that no other process sees._"
246
+ when "NullStore" then "_A NullStore keeps nothing, in this process or any other._"
247
+ when "FileStore"
248
+ "_The app's processes on this machine share these files#{" under `#{relative_path(cache.cache_path)}`" if cache.respond_to?(:cache_path)}._"
249
+ else
250
+ "_The app's processes in this environment share this store, so these numbers are theirs too._"
251
+ end
252
+ end
253
+
254
+ def relative_path(path)
255
+ path.to_s.delete_prefix("#{rails_app.root}/")
256
+ end
257
+
258
+ # A store no other process can read is nobody's real cache.
259
+ PROCESS_STORES = /\A:(memory|null)_store\b/
260
+
261
+ # This process runs one environment's store; another environment that
262
+ # configures a shared one (Redis, Memcached, Solid Cache) keeps the
263
+ # app's real cache there.
264
+ def configured_elsewhere
265
+ config = cached_context[:env_config]
266
+ return [] unless config.is_a?(Hash)
267
+
268
+ Array(config[:environments]).filter_map do |env|
269
+ next if env[:name].to_s == rails_env_name.to_s
270
+
271
+ store = (env[:notable] || {})["cache_store"]
272
+ next unless store && !store.match?(PROCESS_STORES)
273
+
274
+ # The store's name; its options are rails_get_env_config's to show, unshortened.
275
+ "- #{env[:name]}: `#{store[/\A:\w+/] || store}` (`#{env[:file]}`)"
276
+ end
277
+ end
278
+
236
279
  def gather_cache
237
280
  cache = Rails.cache
238
281
  lines = [ "## Cache", "" ]
239
- lines << "**Store:** #{cache.class.name.demodulize}"
282
+ lines << "**Store:** #{cache.class.name.demodulize}, this MCP server process's `Rails.cache` (#{rails_env_name})"
240
283
 
241
284
  if cache.respond_to?(:stats)
242
285
  cache_stat_lines(cache.stats).each { |line| lines << line }
@@ -263,6 +306,13 @@ module RailsAiContext
263
306
  lines << "_Stats not available for #{cache.class.name}. Supported: Redis, MemoryStore._"
264
307
  end
265
308
 
309
+ lines << "" << cache_scope(cache)
310
+ elsewhere = configured_elsewhere
311
+ if elsewhere.any?
312
+ lines.push("", "**The app's cache in other environments:**", *elsewhere,
313
+ "_`rails_get_env_config(environment:\"...\")` shows each one's options._")
314
+ end
315
+
266
316
  lines << ""
267
317
  lines
268
318
  rescue => e
@@ -27,10 +27,31 @@ module RailsAiContext
27
27
  # have its note consumed by the inner one.
28
28
  discarded = discarded_detail(kwargs)
29
29
  kwargs = listify_arrays(normalize_detail(kwargs))
30
- Thread.current[:rails_ai_context_call_params] = session_params(kwargs)
30
+ # Before the call reads anything: an edit since the last call drops
31
+ # the caches and reloads the app's code (see the method).
32
+ RailsAiContext::Tools::BaseTool.refresh_if_files_changed!
33
+
34
+ answer = -> { answer_call(discarded) { super(**kwargs) } }
35
+
36
+ # Only the call the client made is its query. A composite tool answers
37
+ # by calling others, and each of those recorded itself, so one
38
+ # get_context read as four calls the client never made - and the
39
+ # first inner call took the outer one's params with it.
40
+ return answer.call if Thread.current[:rails_ai_context_call]
41
+
42
+ Thread.current[:rails_ai_context_call] = { tool: self, params: session_params(kwargs) }
43
+ begin
44
+ answer.call
45
+ ensure
46
+ Thread.current[:rails_ai_context_call] = nil
47
+ end
48
+ end
49
+
50
+ private
31
51
 
32
- # Held across the tool body: a concurrent live reload must not unload
33
- # constants while this call is reading them.
52
+ def answer_call(discarded)
53
+ # Held across the tool body: a reload another call runs must not
54
+ # unload constants while this call is reading them.
34
55
  #
35
56
  # The rescue sits INSIDE the block on purpose. `executor.wrap` reports
36
57
  # anything that crosses it to the host app's error_reporter as
@@ -40,18 +61,16 @@ module RailsAiContext
40
61
  RailsAiContext::CodeReloader.with_app_code do
41
62
  begin
42
63
  # One call is one run: what it reads per controller or per route is read once.
43
- append_note(RailsAiContext::RunCache.around { super(**kwargs) }, invalid_detail_note(discarded))
44
- rescue StandardError => e
45
- # A failed call must not leak its recorded params into the next
46
- # call's session entry.
47
- Thread.current[:rails_ai_context_call_params] = nil
64
+ append_note(RailsAiContext::RunCache.around { yield }, invalid_detail_note(discarded))
65
+ # ScriptError too: a SyntaxError or LoadError out of an app file the
66
+ # tool reached must cost that call, not the stdio server and every
67
+ # tool with it.
68
+ rescue StandardError, ScriptError => e
48
69
  failure_response(e)
49
70
  end
50
71
  end
51
72
  end
52
73
 
53
- private
54
-
55
74
  # The declared params are the contract, so a tool whose method takes
56
75
  # **kwargs refuses the same keys as one that names them.
57
76
  def unknown_params(kwargs)
@@ -93,7 +112,7 @@ module RailsAiContext
93
112
  text = +"Tool #{label} failed: #{error.class}: #{error.message.to_s.lines.first&.strip}\n"
94
113
  text << "At: #{origin}\n" unless origin.empty?
95
114
  text << "Recovery: retry with a narrower query (a single table, model, or " \
96
- "controller), or run `rails-ai-context doctor` to check app health."
115
+ "controller), or run `#{RailsAiContext.doctor_command}` to check app health."
97
116
  MCP::Tool::Response.new([ { type: "text", text: text } ], error: true)
98
117
  end
99
118
 
@@ -107,9 +126,11 @@ module RailsAiContext
107
126
  return "(eval at #{portable_origin(eval_frame[1])})#{eval_frame[2]}"
108
127
  end
109
128
 
110
- path, rest = frame.split(":", 2)
129
+ # FILE:LINE, split at the line number: a Windows path has a colon of
130
+ # its own after the drive letter (D:/a/app/x.rb:1).
131
+ path, rest = frame.match(/\A(.+?):(\d+(?::.*)?)\z/m)&.captures || [ frame, nil ]
111
132
  # A relative path (`(eval)`, `-e`) names nothing on this machine.
112
- return frame unless File.absolute_path?(path.to_s)
133
+ return frame unless RailsAiContext::SafePath.absolute?(path)
113
134
 
114
135
  # This runs inside the rescue that answers a tool failure, so a raise
115
136
  # here would leave the net and reach the client as a protocol error.
@@ -124,7 +145,7 @@ module RailsAiContext
124
145
  rescue StandardError
125
146
  path
126
147
  end
127
- relative = File.basename(relative) if File.absolute_path?(relative)
148
+ relative = File.basename(relative) if RailsAiContext::SafePath.absolute?(relative)
128
149
  [ relative, rest ].compact.join(":")
129
150
  end
130
151