codebase_index 0.1.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 +7 -0
- data/CHANGELOG.md +29 -0
- data/CODE_OF_CONDUCT.md +83 -0
- data/CONTRIBUTING.md +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +481 -0
- data/exe/codebase-console-mcp +22 -0
- data/exe/codebase-index-mcp +61 -0
- data/exe/codebase-index-mcp-http +64 -0
- data/exe/codebase-index-mcp-start +58 -0
- data/lib/codebase_index/ast/call_site_extractor.rb +106 -0
- data/lib/codebase_index/ast/method_extractor.rb +76 -0
- data/lib/codebase_index/ast/node.rb +88 -0
- data/lib/codebase_index/ast/parser.rb +653 -0
- data/lib/codebase_index/ast.rb +6 -0
- data/lib/codebase_index/builder.rb +137 -0
- data/lib/codebase_index/chunking/chunk.rb +84 -0
- data/lib/codebase_index/chunking/semantic_chunker.rb +290 -0
- data/lib/codebase_index/console/adapters/cache_adapter.rb +58 -0
- data/lib/codebase_index/console/adapters/good_job_adapter.rb +66 -0
- data/lib/codebase_index/console/adapters/sidekiq_adapter.rb +66 -0
- data/lib/codebase_index/console/adapters/solid_queue_adapter.rb +66 -0
- data/lib/codebase_index/console/audit_logger.rb +75 -0
- data/lib/codebase_index/console/bridge.rb +170 -0
- data/lib/codebase_index/console/confirmation.rb +90 -0
- data/lib/codebase_index/console/connection_manager.rb +173 -0
- data/lib/codebase_index/console/console_response_renderer.rb +78 -0
- data/lib/codebase_index/console/model_validator.rb +81 -0
- data/lib/codebase_index/console/safe_context.rb +82 -0
- data/lib/codebase_index/console/server.rb +557 -0
- data/lib/codebase_index/console/sql_validator.rb +172 -0
- data/lib/codebase_index/console/tools/tier1.rb +118 -0
- data/lib/codebase_index/console/tools/tier2.rb +117 -0
- data/lib/codebase_index/console/tools/tier3.rb +110 -0
- data/lib/codebase_index/console/tools/tier4.rb +79 -0
- data/lib/codebase_index/coordination/pipeline_lock.rb +109 -0
- data/lib/codebase_index/cost_model/embedding_cost.rb +88 -0
- data/lib/codebase_index/cost_model/estimator.rb +128 -0
- data/lib/codebase_index/cost_model/provider_pricing.rb +67 -0
- data/lib/codebase_index/cost_model/storage_cost.rb +52 -0
- data/lib/codebase_index/cost_model.rb +22 -0
- data/lib/codebase_index/db/migrations/001_create_units.rb +38 -0
- data/lib/codebase_index/db/migrations/002_create_edges.rb +35 -0
- data/lib/codebase_index/db/migrations/003_create_embeddings.rb +37 -0
- data/lib/codebase_index/db/migrations/004_create_snapshots.rb +45 -0
- data/lib/codebase_index/db/migrations/005_create_snapshot_units.rb +40 -0
- data/lib/codebase_index/db/migrator.rb +71 -0
- data/lib/codebase_index/db/schema_version.rb +73 -0
- data/lib/codebase_index/dependency_graph.rb +227 -0
- data/lib/codebase_index/embedding/indexer.rb +130 -0
- data/lib/codebase_index/embedding/openai.rb +105 -0
- data/lib/codebase_index/embedding/provider.rb +135 -0
- data/lib/codebase_index/embedding/text_preparer.rb +112 -0
- data/lib/codebase_index/evaluation/baseline_runner.rb +115 -0
- data/lib/codebase_index/evaluation/evaluator.rb +146 -0
- data/lib/codebase_index/evaluation/metrics.rb +79 -0
- data/lib/codebase_index/evaluation/query_set.rb +148 -0
- data/lib/codebase_index/evaluation/report_generator.rb +90 -0
- data/lib/codebase_index/extracted_unit.rb +145 -0
- data/lib/codebase_index/extractor.rb +956 -0
- data/lib/codebase_index/extractors/action_cable_extractor.rb +228 -0
- data/lib/codebase_index/extractors/ast_source_extraction.rb +46 -0
- data/lib/codebase_index/extractors/behavioral_profile.rb +309 -0
- data/lib/codebase_index/extractors/caching_extractor.rb +261 -0
- data/lib/codebase_index/extractors/callback_analyzer.rb +232 -0
- data/lib/codebase_index/extractors/concern_extractor.rb +253 -0
- data/lib/codebase_index/extractors/configuration_extractor.rb +219 -0
- data/lib/codebase_index/extractors/controller_extractor.rb +494 -0
- data/lib/codebase_index/extractors/database_view_extractor.rb +278 -0
- data/lib/codebase_index/extractors/decorator_extractor.rb +260 -0
- data/lib/codebase_index/extractors/engine_extractor.rb +204 -0
- data/lib/codebase_index/extractors/event_extractor.rb +211 -0
- data/lib/codebase_index/extractors/factory_extractor.rb +289 -0
- data/lib/codebase_index/extractors/graphql_extractor.rb +917 -0
- data/lib/codebase_index/extractors/i18n_extractor.rb +117 -0
- data/lib/codebase_index/extractors/job_extractor.rb +369 -0
- data/lib/codebase_index/extractors/lib_extractor.rb +249 -0
- data/lib/codebase_index/extractors/mailer_extractor.rb +339 -0
- data/lib/codebase_index/extractors/manager_extractor.rb +202 -0
- data/lib/codebase_index/extractors/middleware_extractor.rb +133 -0
- data/lib/codebase_index/extractors/migration_extractor.rb +469 -0
- data/lib/codebase_index/extractors/model_extractor.rb +960 -0
- data/lib/codebase_index/extractors/phlex_extractor.rb +252 -0
- data/lib/codebase_index/extractors/policy_extractor.rb +214 -0
- data/lib/codebase_index/extractors/poro_extractor.rb +246 -0
- data/lib/codebase_index/extractors/pundit_extractor.rb +223 -0
- data/lib/codebase_index/extractors/rails_source_extractor.rb +473 -0
- data/lib/codebase_index/extractors/rake_task_extractor.rb +343 -0
- data/lib/codebase_index/extractors/route_extractor.rb +181 -0
- data/lib/codebase_index/extractors/scheduled_job_extractor.rb +331 -0
- data/lib/codebase_index/extractors/serializer_extractor.rb +334 -0
- data/lib/codebase_index/extractors/service_extractor.rb +254 -0
- data/lib/codebase_index/extractors/shared_dependency_scanner.rb +91 -0
- data/lib/codebase_index/extractors/shared_utility_methods.rb +99 -0
- data/lib/codebase_index/extractors/state_machine_extractor.rb +398 -0
- data/lib/codebase_index/extractors/test_mapping_extractor.rb +225 -0
- data/lib/codebase_index/extractors/validator_extractor.rb +225 -0
- data/lib/codebase_index/extractors/view_component_extractor.rb +310 -0
- data/lib/codebase_index/extractors/view_template_extractor.rb +261 -0
- data/lib/codebase_index/feedback/gap_detector.rb +89 -0
- data/lib/codebase_index/feedback/store.rb +119 -0
- data/lib/codebase_index/flow_analysis/operation_extractor.rb +209 -0
- data/lib/codebase_index/flow_analysis/response_code_mapper.rb +154 -0
- data/lib/codebase_index/flow_assembler.rb +290 -0
- data/lib/codebase_index/flow_document.rb +191 -0
- data/lib/codebase_index/flow_precomputer.rb +102 -0
- data/lib/codebase_index/formatting/base.rb +40 -0
- data/lib/codebase_index/formatting/claude_adapter.rb +98 -0
- data/lib/codebase_index/formatting/generic_adapter.rb +56 -0
- data/lib/codebase_index/formatting/gpt_adapter.rb +64 -0
- data/lib/codebase_index/formatting/human_adapter.rb +78 -0
- data/lib/codebase_index/graph_analyzer.rb +374 -0
- data/lib/codebase_index/mcp/index_reader.rb +394 -0
- data/lib/codebase_index/mcp/renderers/claude_renderer.rb +81 -0
- data/lib/codebase_index/mcp/renderers/json_renderer.rb +17 -0
- data/lib/codebase_index/mcp/renderers/markdown_renderer.rb +352 -0
- data/lib/codebase_index/mcp/renderers/plain_renderer.rb +240 -0
- data/lib/codebase_index/mcp/server.rb +935 -0
- data/lib/codebase_index/mcp/tool_response_renderer.rb +62 -0
- data/lib/codebase_index/model_name_cache.rb +51 -0
- data/lib/codebase_index/notion/client.rb +217 -0
- data/lib/codebase_index/notion/exporter.rb +219 -0
- data/lib/codebase_index/notion/mapper.rb +39 -0
- data/lib/codebase_index/notion/mappers/column_mapper.rb +65 -0
- data/lib/codebase_index/notion/mappers/migration_mapper.rb +39 -0
- data/lib/codebase_index/notion/mappers/model_mapper.rb +164 -0
- data/lib/codebase_index/notion/rate_limiter.rb +68 -0
- data/lib/codebase_index/observability/health_check.rb +81 -0
- data/lib/codebase_index/observability/instrumentation.rb +34 -0
- data/lib/codebase_index/observability/structured_logger.rb +75 -0
- data/lib/codebase_index/operator/error_escalator.rb +81 -0
- data/lib/codebase_index/operator/pipeline_guard.rb +99 -0
- data/lib/codebase_index/operator/status_reporter.rb +80 -0
- data/lib/codebase_index/railtie.rb +26 -0
- data/lib/codebase_index/resilience/circuit_breaker.rb +99 -0
- data/lib/codebase_index/resilience/index_validator.rb +185 -0
- data/lib/codebase_index/resilience/retryable_provider.rb +108 -0
- data/lib/codebase_index/retrieval/context_assembler.rb +249 -0
- data/lib/codebase_index/retrieval/query_classifier.rb +131 -0
- data/lib/codebase_index/retrieval/ranker.rb +273 -0
- data/lib/codebase_index/retrieval/search_executor.rb +327 -0
- data/lib/codebase_index/retriever.rb +160 -0
- data/lib/codebase_index/ruby_analyzer/class_analyzer.rb +190 -0
- data/lib/codebase_index/ruby_analyzer/dataflow_analyzer.rb +78 -0
- data/lib/codebase_index/ruby_analyzer/fqn_builder.rb +18 -0
- data/lib/codebase_index/ruby_analyzer/mermaid_renderer.rb +275 -0
- data/lib/codebase_index/ruby_analyzer/method_analyzer.rb +143 -0
- data/lib/codebase_index/ruby_analyzer/trace_enricher.rb +139 -0
- data/lib/codebase_index/ruby_analyzer.rb +87 -0
- data/lib/codebase_index/session_tracer/file_store.rb +111 -0
- data/lib/codebase_index/session_tracer/middleware.rb +143 -0
- data/lib/codebase_index/session_tracer/redis_store.rb +112 -0
- data/lib/codebase_index/session_tracer/session_flow_assembler.rb +263 -0
- data/lib/codebase_index/session_tracer/session_flow_document.rb +223 -0
- data/lib/codebase_index/session_tracer/solid_cache_store.rb +145 -0
- data/lib/codebase_index/session_tracer/store.rb +67 -0
- data/lib/codebase_index/storage/graph_store.rb +120 -0
- data/lib/codebase_index/storage/metadata_store.rb +169 -0
- data/lib/codebase_index/storage/pgvector.rb +163 -0
- data/lib/codebase_index/storage/qdrant.rb +172 -0
- data/lib/codebase_index/storage/vector_store.rb +156 -0
- data/lib/codebase_index/temporal/snapshot_store.rb +341 -0
- data/lib/codebase_index/version.rb +5 -0
- data/lib/codebase_index.rb +223 -0
- data/lib/generators/codebase_index/install_generator.rb +32 -0
- data/lib/generators/codebase_index/pgvector_generator.rb +37 -0
- data/lib/generators/codebase_index/templates/add_pgvector_to_codebase_index.rb.erb +15 -0
- data/lib/generators/codebase_index/templates/create_codebase_index_tables.rb.erb +43 -0
- data/lib/tasks/codebase_index.rake +583 -0
- data/lib/tasks/codebase_index_evaluation.rake +115 -0
- metadata +252 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require_relative 'ast_source_extraction'
|
|
5
|
+
require_relative 'shared_utility_methods'
|
|
6
|
+
require_relative 'shared_dependency_scanner'
|
|
7
|
+
|
|
8
|
+
module CodebaseIndex
|
|
9
|
+
module Extractors
|
|
10
|
+
# ControllerExtractor handles ActionController extraction with:
|
|
11
|
+
# - Route mapping (which HTTP endpoints hit which actions)
|
|
12
|
+
# - Before/after action filter chain resolution
|
|
13
|
+
# - Per-action chunking for precise retrieval
|
|
14
|
+
# - Concern inlining
|
|
15
|
+
#
|
|
16
|
+
# Controllers are chunked more aggressively than models because
|
|
17
|
+
# queries are often action-specific ("how does the create action work").
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# extractor = ControllerExtractor.new
|
|
21
|
+
# units = extractor.extract_all
|
|
22
|
+
# registrations = units.find { |u| u.identifier == "Users::RegistrationsController" }
|
|
23
|
+
#
|
|
24
|
+
class ControllerExtractor
|
|
25
|
+
include AstSourceExtraction
|
|
26
|
+
include SharedUtilityMethods
|
|
27
|
+
include SharedDependencyScanner
|
|
28
|
+
|
|
29
|
+
def initialize
|
|
30
|
+
@routes_map = build_routes_map
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Extract all controllers in the application
|
|
34
|
+
#
|
|
35
|
+
# @return [Array<ExtractedUnit>] List of controller units
|
|
36
|
+
def extract_all
|
|
37
|
+
controllers = ApplicationController.descendants
|
|
38
|
+
|
|
39
|
+
controllers = (controllers + ActionController::API.descendants).uniq if defined?(ActionController::API)
|
|
40
|
+
|
|
41
|
+
controllers.map do |controller|
|
|
42
|
+
extract_controller(controller)
|
|
43
|
+
end.compact
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Extract a single controller
|
|
47
|
+
#
|
|
48
|
+
# @param controller [Class] The controller class
|
|
49
|
+
# @return [ExtractedUnit] The extracted unit
|
|
50
|
+
def extract_controller(controller)
|
|
51
|
+
unit = ExtractedUnit.new(
|
|
52
|
+
type: :controller,
|
|
53
|
+
identifier: controller.name,
|
|
54
|
+
file_path: source_file_for(controller)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
source_path = unit.file_path
|
|
58
|
+
source = source_path && File.exist?(source_path) ? File.read(source_path) : ''
|
|
59
|
+
|
|
60
|
+
unit.namespace = extract_namespace(controller)
|
|
61
|
+
unit.source_code = build_composite_source(controller, source)
|
|
62
|
+
unit.metadata = extract_metadata(controller, source)
|
|
63
|
+
unit.dependencies = extract_dependencies(controller, source)
|
|
64
|
+
|
|
65
|
+
# Controllers benefit from per-action chunks
|
|
66
|
+
unit.chunks = build_action_chunks(controller, unit)
|
|
67
|
+
|
|
68
|
+
unit
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
Rails.logger.error("[CodebaseIndex] Failed to extract controller #{controller.name}: #{e.class}: #{e.message}")
|
|
71
|
+
Rails.logger.error("[CodebaseIndex] #{e.backtrace&.first(5)&.join("\n ")}")
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
78
|
+
# Route Mapping
|
|
79
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
# Build a map of controller -> action -> route info from Rails routes
|
|
82
|
+
def build_routes_map
|
|
83
|
+
routes = {}
|
|
84
|
+
|
|
85
|
+
Rails.application.routes.routes.each do |route|
|
|
86
|
+
next unless route.defaults[:controller]
|
|
87
|
+
|
|
88
|
+
controller = "#{route.defaults[:controller].camelize}Controller"
|
|
89
|
+
action = route.defaults[:action]
|
|
90
|
+
|
|
91
|
+
routes[controller] ||= {}
|
|
92
|
+
routes[controller][action] ||= []
|
|
93
|
+
routes[controller][action] << {
|
|
94
|
+
verb: extract_verb(route),
|
|
95
|
+
path: route.path.spec.to_s.gsub('(.:format)', ''),
|
|
96
|
+
name: route.name,
|
|
97
|
+
constraints: route.constraints.except(:request_method)
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
routes
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def extract_verb(route)
|
|
105
|
+
verb = route.verb
|
|
106
|
+
return verb if verb.is_a?(String)
|
|
107
|
+
return verb.source.gsub(/[\^$]/, '') if verb.respond_to?(:source)
|
|
108
|
+
|
|
109
|
+
verb.to_s
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
113
|
+
# Source Building
|
|
114
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
# Find the source file for a controller, validating paths are within Rails.root.
|
|
117
|
+
#
|
|
118
|
+
# Uses a multi-tier strategy to avoid returning gem/vendor paths that appear
|
|
119
|
+
# when controllers include modules from gems (e.g., decent_exposure, appsignal).
|
|
120
|
+
#
|
|
121
|
+
# @param controller [Class] The controller class
|
|
122
|
+
# @return [String] Absolute path to the controller source file
|
|
123
|
+
def source_file_for(controller)
|
|
124
|
+
app_root = Rails.root.to_s
|
|
125
|
+
convention_path = Rails.root.join("app/controllers/#{controller.name.underscore}.rb").to_s
|
|
126
|
+
|
|
127
|
+
# Tier 1: Instance methods defined directly on this controller
|
|
128
|
+
controller.instance_methods(false).each do |method_name|
|
|
129
|
+
loc = controller.instance_method(method_name).source_location&.first
|
|
130
|
+
return loc if loc&.start_with?(app_root)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Tier 2: Class/singleton methods defined on this controller
|
|
134
|
+
controller.methods(false).each do |method_name|
|
|
135
|
+
loc = controller.method(method_name).source_location&.first
|
|
136
|
+
return loc if loc&.start_with?(app_root)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Tier 3: Convention path if file exists
|
|
140
|
+
return convention_path if File.exist?(convention_path)
|
|
141
|
+
|
|
142
|
+
# Tier 4: const_source_location (Ruby 3.0+)
|
|
143
|
+
if Object.respond_to?(:const_source_location)
|
|
144
|
+
loc = Object.const_source_location(controller.name)&.first
|
|
145
|
+
return loc if loc&.start_with?(app_root)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Tier 5: Always return convention path — never a gem path
|
|
149
|
+
convention_path
|
|
150
|
+
rescue StandardError
|
|
151
|
+
Rails.root.join("app/controllers/#{controller.name.underscore}.rb").to_s
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Build composite source with routes and filters as headers
|
|
155
|
+
def build_composite_source(controller, source = nil)
|
|
156
|
+
if source.nil?
|
|
157
|
+
source_path = source_file_for(controller)
|
|
158
|
+
return '' unless source_path && File.exist?(source_path)
|
|
159
|
+
|
|
160
|
+
source = File.read(source_path)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Prepend route information
|
|
164
|
+
routes_comment = build_routes_comment(controller)
|
|
165
|
+
|
|
166
|
+
# Prepend before_action chain
|
|
167
|
+
filters_comment = build_filters_comment(controller)
|
|
168
|
+
|
|
169
|
+
"#{routes_comment}\n#{filters_comment}\n#{source}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def build_routes_comment(controller)
|
|
173
|
+
routes = @routes_map[controller.name] || {}
|
|
174
|
+
return '' if routes.empty?
|
|
175
|
+
|
|
176
|
+
lines = routes.flat_map do |action, route_list|
|
|
177
|
+
route_list.map do |info|
|
|
178
|
+
verb = info[:verb].to_s.ljust(7)
|
|
179
|
+
path = info[:path].ljust(45)
|
|
180
|
+
" #{verb} #{path} → ##{action}"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
<<~ROUTES
|
|
185
|
+
# ╔═══════════════════════════════════════════════════════════════════════╗
|
|
186
|
+
# ║ Routes ║
|
|
187
|
+
# ╚═══════════════════════════════════════════════════════════════════════╝
|
|
188
|
+
#
|
|
189
|
+
#{lines.map { |l| "# #{l}" }.join("\n")}
|
|
190
|
+
#
|
|
191
|
+
ROUTES
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def build_filters_comment(controller)
|
|
195
|
+
filters = extract_filter_chain(controller)
|
|
196
|
+
return '' if filters.empty?
|
|
197
|
+
|
|
198
|
+
lines = filters.map do |f|
|
|
199
|
+
opts = []
|
|
200
|
+
opts << "only: [#{f[:only].map { |a| ":#{a}" }.join(', ')}]" if f[:only]&.any?
|
|
201
|
+
opts << "except: [#{f[:except].map { |a| ":#{a}" }.join(', ')}]" if f[:except]&.any?
|
|
202
|
+
opts << "if: #{f[:if]}" if f[:if]
|
|
203
|
+
|
|
204
|
+
opts_str = opts.any? ? " (#{opts.join('; ')})" : ''
|
|
205
|
+
" #{f[:kind].to_s.ljust(8)} :#{f[:filter]}#{opts_str}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
<<~FILTERS
|
|
209
|
+
# ╔═══════════════════════════════════════════════════════════════════════╗
|
|
210
|
+
# ║ Filter Chain ║
|
|
211
|
+
# ╚═══════════════════════════════════════════════════════════════════════╝
|
|
212
|
+
#
|
|
213
|
+
#{lines.map { |l| "# #{l}" }.join("\n")}
|
|
214
|
+
#
|
|
215
|
+
FILTERS
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def extract_filter_chain(controller)
|
|
219
|
+
controller._process_action_callbacks.map do |callback|
|
|
220
|
+
only, except, if_conds, unless_conds = extract_callback_conditions(callback)
|
|
221
|
+
|
|
222
|
+
result = { kind: callback.kind, filter: callback.filter }
|
|
223
|
+
result[:only] = only if only.any?
|
|
224
|
+
result[:except] = except if except.any?
|
|
225
|
+
result[:if] = if_conds.join(', ') if if_conds.any?
|
|
226
|
+
result[:unless] = unless_conds.join(', ') if unless_conds.any?
|
|
227
|
+
result
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Extract :only/:except action lists and :if/:unless conditions from a callback.
|
|
232
|
+
#
|
|
233
|
+
# Modern Rails (4.2+) stores conditions in @if/@unless ivar arrays.
|
|
234
|
+
# ActionFilter objects hold action Sets; other conditions are procs/symbols.
|
|
235
|
+
#
|
|
236
|
+
# @param callback [ActiveSupport::Callbacks::Callback]
|
|
237
|
+
# @return [Array(Array<String>, Array<String>, Array<String>, Array<String>)]
|
|
238
|
+
# [only_actions, except_actions, if_labels, unless_labels]
|
|
239
|
+
def extract_callback_conditions(callback)
|
|
240
|
+
if_conditions = callback.instance_variable_get(:@if) || []
|
|
241
|
+
unless_conditions = callback.instance_variable_get(:@unless) || []
|
|
242
|
+
|
|
243
|
+
only = []
|
|
244
|
+
except = []
|
|
245
|
+
if_labels = []
|
|
246
|
+
unless_labels = []
|
|
247
|
+
|
|
248
|
+
if_conditions.each do |cond|
|
|
249
|
+
actions = extract_action_filter_actions(cond)
|
|
250
|
+
if actions
|
|
251
|
+
only.concat(actions)
|
|
252
|
+
else
|
|
253
|
+
if_labels << condition_label(cond)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
unless_conditions.each do |cond|
|
|
258
|
+
actions = extract_action_filter_actions(cond)
|
|
259
|
+
if actions
|
|
260
|
+
except.concat(actions)
|
|
261
|
+
else
|
|
262
|
+
unless_labels << condition_label(cond)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
[only, except, if_labels, unless_labels]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Extract action names from an ActionFilter-like condition object.
|
|
270
|
+
# Duck-types on the @actions ivar being a Set, avoiding dependence
|
|
271
|
+
# on private class names across Rails versions.
|
|
272
|
+
#
|
|
273
|
+
# @param condition [Object] A condition from the callback's @if/@unless array
|
|
274
|
+
# @return [Array<String>, nil] Action names, or nil if not an ActionFilter
|
|
275
|
+
def extract_action_filter_actions(condition)
|
|
276
|
+
return nil unless condition.instance_variable_defined?(:@actions)
|
|
277
|
+
|
|
278
|
+
actions = condition.instance_variable_get(:@actions)
|
|
279
|
+
return nil unless actions.is_a?(Set)
|
|
280
|
+
|
|
281
|
+
actions.to_a
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Human-readable label for a non-ActionFilter condition.
|
|
285
|
+
#
|
|
286
|
+
# @param condition [Object] A proc, symbol, or other condition
|
|
287
|
+
# @return [String]
|
|
288
|
+
def condition_label(condition)
|
|
289
|
+
case condition
|
|
290
|
+
when Symbol then ":#{condition}"
|
|
291
|
+
when Proc then 'Proc'
|
|
292
|
+
when String then condition
|
|
293
|
+
else condition.class.name
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
298
|
+
# Metadata Extraction
|
|
299
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
# Extract comprehensive metadata
|
|
302
|
+
def extract_metadata(controller, source = nil)
|
|
303
|
+
own_methods = controller.instance_methods(false).to_set(&:to_s)
|
|
304
|
+
actions = controller.action_methods.select { |m| own_methods.include?(m) }.to_a
|
|
305
|
+
|
|
306
|
+
{
|
|
307
|
+
# Actions and routes
|
|
308
|
+
actions: actions,
|
|
309
|
+
routes: @routes_map[controller.name] || {},
|
|
310
|
+
|
|
311
|
+
# Filter chain
|
|
312
|
+
filters: extract_filter_chain(controller),
|
|
313
|
+
|
|
314
|
+
# Parent chain for understanding inherited behavior
|
|
315
|
+
ancestors: controller.ancestors
|
|
316
|
+
.take_while { |a| a != ActionController::Base && a != ActionController::API }
|
|
317
|
+
.grep(Class)
|
|
318
|
+
.map(&:name)
|
|
319
|
+
.compact,
|
|
320
|
+
|
|
321
|
+
# Concerns included
|
|
322
|
+
included_concerns: extract_included_concerns(controller),
|
|
323
|
+
|
|
324
|
+
# Response formats
|
|
325
|
+
responds_to: extract_respond_formats(controller, source),
|
|
326
|
+
|
|
327
|
+
# Metrics
|
|
328
|
+
action_count: actions.size,
|
|
329
|
+
filter_count: controller._process_action_callbacks.count,
|
|
330
|
+
|
|
331
|
+
# Strong parameters if definable
|
|
332
|
+
permitted_params: extract_permitted_params(controller, source)
|
|
333
|
+
}
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def extract_included_concerns(controller)
|
|
337
|
+
controller.included_modules
|
|
338
|
+
.select { |m| m.name&.include?('Concern') || m.name&.include?('Concerns') }
|
|
339
|
+
.map(&:name)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def extract_respond_formats(controller, source = nil)
|
|
343
|
+
if source.nil?
|
|
344
|
+
source_path = source_file_for(controller)
|
|
345
|
+
return [] unless source_path && File.exist?(source_path)
|
|
346
|
+
|
|
347
|
+
source = File.read(source_path)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
formats = []
|
|
351
|
+
|
|
352
|
+
formats << :html if source.include?('respond_to do') || !source.include?('respond_to')
|
|
353
|
+
formats << :json if source.include?(':json') || source.include?('render json:')
|
|
354
|
+
formats << :xml if source.include?(':xml') || source.include?('render xml:')
|
|
355
|
+
formats << :turbo_stream if source.include?('turbo_stream')
|
|
356
|
+
|
|
357
|
+
formats.uniq
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def extract_permitted_params(controller, source = nil)
|
|
361
|
+
if source.nil?
|
|
362
|
+
source_path = source_file_for(controller)
|
|
363
|
+
return {} unless source_path && File.exist?(source_path)
|
|
364
|
+
|
|
365
|
+
source = File.read(source_path)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
params = {}
|
|
369
|
+
|
|
370
|
+
# Match params.require(:x).permit(...) patterns
|
|
371
|
+
source.scan(/def\s+(\w+_params).*?params\.require\(:(\w+)\)\.permit\((.*?)\)/m) do |method, model, permitted|
|
|
372
|
+
params[method] = {
|
|
373
|
+
model: model,
|
|
374
|
+
permitted: permitted.scan(/:(\w+)/).flatten
|
|
375
|
+
}
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
params
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
382
|
+
# Dependency Extraction
|
|
383
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
def extract_dependencies(controller, source = nil)
|
|
386
|
+
deps = []
|
|
387
|
+
|
|
388
|
+
if source.nil?
|
|
389
|
+
source_path = source_file_for(controller)
|
|
390
|
+
source = File.read(source_path) if source_path && File.exist?(source_path)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
if source
|
|
394
|
+
deps.concat(scan_common_dependencies(source))
|
|
395
|
+
|
|
396
|
+
# Phlex component references
|
|
397
|
+
source.scan(/render\s+(\w+(?:::\w+)*Component)/).flatten.uniq.each do |component|
|
|
398
|
+
deps << { type: :component, target: component, via: :render }
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Other view renders
|
|
402
|
+
source.scan(%r{render\s+["'](\w+/\w+)["']}).flatten.uniq.each do |template|
|
|
403
|
+
deps << { type: :view, target: template, via: :render }
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
deps.uniq { |d| [d[:type], d[:target]] }
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
411
|
+
# Per-Action Chunking
|
|
412
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
# Build per-action chunks for precise retrieval
|
|
415
|
+
def build_action_chunks(controller, unit)
|
|
416
|
+
controller.action_methods.filter_map do |action|
|
|
417
|
+
route_info = @routes_map.dig(controller.name, action.to_s)
|
|
418
|
+
filters = applicable_filters(controller, action)
|
|
419
|
+
|
|
420
|
+
# Extract just this action's source
|
|
421
|
+
action_source = extract_action_source(controller, action)
|
|
422
|
+
next if action_source.nil? || action_source.strip.empty?
|
|
423
|
+
|
|
424
|
+
route_desc = if route_info&.any?
|
|
425
|
+
route_info.map { |r| "#{r[:verb]} #{r[:path]}" }.join(', ')
|
|
426
|
+
else
|
|
427
|
+
'No direct route'
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
chunk_content = <<~ACTION
|
|
431
|
+
# Controller: #{controller.name}
|
|
432
|
+
# Action: #{action}
|
|
433
|
+
# Route: #{route_desc}
|
|
434
|
+
# Filters: #{filters.map { |f| "#{f[:kind]}(:#{f[:filter]})" }.join(', ').presence || 'none'}
|
|
435
|
+
|
|
436
|
+
#{action_source}
|
|
437
|
+
ACTION
|
|
438
|
+
|
|
439
|
+
{
|
|
440
|
+
chunk_type: :action,
|
|
441
|
+
identifier: "#{controller.name}##{action}",
|
|
442
|
+
content: chunk_content,
|
|
443
|
+
content_hash: Digest::SHA256.hexdigest(chunk_content),
|
|
444
|
+
metadata: {
|
|
445
|
+
parent: unit.identifier,
|
|
446
|
+
action: action.to_s,
|
|
447
|
+
route: route_info,
|
|
448
|
+
filters: filters,
|
|
449
|
+
http_methods: route_info&.map { |r| r[:verb] }&.uniq || []
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def applicable_filters(controller, action)
|
|
456
|
+
action_name = action.to_s
|
|
457
|
+
|
|
458
|
+
applicable = controller._process_action_callbacks.select do |cb|
|
|
459
|
+
callback_applies_to_action?(cb, action_name)
|
|
460
|
+
end
|
|
461
|
+
applicable.map { |cb| { kind: cb.kind, filter: cb.filter } }
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# Determine if a callback applies to a given action name.
|
|
465
|
+
#
|
|
466
|
+
# Checks ActionFilter objects in @if (only) and @unless (except).
|
|
467
|
+
# Non-ActionFilter conditions (procs, symbols) are assumed true.
|
|
468
|
+
#
|
|
469
|
+
# @param callback [ActiveSupport::Callbacks::Callback]
|
|
470
|
+
# @param action_name [String]
|
|
471
|
+
# @return [Boolean]
|
|
472
|
+
def callback_applies_to_action?(callback, action_name)
|
|
473
|
+
if_conditions = callback.instance_variable_get(:@if) || []
|
|
474
|
+
unless_conditions = callback.instance_variable_get(:@unless) || []
|
|
475
|
+
|
|
476
|
+
# Check @if conditions — all must pass for the callback to apply
|
|
477
|
+
if_conditions.each do |cond|
|
|
478
|
+
actions = extract_action_filter_actions(cond)
|
|
479
|
+
next unless actions # skip non-ActionFilter conditions (assume true)
|
|
480
|
+
return false unless actions.include?(action_name)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Check @unless conditions — if any match, callback doesn't apply
|
|
484
|
+
unless_conditions.each do |cond|
|
|
485
|
+
actions = extract_action_filter_actions(cond)
|
|
486
|
+
next unless actions
|
|
487
|
+
return false if actions.include?(action_name)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
true
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|