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.
Files changed (171) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +29 -0
  3. data/CODE_OF_CONDUCT.md +83 -0
  4. data/CONTRIBUTING.md +65 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +481 -0
  7. data/exe/codebase-console-mcp +22 -0
  8. data/exe/codebase-index-mcp +61 -0
  9. data/exe/codebase-index-mcp-http +64 -0
  10. data/exe/codebase-index-mcp-start +58 -0
  11. data/lib/codebase_index/ast/call_site_extractor.rb +106 -0
  12. data/lib/codebase_index/ast/method_extractor.rb +76 -0
  13. data/lib/codebase_index/ast/node.rb +88 -0
  14. data/lib/codebase_index/ast/parser.rb +653 -0
  15. data/lib/codebase_index/ast.rb +6 -0
  16. data/lib/codebase_index/builder.rb +137 -0
  17. data/lib/codebase_index/chunking/chunk.rb +84 -0
  18. data/lib/codebase_index/chunking/semantic_chunker.rb +290 -0
  19. data/lib/codebase_index/console/adapters/cache_adapter.rb +58 -0
  20. data/lib/codebase_index/console/adapters/good_job_adapter.rb +66 -0
  21. data/lib/codebase_index/console/adapters/sidekiq_adapter.rb +66 -0
  22. data/lib/codebase_index/console/adapters/solid_queue_adapter.rb +66 -0
  23. data/lib/codebase_index/console/audit_logger.rb +75 -0
  24. data/lib/codebase_index/console/bridge.rb +170 -0
  25. data/lib/codebase_index/console/confirmation.rb +90 -0
  26. data/lib/codebase_index/console/connection_manager.rb +173 -0
  27. data/lib/codebase_index/console/console_response_renderer.rb +78 -0
  28. data/lib/codebase_index/console/model_validator.rb +81 -0
  29. data/lib/codebase_index/console/safe_context.rb +82 -0
  30. data/lib/codebase_index/console/server.rb +557 -0
  31. data/lib/codebase_index/console/sql_validator.rb +172 -0
  32. data/lib/codebase_index/console/tools/tier1.rb +118 -0
  33. data/lib/codebase_index/console/tools/tier2.rb +117 -0
  34. data/lib/codebase_index/console/tools/tier3.rb +110 -0
  35. data/lib/codebase_index/console/tools/tier4.rb +79 -0
  36. data/lib/codebase_index/coordination/pipeline_lock.rb +109 -0
  37. data/lib/codebase_index/cost_model/embedding_cost.rb +88 -0
  38. data/lib/codebase_index/cost_model/estimator.rb +128 -0
  39. data/lib/codebase_index/cost_model/provider_pricing.rb +67 -0
  40. data/lib/codebase_index/cost_model/storage_cost.rb +52 -0
  41. data/lib/codebase_index/cost_model.rb +22 -0
  42. data/lib/codebase_index/db/migrations/001_create_units.rb +38 -0
  43. data/lib/codebase_index/db/migrations/002_create_edges.rb +35 -0
  44. data/lib/codebase_index/db/migrations/003_create_embeddings.rb +37 -0
  45. data/lib/codebase_index/db/migrations/004_create_snapshots.rb +45 -0
  46. data/lib/codebase_index/db/migrations/005_create_snapshot_units.rb +40 -0
  47. data/lib/codebase_index/db/migrator.rb +71 -0
  48. data/lib/codebase_index/db/schema_version.rb +73 -0
  49. data/lib/codebase_index/dependency_graph.rb +227 -0
  50. data/lib/codebase_index/embedding/indexer.rb +130 -0
  51. data/lib/codebase_index/embedding/openai.rb +105 -0
  52. data/lib/codebase_index/embedding/provider.rb +135 -0
  53. data/lib/codebase_index/embedding/text_preparer.rb +112 -0
  54. data/lib/codebase_index/evaluation/baseline_runner.rb +115 -0
  55. data/lib/codebase_index/evaluation/evaluator.rb +146 -0
  56. data/lib/codebase_index/evaluation/metrics.rb +79 -0
  57. data/lib/codebase_index/evaluation/query_set.rb +148 -0
  58. data/lib/codebase_index/evaluation/report_generator.rb +90 -0
  59. data/lib/codebase_index/extracted_unit.rb +145 -0
  60. data/lib/codebase_index/extractor.rb +956 -0
  61. data/lib/codebase_index/extractors/action_cable_extractor.rb +228 -0
  62. data/lib/codebase_index/extractors/ast_source_extraction.rb +46 -0
  63. data/lib/codebase_index/extractors/behavioral_profile.rb +309 -0
  64. data/lib/codebase_index/extractors/caching_extractor.rb +261 -0
  65. data/lib/codebase_index/extractors/callback_analyzer.rb +232 -0
  66. data/lib/codebase_index/extractors/concern_extractor.rb +253 -0
  67. data/lib/codebase_index/extractors/configuration_extractor.rb +219 -0
  68. data/lib/codebase_index/extractors/controller_extractor.rb +494 -0
  69. data/lib/codebase_index/extractors/database_view_extractor.rb +278 -0
  70. data/lib/codebase_index/extractors/decorator_extractor.rb +260 -0
  71. data/lib/codebase_index/extractors/engine_extractor.rb +204 -0
  72. data/lib/codebase_index/extractors/event_extractor.rb +211 -0
  73. data/lib/codebase_index/extractors/factory_extractor.rb +289 -0
  74. data/lib/codebase_index/extractors/graphql_extractor.rb +917 -0
  75. data/lib/codebase_index/extractors/i18n_extractor.rb +117 -0
  76. data/lib/codebase_index/extractors/job_extractor.rb +369 -0
  77. data/lib/codebase_index/extractors/lib_extractor.rb +249 -0
  78. data/lib/codebase_index/extractors/mailer_extractor.rb +339 -0
  79. data/lib/codebase_index/extractors/manager_extractor.rb +202 -0
  80. data/lib/codebase_index/extractors/middleware_extractor.rb +133 -0
  81. data/lib/codebase_index/extractors/migration_extractor.rb +469 -0
  82. data/lib/codebase_index/extractors/model_extractor.rb +960 -0
  83. data/lib/codebase_index/extractors/phlex_extractor.rb +252 -0
  84. data/lib/codebase_index/extractors/policy_extractor.rb +214 -0
  85. data/lib/codebase_index/extractors/poro_extractor.rb +246 -0
  86. data/lib/codebase_index/extractors/pundit_extractor.rb +223 -0
  87. data/lib/codebase_index/extractors/rails_source_extractor.rb +473 -0
  88. data/lib/codebase_index/extractors/rake_task_extractor.rb +343 -0
  89. data/lib/codebase_index/extractors/route_extractor.rb +181 -0
  90. data/lib/codebase_index/extractors/scheduled_job_extractor.rb +331 -0
  91. data/lib/codebase_index/extractors/serializer_extractor.rb +334 -0
  92. data/lib/codebase_index/extractors/service_extractor.rb +254 -0
  93. data/lib/codebase_index/extractors/shared_dependency_scanner.rb +91 -0
  94. data/lib/codebase_index/extractors/shared_utility_methods.rb +99 -0
  95. data/lib/codebase_index/extractors/state_machine_extractor.rb +398 -0
  96. data/lib/codebase_index/extractors/test_mapping_extractor.rb +225 -0
  97. data/lib/codebase_index/extractors/validator_extractor.rb +225 -0
  98. data/lib/codebase_index/extractors/view_component_extractor.rb +310 -0
  99. data/lib/codebase_index/extractors/view_template_extractor.rb +261 -0
  100. data/lib/codebase_index/feedback/gap_detector.rb +89 -0
  101. data/lib/codebase_index/feedback/store.rb +119 -0
  102. data/lib/codebase_index/flow_analysis/operation_extractor.rb +209 -0
  103. data/lib/codebase_index/flow_analysis/response_code_mapper.rb +154 -0
  104. data/lib/codebase_index/flow_assembler.rb +290 -0
  105. data/lib/codebase_index/flow_document.rb +191 -0
  106. data/lib/codebase_index/flow_precomputer.rb +102 -0
  107. data/lib/codebase_index/formatting/base.rb +40 -0
  108. data/lib/codebase_index/formatting/claude_adapter.rb +98 -0
  109. data/lib/codebase_index/formatting/generic_adapter.rb +56 -0
  110. data/lib/codebase_index/formatting/gpt_adapter.rb +64 -0
  111. data/lib/codebase_index/formatting/human_adapter.rb +78 -0
  112. data/lib/codebase_index/graph_analyzer.rb +374 -0
  113. data/lib/codebase_index/mcp/index_reader.rb +394 -0
  114. data/lib/codebase_index/mcp/renderers/claude_renderer.rb +81 -0
  115. data/lib/codebase_index/mcp/renderers/json_renderer.rb +17 -0
  116. data/lib/codebase_index/mcp/renderers/markdown_renderer.rb +352 -0
  117. data/lib/codebase_index/mcp/renderers/plain_renderer.rb +240 -0
  118. data/lib/codebase_index/mcp/server.rb +935 -0
  119. data/lib/codebase_index/mcp/tool_response_renderer.rb +62 -0
  120. data/lib/codebase_index/model_name_cache.rb +51 -0
  121. data/lib/codebase_index/notion/client.rb +217 -0
  122. data/lib/codebase_index/notion/exporter.rb +219 -0
  123. data/lib/codebase_index/notion/mapper.rb +39 -0
  124. data/lib/codebase_index/notion/mappers/column_mapper.rb +65 -0
  125. data/lib/codebase_index/notion/mappers/migration_mapper.rb +39 -0
  126. data/lib/codebase_index/notion/mappers/model_mapper.rb +164 -0
  127. data/lib/codebase_index/notion/rate_limiter.rb +68 -0
  128. data/lib/codebase_index/observability/health_check.rb +81 -0
  129. data/lib/codebase_index/observability/instrumentation.rb +34 -0
  130. data/lib/codebase_index/observability/structured_logger.rb +75 -0
  131. data/lib/codebase_index/operator/error_escalator.rb +81 -0
  132. data/lib/codebase_index/operator/pipeline_guard.rb +99 -0
  133. data/lib/codebase_index/operator/status_reporter.rb +80 -0
  134. data/lib/codebase_index/railtie.rb +26 -0
  135. data/lib/codebase_index/resilience/circuit_breaker.rb +99 -0
  136. data/lib/codebase_index/resilience/index_validator.rb +185 -0
  137. data/lib/codebase_index/resilience/retryable_provider.rb +108 -0
  138. data/lib/codebase_index/retrieval/context_assembler.rb +249 -0
  139. data/lib/codebase_index/retrieval/query_classifier.rb +131 -0
  140. data/lib/codebase_index/retrieval/ranker.rb +273 -0
  141. data/lib/codebase_index/retrieval/search_executor.rb +327 -0
  142. data/lib/codebase_index/retriever.rb +160 -0
  143. data/lib/codebase_index/ruby_analyzer/class_analyzer.rb +190 -0
  144. data/lib/codebase_index/ruby_analyzer/dataflow_analyzer.rb +78 -0
  145. data/lib/codebase_index/ruby_analyzer/fqn_builder.rb +18 -0
  146. data/lib/codebase_index/ruby_analyzer/mermaid_renderer.rb +275 -0
  147. data/lib/codebase_index/ruby_analyzer/method_analyzer.rb +143 -0
  148. data/lib/codebase_index/ruby_analyzer/trace_enricher.rb +139 -0
  149. data/lib/codebase_index/ruby_analyzer.rb +87 -0
  150. data/lib/codebase_index/session_tracer/file_store.rb +111 -0
  151. data/lib/codebase_index/session_tracer/middleware.rb +143 -0
  152. data/lib/codebase_index/session_tracer/redis_store.rb +112 -0
  153. data/lib/codebase_index/session_tracer/session_flow_assembler.rb +263 -0
  154. data/lib/codebase_index/session_tracer/session_flow_document.rb +223 -0
  155. data/lib/codebase_index/session_tracer/solid_cache_store.rb +145 -0
  156. data/lib/codebase_index/session_tracer/store.rb +67 -0
  157. data/lib/codebase_index/storage/graph_store.rb +120 -0
  158. data/lib/codebase_index/storage/metadata_store.rb +169 -0
  159. data/lib/codebase_index/storage/pgvector.rb +163 -0
  160. data/lib/codebase_index/storage/qdrant.rb +172 -0
  161. data/lib/codebase_index/storage/vector_store.rb +156 -0
  162. data/lib/codebase_index/temporal/snapshot_store.rb +341 -0
  163. data/lib/codebase_index/version.rb +5 -0
  164. data/lib/codebase_index.rb +223 -0
  165. data/lib/generators/codebase_index/install_generator.rb +32 -0
  166. data/lib/generators/codebase_index/pgvector_generator.rb +37 -0
  167. data/lib/generators/codebase_index/templates/add_pgvector_to_codebase_index.rb.erb +15 -0
  168. data/lib/generators/codebase_index/templates/create_codebase_index_tables.rb.erb +43 -0
  169. data/lib/tasks/codebase_index.rake +583 -0
  170. data/lib/tasks/codebase_index_evaluation.rake +115 -0
  171. metadata +252 -0
