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
|
@@ -6,15 +6,15 @@ require "prism"
|
|
|
6
6
|
|
|
7
7
|
module RailsAiContext
|
|
8
8
|
# Thread-safe AST parse cache backed by Concurrent::Map.
|
|
9
|
-
# Keyed by
|
|
10
|
-
#
|
|
9
|
+
# Keyed by content hash, so a file and the same text read as a string share one
|
|
10
|
+
# parse, and a changed file misses. Used by all Prism-based introspectors.
|
|
11
11
|
# A settled file's stat stands in for the read and hash (see RACY_WINDOW).
|
|
12
12
|
#
|
|
13
13
|
# Bounded: evicts entries when MAX_SIZE is exceeded, and a file's recorded
|
|
14
14
|
# stat goes with its parse.
|
|
15
15
|
module AstCache
|
|
16
16
|
STORE = Concurrent::Map.new
|
|
17
|
-
SEEN = Concurrent::Map.new # path => [stat signature, STORE key, whether the stat can be trusted]
|
|
17
|
+
SEEN = Concurrent::Map.new # path, or [path, ruby grammar] => [stat signature, STORE key, whether the stat can be trusted]
|
|
18
18
|
MAX_SIZE = 500
|
|
19
19
|
EVICTION_MUTEX = Mutex.new
|
|
20
20
|
|
|
@@ -27,12 +27,15 @@ module RailsAiContext
|
|
|
27
27
|
#
|
|
28
28
|
# Reads content first, then checks size - avoids TOCTOU race where the
|
|
29
29
|
# file could change between File.size and File.read.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
#
|
|
31
|
+
# `ruby:` is the Ruby version whose grammar decides what is a syntax error;
|
|
32
|
+
# nil parses as the newest Ruby prism knows.
|
|
33
|
+
def self.parse(path, ruby: nil)
|
|
34
|
+
version = prism_version(ruby)
|
|
35
|
+
seen_key = version ? [ path, version ] : path
|
|
36
|
+
# An unchanged file answers from its stat; one concern file reaches every model's walk.
|
|
34
37
|
signature = RunCache.fetch([ :stat_signature, path.to_s ]) { stat_signature(path) }
|
|
35
|
-
seen = SEEN[
|
|
38
|
+
seen = SEEN[seen_key]
|
|
36
39
|
if seen && seen[2] && seen.first == signature && (cached = STORE[seen[1]])
|
|
37
40
|
return cached
|
|
38
41
|
end
|
|
@@ -42,16 +45,15 @@ module RailsAiContext
|
|
|
42
45
|
size = content.bytesize
|
|
43
46
|
raise ArgumentError, "File too large for AST parsing: #{path} (#{size} bytes, max #{MAX_PARSE_SIZE})" if size > MAX_PARSE_SIZE
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
key = "#{path}:#{Digest::SHA256.hexdigest(content)}:#{mtime}"
|
|
48
|
+
key = content_key(content, version)
|
|
47
49
|
|
|
48
50
|
cached = STORE[key]
|
|
49
51
|
unless cached
|
|
50
52
|
# Evict BEFORE inserting to avoid running inside compute_if_absent
|
|
51
53
|
evict_if_full
|
|
52
|
-
cached = STORE.compute_if_absent(key) {
|
|
54
|
+
cached = STORE.compute_if_absent(key) { prism_parse(content, version) }
|
|
53
55
|
end
|
|
54
|
-
SEEN[
|
|
56
|
+
SEEN[seen_key] = [ signature, key, settled?(signature, read_at) ]
|
|
55
57
|
cached
|
|
56
58
|
end
|
|
57
59
|
|
|
@@ -83,18 +85,51 @@ module RailsAiContext
|
|
|
83
85
|
|
|
84
86
|
# Parse a Ruby source string, cached by content digest, so every extractor
|
|
85
87
|
# handed the same text shares one parse.
|
|
86
|
-
def self.parse_string(source)
|
|
87
|
-
|
|
88
|
+
def self.parse_string(source, ruby: nil)
|
|
89
|
+
version = prism_version(ruby)
|
|
90
|
+
return prism_parse(source, version) if source.bytesize > MAX_PARSE_SIZE
|
|
88
91
|
|
|
89
|
-
key =
|
|
92
|
+
key = content_key(source, version)
|
|
90
93
|
|
|
91
94
|
cached = STORE[key]
|
|
92
95
|
return cached if cached
|
|
93
96
|
|
|
94
97
|
evict_if_full
|
|
95
98
|
|
|
96
|
-
STORE.compute_if_absent(key) {
|
|
99
|
+
STORE.compute_if_absent(key) { prism_parse(source, version) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.content_key(source, version)
|
|
103
|
+
"src:#{Digest::SHA256.hexdigest(source)}#{":#{version}" if version}"
|
|
104
|
+
end
|
|
105
|
+
private_class_method :content_key
|
|
106
|
+
|
|
107
|
+
OLDEST_GRAMMAR = "3.3"
|
|
108
|
+
GRAMMARS = Concurrent::Map.new # major.minor => whether the loaded prism parses it
|
|
109
|
+
|
|
110
|
+
# The grammar prism parses `ruby` with: its own major.minor, the oldest prism
|
|
111
|
+
# knows for an older Ruby, and nil (the newest) for one newer than prism knows.
|
|
112
|
+
# ponytail: Ruby 3.1 and 3.2 parse as 3.3; syntax only they accept reads as an error.
|
|
113
|
+
def self.prism_version(ruby)
|
|
114
|
+
wanted = ruby.to_s.b[/\A\d+\.\d+/]
|
|
115
|
+
return nil unless wanted
|
|
116
|
+
|
|
117
|
+
wanted = OLDEST_GRAMMAR if Gem::Version.new(wanted) < Gem::Version.new(OLDEST_GRAMMAR)
|
|
118
|
+
wanted if GRAMMARS.compute_if_absent(wanted) { supported?(wanted) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.supported?(version)
|
|
122
|
+
Prism.parse("", version: version)
|
|
123
|
+
true
|
|
124
|
+
rescue ArgumentError
|
|
125
|
+
false
|
|
126
|
+
end
|
|
127
|
+
private_class_method :supported?
|
|
128
|
+
|
|
129
|
+
def self.prism_parse(source, version)
|
|
130
|
+
version ? Prism.parse(source, version: version) : Prism.parse(source)
|
|
97
131
|
end
|
|
132
|
+
private_class_method :prism_parse
|
|
98
133
|
|
|
99
134
|
# Clear the entire cache.
|
|
100
135
|
def self.clear
|
|
@@ -114,8 +149,8 @@ module RailsAiContext
|
|
|
114
149
|
return if STORE.size < MAX_SIZE
|
|
115
150
|
keys = STORE.keys
|
|
116
151
|
evicted = keys.first(keys.size / 4).each { |k| STORE.delete(k) }.to_set
|
|
117
|
-
# A stat
|
|
118
|
-
#
|
|
152
|
+
# A stat goes with the parse it points at; files with the same bytes share
|
|
153
|
+
# one parse, so SEEN can outgrow STORE but not the app's file count.
|
|
119
154
|
SEEN.each_pair { |path, entry| SEEN.delete(path) if evicted.include?(entry[1]) }
|
|
120
155
|
end
|
|
121
156
|
end
|
|
@@ -6,6 +6,8 @@ module RailsAiContext
|
|
|
6
6
|
module BaseMixins
|
|
7
7
|
# `path` is nil when the declaring file is not the app's (a gem's module).
|
|
8
8
|
Mixin = Struct.new(:name, :path, :macro)
|
|
9
|
+
# A base body's statements, the base it runs in, and its file's place in load order (sorted, as Rails loads initializers).
|
|
10
|
+
Body = Struct.new(:statements, :target, :order)
|
|
9
11
|
|
|
10
12
|
TARGETS = %w[ActiveRecord::Base ApplicationRecord].freeze
|
|
11
13
|
HOOK = :active_record
|
|
@@ -21,12 +23,22 @@ module RailsAiContext
|
|
|
21
23
|
RunCache.fetch([ :base_mixins, root ]) { discover(root) }
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
# Each model base body the app writes outside app/models, where a `def self.x` or
|
|
27
|
+
# `class << self` defines a class method every model under that base has.
|
|
28
|
+
# @return [Array<Body>]
|
|
29
|
+
def bodies(root)
|
|
30
|
+
root = File.expand_path(root.to_s)
|
|
31
|
+
RunCache.fetch([ :base_bodies, root ]) do
|
|
32
|
+
marked(root).each_with_index.flat_map do |(_, tree), order|
|
|
33
|
+
base_bodies(tree).map { |statements, target, _| Body.new(statements, target, order) }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
RailsAiContext.debug_fail(e, [], label: "BaseMixins.bodies")
|
|
38
|
+
end
|
|
28
39
|
|
|
29
|
-
|
|
40
|
+
def discover(root)
|
|
41
|
+
marked(root).flat_map do |file, tree|
|
|
30
42
|
mixed_in(tree).map { |macro, name| Mixin.new(name, declaring_file(root, name, file), macro) }
|
|
31
43
|
end.uniq(&:name)
|
|
32
44
|
rescue StandardError => e
|
|
@@ -34,6 +46,17 @@ module RailsAiContext
|
|
|
34
46
|
end
|
|
35
47
|
private_class_method :discover
|
|
36
48
|
|
|
49
|
+
# [file, tree] for each scanned file that names a model base; models and bodies both ask.
|
|
50
|
+
def marked(root)
|
|
51
|
+
RunCache.fetch([ :base_mixins_marked, root ]) do
|
|
52
|
+
scanned_files(root).filter_map do |file|
|
|
53
|
+
source = SafeFile.read(file)
|
|
54
|
+
[ file, AstCache.parse(file).value ] if source && MARKERS.any? { |marker| source.include?(marker) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
private_class_method :marked
|
|
59
|
+
|
|
37
60
|
# Everywhere the app configures itself but app/models: a model file's includes are
|
|
38
61
|
# its own, and the model walk reads that file already.
|
|
39
62
|
def scanned_files(root)
|
|
@@ -50,25 +73,31 @@ module RailsAiContext
|
|
|
50
73
|
private_class_method :scanned_files
|
|
51
74
|
|
|
52
75
|
# [macro, constant] for each module the tree mixes into a model base:
|
|
53
|
-
# sent to it, run in its `on_load` hook, or written in its
|
|
76
|
+
# sent to it, run in its `on_load` hook, or written in its own body.
|
|
54
77
|
def mixed_in(tree)
|
|
55
78
|
Introspectors::AstWalk.each(tree).flat_map do |node|
|
|
56
79
|
next [] unless node.is_a?(Prism::CallNode)
|
|
57
80
|
next hook_mixins(node) if hook?(node)
|
|
58
81
|
|
|
59
82
|
target?(node.receiver) ? mixin_calls(node) : []
|
|
60
|
-
end +
|
|
83
|
+
end + base_bodies(tree).reject(&:last).flat_map(&:first).flat_map { |call| call.is_a?(Prism::CallNode) && call.receiver.nil? ? mixin_calls(call) : [] }
|
|
61
84
|
end
|
|
62
85
|
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
# [statements, base, hook?] for each body a base runs as self: `class ActiveRecord::Base` reopened
|
|
87
|
+
# however the namespace is written, `ActiveRecord::Base.class_eval do`, and an `on_load` block.
|
|
88
|
+
def base_bodies(tree)
|
|
89
|
+
reopened = Introspectors::DeclaredConstant.constants(tree).filter_map do |name, node|
|
|
90
|
+
[ node.body, name, false ] if node.is_a?(Prism::ClassNode) && TARGETS.include?(name)
|
|
91
|
+
end
|
|
92
|
+
evaluated = Introspectors::AstWalk.each(tree).filter_map do |node|
|
|
93
|
+
next unless node.is_a?(Prism::CallNode) && node.block
|
|
94
|
+
next [ node.block.body, TARGETS.first, true ] if hook?(node)
|
|
67
95
|
|
|
68
|
-
node.
|
|
96
|
+
[ node.block.body, node.receiver.slice.delete_prefix("::"), false ] if %i[class_eval class_exec].include?(node.name) && target?(node.receiver)
|
|
69
97
|
end
|
|
98
|
+
(reopened + evaluated).filter_map { |body, *rest| [ body.body, *rest ] if body.is_a?(Prism::StatementsNode) }
|
|
70
99
|
end
|
|
71
|
-
private_class_method :
|
|
100
|
+
private_class_method :base_bodies
|
|
72
101
|
|
|
73
102
|
# Inside `on_load(:active_record) { ... }` the block runs in the base.
|
|
74
103
|
def hook_mixins(node)
|
|
@@ -109,11 +138,12 @@ module RailsAiContext
|
|
|
109
138
|
private_class_method :constants
|
|
110
139
|
|
|
111
140
|
# Past Zeitwerk's path, a plugin's module sits in a lib file its init.rb
|
|
112
|
-
# requires, under a name its path does not spell.
|
|
141
|
+
# requires, under a name its path does not spell; a namespace stub is not it.
|
|
113
142
|
def declaring_file(root, name, file)
|
|
114
|
-
ConcernPaths.find_file(root, name) ||
|
|
143
|
+
ConcernPaths.find_file(root, name, outer: false) ||
|
|
115
144
|
([ file ] + required_files(root, file, 2)).find do |candidate|
|
|
116
|
-
Introspectors::DeclaredConstant.module_node(AstCache.parse(candidate).value, name)
|
|
145
|
+
node = Introspectors::DeclaredConstant.module_node(AstCache.parse(candidate).value, name)
|
|
146
|
+
node && !Introspectors::DeclaredConstant.stub?(node)
|
|
117
147
|
end
|
|
118
148
|
rescue StandardError => e
|
|
119
149
|
RailsAiContext.debug_fail(e, nil, label: "BaseMixins.declaring_file #{name}")
|
|
@@ -113,7 +113,11 @@ module RailsAiContext
|
|
|
113
113
|
)
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
guard(timeout: timeout, announce_exit: false) { require environment_rb }
|
|
116
|
+
result = guard(timeout: timeout, announce_exit: false) { require environment_rb }
|
|
117
|
+
return result unless result.booted?
|
|
118
|
+
return result if defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
|
|
119
|
+
|
|
120
|
+
Result.new(status: :failed, error: BootError.new("config/environment.rb loaded but defined no Rails application"))
|
|
117
121
|
rescue SystemExit => e
|
|
118
122
|
Result.new(status: :failed, error: BootExitError.new("App called exit(#{e.status}) during boot"))
|
|
119
123
|
end
|
|
@@ -40,6 +40,7 @@ module RailsAiContext
|
|
|
40
40
|
# source: an engine keeps its dummy app under spec/dummy, so its root
|
|
41
41
|
# has app/ and no config/, and it is a real target.
|
|
42
42
|
def self.app_present?(root, allow_source_only: false)
|
|
43
|
+
return false if other_framework?(root)
|
|
43
44
|
return true if File.exist?(File.join(root, "config", "environment.rb"))
|
|
44
45
|
return false unless allow_source_only
|
|
45
46
|
|
|
@@ -47,6 +48,19 @@ module RailsAiContext
|
|
|
47
48
|
Dir.glob(File.join(root, "app", "**", "*.rb")).any?
|
|
48
49
|
end
|
|
49
50
|
|
|
51
|
+
# Sinatra MVC trees have config/environment.rb too; without application.rb the lockfile, else the Gemfile, decides.
|
|
52
|
+
def self.other_framework?(root)
|
|
53
|
+
return false if File.exist?(File.join(root, "config", "application.rb"))
|
|
54
|
+
|
|
55
|
+
require_relative "../gem_lock"
|
|
56
|
+
lock = GemLock.for(root)
|
|
57
|
+
return !lock.any?("rails", "railties") unless lock.missing?
|
|
58
|
+
|
|
59
|
+
names = lock.gemfile_gems
|
|
60
|
+
!names.nil? && (names & %w[rails railties]).empty?
|
|
61
|
+
end
|
|
62
|
+
private_class_method :other_framework?
|
|
63
|
+
|
|
50
64
|
def self.call(root:, allow_static:, no_boot: false, allow_source_only: false, command: nil)
|
|
51
65
|
messages = []
|
|
52
66
|
|
|
@@ -63,7 +77,7 @@ module RailsAiContext
|
|
|
63
77
|
unless app_present?(root)
|
|
64
78
|
return absent(root, messages, command: command) unless allow_static
|
|
65
79
|
|
|
66
|
-
return enter_static("no config/environment.rb in
|
|
80
|
+
return enter_static("no config/environment.rb in the app root", :source_only, root, messages)
|
|
67
81
|
end
|
|
68
82
|
|
|
69
83
|
# Bundler.setup (in config/boot.rb) strips $LOAD_PATH and the spec
|
|
@@ -166,7 +180,8 @@ module RailsAiContext
|
|
|
166
180
|
def self.enter_static(reason, kind, root, messages)
|
|
167
181
|
require_gem_without_app!
|
|
168
182
|
RailsAiContext.tier = :static
|
|
169
|
-
|
|
183
|
+
# Every tool response carries this in its footer, so an absolute path in a boot error goes root-relative.
|
|
184
|
+
RailsAiContext.static_reason = RailsAiContext::PortablePath.relativize_text(reason, root)
|
|
170
185
|
RailsAiContext.static_kind = kind
|
|
171
186
|
RailsAiContext.configuration.app_root = root
|
|
172
187
|
Configuration.auto_load!(root)
|
|
@@ -191,7 +191,7 @@ module RailsAiContext
|
|
|
191
191
|
def build_kwargs
|
|
192
192
|
kwargs = case raw_args
|
|
193
193
|
when Hash
|
|
194
|
-
raw_args.transform_keys(&:to_sym).except(:server_context)
|
|
194
|
+
raw_args.transform_keys(&:to_sym).except(:server_context).transform_values { |value| utf8(value) }
|
|
195
195
|
when Array
|
|
196
196
|
return with_environment(parse_cli_args(raw_args).except(:server_context))
|
|
197
197
|
else
|
|
@@ -219,6 +219,7 @@ module RailsAiContext
|
|
|
219
219
|
|
|
220
220
|
# Parse ["--table", "users", "--detail", "full", "--app-only"] into { table: "users", ... }
|
|
221
221
|
def parse_cli_args(args)
|
|
222
|
+
args = args.map { |arg| utf8(arg) }
|
|
222
223
|
result = {}
|
|
223
224
|
i = 0
|
|
224
225
|
properties = (tool_schema[:properties] || {})
|
|
@@ -308,6 +309,19 @@ module RailsAiContext
|
|
|
308
309
|
result
|
|
309
310
|
end
|
|
310
311
|
|
|
312
|
+
# ARGV (here and in the rake task) carries the locale's encoding, BINARY
|
|
313
|
+
# under LANG=C; MCP values are always UTF-8.
|
|
314
|
+
def utf8(arg)
|
|
315
|
+
return arg unless arg.is_a?(String)
|
|
316
|
+
return arg if arg.encoding == Encoding::UTF_8 && arg.valid_encoding?
|
|
317
|
+
|
|
318
|
+
if [ Encoding::UTF_8, Encoding::BINARY, Encoding::US_ASCII ].include?(arg.encoding)
|
|
319
|
+
arg.dup.force_encoding(Encoding::UTF_8).scrub
|
|
320
|
+
else
|
|
321
|
+
arg.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
311
325
|
# The flag a stray word was most likely meant for: a required param
|
|
312
326
|
# first, since that is the one a caller must supply.
|
|
313
327
|
def suggested_flag
|