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
|
@@ -43,6 +43,9 @@ module RailsAiContext
|
|
|
43
43
|
key = fuzzy_find_key(models.keys, model) || model
|
|
44
44
|
data = models[key]
|
|
45
45
|
unless data
|
|
46
|
+
reason = RailsAiContext::AppKind.without_active_record(rails_app.root)
|
|
47
|
+
return empty_response("No Active Record model named #{model}: #{reason}.") if reason && models.empty?
|
|
48
|
+
|
|
46
49
|
return not_found_response("Model", model, models.keys.sort,
|
|
47
50
|
recovery_tool: "Call rails_get_model_details(detail:\"summary\") to see all models")
|
|
48
51
|
end
|
|
@@ -131,6 +134,20 @@ module RailsAiContext
|
|
|
131
134
|
lines
|
|
132
135
|
end
|
|
133
136
|
|
|
137
|
+
private_class_method def self.sti_line(sti)
|
|
138
|
+
place = sti[:sti_parent] ? "subclass of `#{sti[:sti_parent]}`" : "base of #{sti[:sti_children].map { |c| "`#{c}`" }.join(', ')}"
|
|
139
|
+
"**STI:** #{place}#{", type column `#{sti[:type_column]}`" if sti[:type_column]}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private_class_method def self.tenancy_line(tenancy)
|
|
143
|
+
file = tenancy[:declared_in]
|
|
144
|
+
case tenancy[:scope]
|
|
145
|
+
when "shared" then "**Tenancy:** shared schema: Apartment's `excluded_models` lists it (`#{file}`)"
|
|
146
|
+
when "per_tenant" then "**Tenancy:** one table in each tenant's schema: Apartment's `excluded_models` does not list it (`#{file}`)"
|
|
147
|
+
else "**Tenancy:** [INFERRED] Apartment computes `excluded_models` (`#{tenancy[:excluded_models]}` in `#{file}`), so whether this table is per tenant is not read"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
134
151
|
private_class_method def self.unavailable_row(name, data)
|
|
135
152
|
Serializers::SectionFacts.unread_row("- **#{name}**", data)
|
|
136
153
|
end
|
|
@@ -147,6 +164,12 @@ module RailsAiContext
|
|
|
147
164
|
end
|
|
148
165
|
lines = [ "# #{name}#{header_tag}", "" ]
|
|
149
166
|
lines << "**Table:** `#{data[:table_name]}`" if data[:table_name]
|
|
167
|
+
if (database = data[:database])
|
|
168
|
+
lines << "**Database:** `#{database[:connects_to]}`#{" (only #{database[:condition]})" if database[:condition]}" \
|
|
169
|
+
"#{", inherited from `#{database[:declared_in]}`" if database[:declared_in]}"
|
|
170
|
+
end
|
|
171
|
+
lines << tenancy_line(data[:tenancy]) if data[:tenancy].is_a?(Hash)
|
|
172
|
+
lines << sti_line(data[:sti]) if data[:sti].is_a?(Hash) && (data[:sti][:sti_parent] || data[:sti][:sti_children])
|
|
150
173
|
# A base class is not a concern, and the child may have no concerns at
|
|
151
174
|
# all, so this stands outside that section.
|
|
152
175
|
bases_unread = data[:bases_unread]
|
|
@@ -157,25 +180,38 @@ module RailsAiContext
|
|
|
157
180
|
if structure
|
|
158
181
|
lines << "**File:** `#{structure[:path]}` (#{count_phrase(structure[:total_lines], "line")})"
|
|
159
182
|
map = structure[:sections].map { |s| "#{s[:label]}(#{s[:start]}-#{s[:end]})" }.join(" → ")
|
|
160
|
-
lines << "**Structure:** #{map}"
|
|
183
|
+
lines << "**Structure:** #{map}" unless map.empty?
|
|
161
184
|
end
|
|
162
185
|
|
|
163
186
|
# Schema columns - inline from schema introspection
|
|
164
187
|
if data[:table_name]
|
|
165
|
-
|
|
166
|
-
if
|
|
167
|
-
|
|
168
|
-
cols = table_data[:columns] || []
|
|
188
|
+
(source, table_data), *others = Payload.model_tables(Payload.section(cached_context, :schema), data)
|
|
189
|
+
if table_data
|
|
190
|
+
ignored = Array(data[:ignored_columns])
|
|
191
|
+
cols = (table_data[:columns] || []).reject { |c| ignored.include?(c[:name].to_s) }
|
|
192
|
+
if (key = data[:primary_key] || table_data[:primary_key])
|
|
193
|
+
line = "**Primary key:** `#{Introspectors::SchemaConventions.primary_key_label(key)}`"
|
|
194
|
+
table_key = table_data[:primary_key]
|
|
195
|
+
line += " (the table's is `#{Introspectors::SchemaConventions.primary_key_label(table_key)}`)" if table_key && Array(table_key).map(&:to_s) != Array(key).map(&:to_s)
|
|
196
|
+
lines << line
|
|
197
|
+
end
|
|
169
198
|
if cols.any?
|
|
170
199
|
lines << "" << "## Columns"
|
|
171
200
|
cols.each do |c|
|
|
172
|
-
parts = [ "**#{c[:name]}**", c[:type] ]
|
|
201
|
+
parts = [ "**#{c[:name]}**", c[:type] ].compact
|
|
202
|
+
parts << "primary key" if c[:primary_key]
|
|
173
203
|
parts << "NOT NULL" if c[:null] == false
|
|
174
204
|
parts << "default: #{c[:default]}" if c[:default] && !c[:default].to_s.empty?
|
|
175
205
|
parts << "array" if c[:array]
|
|
176
206
|
lines << "- #{parts.join(' | ')}"
|
|
177
207
|
end
|
|
208
|
+
differ = others.reject { |_, other| other[:columns] == table_data[:columns] }.map(&:first)
|
|
209
|
+
if differ.any?
|
|
210
|
+
lines << "" << "Columns of `#{data[:table_name]}` in #{source}; the table differs in #{differ.join(", ")}: " \
|
|
211
|
+
"`rails_get_schema(table:\"#{data[:table_name]}\")` lists each."
|
|
212
|
+
end
|
|
178
213
|
end
|
|
214
|
+
lines << ignored_columns_line(ignored) if ignored.any?
|
|
179
215
|
end
|
|
180
216
|
end
|
|
181
217
|
|
|
@@ -186,10 +222,15 @@ module RailsAiContext
|
|
|
186
222
|
lines << "" << "## Fields"
|
|
187
223
|
data[:fields].each do |f|
|
|
188
224
|
type_str = f[:type] ? ": #{f[:type]}" : ""
|
|
189
|
-
lines << "- `#{f[:name]}`#{type_str}"
|
|
225
|
+
lines << "- `#{f[:name]}`#{type_str}#{", default: #{f[:default]}" if f.key?(:default)}"
|
|
190
226
|
end
|
|
191
227
|
end
|
|
192
228
|
|
|
229
|
+
if data[:indexes]&.any?
|
|
230
|
+
lines << "" << "## Indexes"
|
|
231
|
+
data[:indexes].each { |index| lines << "- `#{index}`" }
|
|
232
|
+
end
|
|
233
|
+
|
|
193
234
|
# embeds_many and embeds_one are listed under Associations too.
|
|
194
235
|
listed = Array(data[:associations]).map { |a| a[:name].to_s }
|
|
195
236
|
embeds = Array(data[:embeds]).reject { |e| listed.include?(e[:name].to_s) }
|
|
@@ -213,6 +254,10 @@ module RailsAiContext
|
|
|
213
254
|
detail += " [polymorphic]" if a[:polymorphic]
|
|
214
255
|
detail += (a[:optional] == true ? " [optional]" : " [optional: #{a[:optional]}]") if a[:optional]
|
|
215
256
|
detail += " dependent: #{a[:dependent]}" if a[:dependent]
|
|
257
|
+
detail += option_pairs(a[:declared_options])
|
|
258
|
+
detail += " (delegated types: #{a[:delegated_types].join(', ')})" if a[:delegated_types]&.any?
|
|
259
|
+
detail += " (delegated types: from `#{a[:delegated_types_source]}`, not read statically)" if a[:delegated_types_source]
|
|
260
|
+
detail += " extension methods: #{a[:extension_methods].join(', ')}" if a[:extension_methods]&.any?
|
|
216
261
|
detail += " (fk: #{Introspectors::SchemaConventions.key_text(a[:foreign_key])})" if a[:foreign_key] && a[:type] == "belongs_to"
|
|
217
262
|
detail += " [UNAVAILABLE: #{a[:unavailable]}]" if a[:unavailable]
|
|
218
263
|
lines << detail
|
|
@@ -272,26 +317,32 @@ module RailsAiContext
|
|
|
272
317
|
end
|
|
273
318
|
|
|
274
319
|
# Custom validate methods (business rules) - show method body when possible
|
|
275
|
-
if data[:custom_validates]&.any?
|
|
320
|
+
if data[:custom_validates]&.any? || data[:custom_validate_blocks]&.any?
|
|
276
321
|
lines << "" << "## Validations" unless data[:validations]&.any?
|
|
277
|
-
|
|
322
|
+
custom = Array(data[:custom_validates])
|
|
323
|
+
bodies = custom.any? ? extract_custom_validate_bodies(name, custom) : {}
|
|
278
324
|
conditions = data[:custom_validate_conditions] || {}
|
|
279
|
-
|
|
325
|
+
custom.each do |v|
|
|
280
326
|
tail = callback_condition_tail(conditions[v.to_s] || conditions[v.to_sym])
|
|
281
327
|
lines << "- **Custom:** `#{v}`#{tail}#{" → #{bodies[v]}" if bodies[v]}"
|
|
282
328
|
end
|
|
329
|
+
Array(data[:custom_validate_blocks]).each do |b|
|
|
330
|
+
lines << "- **Custom:** block#{callback_condition_tail(b[:conditions])}#{" → #{b[:body]}" unless b[:body].to_s.empty?}"
|
|
331
|
+
end
|
|
283
332
|
end
|
|
284
333
|
|
|
285
334
|
# Enums
|
|
286
335
|
if data[:enums]&.any?
|
|
287
336
|
lines << "" << "## Enums"
|
|
337
|
+
enum_options = data[:enum_options] || {}
|
|
288
338
|
data[:enums].each do |attr, values|
|
|
339
|
+
options = enum_options[attr.to_s] || enum_options[attr.to_sym] || {}
|
|
289
340
|
if values.is_a?(Hash)
|
|
290
341
|
backing = values.values.first.is_a?(Integer) ? "integer" : "string"
|
|
291
342
|
entries = values.map { |k, v| "#{k}(#{v})" }.join(", ")
|
|
292
|
-
lines << "- `#{attr}`: #{entries} [#{backing}]"
|
|
343
|
+
lines << "- `#{attr}`: #{entries} [#{backing}]#{enum_options_text(attr, values.keys, options)}"
|
|
293
344
|
else
|
|
294
|
-
lines << "- `#{attr}`: #{Serializers::SectionFacts.enum_values(values)}"
|
|
345
|
+
lines << "- `#{attr}`: #{Serializers::SectionFacts.enum_values(values)}#{enum_options_text(attr, nil, options)}"
|
|
295
346
|
end
|
|
296
347
|
end
|
|
297
348
|
end
|
|
@@ -299,9 +350,13 @@ module RailsAiContext
|
|
|
299
350
|
# Scopes - show lambda body so AI can chain correctly. Each carries
|
|
300
351
|
# the AST confidence: [VERIFIED] literal bodies vs [INFERRED] dynamic
|
|
301
352
|
# expressions the parser can't fully resolve.
|
|
302
|
-
if data[:scopes]&.any?
|
|
353
|
+
if data[:scopes]&.any? || data[:default_scopes]&.any?
|
|
303
354
|
lines << "" << "## Scopes"
|
|
304
|
-
data[:
|
|
355
|
+
Array(data[:default_scopes]).each do |s|
|
|
356
|
+
tail = s[:all_queries] ? " (all_queries: #{s[:all_queries]})" : ""
|
|
357
|
+
lines << "- `default_scope` → #{s[:body] || Confidence::INFERRED}#{tail} _(applies to every query on #{name})_"
|
|
358
|
+
end
|
|
359
|
+
Array(data[:scopes]).each do |s|
|
|
305
360
|
if s.is_a?(Hash)
|
|
306
361
|
tag = s[:confidence] ? " #{s[:confidence]}" : ""
|
|
307
362
|
lines << "- `#{s[:name]}` → #{s[:body]}#{tag}"
|
|
@@ -322,16 +377,28 @@ module RailsAiContext
|
|
|
322
377
|
end
|
|
323
378
|
lines << "- `#{callback_type_label(type)}`: #{targets.join(', ')}"
|
|
324
379
|
end
|
|
380
|
+
lines << "" << commit_order_note([ name ]) if data[:commit_order_unread]
|
|
325
381
|
end
|
|
326
382
|
|
|
327
383
|
# Macros - surface hidden introspector data
|
|
328
384
|
macro_lines = []
|
|
329
|
-
|
|
385
|
+
if data[:secure_passwords]&.any?
|
|
386
|
+
data[:secure_passwords].each { |pw| macro_lines << "- `has_secure_password` :#{pw[:attribute]}#{option_pairs(pw[:options])}" }
|
|
387
|
+
elsif data[:has_secure_password]
|
|
388
|
+
macro_lines << "- `has_secure_password`"
|
|
389
|
+
end
|
|
390
|
+
macro_lines << "- `has_secure_token` #{data[:has_secure_token].map { |f| ":#{f}" }.join(', ')}" if data[:has_secure_token]&.any?
|
|
391
|
+
Array(data[:nested_attributes]).each do |nested|
|
|
392
|
+
macro_lines << "- `accepts_nested_attributes_for` #{nested[:names].map { |n| ":#{n}" }.join(', ')}#{option_pairs(nested[:options])}"
|
|
393
|
+
end
|
|
330
394
|
macro_lines << "- `encrypts` #{data[:encrypts].map { |f| ":#{f}" }.join(', ')}" if data[:encrypts]&.any?
|
|
331
395
|
macro_lines << "- `normalizes` #{data[:normalizes].map { |f| ":#{f}" }.join(', ')}" if data[:normalizes]&.any?
|
|
332
396
|
macro_lines << "- `generates_token_for` #{data[:generates_token_for].map { |f| ":#{f}" }.join(', ')}" if data[:generates_token_for]&.any?
|
|
333
|
-
|
|
334
|
-
|
|
397
|
+
if data[:serialize]&.any?
|
|
398
|
+
serialized = data[:serialize].map { |f| ":#{f}#{option_pairs(data[:serialize_options]&.dig(f))}" }
|
|
399
|
+
macro_lines << "- `serialize` #{serialized.join(', ')}"
|
|
400
|
+
end
|
|
401
|
+
macro_lines << "- `store` #{data[:store].map { |f| store_column_text(f, data[:store_accessors]) }.join(', ')}" if data[:store]&.any?
|
|
335
402
|
macro_lines << "- `broadcasts` #{data[:broadcasts].join(', ')}" if data[:broadcasts]&.any?
|
|
336
403
|
if data[:has_one_attached]&.any?
|
|
337
404
|
macro_lines << "- `has_one_attached` #{data[:has_one_attached].map { |f| ":#{f}" }.join(', ')}"
|
|
@@ -339,11 +406,35 @@ module RailsAiContext
|
|
|
339
406
|
if data[:has_many_attached]&.any?
|
|
340
407
|
macro_lines << "- `has_many_attached` #{data[:has_many_attached].map { |f| ":#{f}" }.join(', ')}"
|
|
341
408
|
end
|
|
409
|
+
macro_lines << "- `has_rich_text` #{data[:has_rich_text].map { |f| ":#{f}" }.join(', ')}" if data[:has_rich_text]&.any?
|
|
410
|
+
if data[:attributes]&.any?
|
|
411
|
+
macro_lines << "- `attribute` #{data[:attributes].map { |a| attribute_api_text(a) }.join(', ')}"
|
|
412
|
+
end
|
|
413
|
+
if data[:alias_attributes]&.any?
|
|
414
|
+
macro_lines << "- `alias_attribute` #{data[:alias_attributes].map { |a| ":#{a[:name]} → :#{a[:target]}" }.join(', ')}"
|
|
415
|
+
end
|
|
416
|
+
(data[:model_settings] || {}).each { |setting, value| macro_lines << "- `self.#{setting} = #{value}`" }
|
|
417
|
+
macro_lines << "- `attr_readonly` #{data[:attr_readonly].map { |f| ":#{f}" }.join(', ')}" if data[:attr_readonly]&.any?
|
|
418
|
+
macro_lines << "- `query_constraints` #{data[:query_constraints].map { |f| ":#{f}" }.join(', ')}" if data[:query_constraints]&.any?
|
|
419
|
+
Array(data[:gem_macros]).each do |gem_macro|
|
|
420
|
+
adds = Array(gem_macro[:adds]).map { |name| "`#{name}`" }
|
|
421
|
+
macro_lines << "- `#{gem_macro[:text]}`#{" (adds #{adds.join(', ')})" if adds.any?}"
|
|
422
|
+
end
|
|
342
423
|
if macro_lines.any?
|
|
343
424
|
lines << "" << "## Macros"
|
|
344
425
|
lines.concat(macro_lines)
|
|
345
426
|
end
|
|
346
427
|
|
|
428
|
+
Array(data[:state_machines]).each do |machine|
|
|
429
|
+
lines << "" << "## State machine (aasm, column `#{machine[:column]}`)"
|
|
430
|
+
states = machine[:states].map { |state| "`#{state}`#{" (initial)" if state == machine[:initial]}" }
|
|
431
|
+
lines << "- states: #{states.join(', ')}" if states.any?
|
|
432
|
+
machine[:events].each do |event|
|
|
433
|
+
moves = event[:transitions].map { |t| "#{t[:from].any? ? t[:from].join(' | ') : 'any'} -> #{t[:to]}" }
|
|
434
|
+
lines << "- event `#{event[:name]}`#{": #{moves.join(', ')}" if moves.any?}"
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
347
438
|
# Encryption details (expanded from encrypts)
|
|
348
439
|
if data[:encryption_details]&.any?
|
|
349
440
|
lines << "" << "## Encryption Details"
|
|
@@ -364,7 +455,7 @@ module RailsAiContext
|
|
|
364
455
|
if data[:token_generation]&.any?
|
|
365
456
|
lines << "" << "## Token Generation"
|
|
366
457
|
data[:token_generation].each do |tg|
|
|
367
|
-
detail_str = tg.is_a?(Hash) ? "**#{tg[:purpose]}** (expires_in: #{tg[:expires_in] || '
|
|
458
|
+
detail_str = tg.is_a?(Hash) ? "**#{tg[:purpose]}** (expires_in: #{tg[:expires_in] || 'never'})" : tg.to_s
|
|
368
459
|
lines << "- #{detail_str}"
|
|
369
460
|
end
|
|
370
461
|
end
|
|
@@ -373,7 +464,11 @@ module RailsAiContext
|
|
|
373
464
|
if data[:delegations]&.any?
|
|
374
465
|
lines << "" << "## Delegations"
|
|
375
466
|
data[:delegations].each do |d|
|
|
376
|
-
|
|
467
|
+
line = "- delegate #{d[:methods].map { |m| ":#{m}" }.join(', ')} to: :#{d[:to]}"
|
|
468
|
+
line += " → #{d[:defines].join(', ')}" if d[:defines]
|
|
469
|
+
line += " → #{RailsAiContext::Confidence::INFERRED} (the prefix is computed)" if d[:prefix_computed]
|
|
470
|
+
line += " (private)" if d[:private]
|
|
471
|
+
lines << line
|
|
377
472
|
end
|
|
378
473
|
end
|
|
379
474
|
lines << "- `delegate_missing_to` :#{data[:delegate_missing_to]}" if data[:delegate_missing_to]
|
|
@@ -409,9 +504,9 @@ module RailsAiContext
|
|
|
409
504
|
lines << "_#{count_phrase(hidden, "concern")} hidden by `excluded_concerns`._" if hidden.positive?
|
|
410
505
|
end
|
|
411
506
|
|
|
412
|
-
#
|
|
413
|
-
# called method on another receiver: named, and left out of every count above.
|
|
414
|
-
{ conditional_declarations: "
|
|
507
|
+
# A declaration whose method name or condition the source does not decide, or one
|
|
508
|
+
# a called method makes on another receiver: named, and left out of every count above.
|
|
509
|
+
{ conditional_declarations: "Not decided from the source (the method name or a condition)",
|
|
415
510
|
foreign_declarations: "Declared on another class" }.each do |key, heading|
|
|
416
511
|
next unless data[key]&.any?
|
|
417
512
|
|
|
@@ -455,6 +550,14 @@ module RailsAiContext
|
|
|
455
550
|
end
|
|
456
551
|
end
|
|
457
552
|
|
|
553
|
+
async = Array(Payload.section(cached_context, :jobs)&.dig(:async_methods)).select { |m| m[:owner] == name }
|
|
554
|
+
if async.any?
|
|
555
|
+
lines << "" << "## Runs in the background (delayed_job)"
|
|
556
|
+
async.each do |m|
|
|
557
|
+
lines << "- `#{m[:method]}`#{" [#{m[:options]}]" if m[:options]}: every call is queued by `handle_asynchronously` (`#{m[:file]}`)"
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
458
561
|
# Cross-reference hints - guide AI to related tools
|
|
459
562
|
hints = []
|
|
460
563
|
hints << "`rails_get_schema(table:\"#{data[:table_name]}\")` for columns/indexes" if data[:table_name]
|
|
@@ -492,6 +595,44 @@ module RailsAiContext
|
|
|
492
595
|
pairs.any? ? "**#{ed[:field]}** (#{pairs.join(', ')})" : "**#{ed[:field]}**"
|
|
493
596
|
end
|
|
494
597
|
|
|
598
|
+
# Rails names each value's methods "#{prefix}#{label}#{suffix}", so the
|
|
599
|
+
# bare label is wrong once either is set.
|
|
600
|
+
private_class_method def self.enum_options_text(attr, labels, options)
|
|
601
|
+
text = options[:default].nil? ? "" : " default: #{options[:default]}"
|
|
602
|
+
prefix, suffix = options.values_at(:prefix, :suffix)
|
|
603
|
+
return text unless prefix || suffix
|
|
604
|
+
if [ prefix, suffix ].include?(Confidence::INFERRED)
|
|
605
|
+
return "#{text} methods: #{Confidence::INFERRED} (the prefix or suffix is computed)"
|
|
606
|
+
end
|
|
607
|
+
return "#{text} methods: #{Confidence::INFERRED} (the values are computed)" unless labels
|
|
608
|
+
|
|
609
|
+
prefix = "#{prefix == true ? attr : prefix}_" if prefix
|
|
610
|
+
suffix = "_#{suffix == true ? attr : suffix}" if suffix
|
|
611
|
+
methods = labels.map { |label| "#{prefix}#{label.to_s.gsub(/[\W&&[:ascii:]]+/, '_')}#{suffix}?" }
|
|
612
|
+
"#{text} methods: #{methods.join(', ')}"
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
private_class_method def self.ignored_columns_line(ignored)
|
|
616
|
+
inferred = RailsAiContext::Confidence::INFERRED
|
|
617
|
+
return "\n**Ignored columns:** #{inferred} (computed in the source)" if ignored.include?(inferred)
|
|
618
|
+
|
|
619
|
+
"\n**Ignored columns:** #{ignored.map { |c| "`#{c}`" }.join(', ')} _(the model cannot read or write them)_"
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
private_class_method def self.store_column_text(column, accessors)
|
|
623
|
+
names = Array(accessors&.dig(column))
|
|
624
|
+
return ":#{column}" if names.empty?
|
|
625
|
+
|
|
626
|
+
inferred = RailsAiContext::Confidence::INFERRED
|
|
627
|
+
names = names.map { |n| n == inferred ? "#{inferred}: the prefix or suffix is computed" : n }
|
|
628
|
+
":#{column} (#{names.join(', ')})"
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
private_class_method def self.attribute_api_text(attribute)
|
|
632
|
+
facts = [ attribute[:type], ("default: #{attribute[:default]}" if attribute[:default]) ].compact
|
|
633
|
+
":#{attribute[:name]}#{" (#{facts.join(', ')})" if facts.any?}"
|
|
634
|
+
end
|
|
635
|
+
|
|
495
636
|
# A transformation the parser could not resolve is a marker, not the
|
|
496
637
|
# name of a transformation, so it never follows the dash.
|
|
497
638
|
private_class_method def self.normalization_line(nd)
|
|
@@ -501,7 +642,11 @@ module RailsAiContext
|
|
|
501
642
|
return "**#{nd[:field]}** #{RailsAiContext::Confidence::INFERRED}" if transformation.nil? ||
|
|
502
643
|
transformation == RailsAiContext::Confidence::INFERRED
|
|
503
644
|
|
|
504
|
-
"**#{nd[:field]}** - #{transformation}"
|
|
645
|
+
"**#{nd[:field]}** - #{transformation}#{option_pairs(nd[:options])}"
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
private_class_method def self.option_pairs(options)
|
|
649
|
+
options&.any? ? " (#{options.map { |k, v| "#{k}: #{option_text(v)}" }.join(', ')})" : ""
|
|
505
650
|
end
|
|
506
651
|
|
|
507
652
|
# Extract bodies of custom validate methods (single-line or first meaningful line)
|
|
@@ -534,7 +679,9 @@ module RailsAiContext
|
|
|
534
679
|
|
|
535
680
|
private_class_method def self.extract_method_signatures(model_name)
|
|
536
681
|
source = model_source(model_name) or return nil
|
|
537
|
-
|
|
682
|
+
# A def carries an end line; delegate and attr_* names do not, and they lead a model, so they go after the cap's defs.
|
|
683
|
+
written, generated = Introspectors::ActionResolver.public_methods_in(source, owner: model_name).partition { |m| m[:end_location] }
|
|
684
|
+
(written + generated).map { |m| Introspectors::ActionResolver.signature(m) }.uniq
|
|
538
685
|
end
|
|
539
686
|
|
|
540
687
|
# On the booted tier reflection has already answered associations,
|
|
@@ -588,13 +735,18 @@ module RailsAiContext
|
|
|
588
735
|
source = RailsAiContext::SafeFile.read(full_path) or return nil
|
|
589
736
|
|
|
590
737
|
source_lines = source.lines
|
|
738
|
+
# A def's kind comes from the walk: a regex cannot see `class << self`,
|
|
739
|
+
# `def Name.x`, or a def that only extends a scope.
|
|
740
|
+
def_scopes = Introspectors::ActionResolver.methods_in(source).to_h { |m| [ m[:location], m[:scope] ] }
|
|
591
741
|
sections = []
|
|
592
742
|
current_section = nil
|
|
593
743
|
current_start = nil
|
|
594
744
|
|
|
595
745
|
source_lines.each_with_index do |line, idx|
|
|
746
|
+
# Anything the listeners cannot scope: a section is a run of lines, which no node carries.
|
|
596
747
|
label = case line
|
|
597
|
-
when /\A\s*
|
|
748
|
+
when /\A\s*(?:[a-z_]+\s+)*def\s/ then { class: "class methods", instance: "instance methods" }[def_scopes[idx + 1]]
|
|
749
|
+
when /\A\s*class\s+(?!<<)/, /\A\s*[A-Z][\w:]*\s*=\s*Class\.new\b/ then "class definition"
|
|
598
750
|
when /\A\s*(include|extend|prepend)\s/ then "includes"
|
|
599
751
|
when /\A\s*[A-Z_]+\s*=/ then "constants"
|
|
600
752
|
when /\A\s*(belongs_to|has_many|has_one|has_and_belongs_to_many)\s/ then "associations"
|
|
@@ -602,13 +754,11 @@ module RailsAiContext
|
|
|
602
754
|
when /\A\s*scope\s/ then "scopes"
|
|
603
755
|
when /\A\s*(enum|encrypts|normalizes|has_secure_password|has_one_attached|has_many_attached)\s/ then "macros"
|
|
604
756
|
when /\A\s*(before_|after_|around_)/ then "callbacks"
|
|
605
|
-
when /\A\s*def\s+self\./ then "class methods"
|
|
606
|
-
when /\A\s*def\s/ then "instance methods"
|
|
607
757
|
when /\A\s*private\s*$/ then "private"
|
|
608
758
|
end
|
|
609
759
|
|
|
610
760
|
if label && label != current_section
|
|
611
|
-
sections << { start: current_start, end: idx
|
|
761
|
+
sections << { start: current_start, end: idx, label: current_section } if current_section
|
|
612
762
|
current_section = label
|
|
613
763
|
current_start = idx + 1
|
|
614
764
|
end
|