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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +868 -0
- data/CONTEXT.md +7 -3
- data/README.md +1 -1
- data/docs/ARCHITECTURE.md +3 -3
- data/docs/CLI.md +1 -1
- data/docs/COMPATIBILITY.md +50 -14
- data/docs/CONFIGURATION.md +4 -1
- data/docs/FAQ.md +1 -1
- data/docs/GUIDE.md +14 -8
- data/docs/INTROSPECTORS.md +50 -23
- data/docs/SECURITY.md +56 -1
- data/docs/TOOLS.md +58 -12
- data/lib/generators/rails_ai_context/install/install_generator.rb +1 -0
- data/lib/rails_ai_context/action_filters.rb +121 -48
- data/lib/rails_ai_context/app_commands.rb +2 -2
- data/lib/rails_ai_context/app_kind.rb +65 -8
- data/lib/rails_ai_context/ast_cache.rb +53 -18
- data/lib/rails_ai_context/base_mixins.rb +46 -16
- data/lib/rails_ai_context/boot_manager.rb +5 -1
- data/lib/rails_ai_context/cli/entry_boot.rb +17 -2
- data/lib/rails_ai_context/cli/tool_runner.rb +15 -1
- data/lib/rails_ai_context/concern_macros.rb +791 -111
- data/lib/rails_ai_context/concern_membership.rb +25 -5
- data/lib/rails_ai_context/concern_paths.rb +63 -17
- data/lib/rails_ai_context/config_yaml.rb +40 -0
- data/lib/rails_ai_context/configuration.rb +3 -3
- data/lib/rails_ai_context/data/docs/index.json +324 -0
- data/lib/rails_ai_context/database_yml.rb +136 -0
- data/lib/rails_ai_context/doctor.rb +30 -37
- data/lib/rails_ai_context/erb_source.rb +40 -0
- data/lib/rails_ai_context/fingerprinter.rb +48 -22
- data/lib/rails_ai_context/fixture_keys.rb +42 -1
- data/lib/rails_ai_context/gem_lock.rb +275 -47
- data/lib/rails_ai_context/git_ignore.rb +13 -11
- data/lib/rails_ai_context/hydrators/hydration_formatter.rb +8 -2
- data/lib/rails_ai_context/hydrators/schema_hint_builder.rb +10 -5
- data/lib/rails_ai_context/introspector.rb +10 -1
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +27 -19
- data/lib/rails_ai_context/introspectors/action_presence.rb +11 -8
- data/lib/rails_ai_context/introspectors/action_resolver.rb +38 -26
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +2 -2
- data/lib/rails_ai_context/introspectors/active_record_settings.rb +56 -0
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/active_support_introspector.rb +98 -22
- data/lib/rails_ai_context/introspectors/admin_resources.rb +101 -0
- data/lib/rails_ai_context/introspectors/anyway_configs.rb +64 -0
- data/lib/rails_ai_context/introspectors/apartment_config.rb +47 -0
- data/lib/rails_ai_context/introspectors/api_introspector.rb +125 -26
- data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +24 -19
- data/lib/rails_ai_context/introspectors/ast_walk.rb +18 -0
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +71 -18
- data/lib/rails_ai_context/introspectors/autoload_introspector.rb +62 -15
- data/lib/rails_ai_context/introspectors/base.rb +38 -0
- data/lib/rails_ai_context/introspectors/call_site_expansion.rb +349 -39
- data/lib/rails_ai_context/introspectors/component_introspector.rb +26 -14
- data/lib/rails_ai_context/introspectors/config_introspector.rb +22 -9
- data/lib/rails_ai_context/introspectors/connection_pool_introspector.rb +1 -4
- data/lib/rails_ai_context/introspectors/controller_filters.rb +335 -31
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +315 -129
- data/lib/rails_ai_context/introspectors/controller_settings.rb +182 -0
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +18 -15
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/declared_constant.rb +108 -23
- data/lib/rails_ai_context/introspectors/eager_load.rb +12 -7
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +13 -5
- data/lib/rails_ai_context/introspectors/env_config_introspector.rb +62 -17
- data/lib/rails_ai_context/introspectors/env_introspector.rb +8 -6
- data/lib/rails_ai_context/introspectors/env_references.rb +21 -4
- data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +37 -22
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +3 -1
- data/lib/rails_ai_context/introspectors/gemfile_gems.rb +53 -5
- data/lib/rails_ai_context/introspectors/grape_endpoints.rb +109 -0
- data/lib/rails_ai_context/introspectors/helper_names.rb +12 -1
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +101 -46
- data/lib/rails_ai_context/introspectors/includers.rb +6 -1
- data/lib/rails_ai_context/introspectors/initializer_introspector.rb +2 -5
- data/lib/rails_ai_context/introspectors/job_introspector.rb +548 -103
- data/lib/rails_ai_context/introspectors/listener_registration.rb +11 -1
- data/lib/rails_ai_context/introspectors/listeners/associations_listener.rb +51 -4
- data/lib/rails_ai_context/introspectors/listeners/autoload_ignore_listener.rb +31 -0
- data/lib/rails_ai_context/introspectors/listeners/autoload_paths_listener.rb +4 -47
- data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +80 -14
- data/lib/rails_ai_context/introspectors/listeners/branch_conditions.rb +156 -0
- data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +85 -23
- data/lib/rails_ai_context/introspectors/listeners/class_definition_listener.rb +35 -6
- data/lib/rails_ai_context/introspectors/listeners/component_structure_listener.rb +30 -2
- data/lib/rails_ai_context/introspectors/listeners/conditional_macro_listener.rb +19 -0
- data/lib/rails_ai_context/introspectors/listeners/config_assignment_listener.rb +255 -10
- data/lib/rails_ai_context/introspectors/listeners/constant_reference_listener.rb +34 -0
- data/lib/rails_ai_context/introspectors/listeners/constructor_macro_listener.rb +123 -0
- data/lib/rails_ai_context/introspectors/listeners/definition_file_paths_listener.rb +66 -0
- data/lib/rails_ai_context/introspectors/listeners/enums_listener.rb +2 -2
- data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +31 -0
- data/lib/rails_ai_context/introspectors/listeners/filter_macro_listener.rb +54 -0
- data/lib/rails_ai_context/introspectors/listeners/fixture_paths_listener.rb +62 -0
- data/lib/rails_ai_context/introspectors/listeners/gemfile_dsl_listener.rb +42 -1
- data/lib/rails_ai_context/introspectors/listeners/generic_macro_listener.rb +28 -2
- data/lib/rails_ai_context/introspectors/listeners/grape_api_listener.rb +137 -0
- data/lib/rails_ai_context/introspectors/listeners/http_client_call_listener.rb +50 -0
- data/lib/rails_ai_context/introspectors/listeners/i18n_load_path_listener.rb +54 -0
- data/lib/rails_ai_context/introspectors/listeners/literal_paths.rb +123 -0
- data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +248 -4
- data/lib/rails_ai_context/introspectors/listeners/mailbox_routing_listener.rb +2 -1
- data/lib/rails_ai_context/introspectors/listeners/method_call_listener.rb +7 -1
- data/lib/rails_ai_context/introspectors/listeners/methods_listener.rb +352 -69
- data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +1 -1
- data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +35 -26
- data/lib/rails_ai_context/introspectors/listeners/mixins_listener.rb +62 -29
- data/lib/rails_ai_context/introspectors/listeners/mount_listener.rb +8 -0
- data/lib/rails_ai_context/introspectors/listeners/namespaced_roots_listener.rb +35 -0
- data/lib/rails_ai_context/introspectors/listeners/nested_constants_listener.rb +32 -0
- data/lib/rails_ai_context/introspectors/listeners/owner_scope.rb +67 -0
- data/lib/rails_ai_context/introspectors/listeners/preview_paths_listener.rb +16 -7
- data/lib/rails_ai_context/introspectors/listeners/proc_literal_listener.rb +42 -0
- data/lib/rails_ai_context/introspectors/listeners/queue_assignment_listener.rb +54 -0
- data/lib/rails_ai_context/introspectors/listeners/rake_task_dsl_listener.rb +21 -8
- data/lib/rails_ai_context/introspectors/listeners/route_files_listener.rb +4 -11
- data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +461 -65
- data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +76 -14
- data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +40 -1
- data/lib/rails_ai_context/introspectors/listeners/validations_listener.rb +10 -1
- data/lib/rails_ai_context/introspectors/listeners/view_paths_listener.rb +37 -0
- data/lib/rails_ai_context/introspectors/middleware_introspector.rb +47 -3
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/migration_replay/helpers.rb +4 -3
- data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +72 -12
- data/lib/rails_ai_context/introspectors/migration_replay.rb +110 -14
- data/lib/rails_ai_context/introspectors/model_introspector.rb +708 -214
- data/lib/rails_ai_context/introspectors/module_aliases.rb +11 -3
- data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +9 -57
- data/lib/rails_ai_context/introspectors/observability_introspector.rb +5 -8
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +28 -8
- data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +118 -51
- data/lib/rails_ai_context/introspectors/recurring_schedules.rb +150 -0
- data/lib/rails_ai_context/introspectors/rendered_record.rb +87 -0
- data/lib/rails_ai_context/introspectors/retry_policy.rb +8 -5
- data/lib/rails_ai_context/introspectors/route_introspector.rb +153 -42
- data/lib/rails_ai_context/introspectors/schema_conventions.rb +127 -40
- data/lib/rails_ai_context/introspectors/schema_dump_path.rb +80 -0
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +346 -112
- data/lib/rails_ai_context/introspectors/schema_reader.rb +69 -29
- data/lib/rails_ai_context/introspectors/service_classes.rb +21 -15
- data/lib/rails_ai_context/introspectors/source_calls.rb +1 -1
- data/lib/rails_ai_context/introspectors/source_introspector.rb +17 -2
- data/lib/rails_ai_context/introspectors/source_scan.rb +174 -26
- data/lib/rails_ai_context/introspectors/sqlite_virtual_tables.rb +28 -0
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +16 -5
- data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +372 -98
- data/lib/rails_ai_context/introspectors/superclass_chain.rb +14 -15
- data/lib/rails_ai_context/introspectors/table_name.rb +46 -45
- data/lib/rails_ai_context/introspectors/test_introspector.rb +289 -78
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +10 -5
- data/lib/rails_ai_context/introspectors/view_introspector.rb +5 -40
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +12 -10
- data/lib/rails_ai_context/method_name.rb +31 -0
- data/lib/rails_ai_context/migration_status.rb +5 -5
- data/lib/rails_ai_context/output_guard.rb +6 -4
- data/lib/rails_ai_context/package_json.rb +96 -5
- data/lib/rails_ai_context/path_resolver.rb +170 -15
- data/lib/rails_ai_context/payload.rb +48 -0
- data/lib/rails_ai_context/pending_migrations.rb +15 -18
- data/lib/rails_ai_context/portable_path.rb +76 -4
- data/lib/rails_ai_context/redaction.rb +8 -2
- data/lib/rails_ai_context/route_coverage.rb +5 -1
- data/lib/rails_ai_context/safe_file.rb +4 -1
- data/lib/rails_ai_context/safe_path.rb +61 -10
- data/lib/rails_ai_context/schema_hint.rb +8 -2
- data/lib/rails_ai_context/schema_version.rb +7 -9
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +13 -4
- data/lib/rails_ai_context/serializers/section_facts.rb +5 -1
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +2 -2
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +7 -4
- data/lib/rails_ai_context/server.rb +26 -7
- data/lib/rails_ai_context/test_framework.rb +32 -5
- data/lib/rails_ai_context/tools/analyze_feature.rb +22 -10
- data/lib/rails_ai_context/tools/base_tool.rb +69 -30
- data/lib/rails_ai_context/tools/diagnose.rb +1 -1
- data/lib/rails_ai_context/tools/generate_test.rb +8 -9
- data/lib/rails_ai_context/tools/get_active_support.rb +19 -4
- data/lib/rails_ai_context/tools/get_api.rb +17 -11
- data/lib/rails_ai_context/tools/get_autoload.rb +10 -5
- data/lib/rails_ai_context/tools/get_callbacks.rb +65 -14
- data/lib/rails_ai_context/tools/get_component_catalog.rb +7 -4
- data/lib/rails_ai_context/tools/get_concern.rb +31 -18
- data/lib/rails_ai_context/tools/get_config.rb +52 -18
- data/lib/rails_ai_context/tools/get_controllers.rb +57 -13
- data/lib/rails_ai_context/tools/get_conventions.rb +44 -22
- data/lib/rails_ai_context/tools/get_engines.rb +2 -0
- data/lib/rails_ai_context/tools/get_env.rb +209 -52
- data/lib/rails_ai_context/tools/get_env_config.rb +38 -7
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +56 -7
- data/lib/rails_ai_context/tools/get_helper_methods.rb +136 -26
- data/lib/rails_ai_context/tools/get_job_pattern.rb +143 -87
- data/lib/rails_ai_context/tools/get_mailers.rb +59 -2
- data/lib/rails_ai_context/tools/get_model_details.rb +178 -28
- data/lib/rails_ai_context/tools/get_partial_interface.rb +101 -62
- data/lib/rails_ai_context/tools/get_routes.rb +35 -9
- data/lib/rails_ai_context/tools/get_schema.rb +160 -61
- data/lib/rails_ai_context/tools/get_service_pattern.rb +121 -25
- data/lib/rails_ai_context/tools/get_stimulus.rb +1 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +152 -82
- data/lib/rails_ai_context/tools/get_view.rb +37 -20
- data/lib/rails_ai_context/tools/migration_advisor.rb +64 -24
- data/lib/rails_ai_context/tools/onboard.rb +174 -88
- data/lib/rails_ai_context/tools/performance_check.rb +5 -13
- data/lib/rails_ai_context/tools/query.rb +122 -28
- data/lib/rails_ai_context/tools/read_logs.rb +45 -17
- data/lib/rails_ai_context/tools/runtime_info.rb +1 -1
- data/lib/rails_ai_context/tools/safe_call.rb +20 -15
- data/lib/rails_ai_context/tools/search_code.rb +114 -40
- data/lib/rails_ai_context/tools/search_docs.rb +22 -11
- data/lib/rails_ai_context/tools/security_scan.rb +51 -4
- data/lib/rails_ai_context/tools/validate.rb +21 -4
- data/lib/rails_ai_context/tools/validate_semantics.rb +18 -6
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/vfs.rb +2 -2
- data/lib/rails_ai_context/view_file.rb +53 -3
- data/lib/rails_ai_context.rb +10 -2
- data/server.json +1 -1
- metadata +34 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
3
5
|
module RailsAiContext
|
|
4
6
|
module Tools
|
|
5
7
|
class GetPartialInterface < BaseTool
|
|
@@ -49,7 +51,7 @@ module RailsAiContext
|
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
if view_dirs.empty?
|
|
52
|
-
note = api_only_note("app/views")
|
|
54
|
+
note = api_only_note("app/views", dir: "app/views")
|
|
53
55
|
return text_response(note) if note
|
|
54
56
|
|
|
55
57
|
return text_response("No app/views/ directory found.")
|
|
@@ -71,7 +73,7 @@ module RailsAiContext
|
|
|
71
73
|
unless located
|
|
72
74
|
available = view_dirs.flat_map { |dir| find_available_partials(dir, root) }.uniq.sort.first(30)
|
|
73
75
|
return not_found_response("Partial", partial, available,
|
|
74
|
-
recovery_tool: "Call rails_get_view(detail:\"summary\") to see all views and partials")
|
|
76
|
+
recovery_tool: "Call rails_get_view(detail:\"summary\") to see all views and partials", note: static_engine_views_note)
|
|
75
77
|
end
|
|
76
78
|
|
|
77
79
|
file_path = located.realpath
|
|
@@ -83,11 +85,14 @@ module RailsAiContext
|
|
|
83
85
|
return text_response("Could not read partial file.") unless source
|
|
84
86
|
|
|
85
87
|
relative_path = located.relative
|
|
86
|
-
partial_name =
|
|
88
|
+
partial_name = view_relative(File.expand_path(relative_path, root), view_dirs)
|
|
87
89
|
|
|
88
90
|
# Parse the partial's interface
|
|
89
91
|
magic_locals = extract_magic_comment_locals(source)
|
|
90
|
-
|
|
92
|
+
# The resolved file, not the caller's spelling: a bare name has no
|
|
93
|
+
# directory for a render site to be matched against.
|
|
94
|
+
prefixed = prefix_partial_paths?(root)
|
|
95
|
+
render_sites = view_dirs.flat_map { |dir| find_render_sites(dir, partial_name, root, prefixed) }
|
|
91
96
|
method_calls = {}
|
|
92
97
|
|
|
93
98
|
# Primary: locals from render call sites (ground truth)
|
|
@@ -98,8 +103,8 @@ module RailsAiContext
|
|
|
98
103
|
|
|
99
104
|
# Combine: render-site locals first, then source-detected locals
|
|
100
105
|
# Filter out noise: single chars, capitalized words, known helpers
|
|
101
|
-
all_locals = (magic_locals + render_locals + source_locals).uniq
|
|
102
|
-
.reject { |l| l.length <= 1 || l.match?(/\A[A-Z]/) || l.match?(/\Arender_/) }
|
|
106
|
+
all_locals = (Array(magic_locals) + render_locals + source_locals).uniq
|
|
107
|
+
.reject { |l| l.length <= 1 || l.match?(/\A[A-Z*]/) || l.match?(/\Arender_/) }
|
|
103
108
|
.sort
|
|
104
109
|
|
|
105
110
|
# Extract method calls only for confirmed locals
|
|
@@ -118,7 +123,7 @@ module RailsAiContext
|
|
|
118
123
|
lines = [ "# Partial: #{partial_name}", "" ]
|
|
119
124
|
|
|
120
125
|
if all_locals.any?
|
|
121
|
-
magic_note = magic_locals
|
|
126
|
+
magic_note = magic_locals&.any? ? " (#{magic_locals.size} declared via magic comment)" : ""
|
|
122
127
|
lines << "**Locals:** #{all_locals.join(', ')}#{magic_note}"
|
|
123
128
|
else
|
|
124
129
|
lines << "**Locals:** none detected"
|
|
@@ -135,8 +140,9 @@ module RailsAiContext
|
|
|
135
140
|
lines = [ "# Partial: #{partial_name}", "" ]
|
|
136
141
|
lines << "**File:** `#{relative_path}` (#{count_phrase(source.lines.size, "line")})"
|
|
137
142
|
|
|
138
|
-
if magic_locals
|
|
139
|
-
|
|
143
|
+
if magic_locals
|
|
144
|
+
declared = magic_locals.any? ? magic_locals.join(", ") : "none, so passing any local raises"
|
|
145
|
+
lines << "**Declared locals** (Rails 7.1+ magic comment): #{declared}"
|
|
140
146
|
end
|
|
141
147
|
|
|
142
148
|
if all_locals.any?
|
|
@@ -151,7 +157,7 @@ module RailsAiContext
|
|
|
151
157
|
lines << "- **#{local}**"
|
|
152
158
|
end
|
|
153
159
|
end
|
|
154
|
-
elsif !full
|
|
160
|
+
elsif !full && !magic_locals
|
|
155
161
|
lines << "" << "_No local variables detected in this partial._"
|
|
156
162
|
end
|
|
157
163
|
|
|
@@ -203,9 +209,11 @@ module RailsAiContext
|
|
|
203
209
|
# A fixed extension list refused `.text.erb`, which the Available
|
|
204
210
|
# list built by globbing had just offered. Rails names a partial by
|
|
205
211
|
# its directory and basename, whatever format and handler follow.
|
|
212
|
+
# The plain file before a locale or variant one: Rails renders it for a request that asks for neither.
|
|
213
|
+
plain_first = ->(paths) { paths.sort_by { |path| [ view_alternate_of(path) ? 1 : 0, path ] } }
|
|
206
214
|
candidates = [
|
|
207
|
-
*Dir.glob(File.join(views_dir, *dir_parts, "#{prefixed_basename}.*"))
|
|
208
|
-
*Dir.glob(File.join(views_dir, *dir_parts, "#{unprefixed_basename}.*"))
|
|
215
|
+
*plain_first.call(Dir.glob(File.join(views_dir, *dir_parts, "#{prefixed_basename}.*"))),
|
|
216
|
+
*plain_first.call(Dir.glob(File.join(views_dir, *dir_parts, "#{unprefixed_basename}.*"))),
|
|
209
217
|
File.join(views_dir, partial)
|
|
210
218
|
]
|
|
211
219
|
|
|
@@ -219,10 +227,10 @@ module RailsAiContext
|
|
|
219
227
|
located.ok? || located.refusal == :too_large ? located : nil
|
|
220
228
|
end
|
|
221
229
|
|
|
222
|
-
# A view file's name as the app renders it: its path under
|
|
223
|
-
# views root holds it.
|
|
230
|
+
# A view file's name as the app renders it: its path under the innermost
|
|
231
|
+
# views root that holds it.
|
|
224
232
|
private_class_method def self.view_relative(path, view_dirs)
|
|
225
|
-
dir =
|
|
233
|
+
dir = RailsAiContext::ViewFile.root_for(path, view_dirs)
|
|
226
234
|
dir ? path.delete_prefix(dir + File::SEPARATOR) : path
|
|
227
235
|
end
|
|
228
236
|
|
|
@@ -233,26 +241,29 @@ module RailsAiContext
|
|
|
233
241
|
return [] if name.include?("/")
|
|
234
242
|
|
|
235
243
|
name = "_#{name}" unless name.start_with?("_")
|
|
236
|
-
|
|
244
|
+
RailsAiContext::ViewFile.glob(rails_app.root.to_s, views_dir, File.join("**", "#{name}.*")).select { |c| File.file?(c) }
|
|
237
245
|
end
|
|
238
246
|
|
|
239
|
-
#
|
|
247
|
+
# ActionView::Template::STRICT_LOCALS_REGEX as of Rails 8.0, held here so
|
|
248
|
+
# the static tier reads the comment the same way.
|
|
249
|
+
STRICT_LOCALS = /\#\s+locals:\s+\((.*?)\)(?=\s*-?%>|\s*$)/m
|
|
250
|
+
|
|
251
|
+
# The names a strict locals comment declares; nil without one, [] for
|
|
252
|
+
# `()`, which allows no locals at all.
|
|
240
253
|
private_class_method def self.extract_magic_comment_locals(source)
|
|
241
|
-
|
|
254
|
+
list = source[STRICT_LOCALS, 1]
|
|
255
|
+
return nil unless list
|
|
242
256
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
params_str = match[1]
|
|
246
|
-
# Parse Ruby-style keyword params: name:, title: "default", count: 0
|
|
247
|
-
params_str.scan(/([\w]+):/) do |param_match|
|
|
248
|
-
locals << param_match[0]
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
end
|
|
257
|
+
result = RailsAiContext::AstCache.parse_string("def _(#{list}); end")
|
|
258
|
+
return list.scan(/(\w+):/).flatten.uniq unless result.success?
|
|
252
259
|
|
|
253
|
-
|
|
260
|
+
params = result.value.statements.body.first.parameters
|
|
261
|
+
return [] unless params
|
|
262
|
+
|
|
263
|
+
rest = params.keyword_rest
|
|
264
|
+
params.keywords.map { |p| p.name.to_s } + (rest.is_a?(Prism::KeywordRestParameterNode) ? [ "**#{rest.name}" ] : [])
|
|
254
265
|
rescue => e
|
|
255
|
-
RailsAiContext.debug_fail(e,
|
|
266
|
+
RailsAiContext.debug_fail(e, nil, label: "extract_magic_comment_locals")
|
|
256
267
|
end
|
|
257
268
|
|
|
258
269
|
# Extract local variable references from ERB source.
|
|
@@ -324,7 +335,7 @@ module RailsAiContext
|
|
|
324
335
|
end
|
|
325
336
|
|
|
326
337
|
# Find all views that render this partial and extract the locals they pass.
|
|
327
|
-
private_class_method def self.find_render_sites(views_dir, partial, root)
|
|
338
|
+
private_class_method def self.find_render_sites(views_dir, partial, root, prefixed)
|
|
328
339
|
sites = []
|
|
329
340
|
# Build search names: the partial can be referenced multiple ways
|
|
330
341
|
# Normalize: strip underscore prefix from basename and extensions
|
|
@@ -346,28 +357,15 @@ module RailsAiContext
|
|
|
346
357
|
basename # status_badge
|
|
347
358
|
].uniq
|
|
348
359
|
|
|
349
|
-
|
|
350
|
-
# to_partial_path: `render @post`, `render post`, `render @posts`,
|
|
351
|
-
# `render posts` all hit posts/_post. These are extremely common and
|
|
352
|
-
# carry no quoted partial name, so the explicit-string match above
|
|
353
|
-
# misses them. Only treat the partial as a conventional resource
|
|
354
|
-
# partial (posts/_post) when its directory is the pluralized basename
|
|
355
|
-
# or it is top-level, to avoid matching unrelated variables.
|
|
356
|
-
plural = basename.pluralize
|
|
357
|
-
implicit_names =
|
|
358
|
-
if dir_prefix.empty? || dir_prefix.split("/").last == plural
|
|
359
|
-
[ basename, plural ].uniq
|
|
360
|
-
else
|
|
361
|
-
[]
|
|
362
|
-
end
|
|
360
|
+
object_paths = {}
|
|
363
361
|
|
|
364
|
-
view_files =
|
|
362
|
+
view_files = RailsAiContext::ViewFile.glob(root, views_dir, RailsAiContext::ViewFile::MARKUP_GLOB)
|
|
365
363
|
|
|
366
364
|
view_files.each do |file|
|
|
367
365
|
content = safe_read(file)
|
|
368
366
|
next unless content
|
|
369
367
|
|
|
370
|
-
relative =
|
|
368
|
+
relative = RailsAiContext::PortablePath.relativize(file, root)
|
|
371
369
|
|
|
372
370
|
lines = content.lines
|
|
373
371
|
# Per call, not per line: `render(` and a call split over lines are
|
|
@@ -380,7 +378,7 @@ module RailsAiContext
|
|
|
380
378
|
|
|
381
379
|
covered = at + args.length
|
|
382
380
|
line_num = content[0...at].count("\n") + 1
|
|
383
|
-
line =
|
|
381
|
+
line = Introspectors::ViewTemplateIntrospector.render_line(args)
|
|
384
382
|
spanned = lines[(line_num - 1)..(line_num - 1 + args.chomp.count("\n"))]
|
|
385
383
|
snippet = spanned.size > 1 ? spanned.join(" ").squish : lines[line_num - 1].strip
|
|
386
384
|
|
|
@@ -411,21 +409,13 @@ module RailsAiContext
|
|
|
411
409
|
|
|
412
410
|
next if matched_line
|
|
413
411
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
# and \b guards against prefixes like post_path / posts_controller.
|
|
417
|
-
implicit_names.each do |var|
|
|
418
|
-
next unless line.match?(/render\s+@?#{Regexp.escape(var)}\b/)
|
|
419
|
-
next if line.match?(/render\s.*["']/) # a quoted render is explicit, handled above
|
|
412
|
+
chain = line[Introspectors::ViewTemplateIntrospector::IMPLICIT_RENDER, 1]
|
|
413
|
+
next unless chain && !line.include?("partial:")
|
|
420
414
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
snippet: snippet
|
|
426
|
-
}
|
|
427
|
-
break
|
|
428
|
-
end
|
|
415
|
+
view_dir = File.dirname(file.delete_prefix(views_dir + File::SEPARATOR))
|
|
416
|
+
next unless implicit_partial(chain, view_dir, prefixed, root, object_paths) == canonical
|
|
417
|
+
|
|
418
|
+
sites << { file: relative, line: line_num, locals: [], snippet: snippet }
|
|
429
419
|
end
|
|
430
420
|
end
|
|
431
421
|
|
|
@@ -434,6 +424,55 @@ module RailsAiContext
|
|
|
434
424
|
RailsAiContext.debug_fail(e, [], label: "find_render_sites")
|
|
435
425
|
end
|
|
436
426
|
|
|
427
|
+
# The partial Rails renders for the records `chain` names from a view in
|
|
428
|
+
# view_dir, which stands in for the controller's path; nil when the
|
|
429
|
+
# model's to_partial_path or the chain cannot be read.
|
|
430
|
+
private_class_method def self.implicit_partial(chain, view_dir, prefixed, root, memo)
|
|
431
|
+
model, = Introspectors::RenderedRecord.resolve(chain, root, memo)
|
|
432
|
+
path = model && Introspectors::RenderedRecord.partial_path(model, root, memo)
|
|
433
|
+
return path unless path && prefixed
|
|
434
|
+
|
|
435
|
+
merge_prefix_into_object_path(view_dir == "." ? "" : view_dir, path)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# ActionView::AbstractRenderer#merge_prefix_into_object_path.
|
|
439
|
+
private_class_method def self.merge_prefix_into_object_path(prefix, object_path)
|
|
440
|
+
return object_path unless prefix.include?("/") && object_path.include?("/")
|
|
441
|
+
|
|
442
|
+
prefixes = []
|
|
443
|
+
object_dirs = object_path.split("/")[0..-3]
|
|
444
|
+
File.dirname(prefix).split("/").each_with_index do |dir, index|
|
|
445
|
+
break if dir == object_dirs[index]
|
|
446
|
+
|
|
447
|
+
prefixes << dir
|
|
448
|
+
end
|
|
449
|
+
(prefixes << object_path).join("/")
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Rails prefixes a record's partial with the controller namespace unless
|
|
453
|
+
# ActionView::Base.prefix_partial_path_with_controller_namespace is false.
|
|
454
|
+
PREFIX_SETTER = :prefix_partial_path_with_controller_namespace=
|
|
455
|
+
|
|
456
|
+
private_class_method def self.prefix_partial_paths?(root)
|
|
457
|
+
return ActionView::Base.prefix_partial_path_with_controller_namespace != false if booted_app? && defined?(ActionView::Base)
|
|
458
|
+
|
|
459
|
+
prefix = "#{root.to_s.chomp('/')}/"
|
|
460
|
+
files = [ "config/application.rb", "config/environments/#{rails_env_name}.rb" ] +
|
|
461
|
+
RailsAiContext::PathResolver.initializer_paths(root).map { |path| path.delete_prefix(prefix) }
|
|
462
|
+
last = nil
|
|
463
|
+
# The setter name is ActionView's own, so config.action_view, ActionView::Base and an on_load `self.` all match.
|
|
464
|
+
listener = -> { Introspectors::Listeners::MethodCallListener.new(names: [ PREFIX_SETTER ]) }
|
|
465
|
+
files.each do |relative|
|
|
466
|
+
next unless RailsAiContext::SafePath.locate(relative, under: root).ok?
|
|
467
|
+
|
|
468
|
+
walked = Introspectors::SourceIntrospector.walk(File.join(root, relative), { setting: listener })
|
|
469
|
+
walked[:setting].each { |call| last = call[:arguments].first if [ true, false ].include?(call[:arguments].first) }
|
|
470
|
+
end
|
|
471
|
+
last != false
|
|
472
|
+
rescue => e
|
|
473
|
+
RailsAiContext.debug_fail(e, true, label: "prefix_partial_paths?")
|
|
474
|
+
end
|
|
475
|
+
|
|
437
476
|
# Extract local variable names from a render call line.
|
|
438
477
|
private_class_method def self.extract_locals_from_render(line)
|
|
439
478
|
locals = []
|
|
@@ -492,7 +531,7 @@ module RailsAiContext
|
|
|
492
531
|
|
|
493
532
|
# Find available partials for fuzzy matching in not_found_response.
|
|
494
533
|
private_class_method def self.find_available_partials(views_dir, root)
|
|
495
|
-
|
|
534
|
+
RailsAiContext::ViewFile.glob(root, views_dir, File.join("**", "_*")).select { |f| File.file?(f) && RailsAiContext::ViewFile.template?(f) }.map do |f|
|
|
496
535
|
relative = f.sub("#{views_dir}/", "")
|
|
497
536
|
# Strip underscore prefix and extension for display
|
|
498
537
|
parts = relative.split("/")
|
|
@@ -36,12 +36,15 @@ module RailsAiContext
|
|
|
36
36
|
|
|
37
37
|
HINTED_FILTERS = 3
|
|
38
38
|
|
|
39
|
+
GEM_DRAWN_NOTE = "_Read from source: #{RailsAiContext::RouteCoverage::GEM_DRAWN} (Active Storage, Action Mailbox, conductor) " \
|
|
40
|
+
"are read only with the app booted, so no count here includes them._"
|
|
41
|
+
|
|
39
42
|
# The filters this controller actually runs, read the way every other
|
|
40
43
|
# surface reads them, so a skipped one is never named here. The cut is
|
|
41
44
|
# stated rather than silent.
|
|
42
45
|
def self.filter_hint(ctrl_class, ctx)
|
|
43
46
|
chain = RailsAiContext::ActionFilters.for_controller(ctx, ctrl_class, root: rails_app&.root&.to_s)
|
|
44
|
-
names =
|
|
47
|
+
names = chain[:chain].map { |f| f[:name] }
|
|
45
48
|
return nil if names.empty?
|
|
46
49
|
|
|
47
50
|
rest = names.size - HINTED_FILTERS
|
|
@@ -57,15 +60,27 @@ module RailsAiContext
|
|
|
57
60
|
|
|
58
61
|
annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
|
|
59
62
|
|
|
63
|
+
GRAPE_LIMIT = 100
|
|
64
|
+
|
|
60
65
|
# A mounted app answers on a path and has no controller#action, so
|
|
61
66
|
# no controller group can hold it. Counting it and then dropping it left
|
|
62
67
|
# the one endpoint a reader was looking for named nowhere.
|
|
63
|
-
private_class_method def self.mounted_apps_lines(mounted_apps, engine_routes = [])
|
|
68
|
+
private_class_method def self.mounted_apps_lines(mounted_apps, engine_routes = [], grape = {}, list_grape: true)
|
|
64
69
|
lines = []
|
|
65
70
|
unless mounted_apps.empty?
|
|
66
71
|
lines << "" << "## Mounted Apps (#{mounted_apps.size})"
|
|
67
72
|
mounted_apps.each do |app|
|
|
68
|
-
|
|
73
|
+
row = app[:path] ? "- **#{app[:engine]}** at `#{app[:path]}`" : "- **#{app[:engine]}**"
|
|
74
|
+
row += " (`#{app[:condition]}`)" if app[:condition]
|
|
75
|
+
endpoints = Array(grape&.dig(app[:engine]))
|
|
76
|
+
if endpoints.any? && !list_grape
|
|
77
|
+
row += " (#{count_phrase(endpoints.size, "Grape endpoint")}, listed at detail:\"standard\")"
|
|
78
|
+
end
|
|
79
|
+
lines << row
|
|
80
|
+
if list_grape
|
|
81
|
+
endpoints.first(GRAPE_LIMIT).each { |endpoint| lines << " - #{RailsAiContext::Introspectors::GrapeEndpoints.line(endpoint)}" }
|
|
82
|
+
lines << " - _#{endpoints.size - GRAPE_LIMIT} more, in #{endpoints.map { |e| e[:file] }.uniq.first(3).join(', ')}_" if endpoints.size > GRAPE_LIMIT
|
|
83
|
+
end
|
|
69
84
|
end
|
|
70
85
|
lines << "_A mounted app's own routes are in its table, not in the count above._"
|
|
71
86
|
end
|
|
@@ -160,8 +175,10 @@ module RailsAiContext
|
|
|
160
175
|
by_controller = by_controller.transform_values { |actions| RailsAiContext::RouteCoverage.dedupe_put_patch_routes(actions) }
|
|
161
176
|
filtered_total = by_controller.values.sum(&:size)
|
|
162
177
|
count_label = count_phrase(filtered_total, "route")
|
|
178
|
+
from_source = routes[:confidence] == RailsAiContext::Confidence::STATIC && controller.nil?
|
|
163
179
|
if excluded_framework_count > 0 && controller.nil?
|
|
164
180
|
count_label += ", excluding #{count_phrase(excluded_framework_count, "framework route")}"
|
|
181
|
+
count_label += " drawn in the app's route files" if from_source
|
|
165
182
|
end
|
|
166
183
|
# "engine mount" named the wrong thing: a plain Rack app attached
|
|
167
184
|
# with `mount` or with `match ... to:` is not an engine, and both
|
|
@@ -177,7 +194,7 @@ module RailsAiContext
|
|
|
177
194
|
# thing an unbooted answer must not do. All three detail levels share
|
|
178
195
|
# this label. Gated on `controller` because the caveat is about the
|
|
179
196
|
# whole table, and a filtered answer is not that.
|
|
180
|
-
count_label += RailsAiContext::RouteCoverage.suffix(routes) if controller.nil?
|
|
197
|
+
count_label += RailsAiContext::RouteCoverage.suffix(routes, gem_drawn: false) if controller.nil?
|
|
181
198
|
|
|
182
199
|
case detail
|
|
183
200
|
when "summary"
|
|
@@ -219,7 +236,8 @@ module RailsAiContext
|
|
|
219
236
|
lines << "- _#{fw_names} framework routes: #{total_fw} total_"
|
|
220
237
|
end
|
|
221
238
|
|
|
222
|
-
lines.concat(mounted_apps_lines(mounted_apps, controller ? [] : routes[:engine_routes]))
|
|
239
|
+
lines.concat(mounted_apps_lines(mounted_apps, controller ? [] : routes[:engine_routes], routes[:grape_endpoints], list_grape: false))
|
|
240
|
+
lines << "" << GEM_DRAWN_NOTE if from_source
|
|
223
241
|
|
|
224
242
|
if routes[:api_namespaces]&.any?
|
|
225
243
|
lines << "" << "API namespaces: #{routes[:api_namespaces].join(', ')}"
|
|
@@ -274,15 +292,17 @@ module RailsAiContext
|
|
|
274
292
|
else
|
|
275
293
|
""
|
|
276
294
|
end
|
|
277
|
-
|
|
295
|
+
condition = r[:condition] ? " (`#{r[:condition]}`)" : ""
|
|
296
|
+
lines << "- `#{r[:verb]}` `#{r[:path]}` → #{r[:action]}#{helper_part}#{params_part}#{condition}"
|
|
278
297
|
end
|
|
279
298
|
|
|
280
|
-
lines.concat(mounted_apps_lines(mounted_apps, controller ? [] : routes[:engine_routes]))
|
|
299
|
+
lines.concat(mounted_apps_lines(mounted_apps, controller ? [] : routes[:engine_routes], routes[:grape_endpoints]))
|
|
281
300
|
|
|
282
301
|
if excluded_framework_count > 0 && controller.nil?
|
|
283
302
|
lines << "" << "_#{count_phrase(excluded_framework_count, "framework route")} hidden. " \
|
|
284
303
|
"Use `app_only:false` to include them._"
|
|
285
304
|
end
|
|
305
|
+
lines << "" << GEM_DRAWN_NOTE if from_source
|
|
286
306
|
|
|
287
307
|
lines << "" << page[:hint] unless page[:hint].empty?
|
|
288
308
|
text_response(lines.join("\n"))
|
|
@@ -295,9 +315,15 @@ module RailsAiContext
|
|
|
295
315
|
lines << "| Verb | Path | Controller#Action | Name |"
|
|
296
316
|
lines << "|------|------|-------------------|------|"
|
|
297
317
|
page[:items].each do |r|
|
|
298
|
-
|
|
318
|
+
path = "`#{r[:path]}`"
|
|
319
|
+
path += " `#{r[:constraints]}`" if r[:constraints]
|
|
320
|
+
path += " (`#{r[:condition]}`)" if r[:condition]
|
|
321
|
+
cells = [ r[:verb], path, "#{r[:_ctrl]}##{r[:action]}", r[:name] || "-" ]
|
|
322
|
+
# A merged verb is `PATCH|PUT`, and a bare pipe splits the row.
|
|
323
|
+
lines << "| #{cells.map { |cell| cell.to_s.gsub("|", "\\|") }.join(' | ')} |"
|
|
299
324
|
end
|
|
300
|
-
lines.concat(mounted_apps_lines(mounted_apps, controller ? [] : routes[:engine_routes]))
|
|
325
|
+
lines.concat(mounted_apps_lines(mounted_apps, controller ? [] : routes[:engine_routes], routes[:grape_endpoints]))
|
|
326
|
+
lines << "" << GEM_DRAWN_NOTE if from_source
|
|
301
327
|
|
|
302
328
|
if routes[:api_namespaces]&.any?
|
|
303
329
|
lines << "" << "## API namespaces: #{routes[:api_namespaces].join(', ')}"
|