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,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'shared_utility_methods'
|
|
4
|
+
require_relative 'shared_dependency_scanner'
|
|
5
|
+
|
|
6
|
+
module CodebaseIndex
|
|
7
|
+
module Extractors
|
|
8
|
+
# ActionCableExtractor handles ActionCable channel extraction via runtime introspection.
|
|
9
|
+
#
|
|
10
|
+
# Reads `ActionCable::Channel::Base.descendants` to discover channels, then inspects
|
|
11
|
+
# each channel's stream subscriptions, actions, broadcast patterns, and source code.
|
|
12
|
+
# Each channel becomes one ExtractedUnit with metadata about streams, actions, and
|
|
13
|
+
# broadcast patterns.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# extractor = ActionCableExtractor.new
|
|
17
|
+
# units = extractor.extract_all
|
|
18
|
+
# chat = units.find { |u| u.identifier == "ChatChannel" }
|
|
19
|
+
# chat.metadata[:stream_names] #=> ["chat_room_#{params[:room_id]}"]
|
|
20
|
+
# chat.metadata[:actions] #=> ["speak", "typing"]
|
|
21
|
+
#
|
|
22
|
+
class ActionCableExtractor
|
|
23
|
+
include SharedUtilityMethods
|
|
24
|
+
include SharedDependencyScanner
|
|
25
|
+
|
|
26
|
+
# Lifecycle methods that are not user-defined actions
|
|
27
|
+
LIFECYCLE_METHODS = %i[subscribed unsubscribed].freeze
|
|
28
|
+
|
|
29
|
+
def initialize
|
|
30
|
+
# No directories to scan — this is runtime introspection
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Extract all ActionCable channels as ExtractedUnits.
|
|
34
|
+
#
|
|
35
|
+
# @return [Array<ExtractedUnit>] List of channel units
|
|
36
|
+
def extract_all
|
|
37
|
+
return [] unless action_cable_available?
|
|
38
|
+
|
|
39
|
+
channels = channel_descendants
|
|
40
|
+
return [] if channels.empty?
|
|
41
|
+
|
|
42
|
+
channels.filter_map { |klass| extract_channel(klass) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Extract a single channel class into an ExtractedUnit.
|
|
46
|
+
#
|
|
47
|
+
# Public for incremental re-extraction via CLASS_BASED dispatch.
|
|
48
|
+
#
|
|
49
|
+
# @param klass [Class] A channel subclass
|
|
50
|
+
# @return [ExtractedUnit, nil]
|
|
51
|
+
def extract_channel(klass)
|
|
52
|
+
name = klass.name
|
|
53
|
+
file_path = discover_source_path(klass, name)
|
|
54
|
+
source = read_source(file_path)
|
|
55
|
+
own_methods = klass.instance_methods(false)
|
|
56
|
+
|
|
57
|
+
unit = ExtractedUnit.new(
|
|
58
|
+
type: :action_cable_channel,
|
|
59
|
+
identifier: name,
|
|
60
|
+
file_path: file_path
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
unit.namespace = extract_namespace(name)
|
|
64
|
+
unit.source_code = source
|
|
65
|
+
unit.metadata = build_metadata(source, own_methods)
|
|
66
|
+
unit.dependencies = source.empty? ? [] : scan_common_dependencies(source)
|
|
67
|
+
|
|
68
|
+
unit
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
log_extraction_error(name, e)
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Check if ActionCable::Channel::Base is defined.
|
|
77
|
+
#
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
def action_cable_available?
|
|
80
|
+
defined?(ActionCable::Channel::Base)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Retrieve channel descendants, filtering out abstract bases and anonymous classes.
|
|
84
|
+
#
|
|
85
|
+
# @return [Array<Class>]
|
|
86
|
+
def channel_descendants
|
|
87
|
+
ActionCable::Channel::Base.descendants.reject do |klass|
|
|
88
|
+
klass.name.nil? || klass.name == 'ApplicationCable::Channel'
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Discover the source file path for a channel class.
|
|
93
|
+
#
|
|
94
|
+
# Tries source_location on instance methods, then falls back to
|
|
95
|
+
# the Rails convention path.
|
|
96
|
+
#
|
|
97
|
+
# @param klass [Class] The channel class
|
|
98
|
+
# @param name [String] The channel class name
|
|
99
|
+
# @return [String, nil]
|
|
100
|
+
def discover_source_path(klass, name)
|
|
101
|
+
path = source_location_from_methods(klass)
|
|
102
|
+
return path if path
|
|
103
|
+
|
|
104
|
+
convention_fallback(name)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Try to get source_location from the channel's instance methods.
|
|
108
|
+
# Tries subscribed first, then any other instance method.
|
|
109
|
+
#
|
|
110
|
+
# @param klass [Class] The channel class
|
|
111
|
+
# @return [String, nil]
|
|
112
|
+
def source_location_from_methods(klass)
|
|
113
|
+
try_methods = [:subscribed] + (klass.instance_methods(false) - [:subscribed])
|
|
114
|
+
try_methods.each do |method_name|
|
|
115
|
+
location = klass.instance_method(method_name).source_location
|
|
116
|
+
return location[0] if location
|
|
117
|
+
rescue NameError, TypeError
|
|
118
|
+
next
|
|
119
|
+
end
|
|
120
|
+
nil
|
|
121
|
+
rescue StandardError
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Fall back to Rails convention path for channel files.
|
|
126
|
+
#
|
|
127
|
+
# @param name [String] Channel class name
|
|
128
|
+
# @return [String, nil]
|
|
129
|
+
def convention_fallback(name)
|
|
130
|
+
return nil unless defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
131
|
+
|
|
132
|
+
path = Rails.root.join('app', 'channels', "#{name.underscore}.rb").to_s
|
|
133
|
+
File.exist?(path) ? path : nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Read source code from a file path.
|
|
137
|
+
#
|
|
138
|
+
# @param file_path [String, nil]
|
|
139
|
+
# @return [String]
|
|
140
|
+
def read_source(file_path)
|
|
141
|
+
return '' unless file_path && File.exist?(file_path)
|
|
142
|
+
|
|
143
|
+
File.read(file_path)
|
|
144
|
+
rescue StandardError
|
|
145
|
+
''
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Build metadata hash for a channel.
|
|
149
|
+
#
|
|
150
|
+
# @param source [String] Channel source code
|
|
151
|
+
# @param own_methods [Array<Symbol>] Methods defined directly on the channel
|
|
152
|
+
# @return [Hash]
|
|
153
|
+
def build_metadata(source, own_methods)
|
|
154
|
+
{
|
|
155
|
+
stream_names: detect_stream_names(source),
|
|
156
|
+
actions: detect_actions(own_methods),
|
|
157
|
+
has_subscribed: own_methods.include?(:subscribed),
|
|
158
|
+
has_unsubscribed: own_methods.include?(:unsubscribed),
|
|
159
|
+
broadcasts_to: detect_broadcasts(source),
|
|
160
|
+
loc: count_loc(source)
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Detect stream names from stream_from and stream_for calls.
|
|
165
|
+
#
|
|
166
|
+
# @param source [String] Channel source code
|
|
167
|
+
# @return [Array<String>]
|
|
168
|
+
def detect_stream_names(source)
|
|
169
|
+
streams = []
|
|
170
|
+
|
|
171
|
+
# stream_from "string" or stream_from 'string' (also catches interpolated strings)
|
|
172
|
+
streams.concat(source.scan(/stream_from\s+["']([^"']+)["']/).flatten)
|
|
173
|
+
|
|
174
|
+
# stream_for model
|
|
175
|
+
streams.concat(source.scan(/stream_for\s+(\w+)/).map { |m| "stream_for:#{m[0]}" })
|
|
176
|
+
|
|
177
|
+
streams.uniq
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Detect action methods (public instance methods minus lifecycle methods).
|
|
181
|
+
#
|
|
182
|
+
# @param own_methods [Array<Symbol>] Methods defined directly on the channel
|
|
183
|
+
# @return [Array<String>]
|
|
184
|
+
def detect_actions(own_methods)
|
|
185
|
+
(own_methods - LIFECYCLE_METHODS).map(&:to_s)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Detect broadcast patterns in source code.
|
|
189
|
+
#
|
|
190
|
+
# @param source [String] Channel source code
|
|
191
|
+
# @return [Array<String>]
|
|
192
|
+
def detect_broadcasts(source)
|
|
193
|
+
broadcasts = []
|
|
194
|
+
|
|
195
|
+
# ActionCable.server.broadcast("channel_name", ...)
|
|
196
|
+
broadcasts.concat(source.scan(/ActionCable\.server\.broadcast\(\s*["']([^"']+)["']/).flatten)
|
|
197
|
+
|
|
198
|
+
# SomeChannel.broadcast_to(target, ...)
|
|
199
|
+
broadcasts.concat(source.scan(/\w+\.broadcast_to\(\s*(\w+)/).map { |m| "broadcast_to:#{m[0]}" })
|
|
200
|
+
|
|
201
|
+
broadcasts.uniq
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Count non-blank, non-comment lines.
|
|
205
|
+
#
|
|
206
|
+
# @param source [String]
|
|
207
|
+
# @return [Integer]
|
|
208
|
+
def count_loc(source)
|
|
209
|
+
return 0 if source.empty?
|
|
210
|
+
|
|
211
|
+
source.each_line.count do |line|
|
|
212
|
+
stripped = line.strip
|
|
213
|
+
!stripped.empty? && !stripped.start_with?('#')
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Log a channel extraction error.
|
|
218
|
+
#
|
|
219
|
+
# @param name [String] Channel class name
|
|
220
|
+
# @param error [StandardError]
|
|
221
|
+
def log_extraction_error(name, error)
|
|
222
|
+
return unless defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
223
|
+
|
|
224
|
+
Rails.logger.error("Failed to extract channel #{name}: #{error.message}")
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../ast/method_extractor'
|
|
4
|
+
|
|
5
|
+
module CodebaseIndex
|
|
6
|
+
module Extractors
|
|
7
|
+
# Shared extraction of individual method source code via the AST layer.
|
|
8
|
+
#
|
|
9
|
+
# Included by extractors that need to pull a single method's source from
|
|
10
|
+
# a class (e.g., ControllerExtractor, MailerExtractor).
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# class FooExtractor
|
|
14
|
+
# include AstSourceExtraction
|
|
15
|
+
#
|
|
16
|
+
# def build_chunk(klass, action)
|
|
17
|
+
# source = extract_action_source(klass, action)
|
|
18
|
+
# # ...
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
module AstSourceExtraction
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Extract the source code of a single action method using the AST layer.
|
|
26
|
+
#
|
|
27
|
+
# @param klass [Class] The class that defines the method
|
|
28
|
+
# @param action [String, Symbol] The method name to extract
|
|
29
|
+
# @return [String, nil] The method source, or nil if not extractable
|
|
30
|
+
def extract_action_source(klass, action)
|
|
31
|
+
method = klass.instance_method(action)
|
|
32
|
+
source_location = method.source_location
|
|
33
|
+
return nil unless source_location
|
|
34
|
+
|
|
35
|
+
file, _line = source_location
|
|
36
|
+
return nil unless File.exist?(file)
|
|
37
|
+
|
|
38
|
+
source = File.read(file)
|
|
39
|
+
Ast::MethodExtractor.new.extract_method_source(source, action.to_s)
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
Rails.logger.debug("Could not extract action source for #{klass}##{action}: #{e.message}")
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CodebaseIndex
|
|
4
|
+
module Extractors
|
|
5
|
+
# BehavioralProfile introspects resolved Rails.application.config values
|
|
6
|
+
# to produce a single ExtractedUnit summarizing the app's runtime
|
|
7
|
+
# behavioral configuration.
|
|
8
|
+
#
|
|
9
|
+
# Sections extracted (each independently guarded):
|
|
10
|
+
# - Database: adapter, schema_format, belongs_to_required, has_many_inversing
|
|
11
|
+
# - Frameworks: ActionCable, ActiveStorage, ActionMailbox, ActionText, Turbo, Stimulus, SolidQueue, SolidCache
|
|
12
|
+
# - Behavior flags: api_only, eager_load, time_zone, strong params action, session store, filter params
|
|
13
|
+
# - Background processing: active_job queue_adapter
|
|
14
|
+
# - Caching: cache_store type
|
|
15
|
+
# - Email: delivery_method
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# profile = BehavioralProfile.new
|
|
19
|
+
# unit = profile.extract
|
|
20
|
+
# unit.metadata[:database][:adapter] #=> "postgresql"
|
|
21
|
+
#
|
|
22
|
+
class BehavioralProfile
|
|
23
|
+
# Frameworks to detect via `defined?` checks
|
|
24
|
+
FRAMEWORK_CHECKS = {
|
|
25
|
+
action_cable: 'ActionCable',
|
|
26
|
+
active_storage: 'ActiveStorage',
|
|
27
|
+
action_mailbox: 'ActionMailbox',
|
|
28
|
+
action_text: 'ActionText',
|
|
29
|
+
turbo: 'Turbo',
|
|
30
|
+
stimulus_reflex: 'StimulusReflex',
|
|
31
|
+
solid_queue: 'SolidQueue',
|
|
32
|
+
solid_cache: 'SolidCache'
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
# Extract a behavioral profile from the current Rails application.
|
|
36
|
+
#
|
|
37
|
+
# @return [ExtractedUnit, nil] A single configuration unit, or nil on catastrophic failure
|
|
38
|
+
def extract
|
|
39
|
+
config = Rails.application.config
|
|
40
|
+
|
|
41
|
+
profile = {
|
|
42
|
+
config_type: 'behavioral_profile',
|
|
43
|
+
rails_version: Rails.version,
|
|
44
|
+
ruby_version: RUBY_VERSION,
|
|
45
|
+
database: extract_database(config),
|
|
46
|
+
frameworks_active: extract_frameworks,
|
|
47
|
+
behavior_flags: extract_behavior_flags(config),
|
|
48
|
+
background_processing: extract_background(config),
|
|
49
|
+
caching: extract_caching(config),
|
|
50
|
+
email: extract_email(config)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
build_unit(profile)
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
Rails.logger.error("BehavioralProfile extraction failed: #{e.message}")
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
62
|
+
# Database
|
|
63
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
# Extract database configuration from ActiveRecord.
|
|
66
|
+
#
|
|
67
|
+
# @param config [Rails::Application::Configuration]
|
|
68
|
+
# @return [Hash]
|
|
69
|
+
def extract_database(config)
|
|
70
|
+
return {} unless defined?(ActiveRecord::Base)
|
|
71
|
+
|
|
72
|
+
result = {}
|
|
73
|
+
|
|
74
|
+
if ActiveRecord::Base.respond_to?(:connection_db_config)
|
|
75
|
+
result[:adapter] = ActiveRecord::Base.connection_db_config.adapter
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if config.respond_to?(:active_record)
|
|
79
|
+
ar = config.active_record
|
|
80
|
+
result[:schema_format] = ar.schema_format if ar.respond_to?(:schema_format)
|
|
81
|
+
if ar.respond_to?(:belongs_to_required_by_default)
|
|
82
|
+
result[:belongs_to_required_by_default] = ar.belongs_to_required_by_default
|
|
83
|
+
end
|
|
84
|
+
result[:has_many_inversing] = ar.has_many_inversing if ar.respond_to?(:has_many_inversing)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
result
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
Rails.logger.error("BehavioralProfile database section failed: #{e.message}")
|
|
90
|
+
{}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
94
|
+
# Frameworks
|
|
95
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
# Detect which optional frameworks are loaded.
|
|
98
|
+
#
|
|
99
|
+
# @return [Hash]
|
|
100
|
+
def extract_frameworks
|
|
101
|
+
FRAMEWORK_CHECKS.transform_values do |constant_name|
|
|
102
|
+
Object.const_defined?(constant_name)
|
|
103
|
+
end
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
Rails.logger.error("BehavioralProfile frameworks section failed: #{e.message}")
|
|
106
|
+
{}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
110
|
+
# Behavior flags
|
|
111
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
# Extract behavior flags from Rails config.
|
|
114
|
+
#
|
|
115
|
+
# @param config [Rails::Application::Configuration]
|
|
116
|
+
# @return [Hash]
|
|
117
|
+
def extract_behavior_flags(config)
|
|
118
|
+
flags = {}
|
|
119
|
+
|
|
120
|
+
safe_read(config, :api_only) { |v| flags[:api_only] = v }
|
|
121
|
+
safe_read(config, :eager_load) { |v| flags[:eager_load] = v }
|
|
122
|
+
safe_read(config, :time_zone) { |v| flags[:time_zone] = v }
|
|
123
|
+
safe_read(config, :session_store) { |v| flags[:session_store] = v }
|
|
124
|
+
safe_read(config, :filter_parameters) { |v| flags[:filter_parameters] = v }
|
|
125
|
+
|
|
126
|
+
if config.respond_to?(:action_controller)
|
|
127
|
+
ac = config.action_controller
|
|
128
|
+
if ac.respond_to?(:action_on_unpermitted_parameters)
|
|
129
|
+
flags[:action_on_unpermitted_parameters] = ac.action_on_unpermitted_parameters
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
flags
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
Rails.logger.error("BehavioralProfile behavior_flags section failed: #{e.message}")
|
|
136
|
+
{}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
140
|
+
# Background processing
|
|
141
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
# Extract background processing configuration.
|
|
144
|
+
#
|
|
145
|
+
# @param config [Rails::Application::Configuration]
|
|
146
|
+
# @return [Hash]
|
|
147
|
+
def extract_background(config)
|
|
148
|
+
return {} unless config.respond_to?(:active_job)
|
|
149
|
+
|
|
150
|
+
aj = config.active_job
|
|
151
|
+
return {} unless aj.respond_to?(:queue_adapter)
|
|
152
|
+
|
|
153
|
+
{ adapter: aj.queue_adapter }
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
Rails.logger.error("BehavioralProfile background section failed: #{e.message}")
|
|
156
|
+
{}
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
160
|
+
# Caching
|
|
161
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
# Extract caching configuration.
|
|
164
|
+
#
|
|
165
|
+
# @param config [Rails::Application::Configuration]
|
|
166
|
+
# @return [Hash]
|
|
167
|
+
def extract_caching(config)
|
|
168
|
+
return {} unless config.respond_to?(:cache_store)
|
|
169
|
+
|
|
170
|
+
raw = config.cache_store
|
|
171
|
+
store = raw.is_a?(Array) ? raw.first : raw
|
|
172
|
+
|
|
173
|
+
{ store: store }
|
|
174
|
+
rescue StandardError => e
|
|
175
|
+
Rails.logger.error("BehavioralProfile caching section failed: #{e.message}")
|
|
176
|
+
{}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
180
|
+
# Email
|
|
181
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
# Extract email delivery configuration.
|
|
184
|
+
#
|
|
185
|
+
# @param config [Rails::Application::Configuration]
|
|
186
|
+
# @return [Hash]
|
|
187
|
+
def extract_email(config)
|
|
188
|
+
return {} unless config.respond_to?(:action_mailer)
|
|
189
|
+
|
|
190
|
+
am = config.action_mailer
|
|
191
|
+
return {} unless am.respond_to?(:delivery_method)
|
|
192
|
+
|
|
193
|
+
{ delivery_method: am.delivery_method }
|
|
194
|
+
rescue StandardError => e
|
|
195
|
+
Rails.logger.error("BehavioralProfile email section failed: #{e.message}")
|
|
196
|
+
{}
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
200
|
+
# Unit construction
|
|
201
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
# Build the ExtractedUnit from the assembled profile hash.
|
|
204
|
+
#
|
|
205
|
+
# @param profile [Hash]
|
|
206
|
+
# @return [ExtractedUnit]
|
|
207
|
+
def build_unit(profile)
|
|
208
|
+
unit = ExtractedUnit.new(
|
|
209
|
+
type: :configuration,
|
|
210
|
+
identifier: 'BehavioralProfile',
|
|
211
|
+
file_path: Rails.root.join('config/application.rb').to_s
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
unit.namespace = 'behavioral_profile'
|
|
215
|
+
unit.metadata = profile
|
|
216
|
+
unit.source_code = build_narrative(profile)
|
|
217
|
+
unit.dependencies = build_dependencies(profile)
|
|
218
|
+
|
|
219
|
+
unit
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Generate a human-readable narrative summary.
|
|
223
|
+
#
|
|
224
|
+
# @param profile [Hash]
|
|
225
|
+
# @return [String]
|
|
226
|
+
def build_narrative(profile)
|
|
227
|
+
lines = []
|
|
228
|
+
lines << '# Behavioral Profile'
|
|
229
|
+
lines << "# Rails #{profile[:rails_version]} / Ruby #{profile[:ruby_version]}"
|
|
230
|
+
lines << '#'
|
|
231
|
+
|
|
232
|
+
# Database
|
|
233
|
+
db = profile[:database]
|
|
234
|
+
if db.any?
|
|
235
|
+
lines << "# Database: #{db[:adapter] || 'unknown'}"
|
|
236
|
+
lines << "# schema_format: #{db[:schema_format]}" if db[:schema_format]
|
|
237
|
+
unless db[:belongs_to_required_by_default].nil?
|
|
238
|
+
lines << "# belongs_to_required: #{db[:belongs_to_required_by_default]}"
|
|
239
|
+
end
|
|
240
|
+
lines << "# has_many_inversing: #{db[:has_many_inversing]}" unless db[:has_many_inversing].nil?
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Frameworks
|
|
244
|
+
active = profile[:frameworks_active].select { |_, v| v }
|
|
245
|
+
if active.any?
|
|
246
|
+
lines << '#'
|
|
247
|
+
lines << "# Active frameworks: #{active.keys.map { |k| FRAMEWORK_CHECKS[k] || k.to_s }.join(', ')}"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Behavior flags
|
|
251
|
+
flags = profile[:behavior_flags]
|
|
252
|
+
if flags.any?
|
|
253
|
+
lines << '#'
|
|
254
|
+
lines << '# Behavior flags:'
|
|
255
|
+
flags.each { |k, v| lines << "# #{k}: #{v}" }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Background
|
|
259
|
+
bg = profile[:background_processing]
|
|
260
|
+
if bg.any?
|
|
261
|
+
lines << '#'
|
|
262
|
+
lines << "# Background: #{bg[:adapter]}"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Caching
|
|
266
|
+
cache = profile[:caching]
|
|
267
|
+
if cache.any?
|
|
268
|
+
lines << '#'
|
|
269
|
+
lines << "# Cache store: #{cache[:store]}"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Email
|
|
273
|
+
email = profile[:email]
|
|
274
|
+
if email.any?
|
|
275
|
+
lines << '#'
|
|
276
|
+
lines << "# Email delivery: #{email[:delivery_method]}"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
lines.join("\n")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Build dependency list from detected frameworks and adapters.
|
|
283
|
+
#
|
|
284
|
+
# @param profile [Hash]
|
|
285
|
+
# @return [Array<Hash>]
|
|
286
|
+
def build_dependencies(profile)
|
|
287
|
+
deps = []
|
|
288
|
+
|
|
289
|
+
profile[:frameworks_active].each do |key, active|
|
|
290
|
+
next unless active
|
|
291
|
+
|
|
292
|
+
constant_name = FRAMEWORK_CHECKS[key] || key.to_s
|
|
293
|
+
deps << { type: :framework, target: constant_name, via: :behavioral_profile }
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
deps
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Safely read a config attribute if it responds to it.
|
|
300
|
+
#
|
|
301
|
+
# @param obj [Object]
|
|
302
|
+
# @param method [Symbol]
|
|
303
|
+
# @yield [value] Yields the value if available
|
|
304
|
+
def safe_read(obj, method)
|
|
305
|
+
yield obj.public_send(method) if obj.respond_to?(method)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|