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
|
@@ -23,11 +23,12 @@ module RailsAiContext
|
|
|
23
23
|
},
|
|
24
24
|
column: {
|
|
25
25
|
type: "string",
|
|
26
|
-
description: "Column name (e.g., 'email', 'status')"
|
|
26
|
+
description: "Column name (e.g., 'email', 'status'). For add_association, the reference (e.g., 'user', 'parent')."
|
|
27
27
|
},
|
|
28
28
|
type: {
|
|
29
29
|
type: "string",
|
|
30
|
-
description: "Column type (e.g., 'string', 'integer', 'boolean', 'references')"
|
|
30
|
+
description: "Column type (e.g., 'string', 'integer', 'boolean', 'references'). " \
|
|
31
|
+
"For add_association with column, the table the reference points at (e.g., 'users')."
|
|
31
32
|
},
|
|
32
33
|
new_name: {
|
|
33
34
|
type: "string",
|
|
@@ -57,11 +58,24 @@ module RailsAiContext
|
|
|
57
58
|
# mode, so the tool says so in its output when it falls back here.
|
|
58
59
|
SUPPORTED_RAILS_FLOOR = "7.0"
|
|
59
60
|
|
|
61
|
+
# Room for every option a column takes, a long `comment:` included.
|
|
62
|
+
MAX_OPTIONS_LENGTH = 1_024
|
|
63
|
+
|
|
60
64
|
def self.call(action: nil, table: nil, column: nil, type: nil, new_name: nil, options: nil, server_context: nil)
|
|
61
65
|
action = action.to_s.strip
|
|
62
66
|
table = table.to_s.strip
|
|
63
67
|
column = column.to_s.strip.presence if column
|
|
64
68
|
|
|
69
|
+
# No database takes a name this long (PostgreSQL stops at 63 bytes,
|
|
70
|
+
# MySQL at 64), and the generated code would carry it whole.
|
|
71
|
+
names = { "table" => table, "type" => type, "new_name" => new_name }
|
|
72
|
+
names["column"] = column unless action == "create_table"
|
|
73
|
+
long_name = names.find { |_, value| name_too_long?(value) }
|
|
74
|
+
if long_name || options.to_s.length > MAX_OPTIONS_LENGTH
|
|
75
|
+
param, value = long_name || [ "options", options ]
|
|
76
|
+
return text_response("**Error:** `#{param}` #{echo_input(value)} is longer than any migration takes.")
|
|
77
|
+
end
|
|
78
|
+
|
|
65
79
|
# Normalize model names to table names: "Post" → "posts", and
|
|
66
80
|
# "Admin::ActionLog" → the table its model recorded, not the
|
|
67
81
|
# `admin/action_logs` an underscore would build.
|
|
@@ -74,17 +88,17 @@ module RailsAiContext
|
|
|
74
88
|
|
|
75
89
|
# Validate identifier characters to produce valid migration code
|
|
76
90
|
unless table.match?(/\A[a-z_][a-z0-9_]*\z/)
|
|
77
|
-
return text_response("**Error:** Invalid table name `#{table}`. Use lowercase letters, digits, and underscores only.")
|
|
91
|
+
return text_response("**Error:** Invalid table name `#{echo_input(table)}`. Use lowercase letters, digits, and underscores only.")
|
|
78
92
|
end
|
|
79
93
|
# create_table uses column param as a comma-separated column:type definition string
|
|
80
94
|
if action != "create_table" && column && !column.empty? && !column.match?(/\A[a-z_][a-z0-9_]*\z/)
|
|
81
|
-
return text_response("**Error:** Invalid column name `#{column}`. Use lowercase letters, digits, and underscores only.")
|
|
95
|
+
return text_response("**Error:** Invalid column name `#{echo_input(column)}`. Use lowercase letters, digits, and underscores only.")
|
|
82
96
|
end
|
|
83
97
|
|
|
84
98
|
unless VALID_ACTIONS.include?(action)
|
|
85
99
|
suggestion = VALID_ACTIONS.find { |a| a.start_with?(action) || a.include?(action) }
|
|
86
100
|
hint = suggestion ? " Did you mean `#{suggestion}`?" : ""
|
|
87
|
-
return text_response("**Error:** Unknown action `#{action}`.#{hint} Valid actions: #{VALID_ACTIONS.join(', ')}")
|
|
101
|
+
return text_response("**Error:** Unknown action `#{echo_input(action)}`.#{hint} Valid actions: #{VALID_ACTIONS.join(', ')}")
|
|
88
102
|
end
|
|
89
103
|
|
|
90
104
|
schema = Payload.section(cached_context, :schema)
|
|
@@ -103,27 +117,35 @@ module RailsAiContext
|
|
|
103
117
|
database_lines, target = database_option(table, databases)
|
|
104
118
|
lines.concat(database_lines)
|
|
105
119
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
when "
|
|
120
|
-
|
|
121
|
-
when "
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
rename_to = new_name.to_s.strip.presence || type.to_s.strip.presence if action == "rename_column"
|
|
121
|
+
if (unknown = unknown_type(action, column, type))
|
|
122
|
+
return text_response((lines + [ unknown ]).join("\n"))
|
|
123
|
+
end
|
|
124
|
+
# What the app's migrations already hold: a file the generator's name
|
|
125
|
+
# collides with, and a pending one that makes the same change.
|
|
126
|
+
prior = prior_migration_lines(action, table, column, rename_to, type, migration_files_for(target), pending_versions(schema, databases))
|
|
127
|
+
lines.concat(prior)
|
|
128
|
+
# The generator refuses a name another migration has, so its command is not offered.
|
|
129
|
+
target = false if prior.any? { |line| line.start_with?("**Warning:** Another migration is already named") }
|
|
130
|
+
|
|
131
|
+
generated = case action
|
|
132
|
+
when "add_column" then generate_add_column(table, column, type, options, table_exists, target)
|
|
133
|
+
when "remove_column" then generate_remove_column(table, column, type, schema, models, target)
|
|
134
|
+
when "rename_column" then generate_rename_column(table, column, rename_to)
|
|
135
|
+
when "add_index" then generate_add_index(table, column, options)
|
|
136
|
+
when "add_association" then generate_add_association(table, column, type, options, schema)
|
|
137
|
+
when "change_type" then generate_change_type(table, column, type, options)
|
|
138
|
+
when "create_table" then generate_create_table(table, column, options, table_exists)
|
|
139
|
+
end
|
|
140
|
+
lines.concat(generated)
|
|
141
|
+
# A missing param writes no migration, so nothing below applies to one.
|
|
142
|
+
return text_response(lines.join("\n")) if generated.first.to_s.start_with?("**Error:**")
|
|
124
143
|
|
|
125
144
|
# Show affected models
|
|
126
|
-
lines.concat(show_affected_models(table, models))
|
|
145
|
+
lines.concat(show_affected_models(table, models, action: action, column: column))
|
|
146
|
+
# A column change breaks the code that reads the column, which no
|
|
147
|
+
# association names: that code is what the reader has to update.
|
|
148
|
+
lines.concat(column_mention_lines(column)) if %w[remove_column rename_column change_type].include?(action) && column
|
|
127
149
|
|
|
128
150
|
# Strong Migrations warnings (only when the gem is present in the project).
|
|
129
151
|
# add_association accepts the associated table via either `column` or
|
|
@@ -143,6 +165,162 @@ module RailsAiContext
|
|
|
143
165
|
parts.join
|
|
144
166
|
end
|
|
145
167
|
|
|
168
|
+
# The class each action's migration is named, which the generator
|
|
169
|
+
# turns into its file name; nil when a needed param is missing.
|
|
170
|
+
def migration_name(action, table, column, rename_to, type)
|
|
171
|
+
case action
|
|
172
|
+
when "add_column" then column && migration_class_name("add", table, column)
|
|
173
|
+
when "remove_column" then column && migration_class_name("remove", table, column)
|
|
174
|
+
when "rename_column" then column && rename_to && "Rename#{column.camelize}To#{rename_to.camelize}In#{table.camelize}"
|
|
175
|
+
when "add_index" then column && "AddIndexTo#{table.camelize}On#{column.camelize}"
|
|
176
|
+
when "add_association" then (reference = column || type) && "Add#{reference.camelize}To#{table.camelize}"
|
|
177
|
+
when "change_type" then column && "Change#{column.camelize}TypeIn#{table.camelize}"
|
|
178
|
+
when "create_table" then "Create#{table.camelize}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# The migration files of the database the migration goes to; none when
|
|
183
|
+
# there is no app root to read them from.
|
|
184
|
+
def migration_files_for(target)
|
|
185
|
+
root = rails_app.root.to_s
|
|
186
|
+
dirs = target.is_a?(Hash) && target[:dirs] ? target[:dirs] : RailsAiContext::PendingMigrations.migrate_dirs_for(root)
|
|
187
|
+
RailsAiContext::PendingMigrations.migration_files(dirs, root: root)
|
|
188
|
+
rescue StandardError => e
|
|
189
|
+
RailsAiContext.debug_fail(e, [], label: "migration_files_for")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The versions not yet run on that database; nil when unknown.
|
|
193
|
+
def pending_versions(schema, databases)
|
|
194
|
+
pending = if databases.empty? || databases.include?("primary")
|
|
195
|
+
schema&.dig(:pending_migrations)
|
|
196
|
+
else
|
|
197
|
+
schema&.dig(:secondary_databases, databases.first, :pending_migrations)
|
|
198
|
+
end
|
|
199
|
+
pending.is_a?(Array) ? pending.map { |m| m[:version].to_s } : nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# A file already named what the generator would name this one, and a
|
|
203
|
+
# pending migration that already makes this change. Rails' generator
|
|
204
|
+
# stops with "Another migration is already named ..." on the first;
|
|
205
|
+
# the second means the change is written and only waits to run.
|
|
206
|
+
def prior_migration_lines(action, table, column, rename_to, type, files, pending)
|
|
207
|
+
lines = []
|
|
208
|
+
name = migration_name(action, table, column, rename_to, type)
|
|
209
|
+
if name && (clash = files.find { |m| migration_file_name(m[:path]) == name.underscore })
|
|
210
|
+
waiting = pending&.include?(clash[:version].to_s)
|
|
211
|
+
lines << "**Warning:** Another migration is already named `#{name.underscore}` " \
|
|
212
|
+
"(`#{RailsAiContext::PortablePath.relativize(clash[:path], rails_app.root.to_s)}`#{", not yet run" if waiting}), " \
|
|
213
|
+
"so `bin/rails generate migration #{name}` stops with \"Another migration is already named #{name.underscore}\". " \
|
|
214
|
+
"#{waiting ? "Edit that migration before it runs, or give this one another name." : "Give this one another name."}"
|
|
215
|
+
lines << ""
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
changes = pending_changes(files, pending)
|
|
219
|
+
wanted = case action
|
|
220
|
+
when "add_column" then [ table, column ]
|
|
221
|
+
when "add_association" then [ table, "#{(column || type).to_s.singularize}_id" ]
|
|
222
|
+
when "create_table" then [ table, nil ]
|
|
223
|
+
end
|
|
224
|
+
if wanted && (found = changes.find { |c| c[:table] == wanted.first && c[:column] == wanted.last })
|
|
225
|
+
what = wanted.last ? "adds `#{wanted.last}`#{" (#{found[:type]})" if found[:type]} to `#{table}`" : "creates `#{table}`"
|
|
226
|
+
lines << "**Warning:** Pending migration `#{found[:version]} #{found[:name]}` already #{what}: " \
|
|
227
|
+
"run `bin/rails db:migrate` rather than generating another, or change that migration before it runs."
|
|
228
|
+
lines << ""
|
|
229
|
+
end
|
|
230
|
+
lines
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def migration_file_name(path)
|
|
234
|
+
File.basename(path, ".rb").sub(/\A\d+_/, "").split(".", 2).first
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# [{ table:, column:, type:, version:, name: }] for each column a pending
|
|
238
|
+
# migration adds, and each table it creates (column nil).
|
|
239
|
+
def pending_changes(files, pending)
|
|
240
|
+
return [] unless pending
|
|
241
|
+
|
|
242
|
+
files.select { |m| pending.include?(m[:version].to_s) }.flat_map do |m|
|
|
243
|
+
source = RailsAiContext::SafeFile.read(m[:path]) or next []
|
|
244
|
+
changes = []
|
|
245
|
+
collect_changes(RailsAiContext::AstCache.parse_string(source).value, nil, changes)
|
|
246
|
+
changes.map { |change| change.merge(version: m[:version], name: m[:name]) }
|
|
247
|
+
end
|
|
248
|
+
rescue => e
|
|
249
|
+
RailsAiContext.debug_fail(e, [], label: "pending_changes")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
TABLE_BLOCKS = %i[create_table change_table].freeze
|
|
253
|
+
|
|
254
|
+
# What a migration does when it runs, not what it takes back on a
|
|
255
|
+
# rollback: `def down`, a reversible's `dir.down` and a `revert`
|
|
256
|
+
# block hold the undo.
|
|
257
|
+
def undo?(node)
|
|
258
|
+
return node.name == :down if node.is_a?(Prism::DefNode)
|
|
259
|
+
return false unless node.is_a?(Prism::CallNode) && node.block
|
|
260
|
+
|
|
261
|
+
node.receiver ? node.name == :down : node.name == :revert
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def collect_changes(node, block_table, changes)
|
|
265
|
+
return unless node.is_a?(Prism::Node)
|
|
266
|
+
return if undo?(node)
|
|
267
|
+
|
|
268
|
+
if node.is_a?(Prism::CallNode)
|
|
269
|
+
args = Array(node.arguments&.arguments)
|
|
270
|
+
literal = ->(arg) { arg.unescaped.to_s if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode) }
|
|
271
|
+
if node.receiver.nil? && %i[add_column add_reference add_belongs_to].include?(node.name) && literal.(args[0]) && literal.(args[1])
|
|
272
|
+
column = node.name == :add_column ? literal.(args[1]) : "#{literal.(args[1])}_id"
|
|
273
|
+
changes << { table: literal.(args[0]), column: column, type: (literal.(args[2]) if node.name == :add_column) }
|
|
274
|
+
elsif node.receiver.nil? && TABLE_BLOCKS.include?(node.name) && literal.(args[0])
|
|
275
|
+
changes << { table: literal.(args[0]), column: nil } if node.name == :create_table
|
|
276
|
+
return collect_changes(node.block, literal.(args[0]), changes)
|
|
277
|
+
elsif block_table && node.receiver.is_a?(Prism::LocalVariableReadNode) && literal.(args[0])
|
|
278
|
+
case node.name
|
|
279
|
+
when :references, :belongs_to then changes << { table: block_table, column: "#{literal.(args[0])}_id" }
|
|
280
|
+
when :column then changes << { table: block_table, column: literal.(args[0]), type: literal.(args[1]) }
|
|
281
|
+
when :index, :remove, :rename, :change, :change_default, :change_null, :remove_references, :remove_belongs_to, :timestamps then nil
|
|
282
|
+
else changes << { table: block_table, column: literal.(args[0]), type: node.name.to_s }
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
node.compact_child_nodes.each { |child| collect_changes(child, block_table, changes) }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Migration column types: Rails' own, the adapters', and the
|
|
290
|
+
# connection's when the app is booted.
|
|
291
|
+
COLUMN_TYPES = %w[
|
|
292
|
+
string text integer bigint float decimal numeric datetime timestamp time date binary blob boolean
|
|
293
|
+
json virtual primary_key references belongs_to
|
|
294
|
+
jsonb uuid inet cidr macaddr hstore citext ltree tsvector tsquery money point line lseg box path polygon
|
|
295
|
+
circle bit bit_varying xml interval oid enum timestamptz int4range int8range numrange tsrange tstzrange daterange
|
|
296
|
+
tinytext mediumtext longtext tinyblob mediumblob longblob unsigned_integer unsigned_bigint
|
|
297
|
+
unsigned_float unsigned_decimal set year
|
|
298
|
+
].freeze
|
|
299
|
+
|
|
300
|
+
def column_types
|
|
301
|
+
types = COLUMN_TYPES.dup
|
|
302
|
+
if defined?(ActiveRecord::Base) && !RailsAiContext.static_tier?
|
|
303
|
+
types |= ActiveRecord::Base.connection.native_database_types.keys.map(&:to_s)
|
|
304
|
+
end
|
|
305
|
+
types
|
|
306
|
+
rescue StandardError
|
|
307
|
+
COLUMN_TYPES
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# A type no adapter knows writes a migration that fails when it runs.
|
|
311
|
+
def unknown_type(action, column, type)
|
|
312
|
+
written = case action
|
|
313
|
+
when "add_column", "change_type" then [ type ]
|
|
314
|
+
when "create_table" then column.to_s.split(",").map { |definition| definition.split(":")[1]&.strip }
|
|
315
|
+
end
|
|
316
|
+
bad = Array(written).compact.reject(&:empty?).find { |t| !column_types.include?(t) }
|
|
317
|
+
return nil unless bad
|
|
318
|
+
|
|
319
|
+
suggestion = find_closest_match(bad, column_types)
|
|
320
|
+
"**Error:** Unknown column type `#{echo_input(bad)}`.#{" Did you mean `#{suggestion}`?" if suggestion} " \
|
|
321
|
+
"Migration types include string, text, integer, bigint, decimal, boolean, date, datetime, json and references."
|
|
322
|
+
end
|
|
323
|
+
|
|
146
324
|
# A table only a secondary database holds needs its migration in that database's migrations_paths.
|
|
147
325
|
# [lines, target]: target is nil for the primary, false when no environment configures the
|
|
148
326
|
# database, else the generator's --database flag and the RAILS_ENV that configures it.
|
|
@@ -160,8 +338,10 @@ module RailsAiContext
|
|
|
160
338
|
|
|
161
339
|
names = found.keys
|
|
162
340
|
env_name = found.values.first.first
|
|
163
|
-
|
|
164
|
-
|
|
341
|
+
configured = Array(found.values.first.last["migrations_paths"])
|
|
342
|
+
dirs = configured.any? ? configured.map { |path| File.expand_path(path, root.to_s) } : [ File.join(root.to_s, "db", "#{names.first}_migrate") ]
|
|
343
|
+
target = { flag: " --database #{names.first}", env: env_name, dirs: dirs }
|
|
344
|
+
paths = configured.join(", ")
|
|
165
345
|
note = "**Database:** `#{table}` is in #{[ names[0..-2].join(", "), names.last ].reject(&:empty?).join(" and ")}," \
|
|
166
346
|
"#{" 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
347
|
"so the migration lands in #{env_name && !paths.empty? ? paths : "that database's migrations_paths"} and " \
|
|
@@ -193,6 +373,11 @@ module RailsAiContext
|
|
|
193
373
|
lines << ""
|
|
194
374
|
end
|
|
195
375
|
|
|
376
|
+
if table_exists && options.to_s.match?(/\bnull:\s*false\b/) && !options.to_s.match?(/\bdefault:/)
|
|
377
|
+
lines << not_null_warning(table, column)
|
|
378
|
+
lines << ""
|
|
379
|
+
end
|
|
380
|
+
|
|
196
381
|
opts = options ? ", #{options}" : ""
|
|
197
382
|
class_name = migration_class_name("add", table, column)
|
|
198
383
|
|
|
@@ -212,6 +397,22 @@ module RailsAiContext
|
|
|
212
397
|
lines
|
|
213
398
|
end
|
|
214
399
|
|
|
400
|
+
# NOT NULL with no default has no value for the rows already there, so
|
|
401
|
+
# the migration fails on a table that holds any. The row count is the
|
|
402
|
+
# database's own estimate when the app is booted.
|
|
403
|
+
def not_null_warning(table, column)
|
|
404
|
+
stats = Array(Payload.section(cached_context, :database_stats)&.dig(:tables)).find { |t| t[:table].to_s == table }
|
|
405
|
+
rows = stats&.dig(:approximate_rows)
|
|
406
|
+
held = if rows.nil? then "if `#{table}` holds any rows"
|
|
407
|
+
elsif stats[:at_least] then "as `#{table}` holds at least #{count_phrase(rows, "row")}"
|
|
408
|
+
else "as `#{table}` holds about #{count_phrase(rows, "row")}"
|
|
409
|
+
end
|
|
410
|
+
return "**Note:** `#{table}` is empty by the database's estimate, so `null: false` without a default runs; on a table with rows it fails." if rows&.zero?
|
|
411
|
+
|
|
412
|
+
"**Warning:** `null: false` without a default fails #{held}: add a `default:`, or add `#{column}` nullable, " \
|
|
413
|
+
"backfill it, then `change_column_null :#{table}, :#{column}, false`."
|
|
414
|
+
end
|
|
415
|
+
|
|
215
416
|
def generate_remove_column(table, column, type, schema, models, target = nil)
|
|
216
417
|
return [ "**Error:** column name is required for remove_column" ] unless column
|
|
217
418
|
|
|
@@ -255,7 +456,7 @@ module RailsAiContext
|
|
|
255
456
|
end
|
|
256
457
|
|
|
257
458
|
def generate_rename_column(table, column, new_name)
|
|
258
|
-
return [ "**Error:** column (old name) and
|
|
459
|
+
return [ "**Error:** `column` (the old name) and `new_name` are required for rename_column" ] unless column && new_name
|
|
259
460
|
|
|
260
461
|
lines = []
|
|
261
462
|
|
|
@@ -312,20 +513,42 @@ module RailsAiContext
|
|
|
312
513
|
lines
|
|
313
514
|
end
|
|
314
515
|
|
|
516
|
+
# `column` names the reference and `type`, when given with it, the
|
|
517
|
+
# table it points at; otherwise that table is the reference pluralized.
|
|
518
|
+
# A name with no table of its own is a role: `parent` on `comments` is
|
|
519
|
+
# a self-reference, which needs `to_table:`, and any other role needs
|
|
520
|
+
# the table named before a foreign key can be written.
|
|
315
521
|
def generate_add_association(table, column, type, options, schema = nil)
|
|
316
|
-
|
|
317
|
-
return [ "**Error:** Specify the associated table in column param (e.g., column: 'users')" ] unless
|
|
522
|
+
written = column || type
|
|
523
|
+
return [ "**Error:** Specify the associated table in column param (e.g., column: 'users')" ] unless written
|
|
318
524
|
|
|
525
|
+
reference = written.to_s.singularize
|
|
319
526
|
lines = []
|
|
320
|
-
fk_column = "#{
|
|
527
|
+
fk_column = "#{reference}_id"
|
|
321
528
|
if column_exists?(table, fk_column)
|
|
322
529
|
lines << "**Warning:** Column `#{fk_column}` already exists on `#{table}`. This migration will fail. Use `add_index` if you only need an index."
|
|
323
530
|
lines << ""
|
|
324
531
|
end
|
|
325
532
|
|
|
326
|
-
to_table =
|
|
533
|
+
to_table = column && type ? type.to_s : reference.pluralize
|
|
534
|
+
if schema_known?(schema) && !table_in_schema?(schema, to_table)
|
|
535
|
+
if column && type
|
|
536
|
+
lines << "**Warning:** No `#{to_table}` table in the schema, so a foreign key to it fails; the reference below has none."
|
|
537
|
+
to_table = nil
|
|
538
|
+
elsif reference == "parent"
|
|
539
|
+
lines << "**Note:** No `#{to_table}` table: `parent` on `#{table}` reads as a reference to `#{table}` itself, " \
|
|
540
|
+
"written with `to_table:`. Pass the table it points at as `type` to point it elsewhere."
|
|
541
|
+
to_table = table
|
|
542
|
+
else
|
|
543
|
+
lines << "**Warning:** No `#{to_table}` table: `#{reference}` reads as a role. Pass the table it points at as `type` " \
|
|
544
|
+
"(e.g. `type:\"users\"`); the reference below has no foreign key until then."
|
|
545
|
+
to_table = nil
|
|
546
|
+
end
|
|
547
|
+
lines << ""
|
|
548
|
+
end
|
|
549
|
+
|
|
327
550
|
own = RailsAiContext::Payload.schema_databases(schema, table)
|
|
328
|
-
theirs = RailsAiContext::Payload.schema_databases(schema, to_table)
|
|
551
|
+
theirs = to_table ? RailsAiContext::Payload.schema_databases(schema, to_table) : []
|
|
329
552
|
across = own.any? && theirs.any? && !own.intersect?(theirs)
|
|
330
553
|
if across
|
|
331
554
|
lines << "**Foreign key:** `#{to_table}` is in #{theirs.join(' and ')} and `#{table}` in #{own.join(' and ')}; " \
|
|
@@ -333,24 +556,58 @@ module RailsAiContext
|
|
|
333
556
|
lines << ""
|
|
334
557
|
end
|
|
335
558
|
|
|
559
|
+
foreign_key = if across || to_table.nil? then ""
|
|
560
|
+
elsif to_table == reference.pluralize then ", foreign_key: true"
|
|
561
|
+
else ", foreign_key: { to_table: :#{to_table} }"
|
|
562
|
+
end
|
|
336
563
|
lines << "```ruby"
|
|
337
|
-
lines << "class Add#{
|
|
564
|
+
lines << "class Add#{written.camelize}To#{table.camelize} < ActiveRecord::Migration[#{rails_version}]"
|
|
338
565
|
lines << " def change"
|
|
339
|
-
lines << " add_reference :#{table}, :#{
|
|
566
|
+
lines << " add_reference :#{table}, :#{reference}#{foreign_key}"
|
|
340
567
|
lines << " end"
|
|
341
568
|
lines << "end"
|
|
342
569
|
lines << "```"
|
|
343
570
|
lines << ""
|
|
344
571
|
lines << "**Reversible:** Yes"
|
|
345
|
-
lines
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
572
|
+
lines.concat(association_model_lines(table, reference, to_table))
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# The belongs_to and has_many the reference wants, in the models that
|
|
576
|
+
# read each table: a role names its class, and a self-reference is
|
|
577
|
+
# optional (a root has no parent) and names its children.
|
|
578
|
+
def association_model_lines(table, reference, to_table)
|
|
579
|
+
models = Payload.models(cached_context)
|
|
580
|
+
owner = models_for_table(table, models).first || table.singularize.camelize
|
|
581
|
+
lines = [ "**Also add to models:**", "```ruby", "# #{model_path(owner)}" ]
|
|
582
|
+
unless to_table
|
|
583
|
+
lines << "belongs_to :#{reference}, class_name: \"...\" # the model of the table it points at"
|
|
584
|
+
return lines << "```"
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
target = models_for_table(to_table, models).first || to_table.singularize.camelize
|
|
588
|
+
class_option = reference.camelize == target ? "" : ", class_name: \"#{target}\""
|
|
589
|
+
if to_table == table
|
|
590
|
+
lines << "belongs_to :#{reference}#{class_option}, optional: true"
|
|
591
|
+
lines << "has_many :children, class_name: \"#{target}\", foreign_key: :#{reference}_id, inverse_of: :#{reference}, dependent: :destroy"
|
|
592
|
+
else
|
|
593
|
+
lines << "belongs_to :#{reference}#{class_option}"
|
|
594
|
+
lines << "" << "# #{model_path(target)}"
|
|
595
|
+
inverse = class_option.empty? ? "" : ", foreign_key: :#{reference}_id, inverse_of: :#{reference}"
|
|
596
|
+
lines << "has_many :#{table}#{inverse}, dependent: :destroy"
|
|
597
|
+
end
|
|
352
598
|
lines << "```"
|
|
353
|
-
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def model_path(model)
|
|
602
|
+
Payload.model_file(cached_context, model) || "app/models/#{model.underscore}.rb"
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def schema_known?(schema)
|
|
606
|
+
schema.is_a?(Hash) && Payload.schema_tables(schema).any?
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def table_in_schema?(schema, name)
|
|
610
|
+
Payload.schema_tables(schema).any? { |_, table, _| table.to_s == name.to_s }
|
|
354
611
|
end
|
|
355
612
|
|
|
356
613
|
def generate_change_type(table, column, type, options)
|
|
@@ -366,7 +623,8 @@ module RailsAiContext
|
|
|
366
623
|
opts = options ? ", #{options}" : ""
|
|
367
624
|
|
|
368
625
|
# Detect original column type from schema for a reversible down method
|
|
369
|
-
|
|
626
|
+
known_type = find_column_type(table, column, cached_context[:schema])
|
|
627
|
+
original_type = known_type || "string"
|
|
370
628
|
|
|
371
629
|
lines << "**Warning:** Changing column type may cause data loss if types are incompatible."
|
|
372
630
|
lines << ""
|
|
@@ -382,12 +640,23 @@ module RailsAiContext
|
|
|
382
640
|
lines << "end"
|
|
383
641
|
lines << "```"
|
|
384
642
|
lines << ""
|
|
385
|
-
|
|
643
|
+
# change_column has no inverse of its own; the `down` above is what
|
|
644
|
+
# rolls it back, and it is only right when the schema knew the type.
|
|
645
|
+
lines << if known_type
|
|
646
|
+
"**Reversible:** Yes, through the `down` above, which restores `#{known_type}` (`change_column` alone cannot be reversed)"
|
|
647
|
+
else
|
|
648
|
+
"**Reversible:** Only if `#{column}` was a `string`: the schema does not list its type, so the `down` above guesses"
|
|
649
|
+
end
|
|
386
650
|
lines
|
|
387
651
|
end
|
|
388
652
|
|
|
389
|
-
def generate_create_table(table, columns_str, options)
|
|
653
|
+
def generate_create_table(table, columns_str, options, table_exists = false)
|
|
390
654
|
lines = []
|
|
655
|
+
if table_exists
|
|
656
|
+
lines << "**Warning:** Table `#{table}` already exists. `create_table` fails on it unless it passes `if_not_exists: true`, " \
|
|
657
|
+
"or `force: :cascade`, which drops the table and every row in it first. Use `add_column` to change it."
|
|
658
|
+
lines << ""
|
|
659
|
+
end
|
|
391
660
|
lines << "```ruby"
|
|
392
661
|
lines << "class Create#{table.camelize} < ActiveRecord::Migration[#{rails_version}]"
|
|
393
662
|
lines << " def change"
|
|
@@ -499,18 +768,56 @@ module RailsAiContext
|
|
|
499
768
|
Payload.model_file(cached_context, name)
|
|
500
769
|
end
|
|
501
770
|
|
|
502
|
-
|
|
503
|
-
|
|
771
|
+
# The models on the table, and, when the change can touch a relation,
|
|
772
|
+
# the associations that read it. Renaming or dropping a plain column
|
|
773
|
+
# moves no association, so those list the models alone.
|
|
774
|
+
def show_affected_models(table, models, action: nil, column: nil)
|
|
775
|
+
relations = !%w[remove_column rename_column change_type].include?(action) || column.to_s.end_with?("_id")
|
|
776
|
+
rows = affected_model_rows(table, models, relations: relations)
|
|
504
777
|
return [] if rows.empty?
|
|
505
778
|
|
|
506
779
|
[ "", "## Affected Models", "" ] + rows
|
|
507
780
|
end
|
|
508
781
|
|
|
509
|
-
|
|
782
|
+
MENTION_CAP = 15
|
|
783
|
+
|
|
784
|
+
# Every line in the app's Ruby and views that names the column, word
|
|
785
|
+
# for word: the code a rename or a drop breaks. A name the code also
|
|
786
|
+
# uses for something else shows up too, which is why each is listed
|
|
787
|
+
# rather than counted.
|
|
788
|
+
def column_mention_lines(column)
|
|
789
|
+
root = rails_app.root.to_s
|
|
790
|
+
pattern = /\b#{Regexp.escape(column)}\b/
|
|
791
|
+
found = []
|
|
792
|
+
%w[app lib config].each do |dir|
|
|
793
|
+
base = File.join(root, dir)
|
|
794
|
+
next unless Dir.exist?(base)
|
|
795
|
+
|
|
796
|
+
safe_glob(base, "**/*.{rb,erb,haml,slim,jbuilder}", File.realpath(root)).sort.each do |path|
|
|
797
|
+
next if sensitive_file?(path.delete_prefix("#{File.realpath(root)}/"))
|
|
798
|
+
|
|
799
|
+
source = RailsAiContext::SafeFile.read(path) or next
|
|
800
|
+
next unless source.match?(pattern)
|
|
801
|
+
|
|
802
|
+
source.each_line.with_index(1) do |line, number|
|
|
803
|
+
found << "- `#{path.delete_prefix("#{File.realpath(root)}/")}:#{number}` #{line.strip.truncate(100)}" if line.match?(pattern)
|
|
804
|
+
end
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
return [ "", "## Code that names `#{column}`", "", "_No line in app/, lib/ or config/ names it._" ] if found.empty?
|
|
808
|
+
|
|
809
|
+
more = found.size > MENTION_CAP ? [ "- _...and #{found.size - MENTION_CAP} more; `rails_search_code(pattern:\"#{column}\", exact_match:true)` lists them all_" ] : []
|
|
810
|
+
[ "", "## Code that names `#{column}` (#{count_phrase(found.size, "line")})", "" ] + found.first(MENTION_CAP) + more
|
|
811
|
+
rescue => e
|
|
812
|
+
RailsAiContext.debug_fail(e, [], label: "column_mention_lines")
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def affected_model_rows(table, models, relations: true)
|
|
510
816
|
return [] if models.empty?
|
|
511
817
|
|
|
512
818
|
owners = models_for_table(table, models)
|
|
513
819
|
rows = owners.map { |name| "- **#{name}** - directly affected (table: #{table})" }
|
|
820
|
+
return rows unless relations
|
|
514
821
|
|
|
515
822
|
models.each do |name, data|
|
|
516
823
|
next unless data.is_a?(Hash)
|