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
|
@@ -51,7 +51,10 @@ module RailsAiContext
|
|
|
51
51
|
when "summary"
|
|
52
52
|
lines = [ "# Test Infrastructure", "" ]
|
|
53
53
|
lines << "- **Framework:** #{data[:framework]}"
|
|
54
|
-
lines
|
|
54
|
+
lines.concat(suite_lines)
|
|
55
|
+
lines << "- **Factories:** #{RailsAiContext::TestFramework.factory_files_phrase(data[:factories]) || data[:factories][:location]}" if data[:factories]
|
|
56
|
+
lines << "- **Fabricators:** #{count_phrase(data[:fabricators][:count], "file")}" if data[:fabricators]
|
|
57
|
+
lines << "- **Cucumber:** #{count_phrase(data[:cucumber][:count], "feature file")}" if data[:cucumber]
|
|
55
58
|
lines << "- **Fixtures:** #{RailsAiContext::TestFramework.fixture_phrase(data[:fixtures])}" if data[:fixtures]
|
|
56
59
|
if data[:test_files]&.any?
|
|
57
60
|
total = data[:test_files].values.sum { |v| v[:count] }
|
|
@@ -63,11 +66,15 @@ module RailsAiContext
|
|
|
63
66
|
when "standard"
|
|
64
67
|
lines = [ "# Test Infrastructure", "" ]
|
|
65
68
|
lines << "- **Framework:** #{data[:framework]}"
|
|
66
|
-
lines
|
|
69
|
+
lines.concat(suite_lines)
|
|
70
|
+
lines << "- **Factories:** #{RailsAiContext::TestFramework.factory_location(data[:factories])}" if data[:factories]
|
|
71
|
+
lines << "- **Fabricators:** #{data[:fabricators][:location]} (#{count_phrase(data[:fabricators][:count], "file")})" if data[:fabricators]
|
|
72
|
+
lines << cucumber_line(data[:cucumber]) if data[:cucumber]
|
|
67
73
|
lines << "- **Fixtures:** #{data[:fixtures][:location]} (#{RailsAiContext::TestFramework.fixture_phrase(data[:fixtures])})" if data[:fixtures]
|
|
68
74
|
lines << "- **System tests:** #{data[:system_tests][:location]}" if data[:system_tests]
|
|
69
75
|
lines << "- **CI:** #{data[:ci_config].join(', ')}" if data[:ci_config]&.any?
|
|
70
76
|
lines << "- **Coverage:** #{data[:coverage]}" if data[:coverage]
|
|
77
|
+
lines.concat(ci_step_lines(data[:ci_steps], data[:ci_steps_dir]))
|
|
71
78
|
|
|
72
79
|
if data[:test_files]&.any?
|
|
73
80
|
lines << "" << "## Test Files"
|
|
@@ -92,35 +99,43 @@ module RailsAiContext
|
|
|
92
99
|
when "full"
|
|
93
100
|
lines = [ "# Test Infrastructure (Full Detail)", "" ]
|
|
94
101
|
lines << "- **Framework:** #{data[:framework]}"
|
|
102
|
+
lines.concat(suite_lines)
|
|
95
103
|
lines << "- **CI:** #{data[:ci_config].join(', ')}" if data[:ci_config]&.any?
|
|
96
104
|
lines << "- **Coverage:** #{data[:coverage]}" if data[:coverage]
|
|
105
|
+
lines << cucumber_line(data[:cucumber]) if data[:cucumber]
|
|
106
|
+
lines.concat(ci_step_lines(data[:ci_steps], data[:ci_steps_dir]))
|
|
97
107
|
|
|
98
108
|
lines.concat(trait_lines(data[:factory_traits], 20))
|
|
99
109
|
|
|
100
110
|
if data[:fixture_names]&.any?
|
|
101
111
|
lines << "" << "## Fixtures"
|
|
102
|
-
parsed_fixtures = parse_all_fixture_contents
|
|
103
|
-
|
|
104
|
-
parsed_fixtures.
|
|
105
|
-
|
|
106
|
-
entries
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
parsed_fixtures = parse_all_fixture_contents(data[:fixtures])
|
|
113
|
+
data[:fixture_names].each do |set, labels|
|
|
114
|
+
entries = parsed_fixtures[set.to_s]
|
|
115
|
+
unless entries.is_a?(Hash)
|
|
116
|
+
why = entries == :too_large ? "over the #{max_test_file_size} byte read limit" : "not parsed as YAML"
|
|
117
|
+
erb_labels = Array((data[:fixture_erb_labels] || {})[set]).map { |label| "`#{label}` (set by ERB)" }
|
|
118
|
+
lines << "- **#{set}:** #{(Array(labels) + erb_labels).join(', ')} _(#{why}; labels only)_"
|
|
119
|
+
next
|
|
110
120
|
end
|
|
111
121
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
lines << "- **#{set}:**"
|
|
123
|
+
entries.each do |entry_name, attrs|
|
|
124
|
+
attr_str = attrs.map { |k, v| "#{k}: #{erb_shown(v)}" }.join(", ")
|
|
125
|
+
label = if RailsAiContext::ConfigYaml.marked?(entry_name.to_s)
|
|
126
|
+
"`#{erb_shown(entry_name)}` _(label set by ERB)_"
|
|
127
|
+
else
|
|
128
|
+
"`#{entry_name}`"
|
|
118
129
|
end
|
|
130
|
+
lines << (attr_str.empty? ? " - #{label}" : " - #{label}: #{attr_str}")
|
|
119
131
|
end
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
relationships = extract_fixture_relationships(parsed_fixtures.select { |_, entries| entries.is_a?(Hash) })
|
|
135
|
+
if relationships.any?
|
|
136
|
+
lines << "" << "## Fixture Relationships"
|
|
137
|
+
relationships.each do |parent, children|
|
|
138
|
+
lines << "- **#{parent}** ← #{children.join(', ')}"
|
|
124
139
|
end
|
|
125
140
|
end
|
|
126
141
|
end
|
|
@@ -137,6 +152,11 @@ module RailsAiContext
|
|
|
137
152
|
end
|
|
138
153
|
end
|
|
139
154
|
|
|
155
|
+
if data[:fabricator_names]&.any?
|
|
156
|
+
lines << "" << "## Fabricators"
|
|
157
|
+
data[:fabricator_names].each { |file, names| lines << "- **#{file}:** #{names.join(', ')}" }
|
|
158
|
+
end
|
|
159
|
+
|
|
140
160
|
if data[:test_helper_setup]&.any?
|
|
141
161
|
lines << "" << "## Test Helper Setup"
|
|
142
162
|
data[:test_helper_setup].each { |m| lines << "- `#{m}`" }
|
|
@@ -163,6 +183,32 @@ module RailsAiContext
|
|
|
163
183
|
RailsAiContext.configuration.max_test_file_size
|
|
164
184
|
end
|
|
165
185
|
|
|
186
|
+
# An engine's test/dummy is tested by the engine's suite, which every path here is under.
|
|
187
|
+
private_class_method def self.suite_root
|
|
188
|
+
RailsAiContext::PathResolver.test_root(rails_app.root.to_s)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private_class_method def self.erb_shown(text)
|
|
192
|
+
RailsAiContext::ConfigYaml.shown(text)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private_class_method def self.shown(rel)
|
|
196
|
+
RailsAiContext::PathResolver.suite_relative(rails_app.root.to_s, rel)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private_class_method def self.suite_lines
|
|
200
|
+
app_root = rails_app.root.to_s
|
|
201
|
+
if suite_root == app_root
|
|
202
|
+
return unread_suite_gemfile ? [ "- **Suite:** not read: #{RailsAiContext::GemLock.for(app_root).unread_bundle}" ] : []
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
[ "- **Suite:** the engine's, at `#{RailsAiContext::PathResolver.suite_relative(app_root, ".")}`; the paths below are under it" ]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private_class_method def self.unread_suite_gemfile
|
|
209
|
+
RailsAiContext::TestFramework.unread_gemfile(suite_root) if suite_root == rails_app.root.to_s
|
|
210
|
+
end
|
|
211
|
+
|
|
166
212
|
private_class_method def self.find_test_file(name, type, detail = "full")
|
|
167
213
|
# Normalize: accept "Admin::PostsController", "admin/posts", "Posts", "posts" (plural)
|
|
168
214
|
snake = case type
|
|
@@ -184,13 +230,13 @@ module RailsAiContext
|
|
|
184
230
|
refused = refuse_unsafe_paths([ name ])
|
|
185
231
|
return refused if refused
|
|
186
232
|
|
|
187
|
-
candidates = RailsAiContext::TestFramework.candidates(
|
|
233
|
+
candidates = RailsAiContext::TestFramework.candidates(suite_root, type, snake, cached_context)
|
|
188
234
|
exercising = type == :controller ? exercising_tests(snake, candidates) : []
|
|
189
235
|
|
|
190
236
|
contained = []
|
|
191
237
|
too_large = nil
|
|
192
238
|
candidates.each do |rel|
|
|
193
|
-
content, resolution = RailsAiContext::SafePath.read(rel, under:
|
|
239
|
+
content, resolution = RailsAiContext::SafePath.read(rel, under: suite_root, max_size: max_test_file_size)
|
|
194
240
|
contained << rel unless ESCAPING_REFUSALS.include?(resolution.refusal)
|
|
195
241
|
too_large ||= resolution if resolution.refusal == :too_large
|
|
196
242
|
next unless content
|
|
@@ -198,15 +244,15 @@ module RailsAiContext
|
|
|
198
244
|
# Summary/standard: return just test names (saves 2000+ tokens vs full source)
|
|
199
245
|
answer = if names_only?(detail)
|
|
200
246
|
listed = examples(content)
|
|
201
|
-
"# #{rel} (#{count_phrase(listed.size, "test")})\n\n#{listed.join("\n")}"
|
|
247
|
+
"# #{shown(rel)} (#{count_phrase(listed.size, "test")})\n\n#{listed.join("\n")}"
|
|
202
248
|
else
|
|
203
|
-
"# #{rel}\n\n```ruby\n#{content}\n```"
|
|
249
|
+
"# #{shown(rel)}\n\n```ruby\n#{content}\n```"
|
|
204
250
|
end
|
|
205
251
|
return text_response(answer + exercising_section(exercising, "## Also exercised by"))
|
|
206
252
|
end
|
|
207
253
|
|
|
208
254
|
if too_large
|
|
209
|
-
return text_response("# #{too_large.relative}\n\n_The test file exists but was not read: #{human_size(File.size(too_large.realpath))} is over " \
|
|
255
|
+
return text_response("# #{shown(too_large.relative)}\n\n_The test file exists but was not read: #{human_size(File.size(too_large.realpath))} is over " \
|
|
210
256
|
"`max_test_file_size` (#{human_size(max_test_file_size)})._" +
|
|
211
257
|
exercising_section(exercising, "## Also exercised by"))
|
|
212
258
|
end
|
|
@@ -222,7 +268,9 @@ module RailsAiContext
|
|
|
222
268
|
"or names a sensitive file.")
|
|
223
269
|
end
|
|
224
270
|
|
|
225
|
-
|
|
271
|
+
unread = unread_suite_gemfile
|
|
272
|
+
unread &&= "\n\nNot searched: the engine's suite at `#{File.dirname(unread)}` is not read, #{RailsAiContext::GemLock.for(suite_root).unread_bundle}."
|
|
273
|
+
empty_response("No test file found for #{name}. Searched: #{contained.map { |rel| shown(rel) }.join(', ')}#{nearby_tests_hint(contained)}#{unread}")
|
|
226
274
|
end
|
|
227
275
|
|
|
228
276
|
# Where a controller is driven from outside its own test, and what each
|
|
@@ -239,7 +287,7 @@ module RailsAiContext
|
|
|
239
287
|
#
|
|
240
288
|
# @return [Array<Array(String, String, String)>] [path, kind, source]
|
|
241
289
|
private_class_method def self.exercising_tests(snake, primary)
|
|
242
|
-
root =
|
|
290
|
+
root = suite_root
|
|
243
291
|
real_root = File.realpath(root)
|
|
244
292
|
routes = Array(RouteCoverage.all_by_controller(cached_context[:routes])[snake])
|
|
245
293
|
helpers = routes.filter_map { |route| route[:name] }.uniq
|
|
@@ -271,7 +319,7 @@ module RailsAiContext
|
|
|
271
319
|
lines = heading ? [ "", "", heading, "" ] : [ "", "" ]
|
|
272
320
|
found.first(MAX_EXERCISING).each do |rel, kind, source|
|
|
273
321
|
listed = examples(source)
|
|
274
|
-
lines << "- `#{rel}` (#{kind}, #{count_phrase(listed.size, "test")})"
|
|
322
|
+
lines << "- `#{shown(rel)}` (#{kind}, #{count_phrase(listed.size, "test")})"
|
|
275
323
|
listed.each { |test| lines << " #{test}" } if names
|
|
276
324
|
end
|
|
277
325
|
lines << "- ... #{found.size - MAX_EXERCISING} more" if found.size > MAX_EXERCISING
|
|
@@ -306,7 +354,7 @@ module RailsAiContext
|
|
|
306
354
|
# Nearby test files, to help the agent find the right one. The glob base
|
|
307
355
|
# is the realpath, so a symlinked test directory cannot widen it.
|
|
308
356
|
private_class_method def self.nearby_tests_hint(candidates)
|
|
309
|
-
root =
|
|
357
|
+
root = suite_root
|
|
310
358
|
real_root = File.realpath(root)
|
|
311
359
|
|
|
312
360
|
nearby = candidates.map { |rel| File.dirname(File.join(root, rel)) }.uniq.flat_map do |dir|
|
|
@@ -315,7 +363,7 @@ module RailsAiContext
|
|
|
315
363
|
real_dir = File.realpath(dir)
|
|
316
364
|
next [] unless RailsAiContext::SafePath.contained?(real_dir, real_root)
|
|
317
365
|
|
|
318
|
-
Dir.glob(File.join(real_dir, "*")).map { |f| f.delete_prefix("#{real_root}/") }.first(10)
|
|
366
|
+
Dir.glob(File.join(real_dir, "*")).map { |f| shown(f.delete_prefix("#{real_root}/")) }.first(10)
|
|
319
367
|
end
|
|
320
368
|
|
|
321
369
|
nearby.any? ? "\n\nFiles in test directory: #{nearby.join(', ')}" : ""
|
|
@@ -323,6 +371,18 @@ module RailsAiContext
|
|
|
323
371
|
""
|
|
324
372
|
end
|
|
325
373
|
|
|
374
|
+
private_class_method def self.cucumber_line(cucumber)
|
|
375
|
+
"- **Cucumber:** #{cucumber[:location]} (#{count_phrase(cucumber[:count], "feature file")}, " \
|
|
376
|
+
"#{count_phrase(cucumber[:step_definitions].to_i, "step definition file")})"
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
private_class_method def self.ci_step_lines(steps, dir)
|
|
380
|
+
return [] unless steps.is_a?(Array) && steps.any?
|
|
381
|
+
|
|
382
|
+
[ "", "## CI Steps (`#{dir}config/ci.rb`, run by `#{dir}bin/ci`)" ] +
|
|
383
|
+
steps.map { |step| "- #{step[:name]}: `#{step[:command]}`" }
|
|
384
|
+
end
|
|
385
|
+
|
|
326
386
|
private_class_method def self.trait_lines(traits, limit)
|
|
327
387
|
return [] unless traits.is_a?(Hash) && traits.any?
|
|
328
388
|
|
|
@@ -364,13 +424,13 @@ module RailsAiContext
|
|
|
364
424
|
# Detect Devise + sign_in pattern from existing tests
|
|
365
425
|
has_devise = false
|
|
366
426
|
has_sign_in = false
|
|
367
|
-
test_dir =
|
|
427
|
+
test_dir = File.join(suite_root, "test")
|
|
368
428
|
if Dir.exist?(test_dir)
|
|
369
|
-
real_root = File.realpath(
|
|
429
|
+
real_root = File.realpath(suite_root).to_s
|
|
370
430
|
safe_glob(test_dir, "**/*_test.rb", real_root).first(5).each do |path|
|
|
371
431
|
content = RailsAiContext::SafeFile.read(path) or next
|
|
372
432
|
has_devise = true if content.include?("Devise::Test")
|
|
373
|
-
has_sign_in = true if content.
|
|
433
|
+
has_sign_in = true if content.match?(/\bsign_in\b/)
|
|
374
434
|
end
|
|
375
435
|
end
|
|
376
436
|
|
|
@@ -433,10 +493,10 @@ module RailsAiContext
|
|
|
433
493
|
private_class_method def self.parse_factory_details(relative_path)
|
|
434
494
|
# Try common factory locations
|
|
435
495
|
candidates = [
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
496
|
+
File.join(suite_root, "spec/factories/#{relative_path}"),
|
|
497
|
+
File.join(suite_root, "test/factories/#{relative_path}"),
|
|
498
|
+
File.join(suite_root, "spec/factories", relative_path),
|
|
499
|
+
File.join(suite_root, "test/factories", relative_path)
|
|
440
500
|
]
|
|
441
501
|
path = candidates.find { |p| File.exist?(p) }
|
|
442
502
|
return nil unless path
|
|
@@ -464,62 +524,68 @@ module RailsAiContext
|
|
|
464
524
|
RailsAiContext.debug_fail(e, nil, label: "parse_factory_details")
|
|
465
525
|
end
|
|
466
526
|
|
|
467
|
-
|
|
527
|
+
FIXTURE_SKIP_KEYS = %w[created_at updated_at id].freeze
|
|
528
|
+
|
|
529
|
+
FIXTURE_VALUE_LIMIT = 100
|
|
530
|
+
|
|
531
|
+
# A fixture file's labels and their short attributes as text, :too_large
|
|
532
|
+
# past the read limit, or nil when it was not read or does not parse.
|
|
468
533
|
private_class_method def self.parse_fixture_contents(file_path)
|
|
469
|
-
return
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
return {} unless parsed.is_a?(Hash)
|
|
481
|
-
|
|
482
|
-
skip_keys = %w[created_at updated_at id]
|
|
483
|
-
entries = {}
|
|
484
|
-
|
|
485
|
-
parsed.each do |entry_name, attributes|
|
|
486
|
-
next unless attributes.is_a?(Hash)
|
|
487
|
-
filtered = {}
|
|
488
|
-
attributes.each do |key, value|
|
|
489
|
-
next if skip_keys.include?(key.to_s)
|
|
490
|
-
str_value = value.to_s
|
|
491
|
-
next if str_value.length > 100
|
|
492
|
-
filtered[key] = value
|
|
534
|
+
return :too_large if File.size(file_path) > max_test_file_size
|
|
535
|
+
|
|
536
|
+
content = RailsAiContext::SafeFile.read(file_path, max_size: max_test_file_size) or return nil
|
|
537
|
+
parsed = RailsAiContext::FixtureKeys.parse(content) or return nil
|
|
538
|
+
|
|
539
|
+
parsed.transform_values do |attributes|
|
|
540
|
+
attributes.each_with_object({}) do |(key, value), shown|
|
|
541
|
+
next if FIXTURE_SKIP_KEYS.include?(key.to_s)
|
|
542
|
+
|
|
543
|
+
text = short_text(value)
|
|
544
|
+
shown[key] = text if text
|
|
493
545
|
end
|
|
494
|
-
entries[entry_name] = filtered if filtered.any?
|
|
495
546
|
end
|
|
547
|
+
end
|
|
496
548
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
549
|
+
# The value as text when it is short, else nil. A container is sized
|
|
550
|
+
# before to_s runs, since YAML aliases can nest one into an exponential
|
|
551
|
+
# string from a few lines.
|
|
552
|
+
private_class_method def self.short_text(value)
|
|
553
|
+
budget = FIXTURE_VALUE_LIMIT
|
|
554
|
+
pending = [ value ]
|
|
555
|
+
until pending.empty?
|
|
556
|
+
item = pending.pop
|
|
557
|
+
case item
|
|
558
|
+
when Hash then budget -= item.size; pending.concat(item.keys, item.values)
|
|
559
|
+
when Array then budget -= item.size; pending.concat(item)
|
|
560
|
+
else budget -= item.to_s.length
|
|
561
|
+
end
|
|
562
|
+
return nil if budget.negative?
|
|
563
|
+
end
|
|
564
|
+
text = value.to_s
|
|
565
|
+
text if text.length <= FIXTURE_VALUE_LIMIT
|
|
500
566
|
end
|
|
501
567
|
|
|
502
|
-
#
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
rails_app.root.join("spec", "fixtures").to_s
|
|
507
|
-
]
|
|
508
|
-
real_root = File.realpath(rails_app.root).to_s
|
|
568
|
+
# { set => entries, or nil when unparsed } over the fixture directories
|
|
569
|
+
# the introspector found, each set named by its path under its directory.
|
|
570
|
+
private_class_method def self.parse_all_fixture_contents(fixtures)
|
|
571
|
+
return {} unless fixtures.is_a?(Hash)
|
|
509
572
|
|
|
573
|
+
root = suite_root
|
|
574
|
+
real_root = File.realpath(root)
|
|
510
575
|
results = {}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
576
|
+
Array(fixtures[:locations]).each do |rel|
|
|
577
|
+
dir = File.join(root, rel)
|
|
578
|
+
Dir.glob(File.join(dir, "**", "*.yml")).sort.each do |path|
|
|
579
|
+
set = path.delete_prefix("#{dir}/").delete_suffix(".yml")
|
|
580
|
+
next if results.key?(set)
|
|
581
|
+
|
|
582
|
+
real = safe_glob_realpath(path, real_root, real_root) or next
|
|
583
|
+
results[set] = parse_fixture_contents(real)
|
|
517
584
|
end
|
|
518
585
|
end
|
|
519
|
-
|
|
520
586
|
results
|
|
521
|
-
rescue => e
|
|
522
|
-
{}
|
|
587
|
+
rescue SystemCallError => e
|
|
588
|
+
RailsAiContext.debug_fail(e, {}, label: "parse_all_fixture_contents")
|
|
523
589
|
end
|
|
524
590
|
|
|
525
591
|
# Extract relationships: find foreign key references between fixtures
|
|
@@ -535,7 +601,11 @@ module RailsAiContext
|
|
|
535
601
|
|
|
536
602
|
parsed_fixtures.each do |file, entries|
|
|
537
603
|
entries.each do |entry_name, attrs|
|
|
604
|
+
entry_name = erb_shown(entry_name)
|
|
538
605
|
attrs.each do |key, value|
|
|
606
|
+
# An id ERB computes names no row we can see.
|
|
607
|
+
next if RailsAiContext::ConfigYaml.marked?(value)
|
|
608
|
+
|
|
539
609
|
# Foreign key pattern: key ends with _id and value matches an entry, or
|
|
540
610
|
# key matches a fixture file name and value is a fixture entry reference
|
|
541
611
|
str_value = value.to_s
|
|
@@ -75,15 +75,21 @@ module RailsAiContext
|
|
|
75
75
|
# commonly), so a controller filter that simply doesn't match
|
|
76
76
|
# anything must not be reported as "views don't exist here".
|
|
77
77
|
if templates.empty? && partials.empty?
|
|
78
|
-
note = api_only_note("app/views")
|
|
78
|
+
note = api_only_note("app/views", dir: "app/views")
|
|
79
79
|
return text_response(note) if note
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
all_dirs = view_directories(templates, partials)
|
|
83
83
|
suggestion = find_closest_match(ctrl_lower, all_dirs)
|
|
84
84
|
hint = suggestion ? " Did you mean '#{suggestion}'?" : ""
|
|
85
|
-
dirs_note = all_dirs.any?
|
|
86
|
-
|
|
85
|
+
dirs_note = if all_dirs.any?
|
|
86
|
+
" Directories with views: #{all_dirs.join(', ')}"
|
|
87
|
+
elsif layout_files.any?
|
|
88
|
+
" The app has only layouts, listed by `controller:\"layouts\"`."
|
|
89
|
+
else
|
|
90
|
+
" No view directories found (API-only apps typically have none)."
|
|
91
|
+
end
|
|
92
|
+
return empty_response([ "No views for '#{controller}'.#{hint}#{dirs_note}", static_engine_views_note ].compact.join("\n\n"))
|
|
87
93
|
end
|
|
88
94
|
|
|
89
95
|
templates = filtered_templates
|
|
@@ -94,7 +100,7 @@ module RailsAiContext
|
|
|
94
100
|
# genuinely has no views anywhere (as opposed to a filter simply not
|
|
95
101
|
# matching anything, handled above).
|
|
96
102
|
if controller.nil? && templates.empty? && partials.empty?
|
|
97
|
-
note = api_only_note("app/views")
|
|
103
|
+
note = api_only_note("app/views", dir: "app/views")
|
|
98
104
|
return text_response(note) if note
|
|
99
105
|
end
|
|
100
106
|
|
|
@@ -103,7 +109,7 @@ module RailsAiContext
|
|
|
103
109
|
case detail
|
|
104
110
|
when "summary"
|
|
105
111
|
all_dirs = view_groups(templates, partials)
|
|
106
|
-
lines = views_header_lines(templates, partials, layouts)
|
|
112
|
+
lines = views_header_lines(templates, partials, layouts, controller: controller)
|
|
107
113
|
all_dirs.each do |ctrl|
|
|
108
114
|
ctrl_templates = views_in_group(templates, ctrl)
|
|
109
115
|
ctrl_partials = views_in_group(partials, ctrl)
|
|
@@ -115,10 +121,12 @@ module RailsAiContext
|
|
|
115
121
|
stim = meta[:stimulus]&.any? ? " stimulus: #{meta[:stimulus].join(', ')}" : ""
|
|
116
122
|
comps = meta[:components]&.any? ? " components: #{meta[:components].join(', ')}" : ""
|
|
117
123
|
phlex_tag = meta[:phlex] ? " [phlex]" : ""
|
|
118
|
-
|
|
124
|
+
alternate = (note = view_alternate_of(name)) ? " #{note}" : ""
|
|
125
|
+
lines << "- #{name} (#{count_phrase(meta[:lines], "line")}#{phlex_tag})#{alternate}#{parts}#{comps}#{stim}"
|
|
119
126
|
end
|
|
120
127
|
ctrl_partials.sort.each do |name, meta|
|
|
121
|
-
|
|
128
|
+
alternate = (note = view_alternate_of(name)) ? " #{note}" : ""
|
|
129
|
+
lines << "- #{name} (#{count_phrase(meta[:lines], "line")})#{alternate}"
|
|
122
130
|
end
|
|
123
131
|
lines << ""
|
|
124
132
|
end
|
|
@@ -129,7 +137,7 @@ module RailsAiContext
|
|
|
129
137
|
# want the same file's metadata.
|
|
130
138
|
metadata = Hash.new { |h, name| h[name] = extract_view_metadata(name) }
|
|
131
139
|
all_dirs = view_groups(templates, partials)
|
|
132
|
-
lines = views_header_lines(templates, partials, layouts)
|
|
140
|
+
lines = views_header_lines(templates, partials, layouts, controller: controller)
|
|
133
141
|
|
|
134
142
|
# Form builders and component usage from views introspector
|
|
135
143
|
form_builders = data[:form_builders_detected]
|
|
@@ -148,7 +156,7 @@ module RailsAiContext
|
|
|
148
156
|
|
|
149
157
|
lines << "## #{group_heading(ctrl)}" unless controller && all_dirs.size == 1
|
|
150
158
|
ctrl_templates.sort.each do |name, meta|
|
|
151
|
-
detail_parts = []
|
|
159
|
+
detail_parts = [ view_alternate_of(name) ].compact
|
|
152
160
|
extra = metadata[name]
|
|
153
161
|
|
|
154
162
|
if meta[:phlex]
|
|
@@ -178,7 +186,8 @@ module RailsAiContext
|
|
|
178
186
|
helpers = meta[:helpers]&.any? ? " helpers: #{meta[:helpers].join(', ')}" : ""
|
|
179
187
|
locals = extract_partial_locals(name, templates)
|
|
180
188
|
locals_str = locals&.any? ? " **locals:** #{locals.join(', ')}" : ""
|
|
181
|
-
|
|
189
|
+
alternate = (note = view_alternate_of(name)) ? " #{note}" : ""
|
|
190
|
+
lines << "- #{name} (#{count_phrase(meta[:lines], "line")})#{alternate}#{fields}#{helpers}#{locals_str}"
|
|
182
191
|
end
|
|
183
192
|
lines << ""
|
|
184
193
|
end
|
|
@@ -198,7 +207,7 @@ module RailsAiContext
|
|
|
198
207
|
|
|
199
208
|
when "full"
|
|
200
209
|
if controller
|
|
201
|
-
lines = [ "# Views: #{controller}/", "" ]
|
|
210
|
+
lines = [ "# Views: #{controller}/", "", *layout_lines(controller) ]
|
|
202
211
|
# Combine all content first for cross-template Tailwind compression
|
|
203
212
|
all_content = []
|
|
204
213
|
templates.sort.each do |name, _meta|
|
|
@@ -208,7 +217,7 @@ module RailsAiContext
|
|
|
208
217
|
all_content << [ name, strip_svg(read_view_content(name)) ]
|
|
209
218
|
end
|
|
210
219
|
# Compress repeated Tailwind classes across all templates
|
|
211
|
-
combined = all_content.map { |name, c| "## #{name}\n
|
|
220
|
+
combined = all_content.map { |name, c| "## #{name}\n```#{RailsAiContext::ViewFile.fence(name)}\n#{c}\n```\n" }.join("\n")
|
|
212
221
|
combined = compress_tailwind(combined)
|
|
213
222
|
lines << combined
|
|
214
223
|
text_response(lines.join("\n"))
|
|
@@ -223,6 +232,7 @@ module RailsAiContext
|
|
|
223
232
|
count = views_in_group(templates, ctrl).size + views_in_group(partials, ctrl).size
|
|
224
233
|
lines << "- `controller:\"#{ctrl}\"` (#{count_phrase(count, "file")})"
|
|
225
234
|
end
|
|
235
|
+
lines << "- `controller:\"layouts\"` (#{count_phrase(layouts.size, "layout")})" if layouts.any?
|
|
226
236
|
lines << "" << "_Or use `path:\"controller/action.html.erb\"` for a specific file._"
|
|
227
237
|
text_response(lines.join("\n"))
|
|
228
238
|
end
|
|
@@ -263,13 +273,20 @@ module RailsAiContext
|
|
|
263
273
|
|
|
264
274
|
# Layouts sit outside the template and partial maps, so a heading naming
|
|
265
275
|
# only those two numbers never added up to the files under app/views.
|
|
266
|
-
private_class_method def self.views_header_lines(templates, partials, layouts)
|
|
276
|
+
private_class_method def self.views_header_lines(templates, partials, layouts, controller: nil)
|
|
267
277
|
parts = [ count_phrase(templates.size, "template"), count_phrase(partials.size, "partial") ]
|
|
268
278
|
parts << count_phrase(layouts.size, "layout") if layouts.any?
|
|
269
279
|
|
|
270
280
|
lines = [ "# Views (#{parts.join(', ')})", "" ]
|
|
281
|
+
lines << static_engine_views_note << "" if static_engine_views_note
|
|
271
282
|
lines << "_Layouts are listed by `controller:\"layouts\"`._" << "" if layouts.any?
|
|
272
|
-
lines
|
|
283
|
+
lines + layout_lines(controller)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
private_class_method def self.layout_lines(controller)
|
|
287
|
+
name = controller && RailsAiContext::Payload.find_controller(cached_context, controller)
|
|
288
|
+
layout = name && Introspectors::ControllerSettings.resolve(cached_context, name, root: rails_app.root.to_s)[:layout]
|
|
289
|
+
layout ? [ "**Layout:** #{Introspectors::ControllerSettings.layout_phrase(layout)}", "" ] : []
|
|
273
290
|
end
|
|
274
291
|
|
|
275
292
|
# The template map excludes app/views/layouts, so the directory is the
|
|
@@ -293,7 +310,7 @@ module RailsAiContext
|
|
|
293
310
|
real = located.realpath
|
|
294
311
|
if RailsAiContext::DetailLevel.full?(detail)
|
|
295
312
|
content = RailsAiContext::SafeFile.read(real) || "(error reading)"
|
|
296
|
-
lines << "## #{relative}" << "
|
|
313
|
+
lines << "## #{relative}" << "```#{RailsAiContext::ViewFile.fence(relative)}" << strip_svg(content) << "```" << ""
|
|
297
314
|
else
|
|
298
315
|
content = RailsAiContext::SafeFile.read(real) || ""
|
|
299
316
|
lines << "- #{relative} (#{count_phrase(content.lines.size, "line")})"
|
|
@@ -313,7 +330,7 @@ module RailsAiContext
|
|
|
313
330
|
siblings = RailsAiContext::ViewFile.each(rails_app.root.to_s, File.join(dir, "*"))
|
|
314
331
|
.map { |_file, relative| relative }.sort.first(10)
|
|
315
332
|
hint = siblings.any? ? " Files in #{dir}/: #{siblings.join(', ')}" : ""
|
|
316
|
-
return empty_response("View not found: #{path}.#{hint}")
|
|
333
|
+
return empty_response([ "View not found: #{path}.#{hint}", static_engine_views_note ].compact.join("\n\n"))
|
|
317
334
|
end
|
|
318
335
|
return text_response("Could not read file: #{path}") unless content
|
|
319
336
|
|
|
@@ -325,7 +342,7 @@ module RailsAiContext
|
|
|
325
342
|
end
|
|
326
343
|
|
|
327
344
|
content = compress_tailwind(strip_svg(content))
|
|
328
|
-
text_response("# #{result.relative}\n\n
|
|
345
|
+
text_response("# #{result.relative}\n\n```#{RailsAiContext::ViewFile.fence(result.relative)}\n#{content}\n```")
|
|
329
346
|
end
|
|
330
347
|
|
|
331
348
|
# Strip inline SVG blocks - they're visual noise that buries the signal AI needs.
|
|
@@ -426,7 +443,7 @@ module RailsAiContext
|
|
|
426
443
|
|
|
427
444
|
private_class_method def self.read_from_disk(controller:, path:, detail:)
|
|
428
445
|
if RailsAiContext::PathResolver.view_dirs(rails_app.root.to_s).empty?
|
|
429
|
-
note = api_only_note("app/views")
|
|
446
|
+
note = api_only_note("app/views", dir: "app/views")
|
|
430
447
|
return text_response(note) if note
|
|
431
448
|
|
|
432
449
|
return text_response("No app/views directory found.")
|
|
@@ -457,8 +474,8 @@ module RailsAiContext
|
|
|
457
474
|
end
|
|
458
475
|
|
|
459
476
|
templates, partials = files.partition { |f| !File.basename(f).start_with?("_") }
|
|
460
|
-
lines = views_header_lines(templates, partials, controller ? [] : layout_files)
|
|
461
|
-
files.each { |f| lines << "- #{f}" }
|
|
477
|
+
lines = views_header_lines(templates, partials, controller ? [] : layout_files, controller: controller)
|
|
478
|
+
files.each { |f| lines << [ "- #{f}", view_alternate_of(f) ].compact.join(" - ") }
|
|
462
479
|
text_response(lines.join("\n"))
|
|
463
480
|
end
|
|
464
481
|
end
|