codebase_index 0.2.1 → 0.3.1
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 +60 -0
- data/README.md +95 -300
- data/exe/codebase-index-mcp +3 -31
- data/exe/codebase-index-mcp-http +3 -31
- data/lib/codebase_index/ast/method_extractor.rb +3 -8
- data/lib/codebase_index/ast/node.rb +28 -0
- data/lib/codebase_index/ast/parser.rb +53 -92
- data/lib/codebase_index/builder.rb +67 -4
- data/lib/codebase_index/cache/cache_middleware.rb +199 -0
- data/lib/codebase_index/cache/cache_store.rb +264 -0
- data/lib/codebase_index/cache/redis_cache_store.rb +116 -0
- data/lib/codebase_index/cache/solid_cache_store.rb +111 -0
- data/lib/codebase_index/chunking/semantic_chunker.rb +29 -24
- data/lib/codebase_index/console/adapters/good_job_adapter.rb +7 -40
- data/lib/codebase_index/console/adapters/job_adapter.rb +68 -0
- data/lib/codebase_index/console/adapters/sidekiq_adapter.rb +7 -40
- data/lib/codebase_index/console/adapters/solid_queue_adapter.rb +7 -40
- data/lib/codebase_index/console/bridge.rb +7 -0
- data/lib/codebase_index/console/console_response_renderer.rb +3 -7
- data/lib/codebase_index/console/embedded_executor.rb +2 -1
- data/lib/codebase_index/console/server.rb +1 -4
- data/lib/codebase_index/dependency_graph.rb +28 -19
- data/lib/codebase_index/embedding/indexer.rb +18 -8
- data/lib/codebase_index/embedding/openai.rb +27 -6
- data/lib/codebase_index/embedding/provider.rb +29 -2
- data/lib/codebase_index/evaluation/evaluator.rb +5 -12
- data/lib/codebase_index/extractor.rb +40 -44
- data/lib/codebase_index/extractors/action_cable_extractor.rb +9 -36
- data/lib/codebase_index/extractors/callback_analyzer.rb +22 -8
- data/lib/codebase_index/extractors/controller_extractor.rb +3 -93
- data/lib/codebase_index/extractors/decorator_extractor.rb +7 -14
- data/lib/codebase_index/extractors/engine_extractor.rb +20 -1
- data/lib/codebase_index/extractors/graphql_extractor.rb +4 -29
- data/lib/codebase_index/extractors/job_extractor.rb +11 -6
- data/lib/codebase_index/extractors/lib_extractor.rb +0 -31
- data/lib/codebase_index/extractors/mailer_extractor.rb +15 -85
- data/lib/codebase_index/extractors/manager_extractor.rb +1 -15
- data/lib/codebase_index/extractors/model_extractor.rb +20 -53
- data/lib/codebase_index/extractors/phlex_extractor.rb +8 -8
- data/lib/codebase_index/extractors/policy_extractor.rb +1 -24
- data/lib/codebase_index/extractors/poro_extractor.rb +0 -17
- data/lib/codebase_index/extractors/serializer_extractor.rb +12 -7
- data/lib/codebase_index/extractors/service_extractor.rb +1 -38
- data/lib/codebase_index/extractors/shared_utility_methods.rb +183 -1
- data/lib/codebase_index/extractors/validator_extractor.rb +3 -17
- data/lib/codebase_index/extractors/view_component_extractor.rb +10 -9
- data/lib/codebase_index/filename_utils.rb +32 -0
- data/lib/codebase_index/flow_analysis/operation_extractor.rb +1 -4
- data/lib/codebase_index/formatting/base.rb +0 -10
- data/lib/codebase_index/graph_analyzer.rb +1 -1
- data/lib/codebase_index/mcp/bootstrapper.rb +58 -0
- data/lib/codebase_index/mcp/renderers/markdown_renderer.rb +35 -34
- data/lib/codebase_index/mcp/renderers/plain_renderer.rb +29 -29
- data/lib/codebase_index/mcp/server.rb +59 -68
- data/lib/codebase_index/mcp/tool_response_renderer.rb +23 -0
- data/lib/codebase_index/notion/client.rb +2 -2
- data/lib/codebase_index/notion/mapper.rb +1 -0
- data/lib/codebase_index/notion/mappers/column_mapper.rb +3 -11
- data/lib/codebase_index/notion/mappers/model_mapper.rb +20 -23
- data/lib/codebase_index/notion/mappers/shared.rb +22 -0
- data/lib/codebase_index/observability/health_check.rb +0 -2
- data/lib/codebase_index/observability/structured_logger.rb +12 -30
- data/lib/codebase_index/operator/pipeline_guard.rb +0 -7
- data/lib/codebase_index/resilience/index_validator.rb +3 -21
- data/lib/codebase_index/retrieval/context_assembler.rb +19 -7
- data/lib/codebase_index/retrieval/query_classifier.rb +14 -12
- data/lib/codebase_index/retrieval/ranker.rb +6 -2
- data/lib/codebase_index/retrieval/search_executor.rb +8 -19
- data/lib/codebase_index/retriever.rb +1 -9
- data/lib/codebase_index/ruby_analyzer/class_analyzer.rb +5 -25
- data/lib/codebase_index/ruby_analyzer/dataflow_analyzer.rb +6 -7
- data/lib/codebase_index/ruby_analyzer/mermaid_renderer.rb +58 -53
- data/lib/codebase_index/ruby_analyzer/trace_enricher.rb +11 -7
- data/lib/codebase_index/session_tracer/file_store.rb +1 -8
- data/lib/codebase_index/session_tracer/redis_store.rb +1 -7
- data/lib/codebase_index/session_tracer/session_flow_assembler.rb +4 -13
- data/lib/codebase_index/session_tracer/solid_cache_store.rb +1 -7
- data/lib/codebase_index/session_tracer/store.rb +14 -0
- data/lib/codebase_index/storage/metadata_store.rb +37 -10
- data/lib/codebase_index/storage/pgvector.rb +37 -5
- data/lib/codebase_index/storage/qdrant.rb +39 -6
- data/lib/codebase_index/storage/vector_store.rb +11 -0
- data/lib/codebase_index/temporal/snapshot_store.rb +14 -10
- data/lib/codebase_index/token_utils.rb +19 -0
- data/lib/codebase_index/version.rb +1 -1
- data/lib/codebase_index.rb +25 -6
- data/lib/tasks/codebase_index.rake +2 -2
- metadata +11 -2
|
@@ -71,13 +71,18 @@ module CodebaseIndex
|
|
|
71
71
|
|
|
72
72
|
private
|
|
73
73
|
|
|
74
|
+
# Locate the source file for a mailer class.
|
|
75
|
+
#
|
|
76
|
+
# Convention path first, then introspection via {#resolve_source_location}
|
|
77
|
+
# which filters out vendor/node_modules paths.
|
|
78
|
+
#
|
|
79
|
+
# @param mailer [Class]
|
|
80
|
+
# @return [String]
|
|
74
81
|
def source_file_for(mailer)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
rescue StandardError
|
|
80
|
-
Rails.root.join("app/mailers/#{mailer.name.underscore}.rb").to_s
|
|
82
|
+
convention_path = Rails.root.join("app/mailers/#{mailer.name.underscore}.rb").to_s
|
|
83
|
+
return convention_path if File.exist?(convention_path)
|
|
84
|
+
|
|
85
|
+
resolve_source_location(mailer, app_root: Rails.root.to_s, fallback: convention_path)
|
|
81
86
|
end
|
|
82
87
|
|
|
83
88
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -139,19 +144,10 @@ module CodebaseIndex
|
|
|
139
144
|
end
|
|
140
145
|
|
|
141
146
|
def extract_defaults(mailer)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
defaults[:from] = mailer_defaults[:from] if mailer_defaults[:from]
|
|
147
|
-
defaults[:reply_to] = mailer_defaults[:reply_to] if mailer_defaults[:reply_to]
|
|
148
|
-
defaults[:cc] = mailer_defaults[:cc] if mailer_defaults[:cc]
|
|
149
|
-
defaults[:bcc] = mailer_defaults[:bcc] if mailer_defaults[:bcc]
|
|
150
|
-
rescue StandardError
|
|
151
|
-
# Defaults not accessible
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
defaults
|
|
147
|
+
mailer_defaults = mailer.default
|
|
148
|
+
mailer_defaults.slice(:from, :reply_to, :cc, :bcc).compact
|
|
149
|
+
rescue StandardError
|
|
150
|
+
{}
|
|
155
151
|
end
|
|
156
152
|
|
|
157
153
|
def extract_callbacks(mailer)
|
|
@@ -172,72 +168,6 @@ module CodebaseIndex
|
|
|
172
168
|
[]
|
|
173
169
|
end
|
|
174
170
|
|
|
175
|
-
# Extract :only/:except action lists and :if/:unless conditions from a callback.
|
|
176
|
-
#
|
|
177
|
-
# Modern Rails (4.2+) stores conditions in @if/@unless ivar arrays.
|
|
178
|
-
# ActionFilter objects hold action Sets; other conditions are procs/symbols.
|
|
179
|
-
#
|
|
180
|
-
# @param callback [ActiveSupport::Callbacks::Callback]
|
|
181
|
-
# @return [Array(Array<String>, Array<String>, Array<String>, Array<String>)]
|
|
182
|
-
# [only_actions, except_actions, if_labels, unless_labels]
|
|
183
|
-
def extract_callback_conditions(callback)
|
|
184
|
-
if_conditions = callback.instance_variable_get(:@if) || []
|
|
185
|
-
unless_conditions = callback.instance_variable_get(:@unless) || []
|
|
186
|
-
|
|
187
|
-
only = []
|
|
188
|
-
except = []
|
|
189
|
-
if_labels = []
|
|
190
|
-
unless_labels = []
|
|
191
|
-
|
|
192
|
-
if_conditions.each do |cond|
|
|
193
|
-
actions = extract_action_filter_actions(cond)
|
|
194
|
-
if actions
|
|
195
|
-
only.concat(actions)
|
|
196
|
-
else
|
|
197
|
-
if_labels << condition_label(cond)
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
unless_conditions.each do |cond|
|
|
202
|
-
actions = extract_action_filter_actions(cond)
|
|
203
|
-
if actions
|
|
204
|
-
except.concat(actions)
|
|
205
|
-
else
|
|
206
|
-
unless_labels << condition_label(cond)
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
[only, except, if_labels, unless_labels]
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# Extract action names from an ActionFilter-like condition object.
|
|
214
|
-
# Duck-types on the @actions ivar being a Set, avoiding dependence
|
|
215
|
-
# on private class names across Rails versions.
|
|
216
|
-
#
|
|
217
|
-
# @param condition [Object] A condition from the callback's @if/@unless array
|
|
218
|
-
# @return [Array<String>, nil] Action names, or nil if not an ActionFilter
|
|
219
|
-
def extract_action_filter_actions(condition)
|
|
220
|
-
return nil unless condition.instance_variable_defined?(:@actions)
|
|
221
|
-
|
|
222
|
-
actions = condition.instance_variable_get(:@actions)
|
|
223
|
-
return nil unless actions.is_a?(Set)
|
|
224
|
-
|
|
225
|
-
actions.to_a
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# Human-readable label for a non-ActionFilter condition.
|
|
229
|
-
#
|
|
230
|
-
# @param condition [Object] A proc, symbol, or other condition
|
|
231
|
-
# @return [String]
|
|
232
|
-
def condition_label(condition)
|
|
233
|
-
case condition
|
|
234
|
-
when Symbol then ":#{condition}"
|
|
235
|
-
when Proc then 'Proc'
|
|
236
|
-
when String then condition
|
|
237
|
-
else condition.class.name
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
|
|
241
171
|
def extract_layout(mailer, source)
|
|
242
172
|
# From class definition
|
|
243
173
|
return ::Regexp.last_match(1) if source =~ /layout\s+['":](\w+)/
|
|
@@ -53,7 +53,7 @@ module CodebaseIndex
|
|
|
53
53
|
# @return [ExtractedUnit, nil] The extracted unit or nil if not a manager
|
|
54
54
|
def extract_manager_file(file_path)
|
|
55
55
|
source = File.read(file_path)
|
|
56
|
-
class_name = extract_class_name(file_path, source)
|
|
56
|
+
class_name = extract_class_name(file_path, source, 'managers')
|
|
57
57
|
|
|
58
58
|
return nil unless class_name
|
|
59
59
|
return nil unless manager_file?(source)
|
|
@@ -81,16 +81,6 @@ module CodebaseIndex
|
|
|
81
81
|
# Class Discovery
|
|
82
82
|
# ──────────────────────────────────────────────────────────────────────
|
|
83
83
|
|
|
84
|
-
def extract_class_name(file_path, source)
|
|
85
|
-
return ::Regexp.last_match(1) if source =~ /^\s*class\s+([\w:]+)/
|
|
86
|
-
|
|
87
|
-
file_path
|
|
88
|
-
.sub("#{Rails.root}/", '')
|
|
89
|
-
.sub(%r{^app/managers/}, '')
|
|
90
|
-
.sub('.rb', '')
|
|
91
|
-
.camelize
|
|
92
|
-
end
|
|
93
|
-
|
|
94
84
|
def manager_file?(source)
|
|
95
85
|
source.match?(/< SimpleDelegator/) ||
|
|
96
86
|
source.match?(/< DelegateClass\(/) ||
|
|
@@ -178,10 +168,6 @@ module CodebaseIndex
|
|
|
178
168
|
source.scan(/def\s+(\w+[?!=]?).*?\n.*?super/m).flatten
|
|
179
169
|
end
|
|
180
170
|
|
|
181
|
-
def extract_custom_errors(source)
|
|
182
|
-
source.scan(/class\s+(\w+(?:Error|Exception))\s*</).flatten
|
|
183
|
-
end
|
|
184
|
-
|
|
185
171
|
# ──────────────────────────────────────────────────────────────────────
|
|
186
172
|
# Dependency Extraction
|
|
187
173
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'digest'
|
|
4
4
|
require_relative '../ast/parser'
|
|
5
|
+
require_relative 'shared_utility_methods'
|
|
5
6
|
require_relative 'shared_dependency_scanner'
|
|
6
7
|
require_relative 'callback_analyzer'
|
|
7
8
|
|
|
@@ -23,15 +24,17 @@ module CodebaseIndex
|
|
|
23
24
|
# user_unit = units.find { |u| u.identifier == "User" }
|
|
24
25
|
#
|
|
25
26
|
class ModelExtractor
|
|
27
|
+
include SharedUtilityMethods
|
|
26
28
|
include SharedDependencyScanner
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
# Single combined regex for filtering AR-generated internal methods.
|
|
31
|
+
# Replaces 5 separate patterns with one alternation for O(1) matching.
|
|
32
|
+
AR_INTERNAL_METHOD_PATTERN = /\A(?:
|
|
33
|
+
_ | # _run_save_callbacks, _validators, etc.
|
|
34
|
+
autosave_associated_records_for_ | # autosave_associated_records_for_comments
|
|
35
|
+
validate_associated_records_for_ | # validate_associated_records_for_comments
|
|
36
|
+
(?:after|before)_(?:add|remove)_for_ # collection callbacks
|
|
37
|
+
)/x
|
|
35
38
|
|
|
36
39
|
def initialize
|
|
37
40
|
@concern_cache = {}
|
|
@@ -84,38 +87,14 @@ module CodebaseIndex
|
|
|
84
87
|
|
|
85
88
|
# Find the source file for a model, handling STI and namespacing.
|
|
86
89
|
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
+
# Uses convention path first (most reliable for models), then falls
|
|
91
|
+
# back to introspection via {#resolve_source_location} which filters
|
|
92
|
+
# out vendor/node_modules paths.
|
|
90
93
|
def source_file_for(model)
|
|
91
|
-
app_root = Rails.root.to_s
|
|
92
94
|
convention_path = Rails.root.join("app/models/#{model.name.underscore}.rb").to_s
|
|
93
|
-
|
|
94
|
-
# Tier 1: Instance methods defined directly on this model
|
|
95
|
-
model.instance_methods(false).each do |method_name|
|
|
96
|
-
loc = model.instance_method(method_name).source_location&.first
|
|
97
|
-
return loc if loc&.start_with?(app_root)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Tier 2: Class/singleton methods (catches models with only scopes)
|
|
101
|
-
model.methods(false).each do |method_name|
|
|
102
|
-
loc = model.method(method_name).source_location&.first
|
|
103
|
-
return loc if loc&.start_with?(app_root)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Tier 3: Convention path if file exists
|
|
107
95
|
return convention_path if File.exist?(convention_path)
|
|
108
96
|
|
|
109
|
-
|
|
110
|
-
if Object.respond_to?(:const_source_location)
|
|
111
|
-
loc = Object.const_source_location(model.name)&.first
|
|
112
|
-
return loc if loc&.start_with?(app_root)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Tier 5: Always return convention path — never a gem path
|
|
116
|
-
convention_path
|
|
117
|
-
rescue StandardError
|
|
118
|
-
convention_path
|
|
97
|
+
resolve_source_location(model, app_root: Rails.root.to_s, fallback: convention_path)
|
|
119
98
|
end
|
|
120
99
|
|
|
121
100
|
# Detect Rails-generated HABTM join models (e.g., Product::HABTM_Categories)
|
|
@@ -241,7 +220,7 @@ module CodebaseIndex
|
|
|
241
220
|
# Skip obvious non-app modules (from gems/stdlib)
|
|
242
221
|
if Object.respond_to?(:const_source_location)
|
|
243
222
|
loc = Object.const_source_location(mod.name)
|
|
244
|
-
next false if loc && !loc.first
|
|
223
|
+
next false if loc && !app_source?(loc.first, app_root)
|
|
245
224
|
end
|
|
246
225
|
|
|
247
226
|
# Include if it's in app/models/concerns or app/controllers/concerns
|
|
@@ -258,16 +237,18 @@ module CodebaseIndex
|
|
|
258
237
|
# @param mod [Module] The module to check
|
|
259
238
|
# @return [Boolean] true if the module is defined within Rails.root
|
|
260
239
|
def defined_in_app?(mod)
|
|
240
|
+
app_root = Rails.root.to_s
|
|
241
|
+
|
|
261
242
|
# Fast path: const_source_location is cheaper than iterating methods
|
|
262
243
|
if mod.respond_to?(:const_source_location) || Object.respond_to?(:const_source_location)
|
|
263
244
|
loc = Object.const_source_location(mod.name)
|
|
264
|
-
return loc.first
|
|
245
|
+
return app_source?(loc.first, app_root) if loc
|
|
265
246
|
end
|
|
266
247
|
|
|
267
248
|
# Slow path: check instance method source locations
|
|
268
249
|
mod.instance_methods(false).any? do |method|
|
|
269
250
|
loc = mod.instance_method(method).source_location&.first
|
|
270
|
-
|
|
251
|
+
app_source?(loc, app_root)
|
|
271
252
|
end
|
|
272
253
|
rescue StandardError
|
|
273
254
|
false
|
|
@@ -836,19 +817,6 @@ module CodebaseIndex
|
|
|
836
817
|
# Condition & Filter Helpers
|
|
837
818
|
# ──────────────────────────────────────────────────────────────────────
|
|
838
819
|
|
|
839
|
-
# Human-readable label for a condition (Symbol, Proc, String, etc.)
|
|
840
|
-
#
|
|
841
|
-
# @param condition [Object] A proc, symbol, or other condition
|
|
842
|
-
# @return [String]
|
|
843
|
-
def condition_label(condition)
|
|
844
|
-
case condition
|
|
845
|
-
when Symbol then ":#{condition}"
|
|
846
|
-
when Proc then 'Proc'
|
|
847
|
-
when String then condition
|
|
848
|
-
else condition.class.name
|
|
849
|
-
end
|
|
850
|
-
end
|
|
851
|
-
|
|
852
820
|
# Build conditions hash from validator options, converting Procs to labels
|
|
853
821
|
#
|
|
854
822
|
# @param validator [ActiveModel::Validator]
|
|
@@ -906,8 +874,7 @@ module CodebaseIndex
|
|
|
906
874
|
# @return [Array<Symbol>]
|
|
907
875
|
def filter_instance_methods(methods)
|
|
908
876
|
methods.reject do |method_name|
|
|
909
|
-
|
|
910
|
-
AR_INTERNAL_METHOD_PATTERNS.any? { |pattern| pattern.match?(name) }
|
|
877
|
+
AR_INTERNAL_METHOD_PATTERN.match?(method_name.to_s)
|
|
911
878
|
end
|
|
912
879
|
end
|
|
913
880
|
|
|
@@ -96,8 +96,14 @@ module CodebaseIndex
|
|
|
96
96
|
defined?(ViewComponent::Base) && klass < ViewComponent::Base
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
# Locate the source file for a Phlex component class.
|
|
100
|
+
#
|
|
101
|
+
# Convention paths first, then introspection via {#resolve_source_location}
|
|
102
|
+
# which filters out vendor/node_modules paths.
|
|
103
|
+
#
|
|
104
|
+
# @param component [Class]
|
|
105
|
+
# @return [String, nil]
|
|
99
106
|
def source_file_for(component)
|
|
100
|
-
# Try common locations
|
|
101
107
|
possible_paths = [
|
|
102
108
|
Rails.root.join("app/views/components/#{component.name.underscore}.rb"),
|
|
103
109
|
Rails.root.join("app/components/#{component.name.underscore}.rb"),
|
|
@@ -107,13 +113,7 @@ module CodebaseIndex
|
|
|
107
113
|
found = possible_paths.find { |p| File.exist?(p) }
|
|
108
114
|
return found.to_s if found
|
|
109
115
|
|
|
110
|
-
|
|
111
|
-
if component.instance_methods(false).any?
|
|
112
|
-
method = component.instance_methods(false).first
|
|
113
|
-
component.instance_method(method).source_location&.first
|
|
114
|
-
end
|
|
115
|
-
rescue StandardError
|
|
116
|
-
nil
|
|
116
|
+
resolve_source_location(component, app_root: Rails.root.to_s, fallback: nil)
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
def read_source(file_path)
|
|
@@ -56,7 +56,7 @@ module CodebaseIndex
|
|
|
56
56
|
# @return [ExtractedUnit, nil] The extracted unit or nil if not a policy
|
|
57
57
|
def extract_policy_file(file_path)
|
|
58
58
|
source = File.read(file_path)
|
|
59
|
-
class_name = extract_class_name(file_path, source)
|
|
59
|
+
class_name = extract_class_name(file_path, source, 'policies')
|
|
60
60
|
|
|
61
61
|
return nil unless class_name
|
|
62
62
|
return nil if skip_file?(source)
|
|
@@ -80,25 +80,6 @@ module CodebaseIndex
|
|
|
80
80
|
|
|
81
81
|
private
|
|
82
82
|
|
|
83
|
-
# ──────────────────────────────────────────────────────────────────────
|
|
84
|
-
# Class Discovery
|
|
85
|
-
# ──────────────────────────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
def extract_class_name(file_path, source)
|
|
88
|
-
return ::Regexp.last_match(1) if source =~ /^\s*class\s+([\w:]+)/
|
|
89
|
-
|
|
90
|
-
file_path
|
|
91
|
-
.sub("#{Rails.root}/", '')
|
|
92
|
-
.sub(%r{^app/policies/}, '')
|
|
93
|
-
.sub('.rb', '')
|
|
94
|
-
.camelize
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def skip_file?(source)
|
|
98
|
-
# Skip module-only files (concerns, base modules)
|
|
99
|
-
source.match?(/^\s*module\s+\w+\s*$/) && !source.match?(/^\s*class\s+/)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
83
|
# ──────────────────────────────────────────────────────────────────────
|
|
103
84
|
# Source Annotation
|
|
104
85
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -189,10 +170,6 @@ module CodebaseIndex
|
|
|
189
170
|
source.match?(/attr_reader\s+:user\s*,\s*:record/)
|
|
190
171
|
end
|
|
191
172
|
|
|
192
|
-
def extract_custom_errors(source)
|
|
193
|
-
source.scan(/class\s+(\w+(?:Error|Exception))\s*</).flatten
|
|
194
|
-
end
|
|
195
|
-
|
|
196
173
|
# ──────────────────────────────────────────────────────────────────────
|
|
197
174
|
# Dependency Extraction
|
|
198
175
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -212,23 +212,6 @@ module CodebaseIndex
|
|
|
212
212
|
}
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
-
# Extract the parent class name from a class definition.
|
|
216
|
-
#
|
|
217
|
-
# @param source [String] Ruby source code
|
|
218
|
-
# @return [String, nil] Parent class name or nil
|
|
219
|
-
def extract_parent_class(source)
|
|
220
|
-
match = source.match(/^\s*class\s+[\w:]+\s*<\s*([\w:]+)/)
|
|
221
|
-
match ? match[1] : nil
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
# Count non-blank, non-comment lines of code.
|
|
225
|
-
#
|
|
226
|
-
# @param source [String] Ruby source code
|
|
227
|
-
# @return [Integer] LOC count
|
|
228
|
-
def count_loc(source)
|
|
229
|
-
source.lines.count { |l| l.strip.length.positive? && !l.strip.start_with?('#') }
|
|
230
|
-
end
|
|
231
|
-
|
|
232
215
|
# ──────────────────────────────────────────────────────────────────────
|
|
233
216
|
# Dependency Extraction
|
|
234
217
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -171,13 +171,18 @@ module CodebaseIndex
|
|
|
171
171
|
source.match?(/view\s+:/) # Blueprinter views
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
+
# Locate the source file for a serializer class.
|
|
175
|
+
#
|
|
176
|
+
# Convention path first, then introspection via {#resolve_source_location}
|
|
177
|
+
# which filters out vendor/node_modules paths.
|
|
178
|
+
#
|
|
179
|
+
# @param klass [Class]
|
|
180
|
+
# @return [String, nil]
|
|
174
181
|
def source_file_for(klass)
|
|
175
|
-
|
|
176
|
-
if
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
rescue StandardError
|
|
180
|
-
nil
|
|
182
|
+
convention_path = Rails.root.join("app/serializers/#{klass.name.underscore}.rb").to_s
|
|
183
|
+
return convention_path if File.exist?(convention_path)
|
|
184
|
+
|
|
185
|
+
resolve_source_location(klass, app_root: Rails.root.to_s, fallback: convention_path)
|
|
181
186
|
end
|
|
182
187
|
|
|
183
188
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -309,7 +314,7 @@ module CodebaseIndex
|
|
|
309
314
|
|
|
310
315
|
def extract_views(source)
|
|
311
316
|
# Blueprinter views: `view :extended do`
|
|
312
|
-
source.scan(/view\s+:(\w+)/).flatten
|
|
317
|
+
source.scan(/view\s+:(\w+)/).flatten
|
|
313
318
|
end
|
|
314
319
|
|
|
315
320
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -57,7 +57,7 @@ module CodebaseIndex
|
|
|
57
57
|
# @return [ExtractedUnit, nil] The extracted unit or nil if not a service
|
|
58
58
|
def extract_service_file(file_path)
|
|
59
59
|
source = File.read(file_path)
|
|
60
|
-
class_name = extract_class_name(file_path, source)
|
|
60
|
+
class_name = extract_class_name(file_path, source, '(?:services|interactors|operations|commands|use_cases)')
|
|
61
61
|
|
|
62
62
|
return nil unless class_name
|
|
63
63
|
return nil if skip_file?(source)
|
|
@@ -81,29 +81,6 @@ module CodebaseIndex
|
|
|
81
81
|
|
|
82
82
|
private
|
|
83
83
|
|
|
84
|
-
# ──────────────────────────────────────────────────────────────────────
|
|
85
|
-
# Class Discovery
|
|
86
|
-
# ──────────────────────────────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
def extract_class_name(file_path, source)
|
|
89
|
-
# Try to extract from source first (handles nested modules)
|
|
90
|
-
return ::Regexp.last_match(1) if source =~ /^\s*class\s+([\w:]+)/
|
|
91
|
-
|
|
92
|
-
# Fall back to convention
|
|
93
|
-
relative_path = file_path.sub("#{Rails.root}/", '')
|
|
94
|
-
|
|
95
|
-
# app/services/payments/stripe_service.rb -> Payments::StripeService
|
|
96
|
-
relative_path
|
|
97
|
-
.sub(%r{^app/(services|interactors|operations|commands|use_cases)/}, '')
|
|
98
|
-
.sub('.rb', '')
|
|
99
|
-
.camelize
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def skip_file?(source)
|
|
103
|
-
# Skip module-only files (concerns, base modules)
|
|
104
|
-
source.match?(/^\s*module\s+\w+\s*$/) && !source.match?(/^\s*class\s+/)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
84
|
# ──────────────────────────────────────────────────────────────────────
|
|
108
85
|
# Source Annotation
|
|
109
86
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -123,16 +100,6 @@ module CodebaseIndex
|
|
|
123
100
|
annotation + source
|
|
124
101
|
end
|
|
125
102
|
|
|
126
|
-
def detect_entry_points(source)
|
|
127
|
-
points = []
|
|
128
|
-
points << 'call' if source.match?(/def (self\.)?call\b/)
|
|
129
|
-
points << 'perform' if source.match?(/def (self\.)?perform\b/)
|
|
130
|
-
points << 'execute' if source.match?(/def (self\.)?execute\b/)
|
|
131
|
-
points << 'run' if source.match?(/def (self\.)?run\b/)
|
|
132
|
-
points << 'process' if source.match?(/def (self\.)?process\b/)
|
|
133
|
-
points.empty? ? ['unknown'] : points
|
|
134
|
-
end
|
|
135
|
-
|
|
136
103
|
# ──────────────────────────────────────────────────────────────────────
|
|
137
104
|
# Metadata Extraction
|
|
138
105
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -188,10 +155,6 @@ module CodebaseIndex
|
|
|
188
155
|
deps.uniq
|
|
189
156
|
end
|
|
190
157
|
|
|
191
|
-
def extract_custom_errors(source)
|
|
192
|
-
source.scan(/class\s+(\w+(?:Error|Exception))\s*</).flatten
|
|
193
|
-
end
|
|
194
|
-
|
|
195
158
|
def extract_rescue_handlers(source)
|
|
196
159
|
source.scan(/rescue\s+([\w:]+)/).flatten.uniq
|
|
197
160
|
end
|