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
|
@@ -98,14 +98,16 @@ module RailsAiContext
|
|
|
98
98
|
lines << ""
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
table_exists =
|
|
101
|
+
databases = RailsAiContext::Payload.schema_databases(schema, table)
|
|
102
|
+
table_exists = databases.any?
|
|
103
|
+
database_lines, target = database_option(table, databases)
|
|
104
|
+
lines.concat(database_lines)
|
|
103
105
|
|
|
104
106
|
case action
|
|
105
107
|
when "add_column"
|
|
106
|
-
lines.concat(generate_add_column(table, column, type, options, table_exists))
|
|
108
|
+
lines.concat(generate_add_column(table, column, type, options, table_exists, target))
|
|
107
109
|
when "remove_column"
|
|
108
|
-
lines.concat(generate_remove_column(table, column, type, schema, models))
|
|
110
|
+
lines.concat(generate_remove_column(table, column, type, schema, models, target))
|
|
109
111
|
when "rename_column"
|
|
110
112
|
rename_to = new_name&.to_s&.strip
|
|
111
113
|
rename_to = type if rename_to.nil? || rename_to.empty?
|
|
@@ -113,7 +115,7 @@ module RailsAiContext
|
|
|
113
115
|
when "add_index"
|
|
114
116
|
lines.concat(generate_add_index(table, column, options))
|
|
115
117
|
when "add_association"
|
|
116
|
-
lines.concat(generate_add_association(table, column, type, options))
|
|
118
|
+
lines.concat(generate_add_association(table, column, type, options, schema))
|
|
117
119
|
when "change_type"
|
|
118
120
|
lines.concat(generate_change_type(table, column, type, options))
|
|
119
121
|
when "create_table"
|
|
@@ -141,7 +143,42 @@ module RailsAiContext
|
|
|
141
143
|
parts.join
|
|
142
144
|
end
|
|
143
145
|
|
|
144
|
-
|
|
146
|
+
# A table only a secondary database holds needs its migration in that database's migrations_paths.
|
|
147
|
+
# [lines, target]: target is nil for the primary, false when no environment configures the
|
|
148
|
+
# database, else the generator's --database flag and the RAILS_ENV that configures it.
|
|
149
|
+
def database_option(table, databases)
|
|
150
|
+
return [ [], nil ] if databases.empty? || databases.include?("primary")
|
|
151
|
+
|
|
152
|
+
yml = RailsAiContext::DatabaseYml
|
|
153
|
+
root = rails_app.root
|
|
154
|
+
found = databases.to_h { |db| [ db, (entry = yml.entry(root, db)) ? [ nil, entry ] : yml.elsewhere(root, db) ] }.compact
|
|
155
|
+
if found.empty?
|
|
156
|
+
note = "**Database:** `#{table}` is in the #{databases.join(", ")} dump, which no environment in config/database.yml configures: " \
|
|
157
|
+
"no `--database` reaches it, and a migration generated here runs on the primary."
|
|
158
|
+
return [ [ note, "" ], false ]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
names = found.keys
|
|
162
|
+
env_name = found.values.first.first
|
|
163
|
+
target = { flag: " --database #{names.first}", env: env_name }
|
|
164
|
+
paths = Array(found.values.first.last["migrations_paths"]).join(", ")
|
|
165
|
+
note = "**Database:** `#{table}` is in #{[ names[0..-2].join(", "), names.last ].reject(&:empty?).join(" and ")}," \
|
|
166
|
+
"#{" which #{env_name} configures and this environment does not," if env_name} not the primary database: generate with #{"`RAILS_ENV=#{env_name}` and " if env_name}`#{target[:flag].strip}` " \
|
|
167
|
+
"so the migration lands in #{env_name && !paths.empty? ? paths : "that database's migrations_paths"} and " \
|
|
168
|
+
"#{"#{env_name}'s " if env_name}`bin/rails db:migrate` runs it there."
|
|
169
|
+
groups = found.group_by { |_, (_, entry)| Array(entry["migrations_paths"]) }
|
|
170
|
+
if groups.size > 1
|
|
171
|
+
note += " Those databases read different migrations_paths, so generate it once per database: " \
|
|
172
|
+
"#{groups.values.map { |dbs| "`--database #{dbs.first.first}`" }.join(', ')}."
|
|
173
|
+
end
|
|
174
|
+
[ [ note, "" ], target ]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def generate_command(args, target)
|
|
178
|
+
"#{"RAILS_ENV=#{target[:env]} " if target && target[:env]}bin/rails generate migration #{args}#{target && target[:flag]}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def generate_add_column(table, column, type, options, table_exists, target = nil)
|
|
145
182
|
return [ "**Error:** column name is required for add_column" ] unless column
|
|
146
183
|
type ||= "string"
|
|
147
184
|
|
|
@@ -159,10 +196,10 @@ module RailsAiContext
|
|
|
159
196
|
opts = options ? ", #{options}" : ""
|
|
160
197
|
class_name = migration_class_name("add", table, column)
|
|
161
198
|
|
|
162
|
-
|
|
163
|
-
lines
|
|
199
|
+
command = generate_command("#{class_name} #{column}:#{type}", target) unless target == false
|
|
200
|
+
lines.push("**Run:** `#{command}`", "") if command
|
|
164
201
|
lines << "```ruby"
|
|
165
|
-
lines << "#
|
|
202
|
+
lines << "# #{command.sub("bin/rails", "rails")}" if command
|
|
166
203
|
lines << "class #{class_name} < ActiveRecord::Migration[#{rails_version}]"
|
|
167
204
|
lines << " def change"
|
|
168
205
|
lines << " add_column :#{table}, :#{column}, :#{type}#{opts}"
|
|
@@ -175,12 +212,12 @@ module RailsAiContext
|
|
|
175
212
|
lines
|
|
176
213
|
end
|
|
177
214
|
|
|
178
|
-
def generate_remove_column(table, column, type, schema, models)
|
|
215
|
+
def generate_remove_column(table, column, type, schema, models, target = nil)
|
|
179
216
|
return [ "**Error:** column name is required for remove_column" ] unless column
|
|
180
217
|
|
|
181
218
|
lines = []
|
|
182
219
|
|
|
183
|
-
table_exists = schema
|
|
220
|
+
table_exists = !RailsAiContext::Payload.schema_table(schema, table).nil?
|
|
184
221
|
unless table_exists
|
|
185
222
|
lines << "**Warning:** Table `#{table}` not found in current schema. This migration will fail."
|
|
186
223
|
lines << ""
|
|
@@ -196,8 +233,7 @@ module RailsAiContext
|
|
|
196
233
|
# Check if column is referenced
|
|
197
234
|
col_type = find_column_type(table, column, schema) || type || "string"
|
|
198
235
|
|
|
199
|
-
lines
|
|
200
|
-
lines << ""
|
|
236
|
+
lines.push("**Run:** `#{generate_command("#{class_name} #{column}:#{col_type}", target)}`", "") unless target == false
|
|
201
237
|
lines << "**Warning:** `remove_column` is irreversible without specifying the column type."
|
|
202
238
|
lines << ""
|
|
203
239
|
lines << "```ruby"
|
|
@@ -276,7 +312,7 @@ module RailsAiContext
|
|
|
276
312
|
lines
|
|
277
313
|
end
|
|
278
314
|
|
|
279
|
-
def generate_add_association(table, column, type, options)
|
|
315
|
+
def generate_add_association(table, column, type, options, schema = nil)
|
|
280
316
|
foreign_table = column || type
|
|
281
317
|
return [ "**Error:** Specify the associated table in column param (e.g., column: 'users')" ] unless foreign_table
|
|
282
318
|
|
|
@@ -287,10 +323,20 @@ module RailsAiContext
|
|
|
287
323
|
lines << ""
|
|
288
324
|
end
|
|
289
325
|
|
|
326
|
+
to_table = foreign_table.singularize.pluralize
|
|
327
|
+
own = RailsAiContext::Payload.schema_databases(schema, table)
|
|
328
|
+
theirs = RailsAiContext::Payload.schema_databases(schema, to_table)
|
|
329
|
+
across = own.any? && theirs.any? && !own.intersect?(theirs)
|
|
330
|
+
if across
|
|
331
|
+
lines << "**Foreign key:** `#{to_table}` is in #{theirs.join(' and ')} and `#{table}` in #{own.join(' and ')}; " \
|
|
332
|
+
"a foreign key cannot reach another database, so the reference below has none."
|
|
333
|
+
lines << ""
|
|
334
|
+
end
|
|
335
|
+
|
|
290
336
|
lines << "```ruby"
|
|
291
337
|
lines << "class Add#{foreign_table.camelize}To#{table.camelize} < ActiveRecord::Migration[#{rails_version}]"
|
|
292
338
|
lines << " def change"
|
|
293
|
-
lines << " add_reference :#{table}, :#{foreign_table.singularize}, foreign_key: true"
|
|
339
|
+
lines << " add_reference :#{table}, :#{foreign_table.singularize}#{", foreign_key: true" unless across}"
|
|
294
340
|
lines << " end"
|
|
295
341
|
lines << "end"
|
|
296
342
|
lines << "```"
|
|
@@ -495,9 +541,7 @@ module RailsAiContext
|
|
|
495
541
|
|
|
496
542
|
def column_exists?(table, column)
|
|
497
543
|
schema = cached_context[:schema]
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
table_data = schema[:tables][table]
|
|
544
|
+
table_data = RailsAiContext::Payload.schema_table(schema, table)
|
|
501
545
|
return false unless table_data
|
|
502
546
|
|
|
503
547
|
col_str = column.to_s
|
|
@@ -506,9 +550,7 @@ module RailsAiContext
|
|
|
506
550
|
|
|
507
551
|
def index_exists?(table, column)
|
|
508
552
|
schema = cached_context[:schema]
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
table_data = schema[:tables][table]
|
|
553
|
+
table_data = RailsAiContext::Payload.schema_table(schema, table)
|
|
512
554
|
return false unless table_data
|
|
513
555
|
|
|
514
556
|
# add_index collides only with the name it would give the index; a
|
|
@@ -521,9 +563,7 @@ module RailsAiContext
|
|
|
521
563
|
end
|
|
522
564
|
|
|
523
565
|
def find_column_type(table, column, schema)
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
table_data = schema[:tables][table]
|
|
566
|
+
table_data = RailsAiContext::Payload.schema_table(schema, table)
|
|
527
567
|
return nil unless table_data
|
|
528
568
|
|
|
529
569
|
col = (table_data[:columns] || []).find { |c| c[:name] == column }
|
|
@@ -5,7 +5,7 @@ module RailsAiContext
|
|
|
5
5
|
class Onboard < BaseTool
|
|
6
6
|
tool_name "rails_onboard"
|
|
7
7
|
description "Get a narrative walkthrough of the Rails application - stack, data model, authentication, key flows, " \
|
|
8
|
-
"background jobs, frontend, testing,
|
|
8
|
+
"background jobs, frontend, testing, getting started instructions, and the app's custom rake tasks, generators and Railties. " \
|
|
9
9
|
"Use when: first encountering a project, onboarding a new developer, or orienting an AI agent. " \
|
|
10
10
|
"Key params: detail (quick/standard/full)."
|
|
11
11
|
|
|
@@ -28,11 +28,14 @@ module RailsAiContext
|
|
|
28
28
|
|
|
29
29
|
annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
|
|
30
30
|
|
|
31
|
-
STANDARD_SECTIONS = %i[stack data_model auth key_flows jobs frontend testing getting_started].freeze
|
|
31
|
+
STANDARD_SECTIONS = %i[stack data_model auth key_flows jobs frontend testing getting_started rake_tasks generators].freeze
|
|
32
32
|
FULL_SECTIONS = %i[
|
|
33
33
|
stack data_model auth key_flows jobs frontend payments realtime storage api devops i18n engines env
|
|
34
|
-
testing getting_started
|
|
34
|
+
testing getting_started all_rake_tasks generators
|
|
35
35
|
].freeze
|
|
36
|
+
AUTH_GEMS = %w[devise omniauth rodauth-rails sorcery clearance authlogic].freeze
|
|
37
|
+
AUTHZ_GEMS = %w[pundit cancancan action_policy rolify].freeze
|
|
38
|
+
RAKE_TASKS_SHOWN = 15
|
|
36
39
|
|
|
37
40
|
def self.call(detail: "standard", server_context: nil)
|
|
38
41
|
ctx = cached_context
|
|
@@ -58,14 +61,14 @@ module RailsAiContext
|
|
|
58
61
|
def compose_quick(ctx)
|
|
59
62
|
app = ctx[:app_name] || "This Rails app"
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
ruby = ruby_phrase(ctx)
|
|
62
65
|
rails = named_rails_version(ctx)
|
|
63
66
|
head = "**#{app}** is a Rails#{" #{rails}" if rails}"
|
|
64
67
|
# The slash pairs two versions; with no Rails version to pair, the
|
|
65
68
|
# Ruby one follows the noun instead of sitting in front of it.
|
|
66
|
-
head += " /
|
|
69
|
+
head += " / #{ruby}" if ruby && rails
|
|
67
70
|
parts = [ head, "app" ]
|
|
68
|
-
parts << "running
|
|
71
|
+
parts << "running #{ruby}" if ruby && !rails
|
|
69
72
|
|
|
70
73
|
# Stats: tables, models, jobs
|
|
71
74
|
stats = []
|
|
@@ -97,10 +100,18 @@ module RailsAiContext
|
|
|
97
100
|
tests = Payload.section(ctx, :tests)
|
|
98
101
|
if tests
|
|
99
102
|
framework = tests[:framework]
|
|
100
|
-
parts <<
|
|
103
|
+
parts << if RailsAiContext::TestFramework.none?(framework) then "with no tests yet"
|
|
104
|
+
elsif framework == RailsAiContext::TestFramework::NOT_READ then "with its tests not read"
|
|
105
|
+
else "tested with #{framework || 'unknown framework'}"
|
|
106
|
+
end
|
|
101
107
|
end
|
|
102
108
|
|
|
103
|
-
parts.join(" ") + "."
|
|
109
|
+
[ parts.join(" ") + ".", unread_bundle(rails) ].compact.join(" ")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def unread_bundle(rails)
|
|
113
|
+
unread = RailsAiContext::GemLock.for(rails_app.root).unread_bundle unless rails
|
|
114
|
+
"Its gems and Rails version are not read: #{unread}." if unread
|
|
104
115
|
end
|
|
105
116
|
|
|
106
117
|
def compose_standard(ctx)
|
|
@@ -129,11 +140,15 @@ module RailsAiContext
|
|
|
129
140
|
elsif RailsAiContext::AppKind.mongoid?(rails_app.root)
|
|
130
141
|
database = RailsAiContext::AppKind.mongoid_database(rails_app.root)
|
|
131
142
|
db = "MongoDB through Mongoid#{" (database #{database})" if database}"
|
|
143
|
+
elsif !RailsAiContext::AppKind.active_record?(rails_app.root)
|
|
144
|
+
db = nil
|
|
132
145
|
else
|
|
133
|
-
db =
|
|
146
|
+
db = RailsAiContext::SchemaAdapter.label(ctx)
|
|
134
147
|
end
|
|
135
148
|
rails = named_rails_version(ctx)
|
|
136
|
-
lines << "#{ctx[:app_name]} is a Rails#{" #{rails}" if rails} application#{ruby_clause(ctx)} on #{db}."
|
|
149
|
+
lines << "#{ctx[:app_name]} is a Rails#{" #{rails}" if rails} application#{ruby_clause(ctx)} #{db ? "on #{db}" : "without Active Record"}."
|
|
150
|
+
unread = unread_bundle(rails)
|
|
151
|
+
lines << unread if unread
|
|
137
152
|
|
|
138
153
|
notable = Payload.notable_gems(ctx)
|
|
139
154
|
if notable.any?
|
|
@@ -190,64 +205,58 @@ module RailsAiContext
|
|
|
190
205
|
end
|
|
191
206
|
|
|
192
207
|
def section_auth(ctx)
|
|
193
|
-
auth = Payload.section(ctx, :auth)
|
|
194
|
-
|
|
195
|
-
|
|
208
|
+
auth = Payload.section(ctx, :auth) || {}
|
|
209
|
+
gem_authn, gem_authz = auth_gem_lines(ctx)
|
|
210
|
+
# Each half falls back to the gems on its own: a policy directory says nothing about login.
|
|
211
|
+
authn = authentication_lines(auth).presence || gem_authn
|
|
212
|
+
authz = authorization_lines(auth).presence || gem_authz
|
|
213
|
+
found = authn + Introspectors::AuthIntrospector.digest_sentences(auth[:authentication]) + authz
|
|
214
|
+
return [] if found.empty?
|
|
215
|
+
|
|
216
|
+
[ "## Authentication & Authorization", "", *found, "" ]
|
|
217
|
+
end
|
|
196
218
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if authentication[:model]
|
|
205
|
-
lines << "The #{authentication[:model]} model handles user accounts."
|
|
206
|
-
has_content = true
|
|
207
|
-
end
|
|
208
|
-
if authorization[:method]
|
|
209
|
-
lines << "Authorization uses #{authorization[:method]}."
|
|
210
|
-
has_content = true
|
|
211
|
-
end
|
|
219
|
+
# What the auth introspector writes, one sentence per finding.
|
|
220
|
+
def authentication_lines(auth)
|
|
221
|
+
authn = auth[:authentication] || {}
|
|
222
|
+
modules = auth[:devise_modules_per_model] || {}
|
|
223
|
+
lines = Array(authn[:devise]).map do |entry|
|
|
224
|
+
used = Array(modules[entry[:model]])
|
|
225
|
+
"Authentication via Devise on #{entry[:model]}#{" (#{used.join(', ')})" if used.any?}."
|
|
212
226
|
end
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if notable.any?
|
|
218
|
-
auth_gem_names = %w[devise omniauth rodauth sorcery clearance authlogic]
|
|
219
|
-
auth_gems = notable.select { |g| g.is_a?(Hash) && auth_gem_names.include?(g[:name].to_s) }
|
|
220
|
-
if auth_gems.any?
|
|
221
|
-
lines << "Authentication via #{auth_gems.map { |g| "#{g[:name]}#{g[:version] ? " (#{g[:version]})" : ""}" }.join(', ')}."
|
|
222
|
-
has_content = true
|
|
223
|
-
end
|
|
224
|
-
authz_gem_names = %w[pundit cancancan action_policy rolify]
|
|
225
|
-
authz_gems = notable.select { |g| g.is_a?(Hash) && authz_gem_names.include?(g[:name].to_s) }
|
|
226
|
-
if authz_gems.any?
|
|
227
|
-
lines << "Authorization via #{authz_gems.map { |g| g[:name] }.join(', ')}."
|
|
228
|
-
has_content = true
|
|
229
|
-
end
|
|
230
|
-
end
|
|
227
|
+
lines << "Authentication via the Rails authentication generator (Session and Current models)." if authn[:rails_auth]
|
|
228
|
+
if (rodauth = authn[:rodauth])
|
|
229
|
+
classes = Array(rodauth[:classes])
|
|
230
|
+
lines << "Authentication via Rodauth#{" (#{classes.join(', ')})" if classes.any?}."
|
|
231
231
|
end
|
|
232
|
+
lines << "has_secure_password on #{authn[:has_secure_password].join(', ')}." if Array(authn[:has_secure_password]).any?
|
|
233
|
+
lines << "OmniAuth providers: #{authn[:omniauth_providers].join(', ')}." if Array(authn[:omniauth_providers]).any?
|
|
234
|
+
lines
|
|
235
|
+
end
|
|
232
236
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
auth_checks = Array(conv[:authorization_checks]) + before_acts
|
|
239
|
-
if auth_checks.any?
|
|
240
|
-
lines << "Auth checks detected: #{auth_checks.first(5).join(', ')}."
|
|
241
|
-
has_content = true
|
|
242
|
-
end
|
|
243
|
-
end
|
|
237
|
+
def authorization_lines(auth)
|
|
238
|
+
authz = auth[:authorization] || {}
|
|
239
|
+
lines = []
|
|
240
|
+
{ pundit: "Pundit", action_policy: "Action Policy" }.each do |key, label|
|
|
241
|
+
lines << "Authorization via #{label} (#{count_phrase(authz[key].size, "policy")})." if Array(authz[key]).any?
|
|
244
242
|
end
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
lines << ""
|
|
243
|
+
lines << "#{count_phrase(authz[:policies].size, "policy class")} in app/policies." if Array(authz[:policies]).any?
|
|
244
|
+
lines << "Authorization via CanCanCan (app/models/ability.rb)." if authz[:cancancan]
|
|
245
|
+
lines << "An Ability class in #{authz[:ability_class]}." if authz[:ability_class]
|
|
248
246
|
lines
|
|
249
247
|
end
|
|
250
248
|
|
|
249
|
+
# [authentication lines, authorization lines] named from the notable gems alone.
|
|
250
|
+
def auth_gem_lines(ctx)
|
|
251
|
+
notable = Payload.notable_gems(ctx).select { |g| g.is_a?(Hash) }
|
|
252
|
+
authn = notable.select { |g| AUTH_GEMS.include?(g[:name].to_s) }
|
|
253
|
+
authz = notable.select { |g| AUTHZ_GEMS.include?(g[:name].to_s) }
|
|
254
|
+
[
|
|
255
|
+
authn.any? ? [ "Authentication via #{authn.map { |g| "#{g[:name]}#{" (#{g[:version]})" if g[:version]}" }.join(', ')}." ] : [],
|
|
256
|
+
authz.any? ? [ "Authorization via #{authz.map { |g| g[:name] }.join(', ')}." ] : []
|
|
257
|
+
]
|
|
258
|
+
end
|
|
259
|
+
|
|
251
260
|
def section_key_flows(ctx)
|
|
252
261
|
routes = Payload.section(ctx, :routes)
|
|
253
262
|
return [] unless routes
|
|
@@ -361,9 +370,7 @@ module RailsAiContext
|
|
|
361
370
|
framework = tests[:framework] || "unknown"
|
|
362
371
|
lines << "Framework: #{framework}."
|
|
363
372
|
|
|
364
|
-
|
|
365
|
-
fixtures = tests[:fixtures]
|
|
366
|
-
lines << "Data setup: #{factories ? "FactoryBot (#{factory_phrase(tests)})" : fixtures ? "fixtures (#{RailsAiContext::TestFramework.fixture_phrase(fixtures)})" : "inline"}."
|
|
373
|
+
lines << "Data setup: #{data_setup_phrase(tests)}." unless framework == RailsAiContext::TestFramework::NOT_READ
|
|
367
374
|
|
|
368
375
|
ci = tests[:ci_config]
|
|
369
376
|
lines << "CI: #{ci.join(', ')}." if ci&.any?
|
|
@@ -377,12 +384,32 @@ module RailsAiContext
|
|
|
377
384
|
lines
|
|
378
385
|
end
|
|
379
386
|
|
|
387
|
+
def data_setup_phrase(tests)
|
|
388
|
+
if tests[:factories]
|
|
389
|
+
"FactoryBot (#{factory_phrase(tests)})"
|
|
390
|
+
elsif tests[:fabricators]
|
|
391
|
+
"Fabrication (#{fabricator_phrase(tests)})"
|
|
392
|
+
elsif tests[:fixtures]
|
|
393
|
+
"fixtures (#{RailsAiContext::TestFramework.fixture_phrase(tests[:fixtures])})"
|
|
394
|
+
else
|
|
395
|
+
"inline"
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def fabricator_phrase(tests)
|
|
400
|
+
files = count_phrase(tests[:fabricators][:count].to_i, "file")
|
|
401
|
+
names = tests[:fabricator_names]
|
|
402
|
+
return files unless names.is_a?(Hash) && names.any?
|
|
403
|
+
|
|
404
|
+
"#{count_phrase(names.values.sum { |list| Array(list).size }, "fabricator")} in #{files}"
|
|
405
|
+
end
|
|
406
|
+
|
|
380
407
|
# The factories a suite defines, and the files they sit in: one file
|
|
381
408
|
# commonly defines several, so the file count is not the factory count.
|
|
382
409
|
def factory_phrase(tests)
|
|
383
|
-
files =
|
|
410
|
+
files = RailsAiContext::TestFramework.factory_files_phrase(tests[:factories])
|
|
384
411
|
names = tests[:factory_names]
|
|
385
|
-
return files unless names.is_a?(Hash) && names.any?
|
|
412
|
+
return files || "files not read" unless names.is_a?(Hash) && names.any?
|
|
386
413
|
|
|
387
414
|
# A factory named in a loop defines at least one, so the count is a floor.
|
|
388
415
|
computed = tests[:computed_factories].to_i
|
|
@@ -406,6 +433,54 @@ module RailsAiContext
|
|
|
406
433
|
].compact
|
|
407
434
|
end
|
|
408
435
|
|
|
436
|
+
def section_rake_tasks(ctx, limit: RAKE_TASKS_SHOWN)
|
|
437
|
+
tasks = Payload.list(ctx, :rake_tasks, :tasks)
|
|
438
|
+
return [] if tasks.empty?
|
|
439
|
+
|
|
440
|
+
limit ||= tasks.size
|
|
441
|
+
lines = [ "## Rake Tasks", "" ]
|
|
442
|
+
tasks.first(limit).each { |task| lines << rake_task_line(task) }
|
|
443
|
+
lines << "...#{tasks.size - limit} more: `detail:\"full\"` lists every task." if tasks.size > limit
|
|
444
|
+
lines << ""
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def section_all_rake_tasks(ctx)
|
|
448
|
+
section_rake_tasks(ctx, limit: nil)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def section_generators(ctx)
|
|
452
|
+
data = Payload.section(ctx, :rake_tasks) || {}
|
|
453
|
+
generators = Array(data[:generators])
|
|
454
|
+
templates = Array(data[:generator_templates])
|
|
455
|
+
railties = Array(data[:railties])
|
|
456
|
+
return [] if generators.empty? && templates.empty? && railties.empty?
|
|
457
|
+
|
|
458
|
+
lines = [ "## Generators and Railties", "" ]
|
|
459
|
+
generators.each do |g|
|
|
460
|
+
lines << "- `#{g[:command]}`#{" - #{g[:usage]}" if g[:usage]} (`#{g[:file]}`)"
|
|
461
|
+
end
|
|
462
|
+
templates.each do |t|
|
|
463
|
+
lines << "- `#{t[:file]}` replaces the template the `#{t[:generator]}` generator writes"
|
|
464
|
+
end
|
|
465
|
+
railties.each do |r|
|
|
466
|
+
line = "- Railtie `#{r[:name]}` (`#{r[:file]}`)"
|
|
467
|
+
notes = []
|
|
468
|
+
notes << "initializers #{r[:initializers].map { |i| "`#{i}`" }.join(', ')}" if Array(r[:initializers]).any?
|
|
469
|
+
notes << "adds rake tasks" if r[:rake_tasks]
|
|
470
|
+
lines << (notes.any? ? "#{line}: #{notes.join('; ')}" : line)
|
|
471
|
+
end
|
|
472
|
+
lines << ""
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def rake_task_line(task)
|
|
476
|
+
return "- `#{task[:file]}`: not read (#{task[:error]})" if task[:error]
|
|
477
|
+
|
|
478
|
+
args = Array(task[:args])
|
|
479
|
+
line = "- `#{task[:name]}#{"[#{args.join(',')}]" if args.any?}`"
|
|
480
|
+
line += " - #{task[:description]}" if task[:description]
|
|
481
|
+
line + " (`#{task[:file]}`)"
|
|
482
|
+
end
|
|
483
|
+
|
|
409
484
|
# ── Full-only sections ───────────────────────────────────────────
|
|
410
485
|
|
|
411
486
|
def section_payments(ctx)
|
|
@@ -470,34 +545,29 @@ module RailsAiContext
|
|
|
470
545
|
text = Payload.section(ctx, :action_text)
|
|
471
546
|
return [] unless storage || text
|
|
472
547
|
|
|
473
|
-
lines = [
|
|
548
|
+
lines = []
|
|
474
549
|
if storage && storage[:attachments]&.any?
|
|
475
550
|
lines << "Active Storage: #{count_phrase(storage[:attachments].size, "attachment")} across models."
|
|
476
551
|
end
|
|
477
552
|
if text && text[:models]&.any?
|
|
478
553
|
lines << "Action Text: #{count_phrase(text[:models].size, "model")} with rich text fields."
|
|
479
554
|
end
|
|
480
|
-
|
|
481
|
-
|
|
555
|
+
return [] if lines.empty?
|
|
556
|
+
|
|
557
|
+
[ "## File Storage & Rich Text", "", *lines, "" ]
|
|
482
558
|
end
|
|
483
559
|
|
|
484
560
|
def section_api(ctx)
|
|
485
561
|
api = Payload.section(ctx, :api)
|
|
486
562
|
return [] unless api
|
|
487
|
-
return [] if api.empty? || (api[:endpoints]&.empty? && api[:graphql].nil?)
|
|
488
563
|
|
|
489
|
-
lines = [
|
|
490
|
-
if api[:graphql]
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if api[:serializers]&.any?
|
|
497
|
-
lines << "Serializers: #{api[:serializers].size}."
|
|
498
|
-
end
|
|
499
|
-
lines << ""
|
|
500
|
-
lines
|
|
564
|
+
lines = []
|
|
565
|
+
lines << "GraphQL API detected." if api[:graphql]
|
|
566
|
+
serialization = GetApi.serialization_found(api)
|
|
567
|
+
lines << "Serialization: #{serialization}." if serialization
|
|
568
|
+
return [] if lines.empty?
|
|
569
|
+
|
|
570
|
+
[ "## API", "", *lines, "" ]
|
|
501
571
|
end
|
|
502
572
|
|
|
503
573
|
def section_devops(ctx)
|
|
@@ -506,9 +576,11 @@ module RailsAiContext
|
|
|
506
576
|
has_content = false
|
|
507
577
|
|
|
508
578
|
if devops
|
|
509
|
-
lines << "Dockerfile: #{devops[:
|
|
510
|
-
|
|
511
|
-
|
|
579
|
+
lines << "Dockerfile: #{devops[:docker] ? 'present' : 'not found'}."
|
|
580
|
+
procfiles = Array(devops[:procfile]).map { |procfile| procfile[:file] }
|
|
581
|
+
lines << "Procfile: #{procfiles.include?('Procfile') ? 'present' : 'not found'}."
|
|
582
|
+
lines << "Procfile.dev: present." if procfiles.include?("Procfile.dev")
|
|
583
|
+
deploy = devops[:deployment]
|
|
512
584
|
lines << "Deployment: #{deploy}." if deploy
|
|
513
585
|
has_content = true
|
|
514
586
|
end
|
|
@@ -517,7 +589,7 @@ module RailsAiContext
|
|
|
517
589
|
unless has_content
|
|
518
590
|
root = rails_app.root.to_s
|
|
519
591
|
has_dockerfile = File.exist?(File.join(root, "Dockerfile")) || File.exist?(File.join(root, "Dockerfile.dev"))
|
|
520
|
-
has_procfile = File.exist?(File.join(root, "Procfile"))
|
|
592
|
+
has_procfile = File.exist?(File.join(root, "Procfile"))
|
|
521
593
|
has_ci = Dir.exist?(File.join(root, ".github", "workflows")) || File.exist?(File.join(root, ".gitlab-ci.yml"))
|
|
522
594
|
|
|
523
595
|
if has_dockerfile || has_procfile || has_ci
|
|
@@ -551,7 +623,11 @@ module RailsAiContext
|
|
|
551
623
|
mounted.each do |e|
|
|
552
624
|
name = e[:engine]
|
|
553
625
|
path = e[:path]
|
|
554
|
-
|
|
626
|
+
next unless name
|
|
627
|
+
|
|
628
|
+
line = path ? "- **#{name}** at `#{path}`" : "- **#{name}**"
|
|
629
|
+
line += " (`#{e[:condition]}`)" if e[:condition]
|
|
630
|
+
lines << line
|
|
555
631
|
end
|
|
556
632
|
lines << ""
|
|
557
633
|
lines
|
|
@@ -588,10 +664,20 @@ module RailsAiContext
|
|
|
588
664
|
# is the interpreter running this tool, which says nothing about the
|
|
589
665
|
# app. Then the sentence names no Ruby at all.
|
|
590
666
|
def ruby_clause(ctx)
|
|
667
|
+
ruby = ruby_phrase(ctx)
|
|
668
|
+
return "" unless ruby
|
|
669
|
+
|
|
670
|
+
ctx[:tier].to_s == "static" ? " declaring #{ruby}" : " running #{ruby}"
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# "Ruby 3.4.9", or the engine first ("JRuby 9.4.8.0 (Ruby 3.1.4)"):
|
|
674
|
+
# JRuby's own version is not the Ruby version it implements.
|
|
675
|
+
def ruby_phrase(ctx)
|
|
591
676
|
version = named_ruby_version(ctx)
|
|
592
|
-
|
|
677
|
+
engine = ctx[:ruby_engine].to_s
|
|
678
|
+
return version && "Ruby #{version}" if engine.empty?
|
|
593
679
|
|
|
594
|
-
|
|
680
|
+
version ? "#{engine} (Ruby #{version})" : engine
|
|
595
681
|
end
|
|
596
682
|
|
|
597
683
|
# The version a sentence may name, or nil. Statically that is the one
|
|
@@ -66,24 +66,16 @@ module RailsAiContext
|
|
|
66
66
|
|
|
67
67
|
lines = [ "# Performance Analysis", "" ]
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# Apply model filter to count
|
|
73
|
-
filtered_count = if model && !model.empty?
|
|
74
|
-
all_sections.values.sum { |items| filter_items(items, model).size }
|
|
75
|
-
elsif category != "all"
|
|
76
|
-
(all_sections[category] || []).size
|
|
77
|
-
else
|
|
78
|
-
all_sections.values.sum(&:size)
|
|
79
|
-
end
|
|
69
|
+
kept = CATEGORIES.select { |name, _| category == "all" || category == name }
|
|
70
|
+
.to_h { |name, (key, _, _)| [ name, filter_items(data[key], model) ] }
|
|
71
|
+
filtered_count = kept.values.sum(&:size)
|
|
80
72
|
|
|
81
73
|
lines << "**Total issues found:** #{filtered_count}"
|
|
82
74
|
lines << ""
|
|
83
75
|
|
|
84
76
|
if RailsAiContext::DetailLevel.summary?(detail)
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
kept.each do |name, items|
|
|
78
|
+
label = CATEGORIES[name][1]
|
|
87
79
|
counts = name == "n_plus_one" ? risk_summary_counts(items) : ""
|
|
88
80
|
lines << "- #{label}: #{items.size}#{counts}"
|
|
89
81
|
end
|