rails-ai-context 4.3.2 → 4.4.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 +127 -53
- data/CLAUDE.md +3 -1
- data/README.md +268 -197
- data/demo-trace.gif +0 -0
- data/demo-trace.tape +21 -0
- data/demo.gif +0 -0
- data/demo.tape +33 -0
- data/docs/GUIDE.md +9 -9
- data/lib/generators/rails_ai_context/install/install_generator.rb +2 -1
- data/lib/rails_ai_context/cli/tool_runner.rb +1 -1
- data/lib/rails_ai_context/configuration.rb +25 -1
- data/lib/rails_ai_context/doctor.rb +4 -2
- data/lib/rails_ai_context/fingerprinter.rb +6 -1
- data/lib/rails_ai_context/introspectors/accessibility_introspector.rb +52 -1
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +10 -2
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +22 -2
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +50 -4
- data/lib/rails_ai_context/introspectors/api_introspector.rb +41 -5
- data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +10 -4
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +62 -7
- data/lib/rails_ai_context/introspectors/component_introspector.rb +6 -0
- data/lib/rails_ai_context/introspectors/config_introspector.rb +59 -9
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +45 -13
- data/lib/rails_ai_context/introspectors/convention_detector.rb +25 -2
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +58 -4
- data/lib/rails_ai_context/introspectors/design_token_introspector.rb +27 -5
- data/lib/rails_ai_context/introspectors/devops_introspector.rb +15 -8
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +12 -3
- data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +36 -1
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +47 -1
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +49 -3
- data/lib/rails_ai_context/introspectors/job_introspector.rb +48 -5
- data/lib/rails_ai_context/introspectors/middleware_introspector.rb +24 -3
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +4 -1
- data/lib/rails_ai_context/introspectors/model_introspector.rb +108 -11
- data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +57 -12
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +34 -9
- data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +12 -2
- data/lib/rails_ai_context/introspectors/route_introspector.rb +25 -8
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +45 -7
- data/lib/rails_ai_context/introspectors/seeds_introspector.rb +5 -2
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +59 -6
- data/lib/rails_ai_context/introspectors/test_introspector.rb +50 -5
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +44 -13
- data/lib/rails_ai_context/introspectors/view_introspector.rb +46 -7
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +25 -7
- data/lib/rails_ai_context/resources.rb +1 -1
- data/lib/rails_ai_context/server.rb +6 -3
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +8 -4
- data/lib/rails_ai_context/tools/analyze_feature.rb +66 -19
- data/lib/rails_ai_context/tools/base_tool.rb +1 -1
- data/lib/rails_ai_context/tools/diagnose.rb +4 -2
- data/lib/rails_ai_context/tools/get_callbacks.rb +4 -2
- data/lib/rails_ai_context/tools/get_concern.rb +12 -6
- data/lib/rails_ai_context/tools/get_controllers.rb +10 -5
- data/lib/rails_ai_context/tools/get_conventions.rb +4 -2
- data/lib/rails_ai_context/tools/get_design_system.rb +2 -1
- data/lib/rails_ai_context/tools/get_env.rb +8 -4
- data/lib/rails_ai_context/tools/get_helper_methods.rb +6 -3
- data/lib/rails_ai_context/tools/get_job_pattern.rb +2 -1
- data/lib/rails_ai_context/tools/get_model_details.rb +10 -5
- data/lib/rails_ai_context/tools/get_partial_interface.rb +14 -7
- data/lib/rails_ai_context/tools/get_schema.rb +2 -1
- data/lib/rails_ai_context/tools/get_service_pattern.rb +2 -1
- data/lib/rails_ai_context/tools/get_stimulus.rb +2 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +4 -2
- data/lib/rails_ai_context/tools/get_turbo_map.rb +22 -11
- data/lib/rails_ai_context/tools/get_view.rb +6 -3
- data/lib/rails_ai_context/tools/migration_advisor.rb +2 -1
- data/lib/rails_ai_context/tools/onboard.rb +2 -1
- data/lib/rails_ai_context/tools/performance_check.rb +2 -1
- data/lib/rails_ai_context/tools/query.rb +5 -1
- data/lib/rails_ai_context/tools/read_logs.rb +3 -0
- data/lib/rails_ai_context/tools/runtime_info.rb +10 -5
- data/lib/rails_ai_context/tools/search_code.rb +8 -4
- data/lib/rails_ai_context/tools/search_docs.rb +2 -1
- data/lib/rails_ai_context/tools/session_context.rb +2 -1
- data/lib/rails_ai_context/tools/validate.rb +16 -8
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +5 -1
|
@@ -99,11 +99,20 @@ module RailsAiContext
|
|
|
99
99
|
next if engine.name == "RailsAiContext::Engine"
|
|
100
100
|
next if engine.name.start_with?("Rails::", "ActionPack::", "ActionView::", "ActiveModel::")
|
|
101
101
|
|
|
102
|
-
{ name: engine.name, root: engine.root.to_s.sub("#{Gem.dir}/gems/", "") }
|
|
103
|
-
|
|
102
|
+
entry = { name: engine.name, root: engine.root.to_s.sub("#{Gem.dir}/gems/", "") }
|
|
103
|
+
# Count routes and models inside the engine
|
|
104
|
+
if engine.respond_to?(:routes) && engine.routes.respond_to?(:routes)
|
|
105
|
+
entry[:route_count] = engine.routes.routes.size rescue nil
|
|
106
|
+
end
|
|
107
|
+
models_dir = File.join(engine.root.to_s, "app", "models")
|
|
108
|
+
entry[:model_count] = Dir.glob(File.join(models_dir, "**/*.rb")).size if Dir.exist?(models_dir)
|
|
109
|
+
entry.compact
|
|
110
|
+
rescue => e
|
|
111
|
+
$stderr.puts "[rails-ai-context] discover_rails_engines failed: #{e.message}" if ENV["DEBUG"]
|
|
104
112
|
nil
|
|
105
113
|
end.sort_by { |e| e[:name] }
|
|
106
|
-
rescue
|
|
114
|
+
rescue => e
|
|
115
|
+
$stderr.puts "[rails-ai-context] discover_rails_engines failed: #{e.message}" if ENV["DEBUG"]
|
|
107
116
|
[]
|
|
108
117
|
end
|
|
109
118
|
end
|
|
@@ -106,6 +106,8 @@ module RailsAiContext
|
|
|
106
106
|
typescript: ts,
|
|
107
107
|
monorepo: mono,
|
|
108
108
|
build_tool: build,
|
|
109
|
+
api_clients: detect_api_clients(all_deps),
|
|
110
|
+
component_libraries: detect_component_libraries(all_deps),
|
|
109
111
|
summary: build_summary(frameworks, mounting, build, ts, total_components)
|
|
110
112
|
}
|
|
111
113
|
rescue => e
|
|
@@ -413,12 +415,45 @@ module RailsAiContext
|
|
|
413
415
|
false
|
|
414
416
|
end
|
|
415
417
|
|
|
418
|
+
API_CLIENT_MARKERS = {
|
|
419
|
+
"axios" => "Axios", "ky" => "Ky", "got" => "Got",
|
|
420
|
+
"@tanstack/react-query" => "TanStack Query", "swr" => "SWR",
|
|
421
|
+
"apollo-client" => "Apollo Client", "@apollo/client" => "Apollo Client",
|
|
422
|
+
"urql" => "URQL", "graphql-request" => "graphql-request",
|
|
423
|
+
"relay-runtime" => "Relay"
|
|
424
|
+
}.freeze
|
|
425
|
+
|
|
426
|
+
COMPONENT_LIB_MARKERS = {
|
|
427
|
+
"@mui/material" => "MUI", "@chakra-ui/react" => "Chakra UI",
|
|
428
|
+
"@radix-ui/react-dialog" => "Radix UI", "@headlessui/react" => "Headless UI",
|
|
429
|
+
"antd" => "Ant Design", "@mantine/core" => "Mantine",
|
|
430
|
+
"shadcn-ui" => "shadcn/ui", "@shadcn/ui" => "shadcn/ui",
|
|
431
|
+
"daisyui" => "DaisyUI", "flowbite" => "Flowbite",
|
|
432
|
+
"primereact" => "PrimeReact", "vuetify" => "Vuetify",
|
|
433
|
+
"element-plus" => "Element Plus", "naive-ui" => "Naive UI"
|
|
434
|
+
}.freeze
|
|
435
|
+
|
|
436
|
+
def detect_api_clients(all_deps)
|
|
437
|
+
API_CLIENT_MARKERS.filter_map { |pkg, label| label if all_deps.key?(pkg) }.uniq
|
|
438
|
+
rescue => e
|
|
439
|
+
$stderr.puts "[rails-ai-context] detect_api_clients failed: #{e.message}" if ENV["DEBUG"]
|
|
440
|
+
[]
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def detect_component_libraries(all_deps)
|
|
444
|
+
COMPONENT_LIB_MARKERS.filter_map { |pkg, label| label if all_deps.key?(pkg) }.uniq
|
|
445
|
+
rescue => e
|
|
446
|
+
$stderr.puts "[rails-ai-context] detect_component_libraries failed: #{e.message}" if ENV["DEBUG"]
|
|
447
|
+
[]
|
|
448
|
+
end
|
|
449
|
+
|
|
416
450
|
def package_json_has_script?(name)
|
|
417
451
|
path = File.join(root, "package.json")
|
|
418
452
|
return false unless File.exist?(path)
|
|
419
453
|
content = File.read(path, encoding: "bom|utf-8")
|
|
420
454
|
content.include?("\"#{name}\"")
|
|
421
|
-
rescue
|
|
455
|
+
rescue => e
|
|
456
|
+
$stderr.puts "[rails-ai-context] package_json_has_script? failed: #{e.message}" if ENV["DEBUG"]
|
|
422
457
|
false
|
|
423
458
|
end
|
|
424
459
|
end
|
|
@@ -161,12 +161,58 @@ module RailsAiContext
|
|
|
161
161
|
total_gems: specs.size,
|
|
162
162
|
ruby_version: specs["ruby"]&.first,
|
|
163
163
|
notable_gems: detect_notable_gems(specs),
|
|
164
|
-
categories: categorize_gems(specs)
|
|
164
|
+
categories: categorize_gems(specs),
|
|
165
|
+
local_gems: detect_local_gems,
|
|
166
|
+
gem_groups: detect_gem_groups
|
|
165
167
|
}
|
|
166
168
|
end
|
|
167
169
|
|
|
168
170
|
private
|
|
169
171
|
|
|
172
|
+
def detect_local_gems
|
|
173
|
+
gemfile = File.join(app.root, "Gemfile")
|
|
174
|
+
return [] unless File.exist?(gemfile)
|
|
175
|
+
|
|
176
|
+
content = File.read(gemfile)
|
|
177
|
+
local = []
|
|
178
|
+
content.each_line do |line|
|
|
179
|
+
next if line.strip.start_with?("#")
|
|
180
|
+
if (match = line.match(/gem\s+["'](\w[\w-]*)["'].*path:\s*["']([^"']+)["']/))
|
|
181
|
+
local << { name: match[1], source: "path", location: match[2] }
|
|
182
|
+
elsif (match = line.match(/gem\s+["'](\w[\w-]*)["'].*git:\s*["']([^"']+)["']/))
|
|
183
|
+
local << { name: match[1], source: "git", location: match[2] }
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
local
|
|
187
|
+
rescue => e
|
|
188
|
+
$stderr.puts "[rails-ai-context] detect_local_gems failed: #{e.message}" if ENV["DEBUG"]
|
|
189
|
+
[]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def detect_gem_groups
|
|
193
|
+
gemfile = File.join(app.root, "Gemfile")
|
|
194
|
+
return {} unless File.exist?(gemfile)
|
|
195
|
+
|
|
196
|
+
content = File.read(gemfile)
|
|
197
|
+
groups = {}
|
|
198
|
+
current_group = nil
|
|
199
|
+
content.each_line do |line|
|
|
200
|
+
stripped = line.strip
|
|
201
|
+
next if stripped.start_with?("#")
|
|
202
|
+
if (match = stripped.match(/\Agroup\s+(.+?)\s+do\b/))
|
|
203
|
+
current_group = match[1].scan(/:(\w+)/).flatten
|
|
204
|
+
elsif stripped == "end" && current_group
|
|
205
|
+
current_group = nil
|
|
206
|
+
elsif current_group && (match = stripped.match(/\Agem\s+["'](\w[\w-]*)["']/))
|
|
207
|
+
current_group.each { |g| (groups[g] ||= []) << match[1] }
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
groups
|
|
211
|
+
rescue => e
|
|
212
|
+
$stderr.puts "[rails-ai-context] detect_gem_groups failed: #{e.message}" if ENV["DEBUG"]
|
|
213
|
+
{}
|
|
214
|
+
end
|
|
215
|
+
|
|
170
216
|
def parse_lockfile(path)
|
|
171
217
|
gems = {}
|
|
172
218
|
in_gems = false
|
|
@@ -13,13 +13,16 @@ module RailsAiContext
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def call
|
|
16
|
-
{
|
|
16
|
+
result = {
|
|
17
17
|
default_locale: I18n.default_locale.to_s,
|
|
18
18
|
available_locales: I18n.available_locales.map(&:to_s).sort,
|
|
19
19
|
backend: I18n.backend.class.name,
|
|
20
20
|
locale_files: extract_locale_files,
|
|
21
|
-
total_locale_files: count_locale_files
|
|
21
|
+
total_locale_files: count_locale_files,
|
|
22
|
+
locale_coverage: detect_locale_coverage
|
|
22
23
|
}
|
|
24
|
+
result.merge!(detect_fallback_config)
|
|
25
|
+
result
|
|
23
26
|
rescue => e
|
|
24
27
|
{ error: e.message }
|
|
25
28
|
end
|
|
@@ -42,7 +45,8 @@ module RailsAiContext
|
|
|
42
45
|
begin
|
|
43
46
|
data = YAML.load_file(path, permitted_classes: [ Symbol ], aliases: true) || {}
|
|
44
47
|
info[:key_count] = count_keys(data)
|
|
45
|
-
rescue
|
|
48
|
+
rescue => e
|
|
49
|
+
$stderr.puts "[rails-ai-context] extract_locale_files failed: #{e.message}" if ENV["DEBUG"]
|
|
46
50
|
info[:parse_error] = true
|
|
47
51
|
end
|
|
48
52
|
end
|
|
@@ -61,6 +65,48 @@ module RailsAiContext
|
|
|
61
65
|
return 0 unless hash.is_a?(Hash)
|
|
62
66
|
hash.sum { |_, v| v.is_a?(Hash) ? count_keys(v, depth: depth + 1) : 1 }
|
|
63
67
|
end
|
|
68
|
+
|
|
69
|
+
def detect_fallback_config
|
|
70
|
+
config = {}
|
|
71
|
+
config[:fallbacks] = I18n.fallbacks.to_h.transform_values { |v| v.map(&:to_s) } if I18n.respond_to?(:fallbacks) && I18n.fallbacks
|
|
72
|
+
config
|
|
73
|
+
rescue => e
|
|
74
|
+
$stderr.puts "[rails-ai-context] detect_fallback_config failed: #{e.message}" if ENV["DEBUG"]
|
|
75
|
+
{}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def detect_locale_coverage
|
|
79
|
+
locales = I18n.available_locales
|
|
80
|
+
return {} if locales.size < 2
|
|
81
|
+
|
|
82
|
+
# Compare key counts between default and other locales
|
|
83
|
+
coverage = {}
|
|
84
|
+
default_count = count_keys_for_locale(I18n.default_locale)
|
|
85
|
+
locales.reject { |l| l == I18n.default_locale }.each do |locale|
|
|
86
|
+
locale_count = count_keys_for_locale(locale)
|
|
87
|
+
coverage[locale.to_s] = { keys: locale_count, coverage_pct: default_count > 0 ? ((locale_count.to_f / default_count) * 100).round(1) : 0 }
|
|
88
|
+
end
|
|
89
|
+
coverage
|
|
90
|
+
rescue => e
|
|
91
|
+
$stderr.puts "[rails-ai-context] detect_locale_coverage failed: #{e.message}" if ENV["DEBUG"]
|
|
92
|
+
{}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def count_keys_for_locale(locale)
|
|
96
|
+
path = Dir.glob(File.join(app.root, "config", "locales", "#{locale}.yml")).first
|
|
97
|
+
return 0 unless path && File.exist?(path)
|
|
98
|
+
data = YAML.safe_load(File.read(path), permitted_classes: [ Symbol ])
|
|
99
|
+
count_nested_keys(data)
|
|
100
|
+
rescue => e
|
|
101
|
+
$stderr.puts "[rails-ai-context] count_keys_for_locale failed: #{e.message}" if ENV["DEBUG"]
|
|
102
|
+
0
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def count_nested_keys(hash, count = 0)
|
|
106
|
+
return count unless hash.is_a?(Hash)
|
|
107
|
+
hash.each_value { |v| count = v.is_a?(Hash) ? count_nested_keys(v, count) : count + 1 }
|
|
108
|
+
count
|
|
109
|
+
end
|
|
64
110
|
end
|
|
65
111
|
end
|
|
66
112
|
end
|
|
@@ -20,7 +20,9 @@ module RailsAiContext
|
|
|
20
20
|
{
|
|
21
21
|
jobs: jobs,
|
|
22
22
|
mailers: extract_mailers,
|
|
23
|
-
channels: extract_channels
|
|
23
|
+
channels: extract_channels,
|
|
24
|
+
recurring_jobs: extract_solid_queue_recurring,
|
|
25
|
+
sidekiq_config: extract_sidekiq_config
|
|
24
26
|
}
|
|
25
27
|
end
|
|
26
28
|
|
|
@@ -42,7 +44,8 @@ module RailsAiContext
|
|
|
42
44
|
priority: job.priority
|
|
43
45
|
}.compact
|
|
44
46
|
end.sort_by { |j| j[:name] }
|
|
45
|
-
rescue
|
|
47
|
+
rescue => e
|
|
48
|
+
$stderr.puts "[rails-ai-context] extract_jobs failed: #{e.message}" if ENV["DEBUG"]
|
|
46
49
|
[]
|
|
47
50
|
end
|
|
48
51
|
|
|
@@ -73,17 +76,55 @@ module RailsAiContext
|
|
|
73
76
|
perform_match = source.match(/def\s+perform\s*\(([^)]*)\)/)
|
|
74
77
|
perform_signature = perform_match ? perform_match[1].strip : nil
|
|
75
78
|
|
|
79
|
+
callbacks = source.scan(/\b(before_enqueue|after_enqueue|before_perform|after_perform|around_perform|around_enqueue)\b/).flatten.uniq
|
|
80
|
+
|
|
76
81
|
job = { name: name }
|
|
77
82
|
job[:queue] = queue if queue
|
|
78
83
|
job[:retry_on] = retry_on if retry_on.any?
|
|
79
84
|
job[:discard_on] = discard_on if discard_on.any?
|
|
80
85
|
job[:perform_signature] = perform_signature if perform_signature
|
|
86
|
+
job[:callbacks] = callbacks if callbacks.any?
|
|
81
87
|
job
|
|
82
88
|
end.sort_by { |j| j[:name] }
|
|
83
|
-
rescue
|
|
89
|
+
rescue => e
|
|
90
|
+
$stderr.puts "[rails-ai-context] extract_jobs_from_source failed: #{e.message}" if ENV["DEBUG"]
|
|
91
|
+
[]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def extract_solid_queue_recurring
|
|
95
|
+
paths = [
|
|
96
|
+
File.join(app.root, "config", "recurring.yml"),
|
|
97
|
+
File.join(app.root, "config", "solid_queue.yml")
|
|
98
|
+
]
|
|
99
|
+
path = paths.find { |p| File.exist?(p) }
|
|
100
|
+
return [] unless path
|
|
101
|
+
|
|
102
|
+
content = File.read(path)
|
|
103
|
+
jobs = []
|
|
104
|
+
content.scan(/(\w+):\s*\n\s+class:\s*(\w+).*?(?:schedule:\s*["']?([^"'\n]+))?/m) do |name, klass, schedule|
|
|
105
|
+
jobs << { name: name, class: klass, schedule: schedule&.strip }.compact
|
|
106
|
+
end
|
|
107
|
+
jobs
|
|
108
|
+
rescue => e
|
|
109
|
+
$stderr.puts "[rails-ai-context] extract_solid_queue_recurring failed: #{e.message}" if ENV["DEBUG"]
|
|
84
110
|
[]
|
|
85
111
|
end
|
|
86
112
|
|
|
113
|
+
def extract_sidekiq_config
|
|
114
|
+
path = File.join(app.root, "config", "sidekiq.yml")
|
|
115
|
+
return nil unless File.exist?(path)
|
|
116
|
+
|
|
117
|
+
content = File.read(path)
|
|
118
|
+
config = {}
|
|
119
|
+
config[:concurrency] = $1.to_i if content.match(/concurrency:\s*(\d+)/)
|
|
120
|
+
queues = content.scan(/-\s*(?:\[?\s*)?(\w+)/).flatten.uniq
|
|
121
|
+
config[:queues] = queues if queues.any?
|
|
122
|
+
config.empty? ? nil : config
|
|
123
|
+
rescue => e
|
|
124
|
+
$stderr.puts "[rails-ai-context] extract_sidekiq_config failed: #{e.message}" if ENV["DEBUG"]
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
87
128
|
def extract_mailers
|
|
88
129
|
return [] unless defined?(ActionMailer::Base)
|
|
89
130
|
|
|
@@ -99,7 +140,8 @@ module RailsAiContext
|
|
|
99
140
|
delivery_method: mailer.delivery_method.to_s
|
|
100
141
|
}
|
|
101
142
|
end.sort_by { |m| m[:name] }
|
|
102
|
-
rescue
|
|
143
|
+
rescue => e
|
|
144
|
+
$stderr.puts "[rails-ai-context] extract_mailers failed: #{e.message}" if ENV["DEBUG"]
|
|
103
145
|
[]
|
|
104
146
|
end
|
|
105
147
|
|
|
@@ -116,7 +158,8 @@ module RailsAiContext
|
|
|
116
158
|
.map(&:to_s)
|
|
117
159
|
}
|
|
118
160
|
end.sort_by { |c| c[:name] }
|
|
119
|
-
rescue
|
|
161
|
+
rescue => e
|
|
162
|
+
$stderr.puts "[rails-ai-context] extract_channels failed: #{e.message}" if ENV["DEBUG"]
|
|
120
163
|
[]
|
|
121
164
|
end
|
|
122
165
|
end
|
|
@@ -17,7 +17,8 @@ module RailsAiContext
|
|
|
17
17
|
{
|
|
18
18
|
custom_middleware: custom,
|
|
19
19
|
middleware_stack: extract_middleware_stack,
|
|
20
|
-
middleware_count: middleware_count(custom)
|
|
20
|
+
middleware_count: middleware_count(custom),
|
|
21
|
+
middleware_from_initializers: detect_middleware_from_initializers
|
|
21
22
|
}
|
|
22
23
|
rescue => e
|
|
23
24
|
{ error: e.message }
|
|
@@ -65,7 +66,8 @@ module RailsAiContext
|
|
|
65
66
|
name = middleware.name || middleware.klass.to_s
|
|
66
67
|
{ name: name, category: categorize_middleware(name) }
|
|
67
68
|
end
|
|
68
|
-
rescue
|
|
69
|
+
rescue => e
|
|
70
|
+
$stderr.puts "[rails-ai-context] extract_middleware_stack failed: #{e.message}" if ENV["DEBUG"]
|
|
69
71
|
[]
|
|
70
72
|
end
|
|
71
73
|
|
|
@@ -74,10 +76,29 @@ module RailsAiContext
|
|
|
74
76
|
total: app.middleware.size,
|
|
75
77
|
custom: custom.size
|
|
76
78
|
}
|
|
77
|
-
rescue
|
|
79
|
+
rescue => e
|
|
80
|
+
$stderr.puts "[rails-ai-context] middleware_count failed: #{e.message}" if ENV["DEBUG"]
|
|
78
81
|
{}
|
|
79
82
|
end
|
|
80
83
|
|
|
84
|
+
def detect_middleware_from_initializers
|
|
85
|
+
init_dir = File.join(root, "config/initializers")
|
|
86
|
+
return [] unless Dir.exist?(init_dir)
|
|
87
|
+
|
|
88
|
+
additions = []
|
|
89
|
+
Dir.glob(File.join(init_dir, "*.rb")).each do |path|
|
|
90
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
91
|
+
content.scan(/config\.middleware\.(?:use|insert_before|insert_after|insert|unshift)\s+(\S+)/).each do |match|
|
|
92
|
+
name = match[0].gsub(/,.*/, "").strip
|
|
93
|
+
additions << { middleware: name, file: File.basename(path) } unless name.empty?
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
additions.uniq
|
|
97
|
+
rescue => e
|
|
98
|
+
$stderr.puts "[rails-ai-context] detect_middleware_from_initializers failed: #{e.message}" if ENV["DEBUG"]
|
|
99
|
+
[]
|
|
100
|
+
end
|
|
101
|
+
|
|
81
102
|
def categorize_middleware(name)
|
|
82
103
|
case name
|
|
83
104
|
when /ActionDispatch::SSL|ForceSSL/ then "security"
|
|
@@ -93,7 +93,8 @@ module RailsAiContext
|
|
|
93
93
|
content = File.read(schema_path)
|
|
94
94
|
match = content.match(/version:\s*([\d_]+)/)
|
|
95
95
|
match ? match[1].delete("_") : nil
|
|
96
|
-
rescue
|
|
96
|
+
rescue => e
|
|
97
|
+
$stderr.puts "[rails-ai-context] current_schema_version failed: #{e.message}" if ENV["DEBUG"]
|
|
97
98
|
nil
|
|
98
99
|
end
|
|
99
100
|
|
|
@@ -117,9 +118,11 @@ module RailsAiContext
|
|
|
117
118
|
%w[
|
|
118
119
|
create_table drop_table rename_table
|
|
119
120
|
add_column remove_column rename_column change_column
|
|
121
|
+
change_column_default change_column_null
|
|
120
122
|
add_index remove_index add_reference remove_reference
|
|
121
123
|
add_foreign_key remove_foreign_key
|
|
122
124
|
add_timestamps create_join_table enable_extension execute
|
|
125
|
+
add_check_constraint remove_check_constraint
|
|
123
126
|
].select { |action| content.include?(action) }
|
|
124
127
|
end
|
|
125
128
|
end
|
|
@@ -39,7 +39,8 @@ module RailsAiContext
|
|
|
39
39
|
else
|
|
40
40
|
Rails.application.eager_load!
|
|
41
41
|
end
|
|
42
|
-
rescue
|
|
42
|
+
rescue => e
|
|
43
|
+
$stderr.puts "[rails-ai-context] eager_load_models! failed: #{e.message}" if ENV["DEBUG"]
|
|
43
44
|
# In some environments (CI, Claude Code) eager_load may partially fail
|
|
44
45
|
nil
|
|
45
46
|
end
|
|
@@ -90,6 +91,14 @@ module RailsAiContext
|
|
|
90
91
|
instance_methods: extract_public_instance_methods(model)
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
# STI hierarchy detection
|
|
95
|
+
sti_info = extract_sti_info(model)
|
|
96
|
+
details[:sti] = sti_info if sti_info
|
|
97
|
+
|
|
98
|
+
# Enum prefix/suffix options
|
|
99
|
+
enum_options = extract_enum_options(model)
|
|
100
|
+
details[:enum_options] = enum_options if enum_options.any?
|
|
101
|
+
|
|
93
102
|
# Source-based macro extractions
|
|
94
103
|
macros = extract_source_macros(model)
|
|
95
104
|
details.merge!(macros)
|
|
@@ -101,6 +110,45 @@ module RailsAiContext
|
|
|
101
110
|
details.compact
|
|
102
111
|
end
|
|
103
112
|
|
|
113
|
+
def extract_sti_info(model)
|
|
114
|
+
has_type_column = if model.connected? && model.table_exists?
|
|
115
|
+
model.columns_hash.key?("type")
|
|
116
|
+
else
|
|
117
|
+
# Fallback: check schema.rb for type column
|
|
118
|
+
schema_path = File.join(app.root.to_s, "db", "schema.rb")
|
|
119
|
+
if File.exist?(schema_path)
|
|
120
|
+
schema = File.read(schema_path)
|
|
121
|
+
table_section = schema[/create_table\s+"#{Regexp.escape(model.table_name)}".*?end/m]
|
|
122
|
+
table_section&.match?(/t\.\w+\s+"type"/)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
return nil unless has_type_column
|
|
127
|
+
|
|
128
|
+
children = if model.respond_to?(:descendants)
|
|
129
|
+
model.descendants.map(&:name).compact.sort
|
|
130
|
+
elsif model.respond_to?(:subclasses)
|
|
131
|
+
model.subclasses.map(&:name).compact.sort
|
|
132
|
+
else
|
|
133
|
+
[]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
parent = model.superclass
|
|
137
|
+
sti_parent = if parent && parent != ActiveRecord::Base &&
|
|
138
|
+
(!defined?(ApplicationRecord) || parent != ApplicationRecord)
|
|
139
|
+
parent.name
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
{
|
|
143
|
+
sti_base: sti_parent.nil? && children.any?,
|
|
144
|
+
sti_parent: sti_parent,
|
|
145
|
+
sti_children: children.empty? ? nil : children
|
|
146
|
+
}.compact
|
|
147
|
+
rescue => e
|
|
148
|
+
$stderr.puts "[rails-ai-context] extract_sti_info failed: #{e.message}" if ENV["DEBUG"]
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
104
152
|
def extract_associations(model)
|
|
105
153
|
model.reflect_on_all_associations.map do |assoc|
|
|
106
154
|
detail = {
|
|
@@ -135,7 +183,8 @@ module RailsAiContext
|
|
|
135
183
|
source.scan(/^\s*scope\s+:(\w+)\s*,\s*->\s*(?:\([^)]*\)\s*)?\{([^}]*)\}/m).map do |name, body|
|
|
136
184
|
{ name: name, body: body.strip }
|
|
137
185
|
end
|
|
138
|
-
rescue
|
|
186
|
+
rescue => e
|
|
187
|
+
$stderr.puts "[rails-ai-context] extract_scopes failed: #{e.message}" if ENV["DEBUG"]
|
|
139
188
|
[]
|
|
140
189
|
end
|
|
141
190
|
|
|
@@ -146,7 +195,8 @@ module RailsAiContext
|
|
|
146
195
|
return [] unless source_path && File.exist?(source_path)
|
|
147
196
|
|
|
148
197
|
File.read(source_path).scan(/^\s*validate\s+:(\w+)/).flatten
|
|
149
|
-
rescue
|
|
198
|
+
rescue => e
|
|
199
|
+
$stderr.puts "[rails-ai-context] extract_custom_validates failed: #{e.message}" if ENV["DEBUG"]
|
|
150
200
|
[]
|
|
151
201
|
end
|
|
152
202
|
|
|
@@ -162,6 +212,35 @@ module RailsAiContext
|
|
|
162
212
|
model.defined_enums.transform_values { |mapping| mapping.dup }
|
|
163
213
|
end
|
|
164
214
|
|
|
215
|
+
def extract_enum_options(model)
|
|
216
|
+
source_path = model_source_path(model)
|
|
217
|
+
return {} unless source_path && File.exist?(source_path)
|
|
218
|
+
|
|
219
|
+
source = File.read(source_path)
|
|
220
|
+
options = {}
|
|
221
|
+
source.each_line do |line|
|
|
222
|
+
next unless line.match?(/\A\s*enum\s+/)
|
|
223
|
+
# Match both `enum :name, { ... }` and `enum name: { ... }` forms
|
|
224
|
+
name_match = line.match(/enum\s+:(\w+)/) || line.match(/enum\s+(\w+):/)
|
|
225
|
+
next unless name_match
|
|
226
|
+
enum_name = name_match[1]
|
|
227
|
+
entry = {}
|
|
228
|
+
entry[:prefix] = true if line.match?(/_?prefix:\s*true/)
|
|
229
|
+
entry[:suffix] = true if line.match?(/_?suffix:\s*true/)
|
|
230
|
+
if (prefix_val = line.match(/_?prefix:\s*:(\w+)/))
|
|
231
|
+
entry[:prefix] = prefix_val[1]
|
|
232
|
+
end
|
|
233
|
+
if (suffix_val = line.match(/_?suffix:\s*:(\w+)/))
|
|
234
|
+
entry[:suffix] = suffix_val[1]
|
|
235
|
+
end
|
|
236
|
+
options[enum_name] = entry if entry.any?
|
|
237
|
+
end
|
|
238
|
+
options
|
|
239
|
+
rescue => e
|
|
240
|
+
$stderr.puts "[rails-ai-context] extract_enum_options failed: #{e.message}" if ENV["DEBUG"]
|
|
241
|
+
{}
|
|
242
|
+
end
|
|
243
|
+
|
|
165
244
|
def extract_callbacks(model)
|
|
166
245
|
callback_types = %i[
|
|
167
246
|
before_validation after_validation
|
|
@@ -174,7 +253,7 @@ module RailsAiContext
|
|
|
174
253
|
|
|
175
254
|
result = callback_types.each_with_object({}) do |type, hash|
|
|
176
255
|
callbacks = model.send(:"_#{type}_callbacks").reject do |cb|
|
|
177
|
-
cb.filter.to_s.start_with?(*EXCLUDED_CALLBACKS) || cb.filter.is_a?(Proc)
|
|
256
|
+
cb.filter.nil? || cb.filter.to_s.start_with?(*EXCLUDED_CALLBACKS) || cb.filter.is_a?(Proc)
|
|
178
257
|
end
|
|
179
258
|
|
|
180
259
|
next if callbacks.empty?
|
|
@@ -185,7 +264,8 @@ module RailsAiContext
|
|
|
185
264
|
# If reflection returned nothing, fall back to source parsing
|
|
186
265
|
return result if result.any?
|
|
187
266
|
extract_callbacks_from_source(model)
|
|
188
|
-
rescue
|
|
267
|
+
rescue => e
|
|
268
|
+
$stderr.puts "[rails-ai-context] extract_callbacks failed: #{e.message}" if ENV["DEBUG"]
|
|
189
269
|
extract_callbacks_from_source(model)
|
|
190
270
|
end
|
|
191
271
|
|
|
@@ -200,11 +280,17 @@ module RailsAiContext
|
|
|
200
280
|
if (match = line.match(/\A\s*(before_validation|after_validation|before_save|after_save|before_create|after_create|before_update|after_update|before_destroy|after_destroy|after_commit|after_rollback)\s+:(\w+)/))
|
|
201
281
|
type = match[1]
|
|
202
282
|
method_name = match[2]
|
|
283
|
+
on_match = line.match(/on:\s*(?::(\w+)|\[([^\]]+)\])/)
|
|
284
|
+
if on_match && type.start_with?("after_commit")
|
|
285
|
+
events = on_match[1] ? [ on_match[1] ] : on_match[2].scan(/:(\w+)/).flatten
|
|
286
|
+
type = events.map { |e| "after_commit_on_#{e}" }.join(", ")
|
|
287
|
+
end
|
|
203
288
|
(callbacks[type] ||= []) << method_name
|
|
204
289
|
end
|
|
205
290
|
end
|
|
206
291
|
callbacks
|
|
207
|
-
rescue
|
|
292
|
+
rescue => e
|
|
293
|
+
$stderr.puts "[rails-ai-context] extract_callbacks_from_source failed: #{e.message}" if ENV["DEBUG"]
|
|
208
294
|
{}
|
|
209
295
|
end
|
|
210
296
|
|
|
@@ -276,7 +362,8 @@ module RailsAiContext
|
|
|
276
362
|
in_class_methods = false if in_class_methods && line.match?(/\A\s*end\s*$/) && !line.match?(/def/)
|
|
277
363
|
end
|
|
278
364
|
methods.uniq
|
|
279
|
-
rescue
|
|
365
|
+
rescue => e
|
|
366
|
+
$stderr.puts "[rails-ai-context] extract_source_class_methods failed: #{e.message}" if ENV["DEBUG"]
|
|
280
367
|
[]
|
|
281
368
|
end
|
|
282
369
|
|
|
@@ -326,13 +413,16 @@ module RailsAiContext
|
|
|
326
413
|
source.each_line do |line|
|
|
327
414
|
in_private = true if line.match?(/\A\s*private\s*$/)
|
|
328
415
|
next if in_private
|
|
416
|
+
# Also detect inline private/protected modifiers
|
|
417
|
+
next if line.match?(/\A\s*(?:private|protected)\s+def\s/)
|
|
329
418
|
next if line.match?(/\A\s*def self\./)
|
|
330
419
|
if (match = line.match(/\A\s*def (\w+[?!]?)/))
|
|
331
420
|
methods << match[1] unless match[1] == "initialize"
|
|
332
421
|
end
|
|
333
422
|
end
|
|
334
423
|
methods.uniq
|
|
335
|
-
rescue
|
|
424
|
+
rescue => e
|
|
425
|
+
$stderr.puts "[rails-ai-context] extract_source_instance_methods failed: #{e.message}" if ENV["DEBUG"]
|
|
336
426
|
[]
|
|
337
427
|
end
|
|
338
428
|
|
|
@@ -350,7 +440,8 @@ module RailsAiContext
|
|
|
350
440
|
])
|
|
351
441
|
end
|
|
352
442
|
methods
|
|
353
|
-
rescue
|
|
443
|
+
rescue => e
|
|
444
|
+
$stderr.puts "[rails-ai-context] generated_association_methods failed: #{e.message}" if ENV["DEBUG"]
|
|
354
445
|
[]
|
|
355
446
|
end
|
|
356
447
|
|
|
@@ -372,6 +463,10 @@ module RailsAiContext
|
|
|
372
463
|
macros[:serialize] = source.scan(/\bserialize\s+:(\w+)/).flatten if source.match?(/\bserialize\s+:/)
|
|
373
464
|
macros[:store] = source.scan(/\bstore(?:_accessor)?\s+:(\w+)/).flatten if source.match?(/\bstore(?:_accessor)?\s+:/)
|
|
374
465
|
|
|
466
|
+
# attribute API declarations (e.g. attribute :field, :type)
|
|
467
|
+
attributes = source.scan(/\battribute\s+:(\w+),\s*:(\w+)/).map { |name, type| { name: name, type: type } }
|
|
468
|
+
macros[:attributes] = attributes if attributes.any?
|
|
469
|
+
|
|
375
470
|
# Constants with value lists (e.g. STATUSES = %w[pending completed])
|
|
376
471
|
constants = source.scan(/\b([A-Z][A-Z_]+)\s*=\s*%[wi]\[([^\]]+)\]/).map do |name, values|
|
|
377
472
|
{ name: name, values: values.split }
|
|
@@ -390,7 +485,8 @@ module RailsAiContext
|
|
|
390
485
|
|
|
391
486
|
# Remove empty arrays
|
|
392
487
|
macros.reject { |_, v| v.is_a?(Array) && v.empty? }
|
|
393
|
-
rescue
|
|
488
|
+
rescue => e
|
|
489
|
+
$stderr.puts "[rails-ai-context] extract_source_macros failed: #{e.message}" if ENV["DEBUG"]
|
|
394
490
|
{}
|
|
395
491
|
end
|
|
396
492
|
|
|
@@ -443,7 +539,8 @@ module RailsAiContext
|
|
|
443
539
|
macros[:token_generation] = tokens if tokens.any?
|
|
444
540
|
|
|
445
541
|
macros
|
|
446
|
-
rescue
|
|
542
|
+
rescue => e
|
|
543
|
+
$stderr.puts "[rails-ai-context] extract_detailed_macros failed: #{e.message}" if ENV["DEBUG"]
|
|
447
544
|
{}
|
|
448
545
|
end
|
|
449
546
|
|