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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1491 -0
- data/CONTEXT.md +27 -3
- data/README.md +16 -12
- data/Rakefile +6 -0
- data/app/controllers/rails_ai_context/mcp_controller.rb +112 -84
- data/config/routes.rb +2 -3
- data/docs/ARCHITECTURE.md +7 -5
- data/docs/CLI.md +79 -6
- data/docs/COMPATIBILITY.md +75 -1
- data/docs/CONFIGURATION.md +30 -11
- data/docs/CUSTOM_TOOLS.md +2 -1
- data/docs/FAQ.md +16 -6
- data/docs/GUIDE.md +117 -71
- data/docs/INTROSPECTORS.md +10 -8
- data/docs/QUICKSTART.md +3 -3
- data/docs/RECIPES.md +1 -1
- data/docs/SECURITY.md +109 -25
- data/docs/SETUP.md +30 -6
- data/docs/STANDALONE.md +10 -3
- data/docs/TOOLS.md +152 -34
- data/docs/TROUBLESHOOTING.md +90 -12
- data/docs/adr/0003-shared-tool-cache-semantics.md +8 -0
- data/docs/adr/0005-workspace-mcp-entries.md +39 -0
- data/exe/rails-ai-context +733 -104
- data/lib/generators/rails_ai_context/install/install_generator.rb +341 -125
- data/lib/rails_ai_context/app_kind.rb +18 -5
- data/lib/rails_ai_context/ast_cache.rb +7 -0
- data/lib/rails_ai_context/base_mixins.rb +8 -8
- data/lib/rails_ai_context/boot_manager.rb +22 -3
- data/lib/rails_ai_context/change_watch.rb +14 -11
- data/lib/rails_ai_context/cli/app_root.rb +401 -0
- data/lib/rails_ai_context/cli/entry_boot.rb +58 -22
- data/lib/rails_ai_context/cli/tool_runner.rb +47 -21
- data/lib/rails_ai_context/code_reloader.rb +115 -1
- data/lib/rails_ai_context/concern_membership.rb +3 -1
- data/lib/rails_ai_context/concern_paths.rb +8 -3
- data/lib/rails_ai_context/configuration.rb +41 -9
- data/lib/rails_ai_context/context_file_report.rb +19 -2
- data/lib/rails_ai_context/database_file.rb +26 -0
- data/lib/rails_ai_context/doctor.rb +1122 -184
- data/lib/rails_ai_context/engine.rb +18 -1
- data/lib/rails_ai_context/erb_source.rb +19 -0
- data/lib/rails_ai_context/executable.rb +21 -0
- data/lib/rails_ai_context/facts_formatter.rb +5 -2
- data/lib/rails_ai_context/file_walk.rb +61 -10
- data/lib/rails_ai_context/fingerprinter/snapshot.rb +234 -0
- data/lib/rails_ai_context/fingerprinter.rb +40 -12
- data/lib/rails_ai_context/gem_lock.rb +186 -27
- data/lib/rails_ai_context/git_ignore.rb +22 -0
- data/lib/rails_ai_context/install/ai_tool.rb +31 -5
- data/lib/rails_ai_context/install/cleanup.rb +21 -0
- data/lib/rails_ai_context/install/program.rb +209 -35
- data/lib/rails_ai_context/install/selection_record.rb +258 -97
- data/lib/rails_ai_context/install/surface.rb +6 -1
- data/lib/rails_ai_context/install/validation_hook.rb +282 -0
- data/lib/rails_ai_context/install/workspace.rb +166 -0
- data/lib/rails_ai_context/install_mode.rb +71 -13
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/action_presence.rb +11 -6
- data/lib/rails_ai_context/introspectors/action_source.rb +182 -0
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +1 -2
- data/lib/rails_ai_context/introspectors/active_record_settings.rb +1 -1
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +1 -3
- data/lib/rails_ai_context/introspectors/active_support_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/admin_resources.rb +1 -1
- data/lib/rails_ai_context/introspectors/anyway_configs.rb +1 -1
- data/lib/rails_ai_context/introspectors/api_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +15 -10
- data/lib/rails_ai_context/introspectors/call_site_expansion.rb +24 -1
- data/lib/rails_ai_context/introspectors/component_introspector.rb +3 -2
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +387 -60
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +15 -3
- data/lib/rails_ai_context/introspectors/devops_introspector.rb +3 -1
- data/lib/rails_ai_context/introspectors/eager_load.rb +201 -9
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/env_config_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/env_references.rb +10 -1
- data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +23 -10
- data/lib/rails_ai_context/introspectors/grape_endpoints.rb +1 -1
- data/lib/rails_ai_context/introspectors/habtm_join_tables.rb +1 -1
- data/lib/rails_ai_context/introspectors/helper_names.rb +1 -1
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/job_introspector.rb +8 -6
- data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +12 -8
- data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +78 -4
- data/lib/rails_ai_context/introspectors/listeners/config_assignment_listener.rb +1 -0
- data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +10 -1
- data/lib/rails_ai_context/introspectors/listeners/foreign_evals_listener.rb +61 -0
- data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +13 -4
- data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +2 -1
- data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +3 -2
- data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +29 -8
- data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +2 -2
- data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +88 -12
- data/lib/rails_ai_context/introspectors/middleware_introspector.rb +2 -2
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +27 -0
- data/lib/rails_ai_context/introspectors/migration_replay/helpers.rb +1 -1
- data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +83 -27
- data/lib/rails_ai_context/introspectors/migration_replay.rb +15 -14
- data/lib/rails_ai_context/introspectors/model_introspector.rb +231 -23
- data/lib/rails_ai_context/introspectors/n_plus_one_scan.rb +620 -0
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +11 -174
- data/lib/rails_ai_context/introspectors/route_introspector.rb +10 -1
- data/lib/rails_ai_context/introspectors/schema_conventions.rb +23 -3
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +66 -16
- data/lib/rails_ai_context/introspectors/schema_reader.rb +4 -3
- data/lib/rails_ai_context/introspectors/seeds_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/source_scan.rb +39 -9
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +15 -6
- data/lib/rails_ai_context/introspectors/test_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +131 -12
- data/lib/rails_ai_context/introspectors/view_introspector.rb +4 -4
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +15 -1
- data/lib/rails_ai_context/legacy_cleanup.rb +17 -4
- data/lib/rails_ai_context/live_reload.rb +20 -20
- data/lib/rails_ai_context/mcp_config_generator.rb +916 -109
- data/lib/rails_ai_context/mcp_edge.rb +182 -32
- data/lib/rails_ai_context/middleware.rb +5 -1
- data/lib/rails_ai_context/migration_status.rb +88 -25
- data/lib/rails_ai_context/output_guard.rb +48 -2
- data/lib/rails_ai_context/package_json.rb +1 -1
- data/lib/rails_ai_context/path_resolver.rb +46 -13
- data/lib/rails_ai_context/payload.rb +92 -8
- data/lib/rails_ai_context/pending_migrations.rb +5 -1
- data/lib/rails_ai_context/portable_path.rb +3 -2
- data/lib/rails_ai_context/presets.rb +15 -11
- data/lib/rails_ai_context/redaction.rb +262 -51
- data/lib/rails_ai_context/resources.rb +55 -20
- data/lib/rails_ai_context/route_coverage.rb +8 -0
- data/lib/rails_ai_context/safe_file.rb +14 -1
- data/lib/rails_ai_context/safe_path.rb +101 -4
- data/lib/rails_ai_context/schema_adapter.rb +8 -0
- data/lib/rails_ai_context/schema_version.rb +9 -0
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +88 -33
- data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +36 -34
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +21 -29
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +8 -6
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +2 -7
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +19 -16
- data/lib/rails_ai_context/serializers/guide_examples.rb +142 -0
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +146 -30
- data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +38 -15
- data/lib/rails_ai_context/serializers/section_facts.rb +44 -1
- data/lib/rails_ai_context/serializers/section_marker_writer.rb +94 -12
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +24 -12
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +244 -129
- data/lib/rails_ai_context/server.rb +304 -71
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +447 -372
- data/lib/rails_ai_context/tools/analyze_feature.rb +72 -18
- data/lib/rails_ai_context/tools/argument_schema.rb +49 -0
- data/lib/rails_ai_context/tools/base_tool.rb +367 -46
- data/lib/rails_ai_context/tools/brakeman_guard.rb +24 -0
- data/lib/rails_ai_context/tools/dependency_graph.rb +63 -28
- data/lib/rails_ai_context/tools/diagnose.rb +218 -11
- data/lib/rails_ai_context/tools/generate_test.rb +339 -39
- data/lib/rails_ai_context/tools/get_callbacks.rb +24 -13
- data/lib/rails_ai_context/tools/get_component_catalog.rb +1 -1
- data/lib/rails_ai_context/tools/get_concern.rb +91 -27
- data/lib/rails_ai_context/tools/get_config.rb +13 -4
- data/lib/rails_ai_context/tools/get_context.rb +71 -14
- data/lib/rails_ai_context/tools/get_controllers.rb +179 -44
- data/lib/rails_ai_context/tools/get_conventions.rb +28 -6
- data/lib/rails_ai_context/tools/get_edit_context.rb +27 -14
- data/lib/rails_ai_context/tools/get_env.rb +18 -9
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +3 -1
- data/lib/rails_ai_context/tools/get_gems.rb +1 -1
- data/lib/rails_ai_context/tools/get_helper_methods.rb +10 -6
- data/lib/rails_ai_context/tools/get_i18n.rb +1 -1
- data/lib/rails_ai_context/tools/get_job_pattern.rb +3 -2
- data/lib/rails_ai_context/tools/get_mailers.rb +3 -3
- data/lib/rails_ai_context/tools/get_model_details.rb +30 -10
- data/lib/rails_ai_context/tools/get_partial_interface.rb +242 -23
- data/lib/rails_ai_context/tools/get_routes.rb +29 -8
- data/lib/rails_ai_context/tools/get_schema.rb +52 -19
- data/lib/rails_ai_context/tools/get_service_pattern.rb +24 -10
- data/lib/rails_ai_context/tools/get_stimulus.rb +37 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +18 -4
- data/lib/rails_ai_context/tools/get_turbo_map.rb +221 -58
- data/lib/rails_ai_context/tools/get_view.rb +14 -14
- data/lib/rails_ai_context/tools/migration_advisor.rb +355 -48
- data/lib/rails_ai_context/tools/onboard.rb +52 -8
- data/lib/rails_ai_context/tools/performance_check.rb +5 -3
- data/lib/rails_ai_context/tools/query.rb +914 -86
- data/lib/rails_ai_context/tools/read_logs.rb +69 -15
- data/lib/rails_ai_context/tools/review_changes.rb +215 -53
- data/lib/rails_ai_context/tools/runtime_info.rb +51 -1
- data/lib/rails_ai_context/tools/safe_call.rb +35 -14
- data/lib/rails_ai_context/tools/search_code.rb +243 -67
- data/lib/rails_ai_context/tools/search_docs.rb +4 -4
- data/lib/rails_ai_context/tools/security_scan.rb +143 -33
- data/lib/rails_ai_context/tools/session_context.rb +39 -5
- data/lib/rails_ai_context/tools/validate.rb +259 -40
- data/lib/rails_ai_context/tools/validate_semantics.rb +130 -56
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/vfs.rb +67 -40
- data/lib/rails_ai_context/view_file.rb +41 -3
- data/lib/rails_ai_context/watcher.rb +51 -13
- data/lib/rails_ai_context.rb +21 -0
- data/server.json +1 -1
- metadata +14 -1
|
@@ -40,6 +40,8 @@ module RailsAiContext
|
|
|
40
40
|
# the filesystem. Tool output notes when the cap is hit so the AI agent knows
|
|
41
41
|
# to narrow its feature keyword. v5.8.1 hardening; all glob sites in r2.
|
|
42
42
|
MAX_SCAN_FILES = 500
|
|
43
|
+
# The callers named under each service; rails_get_service_pattern(service:) lists more.
|
|
44
|
+
CALLERS_SHOWN = 5
|
|
43
45
|
|
|
44
46
|
# The directories rspec-rails and minitest file tests under by kind.
|
|
45
47
|
SUITE_TYPE_DIRS = %w[
|
|
@@ -51,6 +53,11 @@ module RailsAiContext
|
|
|
51
53
|
def self.call(feature:, server_context: nil)
|
|
52
54
|
feature = feature.to_s.strip
|
|
53
55
|
return text_response("Please provide a feature keyword (e.g. 'post', 'payment', 'authentication').") if feature.empty?
|
|
56
|
+
# Every layer word-matches the keyword against every name it holds, so
|
|
57
|
+
# a 100 KB keyword took ten seconds to match nothing.
|
|
58
|
+
if name_too_long?(feature)
|
|
59
|
+
return empty_response("No matches found for '#{echo_input(feature)}'. A feature keyword is a word or two, such as 'payment'.")
|
|
60
|
+
end
|
|
54
61
|
ctx = cached_context
|
|
55
62
|
pattern = feature.downcase
|
|
56
63
|
root = rails_app.root.to_s
|
|
@@ -64,14 +71,14 @@ module RailsAiContext
|
|
|
64
71
|
discover_jobs(ctx, pattern, lines)
|
|
65
72
|
discover_views(ctx, root, pattern, lines)
|
|
66
73
|
discover_stimulus(ctx, pattern, lines)
|
|
67
|
-
|
|
74
|
+
_feature_tests, all_tests, tests_truncated = discover_tests(root, pattern, lines)
|
|
68
75
|
discover_related_models(ctx, matched_models, lines)
|
|
69
76
|
discover_concerns(ctx, matched_models, lines)
|
|
70
77
|
discover_callbacks(ctx, matched_models, lines)
|
|
71
78
|
discover_channels(ctx, pattern, lines)
|
|
72
79
|
discover_mailers(ctx, pattern, lines)
|
|
73
80
|
discover_env_dependencies(root, pattern, matched_models, lines)
|
|
74
|
-
discover_test_gaps(root, pattern, matched_models, ctx,
|
|
81
|
+
discover_test_gaps(root, pattern, matched_models, ctx, all_tests || [], lines, truncated: tests_truncated)
|
|
75
82
|
discover_components(ctx, pattern, lines)
|
|
76
83
|
|
|
77
84
|
# For auth-related keywords, also discover auth gems
|
|
@@ -95,7 +102,7 @@ module RailsAiContext
|
|
|
95
102
|
else
|
|
96
103
|
"\n\nThis app has no models yet, so there are no feature names to suggest."
|
|
97
104
|
end
|
|
98
|
-
return text_response("No matches found for '#{feature}'. No models, controllers, routes, services, or views match this keyword.#{hint}")
|
|
105
|
+
return text_response("No matches found for '#{echo_input(feature)}'. No models, controllers, routes, services, or views match this keyword.#{hint}")
|
|
99
106
|
end
|
|
100
107
|
|
|
101
108
|
text_response(lines.join("\n"))
|
|
@@ -184,9 +191,11 @@ module RailsAiContext
|
|
|
184
191
|
lines << "**Enums:** #{enum_strs.join('; ')}"
|
|
185
192
|
end
|
|
186
193
|
end
|
|
194
|
+
# Only after a section: an empty one left a blank line per layer,
|
|
195
|
+
# four in a row under the heading of a feature with no models.
|
|
196
|
+
lines << ""
|
|
187
197
|
end
|
|
188
198
|
|
|
189
|
-
lines << ""
|
|
190
199
|
matched
|
|
191
200
|
end
|
|
192
201
|
|
|
@@ -218,8 +227,8 @@ module RailsAiContext
|
|
|
218
227
|
lines << "- **Skipped filters:** #{split[:skipped].map { |n| "~~#{n}~~" }.join(', ')}"
|
|
219
228
|
end
|
|
220
229
|
end
|
|
230
|
+
lines << ""
|
|
221
231
|
end
|
|
222
|
-
lines << ""
|
|
223
232
|
end
|
|
224
233
|
|
|
225
234
|
# --- AF: Routes ---
|
|
@@ -242,8 +251,8 @@ module RailsAiContext
|
|
|
242
251
|
lines << "- `#{r[:verb]}` `#{r[:path]}` → #{ctrl}##{r[:action]}#{helper}"
|
|
243
252
|
end
|
|
244
253
|
end
|
|
254
|
+
lines << ""
|
|
245
255
|
end
|
|
246
|
-
lines << ""
|
|
247
256
|
end
|
|
248
257
|
|
|
249
258
|
# --- AF1: Services ---
|
|
@@ -252,6 +261,7 @@ module RailsAiContext
|
|
|
252
261
|
return unless Dir.exist?(dir)
|
|
253
262
|
|
|
254
263
|
real_root = File.realpath(root).to_s
|
|
264
|
+
real_dir = File.realpath(dir).to_s
|
|
255
265
|
candidates = safe_glob(dir, "**/*.rb", real_root).first(MAX_SCAN_FILES)
|
|
256
266
|
# Prefer basename match (fast, no file read) and only fall back to
|
|
257
267
|
# a content scan for files whose basename doesn't match. This
|
|
@@ -262,20 +272,46 @@ module RailsAiContext
|
|
|
262
272
|
end
|
|
263
273
|
return if found.empty?
|
|
264
274
|
|
|
265
|
-
|
|
266
|
-
found.each do |path|
|
|
267
|
-
relative = path.sub("#{real_root}/", "")
|
|
275
|
+
services = found.filter_map do |path|
|
|
268
276
|
source = RailsAiContext::SafeFile.read(path) or next
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
277
|
+
# The path carries the namespace Zeitwerk expects, as rails_get_service_pattern reads it.
|
|
278
|
+
constant = path.delete_prefix("#{real_dir}/").delete_suffix(".rb").camelize
|
|
279
|
+
{ path: path, source: source, name: Introspectors::ServiceClasses.declaration(source, constant).first }
|
|
280
|
+
end
|
|
281
|
+
# A feature's services are usually run from controllers its name does
|
|
282
|
+
# not match: CheckoutService from two OrdersControllers.
|
|
283
|
+
named = services.select { |s| s[:name] }
|
|
284
|
+
callers, callers_truncated = GetServicePattern.callers_of(named.to_h { |s| [ s[:name], s[:path] ] }, real_root)
|
|
285
|
+
|
|
286
|
+
lines << "## Services (#{found.size}#{candidates.size == MAX_SCAN_FILES ? " - first #{MAX_SCAN_FILES} scanned" : ""})"
|
|
287
|
+
services.each do |service|
|
|
288
|
+
relative = service[:path].sub("#{real_root}/", "")
|
|
289
|
+
methods = service[:name] ? service_methods(service[:source], service[:name]) : []
|
|
290
|
+
lines << "- `#{relative}` (#{count_phrase(service[:source].lines.size, "line")})"
|
|
272
291
|
lines << " Methods: #{methods.first(20).join(', ')}" if methods.any?
|
|
292
|
+
called_by = callers.fetch(service[:name], [])
|
|
293
|
+
next if called_by.empty?
|
|
294
|
+
|
|
295
|
+
more = called_by.size > CALLERS_SHOWN ? " and #{called_by.size - CALLERS_SHOWN} more" : ""
|
|
296
|
+
lines << " Called by: #{called_by.first(CALLERS_SHOWN).map { |c| "`#{c}`" }.join(', ')}#{more}"
|
|
297
|
+
end
|
|
298
|
+
if callers_truncated
|
|
299
|
+
lines << "_The caller scan stopped after #{count_phrase(GetServicePattern::MAX_CALLER_SCAN_FILES, "file")}._"
|
|
273
300
|
end
|
|
274
301
|
lines << ""
|
|
275
302
|
rescue => e
|
|
276
303
|
RailsAiContext.debug_fail(e, nil, label: "discover_services")
|
|
277
304
|
end
|
|
278
305
|
|
|
306
|
+
# The service class's own public methods, by the names Ruby calls
|
|
307
|
+
# them: a line scan listed `success` for CheckoutService, read off the
|
|
308
|
+
# `success?` of the Result struct nested in it.
|
|
309
|
+
def service_methods(source, name)
|
|
310
|
+
Introspectors::ActionResolver.own_methods_in(source, name)
|
|
311
|
+
.select { |m| m[:visibility] == :public }
|
|
312
|
+
.map { |m| m[:scope] == :class ? "self.#{m[:name]}" : m[:name].to_s }
|
|
313
|
+
end
|
|
314
|
+
|
|
279
315
|
# --- AF2: Jobs ---
|
|
280
316
|
def discover_jobs(ctx, pattern, lines)
|
|
281
317
|
payload_section(Payload.jobs(ctx), pattern, lines, "Jobs") do |job|
|
|
@@ -285,6 +321,16 @@ module RailsAiContext
|
|
|
285
321
|
bits << "perform(#{job[:perform_signature]})" if job[:perform_signature]
|
|
286
322
|
bits.any? ? " (#{bits.join(', ')})" : ""
|
|
287
323
|
end
|
|
324
|
+
# A Sidekiq worker is not an ActiveJob job, and in an app that runs
|
|
325
|
+
# its background work that way it is the only job there is.
|
|
326
|
+
payload_section(Payload.workers(ctx), pattern, lines, "Sidekiq Workers") do |worker|
|
|
327
|
+
bits = []
|
|
328
|
+
queue = (worker[:options] || {}).find { |key, _| key.to_s == "queue" }&.last
|
|
329
|
+
bits << "queue: #{queue}" if queue
|
|
330
|
+
bits << Array(worker[:retries]).join("; ") if Array(worker[:retries]).any?
|
|
331
|
+
bits << "#{worker[:entry_point] || "perform"}(#{worker[:perform_signature]})" if worker[:perform_signature]
|
|
332
|
+
bits.any? ? " (#{bits.join(', ')})" : ""
|
|
333
|
+
end
|
|
288
334
|
end
|
|
289
335
|
|
|
290
336
|
# The jobs, mailers and channels the :jobs introspector read, matched
|
|
@@ -386,9 +432,13 @@ module RailsAiContext
|
|
|
386
432
|
relative.sub(%r{(\A|/)(?:spec|test)_}, "\\1")
|
|
387
433
|
end
|
|
388
434
|
|
|
435
|
+
# @return [Array(Array<String>, Array<String>, Boolean)] the tests the
|
|
436
|
+
# feature word names, every test read (relative to the suite's root),
|
|
437
|
+
# and whether a glob stopped at MAX_SCAN_FILES
|
|
389
438
|
def discover_tests(root, pattern, lines)
|
|
390
439
|
real_root = File.realpath(root).to_s
|
|
391
440
|
found = []
|
|
441
|
+
all = []
|
|
392
442
|
truncated = false
|
|
393
443
|
|
|
394
444
|
base = RailsAiContext::PathResolver.test_root(root)
|
|
@@ -409,9 +459,11 @@ module RailsAiContext
|
|
|
409
459
|
prefix_glob.each do |path|
|
|
410
460
|
found << path if feature_word_match?(relative_test_name(path, real_base), pattern)
|
|
411
461
|
end
|
|
462
|
+
all.concat(suffix_glob, prefix_glob)
|
|
412
463
|
end
|
|
413
464
|
found.uniq!
|
|
414
|
-
|
|
465
|
+
all = all.uniq.map { |path| path.delete_prefix("#{real_base}/") }
|
|
466
|
+
return [ found, all, truncated ] if found.empty?
|
|
415
467
|
|
|
416
468
|
header = "## Tests (#{found.size}#{truncated ? " - first #{MAX_SCAN_FILES} per glob scanned" : ""})"
|
|
417
469
|
lines << header
|
|
@@ -424,9 +476,9 @@ module RailsAiContext
|
|
|
424
476
|
lines << "- `#{relative}` (#{count_phrase(test_count, "test")})"
|
|
425
477
|
end
|
|
426
478
|
lines << ""
|
|
427
|
-
[ found, truncated ]
|
|
479
|
+
[ found, all, truncated ]
|
|
428
480
|
rescue => e
|
|
429
|
-
RailsAiContext.debug_fail(e, [ [], false ], label: "discover_tests")
|
|
481
|
+
RailsAiContext.debug_fail(e, [ [], [], false ], label: "discover_tests")
|
|
430
482
|
end
|
|
431
483
|
|
|
432
484
|
# --- Test coverage gaps ---
|
|
@@ -441,8 +493,10 @@ module RailsAiContext
|
|
|
441
493
|
# Paths, not basenames: a namespaced name underscores to admin/badges,
|
|
442
494
|
# which no basename can contain, so every namespaced controller was
|
|
443
495
|
# reported as having no test - Discourse's Admin::BadgesController
|
|
444
|
-
# against spec/requests/admin/badges_controller_spec.rb.
|
|
445
|
-
|
|
496
|
+
# against spec/requests/admin/badges_controller_spec.rb. Every test
|
|
497
|
+
# in the suite, not only those the feature word names: the auth rule
|
|
498
|
+
# matches User for `authentication`, a word its user_test.rb lacks.
|
|
499
|
+
test_basenames = test_files.map { |f| f.to_s.sub(/\.rb\z/, "") }
|
|
446
500
|
|
|
447
501
|
# Check models
|
|
448
502
|
matched_models&.each do |name, _data|
|
|
@@ -544,7 +598,7 @@ module RailsAiContext
|
|
|
544
598
|
next unless data.is_a?(Hash)
|
|
545
599
|
(data[:callbacks] || {}).each do |type, methods|
|
|
546
600
|
next unless methods.is_a?(Array)
|
|
547
|
-
methods.each { |m| callbacks << "#{name}: #{type} #{callback_target(m)}" }
|
|
601
|
+
methods.each { |m| callbacks << "#{name}: #{callback_type_label(type)} #{callback_target(m)}" }
|
|
548
602
|
end
|
|
549
603
|
end
|
|
550
604
|
return if callbacks.empty?
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
module RailsAiContext
|
|
6
|
+
module Tools
|
|
7
|
+
# json_schemer, which mcp 1.x checks tool arguments with, words each error
|
|
8
|
+
# through I18n when the process has I18n, after asking it once whether
|
|
9
|
+
# json_schemer's own translations exist. Asking loads every locale file
|
|
10
|
+
# the app has, so where one does not parse, the question raised on every
|
|
11
|
+
# invalid argument (the answer is only kept when there is one) and the
|
|
12
|
+
# client got an internal error, stderr a stack trace, instead of
|
|
13
|
+
# "Invalid arguments: ...". When it raises, json_schemer is given the
|
|
14
|
+
# answer an app without its translations gets, and the check runs again.
|
|
15
|
+
class ArgumentSchema < MCP::Tool::InputSchema
|
|
16
|
+
def validate_arguments(arguments)
|
|
17
|
+
super
|
|
18
|
+
rescue ValidationError
|
|
19
|
+
raise
|
|
20
|
+
rescue StandardError => e
|
|
21
|
+
raise unless locale_failure?(e) && words_without_i18n!
|
|
22
|
+
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Only an error out of I18n loading the app's locale files: the answer
|
|
29
|
+
# is process-wide, so any other failure leaves json_schemer as it is.
|
|
30
|
+
# A YAML locale that does not parse raises I18n::InvalidLocaleData; a
|
|
31
|
+
# Ruby one raises its own error from inside I18n's loader.
|
|
32
|
+
def locale_failure?(error)
|
|
33
|
+
return true if defined?(::I18n::ArgumentError) && error.is_a?(::I18n::ArgumentError)
|
|
34
|
+
|
|
35
|
+
Array(error.backtrace).any? { |line| line.match?(%r{/i18n-[^/]+/lib/i18n/}) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Once: a check that fails the same way again has another cause. The
|
|
39
|
+
# answer is kept on the JSONSchemer module, where Result#i18n? reads it.
|
|
40
|
+
def words_without_i18n!
|
|
41
|
+
return false unless defined?(::JSONSchemer::Result) && ::JSONSchemer::Result.method_defined?(:i18n?)
|
|
42
|
+
return false if ::JSONSchemer.class_variable_defined?(:@@i18n) && ::JSONSchemer.class_variable_get(:@@i18n) == false
|
|
43
|
+
|
|
44
|
+
::JSONSchemer.class_variable_set(:@@i18n, false)
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|