@@ -0,0 +1,254 @@
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
+ # ServiceExtractor handles service object extraction.
9
+ #
10
+ # Service objects often contain the most important business logic.
11
+ # Unlike models (which are discovered via ActiveRecord), services
12
+ # are discovered by scanning conventional directories.
13
+ #
14
+ # We extract:
15
+ # - Public interface (call/perform/execute methods)
16
+ # - Dependencies (what models/services/jobs they use)
17
+ # - Error classes (custom exceptions defined)
18
+ # - Input/output patterns
19
+ #
20
+ # @example
21
+ # extractor = ServiceExtractor.new
22
+ # units = extractor.extract_all
23
+ # checkout = units.find { |u| u.identifier == "CheckoutService" }
24
+ #
25
+ class ServiceExtractor
26
+ include SharedUtilityMethods
27
+ include SharedDependencyScanner
28
+
29
+ # Directories to scan for service objects
30
+ SERVICE_DIRECTORIES = %w[
31
+ app/services
32
+ app/interactors
33
+ app/operations
34
+ app/commands
35
+ app/use_cases
36
+ ].freeze
37
+
38
+ def initialize
39
+ @directories = SERVICE_DIRECTORIES.map { |d| Rails.root.join(d) }
40
+ .select(&:directory?)
41
+ end
42
+
43
+ # Extract all service objects
44
+ #
45
+ # @return [Array<ExtractedUnit>] List of service units
46
+ def extract_all
47
+ @directories.flat_map do |dir|
48
+ Dir[dir.join('**/*.rb')].filter_map do |file|
49
+ extract_service_file(file)
50
+ end
51
+ end
52
+ end
53
+
54
+ # Extract a single service file
55
+ #
56
+ # @param file_path [String] Path to the service file
57
+ # @return [ExtractedUnit, nil] The extracted unit or nil if not a service
58
+ def extract_service_file(file_path)
59
+ source = File.read(file_path)
60
+ class_name = extract_class_name(file_path, source)
61
+
62
+ return nil unless class_name
63
+ return nil if skip_file?(source)
64
+
65
+ unit = ExtractedUnit.new(
66
+ type: :service,
67
+ identifier: class_name,
68
+ file_path: file_path
69
+ )
70
+
71
+ unit.namespace = extract_namespace(class_name)
72
+ unit.source_code = annotate_source(source, class_name)
73
+ unit.metadata = extract_metadata(source, class_name, file_path)
74
+ unit.dependencies = extract_dependencies(source)
75
+
76
+ unit
77
+ rescue StandardError => e
78
+ Rails.logger.error("Failed to extract service #{file_path}: #{e.message}")
79
+ nil
80
+ end
81
+
82
+ private
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
+ # ──────────────────────────────────────────────────────────────────────
108
+ # Source Annotation
109
+ # ──────────────────────────────────────────────────────────────────────
110
+
111
+ # Add annotations to help with retrieval
112
+ def annotate_source(source, class_name)
113
+ entry_points = detect_entry_points(source)
114
+
115
+ annotation = <<~ANNOTATION
116
+ # ╔═══════════════════════════════════════════════════════════════════════╗
117
+ # ║ Service: #{class_name.ljust(60)}║
118
+ # ║ Entry Points: #{entry_points.join(', ').ljust(55)}║
119
+ # ╚═══════════════════════════════════════════════════════════════════════╝
120
+
121
+ ANNOTATION
122
+
123
+ annotation + source
124
+ end
125
+
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
+ # ──────────────────────────────────────────────────────────────────────
137
+ # Metadata Extraction
138
+ # ──────────────────────────────────────────────────────────────────────
139
+
140
+ def extract_metadata(source, _class_name, file_path)
141
+ {
142
+ # Entry points
143
+ public_methods: extract_public_methods(source),
144
+ entry_points: detect_entry_points(source),
145
+ class_methods: extract_class_methods(source),
146
+
147
+ # Patterns
148
+ is_callable: source.match?(/def (self\.)?call\b/),
149
+ is_interactor: source.match?(/include\s+Interactor/),
150
+ uses_dry_monads: source.match?(/include\s+Dry::Monads/),
151
+
152
+ # Dependency injection
153
+ initialize_params: extract_initialize_params(source),
154
+ injected_dependencies: extract_injected_deps(source),
155
+
156
+ # Error handling
157
+ custom_errors: extract_custom_errors(source),
158
+ rescues: extract_rescue_handlers(source),
159
+
160
+ # Return patterns
161
+ return_type: infer_return_type(source),
162
+
163
+ # Metrics
164
+ loc: source.lines.count { |l| l.strip.present? && !l.strip.start_with?('#') },
165
+ method_count: source.scan(/def\s+(?:self\.)?\w+/).size,
166
+ complexity: estimate_complexity(source),
167
+
168
+ # Directory context (what kind of service pattern)
169
+ service_type: infer_service_type(file_path)
170
+ }
171
+ end
172
+
173
+ def extract_injected_deps(source)
174
+ # Look for attr_reader/accessor that match common dependency patterns
175
+ deps = []
176
+
177
+ source.scan(/attr_(?:reader|accessor)\s+(.+)/) do |match|
178
+ match[0].scan(/:(\w+)/).flatten.each do |attr|
179
+ deps << attr if attr.match?(/service|repository|client|adapter|gateway|notifier|mailer/)
180
+ end
181
+ end
182
+
183
+ # Also look for initialize assignments
184
+ source.scan(/@(\w+)\s*=\s*(\w+)/) do |ivar, value|
185
+ deps << ivar if value.match?(/Service|Client|Repository|Adapter|Gateway/)
186
+ end
187
+
188
+ deps.uniq
189
+ end
190
+
191
+ def extract_custom_errors(source)
192
+ source.scan(/class\s+(\w+(?:Error|Exception))\s*</).flatten
193
+ end
194
+
195
+ def extract_rescue_handlers(source)
196
+ source.scan(/rescue\s+([\w:]+)/).flatten.uniq
197
+ end
198
+
199
+ def infer_return_type(source)
200
+ return :dry_monad if source.match?(/Success\(|Failure\(/)
201
+ return :result_object if source.match?(/Result\.new|OpenStruct\.new/)
202
+ return :boolean if source.match?(/def call.*?(?:true|false)\s*$/m)
203
+
204
+ :unknown
205
+ end
206
+
207
+ def estimate_complexity(source)
208
+ # Simple cyclomatic complexity estimate
209
+ branches = source.scan(/\b(?:if|unless|elsif|when|while|until|for|rescue|&&|\|\|)\b/).size
210
+ branches + 1
211
+ end
212
+
213
+ def infer_service_type(file_path)
214
+ case file_path
215
+ when /interactors/ then :interactor
216
+ when /operations/ then :operation
217
+ when /commands/ then :command
218
+ when /use_cases/ then :use_case
219
+ else :service
220
+ end
221
+ end
222
+
223
+ # ──────────────────────────────────────────────────────────────────────
224
+ # Dependency Extraction
225
+ # ──────────────────────────────────────────────────────────────────────
226
+
227
+ def extract_dependencies(source)
228
+ deps = scan_common_dependencies(source)
229
+
230
+ # Interactors
231
+ source.scan(/(\w+Interactor)(?:\.|::)/).flatten.uniq.each do |interactor|
232
+ deps << { type: :interactor, target: interactor, via: :code_reference }
233
+ end
234
+
235
+ # External API clients
236
+ source.scan(/(\w+Client)(?:\.|::new)/).flatten.uniq.each do |client|
237
+ deps << { type: :api_client, target: client, via: :code_reference }
238
+ end
239
+
240
+ # HTTP calls
241
+ if source.match?(/HTTParty|Faraday|RestClient|Net::HTTP/)
242
+ deps << { type: :external, target: :http_api, via: :code_reference }
243
+ end
244
+
245
+ # Redis
246
+ if source.match?(/Redis\.current|REDIS|Sidekiq\.redis/)
247
+ deps << { type: :infrastructure, target: :redis, via: :code_reference }
248
+ end
249
+
250
+ deps.uniq { |d| [d[:type], d[:target]] }
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../model_name_cache'
4
+
5
+ module CodebaseIndex
6
+ module Extractors
7
+ # Common dependency scanning patterns shared across extractors.
8
+ #
9
+ # Most extractors scan source code for the same four dependency types:
10
+ # model references (via ModelNameCache), service objects, background jobs,
11
+ # and mailers. This module centralizes those scanning patterns.
12
+ #
13
+ # Individual scan methods accept an optional +:via+ parameter so
14
+ # extractors can customize the relationship label (e.g., +:serialization+
15
+ # instead of the default +:code_reference+).
16
+ #
17
+ # @example
18
+ # class FooExtractor
19
+ # include SharedDependencyScanner
20
+ #
21
+ # def extract_dependencies(source)
22
+ # deps = scan_common_dependencies(source)
23
+ # deps << { type: :custom, target: "Bar", via: :special }
24
+ # deps.uniq { |d| [d[:type], d[:target]] }
25
+ # end
26
+ # end
27
+ #
28
+ module SharedDependencyScanner
29
+ # Scan for ActiveRecord model references using the precomputed regex.
30
+ #
31
+ # @param source [String] Ruby source code to scan
32
+ # @param via [Symbol] Relationship label (default: :code_reference)
33
+ # @return [Array<Hash>] Dependency hashes with :type, :target, :via
34
+ def scan_model_dependencies(source, via: :code_reference)
35
+ source.scan(ModelNameCache.model_names_regex).uniq.map do |model_name|
36
+ { type: :model, target: model_name, via: via }
37
+ end
38
+ end
39
+
40
+ # Scan for service object references (e.g., FooService.call, FooService::new).
41
+ #
42
+ # @param source [String] Ruby source code to scan
43
+ # @param via [Symbol] Relationship label (default: :code_reference)
44
+ # @return [Array<Hash>] Dependency hashes
45
+ def scan_service_dependencies(source, via: :code_reference)
46
+ source.scan(/(\w+Service)(?:\.|::)/).flatten.uniq.map do |service|
47
+ { type: :service, target: service, via: via }
48
+ end
49
+ end
50
+
51
+ # Scan for background job references (e.g., FooJob.perform_later).
52
+ #
53
+ # @param source [String] Ruby source code to scan
54
+ # @param via [Symbol] Relationship label (default: :code_reference)
55
+ # @return [Array<Hash>] Dependency hashes
56
+ def scan_job_dependencies(source, via: :code_reference)
57
+ source.scan(/(\w+Job)\.perform/).flatten.uniq.map do |job|
58
+ { type: :job, target: job, via: via }
59
+ end
60
+ end
61
+
62
+ # Scan for mailer references (e.g., UserMailer.welcome_email).
63
+ #
64
+ # @param source [String] Ruby source code to scan
65
+ # @param via [Symbol] Relationship label (default: :code_reference)
66
+ # @return [Array<Hash>] Dependency hashes
67
+ def scan_mailer_dependencies(source, via: :code_reference)
68
+ source.scan(/(\w+Mailer)\./).flatten.uniq.map do |mailer|
69
+ { type: :mailer, target: mailer, via: via }
70
+ end
71
+ end
72
+
73
+ # Scan for all common dependency types and return a deduplicated array.
74
+ #
75
+ # Combines model, service, job, and mailer scans. Use this when an
76
+ # extractor needs all four standard dependency types with the default
77
+ # +:code_reference+ via label.
78
+ #
79
+ # @param source [String] Ruby source code to scan
80
+ # @return [Array<Hash>] Deduplicated dependency hashes
81
+ def scan_common_dependencies(source)
82
+ deps = []
83
+ deps.concat(scan_model_dependencies(source))
84
+ deps.concat(scan_service_dependencies(source))
85
+ deps.concat(scan_job_dependencies(source))
86
+ deps.concat(scan_mailer_dependencies(source))
87
+ deps.uniq { |d| [d[:type], d[:target]] }
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CodebaseIndex
4
+ module Extractors
5
+ # Utility methods shared across multiple extractors.
6
+ #
7
+ # Provides common helpers for namespace extraction, public method
8
+ # scanning, class method scanning, and initialize parameter parsing.
9
+ # These methods are duplicated across 4-11 extractors; this module
10
+ # centralizes them.
11
+ #
12
+ # @example
13
+ # class FooExtractor
14
+ # include SharedUtilityMethods
15
+ #
16
+ # def extract_foo(klass)
17
+ # namespace = extract_namespace(klass)
18
+ # # ...
19
+ # end
20
+ # end
21
+ #
22
+ module SharedUtilityMethods
23
+ # Extract namespace from a class name string or class object.
24
+ #
25
+ # Handles both string input (e.g., "Payments::StripeService")
26
+ # and class object input (e.g., a Controller class).
27
+ #
28
+ # @param name_or_object [String, Class, Module] A class name or class object
29
+ # @return [String, nil] The namespace, or nil if top-level
30
+ def extract_namespace(name_or_object)
31
+ name = name_or_object.is_a?(String) ? name_or_object : name_or_object.name
32
+ parts = name.split('::')
33
+ parts.size > 1 ? parts[0..-2].join('::') : nil
34
+ end
35
+
36
+ # Extract public instance and class methods from source code.
37
+ #
38
+ # Walks source line-by-line tracking private/protected visibility.
39
+ # Returns method names that are in public scope and don't start with underscore.
40
+ #
41
+ # @param source [String] Ruby source code
42
+ # @return [Array<String>] Public method names
43
+ def extract_public_methods(source)
44
+ methods = []
45
+ in_private = false
46
+ in_protected = false
47
+
48
+ source.each_line do |line|
49
+ stripped = line.strip
50
+
51
+ in_private = true if stripped == 'private'
52
+ in_protected = true if stripped == 'protected'
53
+ in_private = false if stripped == 'public'
54
+ in_protected = false if stripped == 'public'
55
+
56
+ if !in_private && !in_protected && stripped =~ /def\s+((?:self\.)?\w+[?!=]?)/
57
+ method_name = ::Regexp.last_match(1)
58
+ methods << method_name unless method_name.start_with?('_')
59
+ end
60
+ end
61
+
62
+ methods
63
+ end
64
+
65
+ # Extract class-level (self.) method names from source code.
66
+ #
67
+ # @param source [String] Ruby source code
68
+ # @return [Array<String>] Class method names
69
+ def extract_class_methods(source)
70
+ source.scan(/def\s+self\.(\w+[?!=]?)/).flatten
71
+ end
72
+
73
+ # Extract initialize parameters from source code.
74
+ #
75
+ # Parses the parameter list of the initialize method to determine
76
+ # parameter names, defaults, and whether they are keyword arguments.
77
+ #
78
+ # @param source [String] Ruby source code
79
+ # @return [Array<Hash>] Parameter info hashes with :name, :has_default, :keyword
80
+ def extract_initialize_params(source)
81
+ init_match = source.match(/def\s+initialize\s*\((.*?)\)/m)
82
+ return [] unless init_match
83
+
84
+ params_str = init_match[1]
85
+ params = []
86
+
87
+ params_str.scan(/(\w+)(?::\s*([^,\n]+))?/) do |name, default|
88
+ params << {
89
+ name: name,
90
+ has_default: !default.nil?,
91
+ keyword: params_str.include?("#{name}:")
92
+ }
93
+ end
94
+
95
+ params
96
+ end
97
+ end
98
+ end
99
+ end