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,253 @@
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
+ # ConcernExtractor handles ActiveSupport::Concern module extraction.
9
+ #
10
+ # Concerns are mixins that extend model and controller behavior.
11
+ # They live in `app/models/concerns/` and `app/controllers/concerns/`.
12
+ #
13
+ # We extract:
14
+ # - Module name and namespace
15
+ # - Included/extended hooks and class methods block
16
+ # - Instance methods and class methods added by the concern
17
+ # - Dependencies on models and other concerns
18
+ #
19
+ # @example
20
+ # extractor = ConcernExtractor.new
21
+ # units = extractor.extract_all
22
+ # searchable = units.find { |u| u.identifier == "Searchable" }
23
+ #
24
+ class ConcernExtractor
25
+ include SharedUtilityMethods
26
+ include SharedDependencyScanner
27
+
28
+ # Directories to scan for concern modules
29
+ CONCERN_DIRECTORIES = %w[
30
+ app/models/concerns
31
+ app/controllers/concerns
32
+ ].freeze
33
+
34
+ def initialize
35
+ @directories = CONCERN_DIRECTORIES.map { |d| Rails.root.join(d) }
36
+ .select(&:directory?)
37
+ end
38
+
39
+ # Extract all concern modules
40
+ #
41
+ # @return [Array<ExtractedUnit>] List of concern units
42
+ def extract_all
43
+ @directories.flat_map do |dir|
44
+ Dir[dir.join('**/*.rb')].filter_map do |file|
45
+ extract_concern_file(file)
46
+ end
47
+ end
48
+ end
49
+
50
+ # Extract a single concern file
51
+ #
52
+ # @param file_path [String] Path to the concern file
53
+ # @return [ExtractedUnit, nil] The extracted unit or nil if not a concern
54
+ def extract_concern_file(file_path)
55
+ source = File.read(file_path)
56
+ module_name = extract_module_name(file_path, source)
57
+
58
+ return nil unless module_name
59
+ return nil unless concern_module?(source)
60
+
61
+ unit = ExtractedUnit.new(
62
+ type: :concern,
63
+ identifier: module_name,
64
+ file_path: file_path
65
+ )
66
+
67
+ unit.namespace = extract_namespace(module_name)
68
+ unit.source_code = annotate_source(source, module_name)
69
+ unit.metadata = extract_metadata(source, file_path)
70
+ unit.dependencies = extract_dependencies(source)
71
+
72
+ unit
73
+ rescue StandardError => e
74
+ Rails.logger.error("Failed to extract concern #{file_path}: #{e.message}")
75
+ nil
76
+ end
77
+
78
+ private
79
+
80
+ # ──────────────────────────────────────────────────────────────────────
81
+ # Module Discovery
82
+ # ──────────────────────────────────────────────────────────────────────
83
+
84
+ # Extract the module name from source or infer from file path.
85
+ #
86
+ # @param file_path [String] Path to the concern file
87
+ # @param source [String] Ruby source code
88
+ # @return [String, nil] The module name
89
+ def extract_module_name(file_path, source)
90
+ # Try to find the outermost module definition
91
+ modules = source.scan(/^\s*module\s+([\w:]+)/).flatten
92
+ return modules.last if modules.any?
93
+
94
+ # Infer from file path
95
+ relative = file_path.sub("#{Rails.root}/", '')
96
+ relative
97
+ .sub(%r{^app/(models|controllers)/concerns/}, '')
98
+ .sub('.rb', '')
99
+ .split('/')
100
+ .map { |segment| segment.split('_').map(&:capitalize).join }
101
+ .join('::')
102
+ end
103
+
104
+ # Detect whether source defines an ActiveSupport::Concern or a plain mixin.
105
+ #
106
+ # @param source [String] Ruby source code
107
+ # @return [Boolean]
108
+ def concern_module?(source)
109
+ # ActiveSupport::Concern usage or plain module with methods
110
+ source.match?(/^\s*module\s+/) &&
111
+ (source.match?(/extend\s+ActiveSupport::Concern/) ||
112
+ source.match?(/included\s+do/) ||
113
+ source.match?(/class_methods\s+do/) ||
114
+ source.match?(/def\s+\w+/))
115
+ end
116
+
117
+ # ──────────────────────────────────────────────────────────────────────
118
+ # Source Annotation
119
+ # ──────────────────────────────────────────────────────────────────────
120
+
121
+ # @param source [String] Ruby source code
122
+ # @param module_name [String] The concern module name
123
+ # @return [String] Annotated source
124
+ def annotate_source(source, module_name)
125
+ concern_type = detect_concern_type(source)
126
+ instance_methods = extract_instance_method_names(source)
127
+
128
+ <<~ANNOTATION
129
+ # ╔═══════════════════════════════════════════════════════════════════════╗
130
+ # ║ Concern: #{module_name.ljust(59)}║
131
+ # ║ Type: #{concern_type.ljust(62)}║
132
+ # ║ Methods: #{instance_methods.join(', ').ljust(59)}║
133
+ # ╚═══════════════════════════════════════════════════════════════════════╝
134
+
135
+ #{source}
136
+ ANNOTATION
137
+ end
138
+
139
+ # ──────────────────────────────────────────────────────────────────────
140
+ # Metadata Extraction
141
+ # ──────────────────────────────────────────────────────────────────────
142
+
143
+ # @param source [String] Ruby source code
144
+ # @param file_path [String] Path to the concern file
145
+ # @return [Hash] Concern metadata
146
+ def extract_metadata(source, file_path)
147
+ {
148
+ concern_type: detect_concern_type(source),
149
+ concern_scope: detect_concern_scope(file_path),
150
+ uses_active_support: source.match?(/extend\s+ActiveSupport::Concern/),
151
+ has_included_block: source.match?(/included\s+do/) || false,
152
+ has_class_methods_block: source.match?(/class_methods\s+do/) || false,
153
+ included_modules: detect_included_modules(source),
154
+ instance_methods: extract_instance_method_names(source),
155
+ class_methods: extract_class_methods(source),
156
+ public_methods: extract_public_methods(source),
157
+ callbacks_defined: detect_callbacks(source),
158
+ scopes_defined: detect_scopes(source),
159
+ validations_defined: detect_validations(source),
160
+ loc: source.lines.count { |l| l.strip.length.positive? && !l.strip.start_with?('#') },
161
+ method_count: source.scan(/def\s+(?:self\.)?\w+/).size
162
+ }
163
+ end
164
+
165
+ # Detect whether this is a model concern, controller concern, or generic.
166
+ #
167
+ # @param source [String] Ruby source code
168
+ # @return [String] One of "active_support", "plain_mixin"
169
+ def detect_concern_type(source)
170
+ if source.match?(/extend\s+ActiveSupport::Concern/)
171
+ 'active_support'
172
+ else
173
+ 'plain_mixin'
174
+ end
175
+ end
176
+
177
+ # Detect concern scope from file path (model vs controller).
178
+ #
179
+ # @param file_path [String] Path to the concern file
180
+ # @return [String] One of "model", "controller", "unknown"
181
+ def detect_concern_scope(file_path)
182
+ if file_path.include?('app/models/concerns')
183
+ 'model'
184
+ elsif file_path.include?('app/controllers/concerns')
185
+ 'controller'
186
+ else
187
+ 'unknown'
188
+ end
189
+ end
190
+
191
+ # Extract instance method names (not self. methods).
192
+ #
193
+ # @param source [String] Ruby source code
194
+ # @return [Array<String>] Instance method names
195
+ def extract_instance_method_names(source)
196
+ source.scan(/^\s*def\s+(\w+[?!=]?)/).flatten.reject { |m| m.start_with?('self.') }
197
+ end
198
+
199
+ # Detect other modules included by this concern.
200
+ #
201
+ # @param source [String] Ruby source code
202
+ # @return [Array<String>] Module names
203
+ def detect_included_modules(source)
204
+ source.scan(/(?:include|extend)\s+([\w:]+)/).flatten
205
+ .reject { |m| m == 'ActiveSupport::Concern' }
206
+ end
207
+
208
+ # Detect callback declarations.
209
+ #
210
+ # @param source [String] Ruby source code
211
+ # @return [Array<String>] Callback names
212
+ def detect_callbacks(source)
213
+ source.scan(/(before_\w+|after_\w+|around_\w+)\s/).flatten.uniq
214
+ end
215
+
216
+ # Detect scope declarations.
217
+ #
218
+ # @param source [String] Ruby source code
219
+ # @return [Array<String>] Scope names
220
+ def detect_scopes(source)
221
+ source.scan(/scope\s+:(\w+)/).flatten
222
+ end
223
+
224
+ # Detect validation declarations.
225
+ #
226
+ # @param source [String] Ruby source code
227
+ # @return [Array<String>] Validation types
228
+ def detect_validations(source)
229
+ source.scan(/(validates?(?:_\w+)?)\s/).flatten.uniq
230
+ end
231
+
232
+ # ──────────────────────────────────────────────────────────────────────
233
+ # Dependency Extraction
234
+ # ──────────────────────────────────────────────────────────────────────
235
+
236
+ # @param source [String] Ruby source code
237
+ # @return [Array<Hash>] Dependency hashes
238
+ def extract_dependencies(source)
239
+ # Other concerns included by this concern
240
+ deps = detect_included_modules(source).map do |mod|
241
+ { type: :concern, target: mod, via: :include }
242
+ end
243
+
244
+ # Standard dependency scanning
245
+ deps.concat(scan_model_dependencies(source))
246
+ deps.concat(scan_service_dependencies(source))
247
+ deps.concat(scan_job_dependencies(source))
248
+
249
+ deps.uniq { |d| [d[:type], d[:target]] }
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'shared_utility_methods'
4
+ require_relative 'shared_dependency_scanner'
5
+ require_relative 'behavioral_profile'
6
+
7
+ module CodebaseIndex
8
+ module Extractors
9
+ # ConfigurationExtractor handles Rails configuration file extraction.
10
+ #
11
+ # Scans `config/initializers/` and `config/environments/` for Ruby
12
+ # configuration files. Each file becomes one ExtractedUnit with metadata
13
+ # about config type, gem references, and detected settings.
14
+ #
15
+ # @example
16
+ # extractor = ConfigurationExtractor.new
17
+ # units = extractor.extract_all
18
+ # devise = units.find { |u| u.identifier == "initializers/devise.rb" }
19
+ #
20
+ class ConfigurationExtractor
21
+ include SharedUtilityMethods
22
+ include SharedDependencyScanner
23
+
24
+ # Directories to scan for configuration files
25
+ CONFIG_DIRECTORIES = %w[
26
+ config/initializers
27
+ config/environments
28
+ ].freeze
29
+
30
+ def initialize
31
+ @directories = CONFIG_DIRECTORIES.map { |d| Rails.root.join(d) }
32
+ .select(&:directory?)
33
+ end
34
+
35
+ # Extract all configuration files and the behavioral profile.
36
+ #
37
+ # @return [Array<ExtractedUnit>] List of configuration units
38
+ def extract_all
39
+ units = @directories.flat_map do |dir|
40
+ Dir[dir.join('**/*.rb')].filter_map do |file|
41
+ extract_configuration_file(file)
42
+ end
43
+ end
44
+
45
+ profile = BehavioralProfile.new.extract
46
+ units << profile if profile
47
+
48
+ units
49
+ rescue StandardError => e
50
+ Rails.logger.error("BehavioralProfile integration failed: #{e.message}")
51
+ units || []
52
+ end
53
+
54
+ # Extract a single configuration file
55
+ #
56
+ # @param file_path [String] Path to the configuration file
57
+ # @return [ExtractedUnit, nil] The extracted unit or nil on failure
58
+ def extract_configuration_file(file_path)
59
+ source = File.read(file_path)
60
+ identifier = build_identifier(file_path)
61
+ config_type = detect_config_type(file_path)
62
+
63
+ unit = ExtractedUnit.new(
64
+ type: :configuration,
65
+ identifier: identifier,
66
+ file_path: file_path
67
+ )
68
+
69
+ unit.namespace = config_type
70
+ unit.source_code = annotate_source(source, identifier, config_type)
71
+ unit.metadata = extract_metadata(source, config_type)
72
+ unit.dependencies = extract_dependencies(source)
73
+
74
+ unit
75
+ rescue StandardError => e
76
+ Rails.logger.error("Failed to extract configuration #{file_path}: #{e.message}")
77
+ nil
78
+ end
79
+
80
+ private
81
+
82
+ # ──────────────────────────────────────────────────────────────────────
83
+ # Identification
84
+ # ──────────────────────────────────────────────────────────────────────
85
+
86
+ # Build a readable identifier from the file path.
87
+ #
88
+ # @param file_path [String]
89
+ # @return [String] e.g., "initializers/devise.rb" or "environments/production.rb"
90
+ def build_identifier(file_path)
91
+ relative = file_path.sub("#{Rails.root}/", '')
92
+ relative.sub(%r{^config/}, '')
93
+ end
94
+
95
+ # Detect whether this is an initializer or environment config.
96
+ #
97
+ # @param file_path [String]
98
+ # @return [String]
99
+ def detect_config_type(file_path)
100
+ if file_path.include?('config/initializers')
101
+ 'initializer'
102
+ elsif file_path.include?('config/environments')
103
+ 'environment'
104
+ else
105
+ 'configuration'
106
+ end
107
+ end
108
+
109
+ # ──────────────────────────────────────────────────────────────────────
110
+ # Source Annotation
111
+ # ──────────────────────────────────────────────────────────────────────
112
+
113
+ # @param source [String]
114
+ # @param identifier [String]
115
+ # @param config_type [String]
116
+ # @return [String]
117
+ def annotate_source(source, identifier, config_type)
118
+ gem_refs = detect_gem_references(source)
119
+
120
+ <<~ANNOTATION
121
+ # ╔═══════════════════════════════════════════════════════════════════════╗
122
+ # ║ Configuration: #{identifier.ljust(53)}║
123
+ # ║ Type: #{config_type.ljust(62)}║
124
+ # ║ Gems: #{gem_refs.join(', ').ljust(62)}║
125
+ # ╚═══════════════════════════════════════════════════════════════════════╝
126
+
127
+ #{source}
128
+ ANNOTATION
129
+ end
130
+
131
+ # ──────────────────────────────────────────────────────────────────────
132
+ # Metadata Extraction
133
+ # ──────────────────────────────────────────────────────────────────────
134
+
135
+ # @param source [String]
136
+ # @param config_type [String]
137
+ # @return [Hash]
138
+ def extract_metadata(source, config_type)
139
+ {
140
+ config_type: config_type,
141
+ gem_references: detect_gem_references(source),
142
+ config_settings: detect_config_settings(source),
143
+ rails_config_blocks: detect_rails_config_blocks(source),
144
+ loc: source.lines.count { |l| l.strip.length.positive? && !l.strip.start_with?('#') },
145
+ method_count: source.scan(/def\s+(?:self\.)?\w+/).size
146
+ }
147
+ end
148
+
149
+ # Detect gem/library references in configuration.
150
+ #
151
+ # @param source [String]
152
+ # @return [Array<String>]
153
+ def detect_gem_references(source)
154
+ refs = []
155
+
156
+ # Gem.configure style: Devise.setup, Sidekiq.configure_server
157
+ source.scan(/(\w+)\.(setup|configure\w*|config)\b/).each do |match|
158
+ name = match[0]
159
+ refs << name unless generic_config_name?(name)
160
+ end
161
+
162
+ # require statements for gems
163
+ source.scan(/require\s+['"]([^'"]+)['"]/).each do |match|
164
+ refs << match[0]
165
+ end
166
+
167
+ refs.uniq
168
+ end
169
+
170
+ # Detect configuration settings (key = value patterns).
171
+ #
172
+ # @param source [String]
173
+ # @return [Array<String>]
174
+ def detect_config_settings(source)
175
+ # config.something = value
176
+ settings = source.scan(/config\.(\w+(?:\.\w+)*)\s*=/).map { |match| match[0] }
177
+
178
+ # self.something = value (inside configure blocks)
179
+ settings.concat(source.scan(/(?:self|config)\.(\w+)\s*=/).map { |match| match[0] })
180
+
181
+ settings.uniq
182
+ end
183
+
184
+ # Detect Rails.application.configure or similar blocks.
185
+ #
186
+ # @param source [String]
187
+ # @return [Array<String>]
188
+ def detect_rails_config_blocks(source)
189
+ source.scan(/(Rails\.application\.configure|Rails\.application\.config\.\w+)/)
190
+ .map { |match| match[0] }
191
+ .uniq
192
+ end
193
+
194
+ # Check if a name is too generic to be a gem reference.
195
+ #
196
+ # @param name [String]
197
+ # @return [Boolean]
198
+ def generic_config_name?(name)
199
+ %w[Rails ActiveRecord ActiveJob ActionMailer ActionController ActiveStorage ActionCable].include?(name)
200
+ end
201
+
202
+ # ──────────────────────────────────────────────────────────────────────
203
+ # Dependency Extraction
204
+ # ──────────────────────────────────────────────────────────────────────
205
+
206
+ # @param source [String]
207
+ # @return [Array<Hash>]
208
+ def extract_dependencies(source)
209
+ deps = detect_gem_references(source).map do |gem_ref|
210
+ { type: :gem, target: gem_ref, via: :configuration }
211
+ end
212
+
213
+ deps.concat(scan_service_dependencies(source))
214
+
215
+ deps.uniq { |d| [d[:type], d[:target]] }
216
+ end
217
+ end
218
+ end
219
+ end