codebase_index 0.3.2 → 0.4.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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/lib/codebase_index.rb +3 -243
  3. metadata +28 -223
  4. data/CHANGELOG.md +0 -89
  5. data/CODE_OF_CONDUCT.md +0 -83
  6. data/CONTRIBUTING.md +0 -65
  7. data/LICENSE.txt +0 -21
  8. data/README.md +0 -325
  9. data/exe/codebase-console +0 -59
  10. data/exe/codebase-console-mcp +0 -22
  11. data/exe/codebase-index-mcp +0 -34
  12. data/exe/codebase-index-mcp-http +0 -37
  13. data/exe/codebase-index-mcp-start +0 -58
  14. data/lib/codebase_index/ast/call_site_extractor.rb +0 -106
  15. data/lib/codebase_index/ast/method_extractor.rb +0 -71
  16. data/lib/codebase_index/ast/node.rb +0 -116
  17. data/lib/codebase_index/ast/parser.rb +0 -614
  18. data/lib/codebase_index/ast.rb +0 -6
  19. data/lib/codebase_index/builder.rb +0 -200
  20. data/lib/codebase_index/cache/cache_middleware.rb +0 -199
  21. data/lib/codebase_index/cache/cache_store.rb +0 -264
  22. data/lib/codebase_index/cache/redis_cache_store.rb +0 -116
  23. data/lib/codebase_index/cache/solid_cache_store.rb +0 -111
  24. data/lib/codebase_index/chunking/chunk.rb +0 -84
  25. data/lib/codebase_index/chunking/semantic_chunker.rb +0 -295
  26. data/lib/codebase_index/console/adapters/cache_adapter.rb +0 -58
  27. data/lib/codebase_index/console/adapters/good_job_adapter.rb +0 -33
  28. data/lib/codebase_index/console/adapters/job_adapter.rb +0 -68
  29. data/lib/codebase_index/console/adapters/sidekiq_adapter.rb +0 -33
  30. data/lib/codebase_index/console/adapters/solid_queue_adapter.rb +0 -33
  31. data/lib/codebase_index/console/audit_logger.rb +0 -75
  32. data/lib/codebase_index/console/bridge.rb +0 -177
  33. data/lib/codebase_index/console/confirmation.rb +0 -90
  34. data/lib/codebase_index/console/connection_manager.rb +0 -173
  35. data/lib/codebase_index/console/console_response_renderer.rb +0 -74
  36. data/lib/codebase_index/console/embedded_executor.rb +0 -373
  37. data/lib/codebase_index/console/model_validator.rb +0 -81
  38. data/lib/codebase_index/console/rack_middleware.rb +0 -87
  39. data/lib/codebase_index/console/safe_context.rb +0 -82
  40. data/lib/codebase_index/console/server.rb +0 -612
  41. data/lib/codebase_index/console/sql_validator.rb +0 -172
  42. data/lib/codebase_index/console/tools/tier1.rb +0 -118
  43. data/lib/codebase_index/console/tools/tier2.rb +0 -117
  44. data/lib/codebase_index/console/tools/tier3.rb +0 -110
  45. data/lib/codebase_index/console/tools/tier4.rb +0 -79
  46. data/lib/codebase_index/coordination/pipeline_lock.rb +0 -109
  47. data/lib/codebase_index/cost_model/embedding_cost.rb +0 -88
  48. data/lib/codebase_index/cost_model/estimator.rb +0 -128
  49. data/lib/codebase_index/cost_model/provider_pricing.rb +0 -67
  50. data/lib/codebase_index/cost_model/storage_cost.rb +0 -52
  51. data/lib/codebase_index/cost_model.rb +0 -22
  52. data/lib/codebase_index/db/migrations/001_create_units.rb +0 -38
  53. data/lib/codebase_index/db/migrations/002_create_edges.rb +0 -35
  54. data/lib/codebase_index/db/migrations/003_create_embeddings.rb +0 -37
  55. data/lib/codebase_index/db/migrations/004_create_snapshots.rb +0 -45
  56. data/lib/codebase_index/db/migrations/005_create_snapshot_units.rb +0 -40
  57. data/lib/codebase_index/db/migrator.rb +0 -71
  58. data/lib/codebase_index/db/schema_version.rb +0 -73
  59. data/lib/codebase_index/dependency_graph.rb +0 -236
  60. data/lib/codebase_index/embedding/indexer.rb +0 -140
  61. data/lib/codebase_index/embedding/openai.rb +0 -126
  62. data/lib/codebase_index/embedding/provider.rb +0 -162
  63. data/lib/codebase_index/embedding/text_preparer.rb +0 -112
  64. data/lib/codebase_index/evaluation/baseline_runner.rb +0 -115
  65. data/lib/codebase_index/evaluation/evaluator.rb +0 -139
  66. data/lib/codebase_index/evaluation/metrics.rb +0 -79
  67. data/lib/codebase_index/evaluation/query_set.rb +0 -148
  68. data/lib/codebase_index/evaluation/report_generator.rb +0 -90
  69. data/lib/codebase_index/extracted_unit.rb +0 -145
  70. data/lib/codebase_index/extractor.rb +0 -1028
  71. data/lib/codebase_index/extractors/action_cable_extractor.rb +0 -201
  72. data/lib/codebase_index/extractors/ast_source_extraction.rb +0 -46
  73. data/lib/codebase_index/extractors/behavioral_profile.rb +0 -309
  74. data/lib/codebase_index/extractors/caching_extractor.rb +0 -261
  75. data/lib/codebase_index/extractors/callback_analyzer.rb +0 -246
  76. data/lib/codebase_index/extractors/concern_extractor.rb +0 -292
  77. data/lib/codebase_index/extractors/configuration_extractor.rb +0 -219
  78. data/lib/codebase_index/extractors/controller_extractor.rb +0 -404
  79. data/lib/codebase_index/extractors/database_view_extractor.rb +0 -278
  80. data/lib/codebase_index/extractors/decorator_extractor.rb +0 -253
  81. data/lib/codebase_index/extractors/engine_extractor.rb +0 -223
  82. data/lib/codebase_index/extractors/event_extractor.rb +0 -211
  83. data/lib/codebase_index/extractors/factory_extractor.rb +0 -289
  84. data/lib/codebase_index/extractors/graphql_extractor.rb +0 -892
  85. data/lib/codebase_index/extractors/i18n_extractor.rb +0 -117
  86. data/lib/codebase_index/extractors/job_extractor.rb +0 -374
  87. data/lib/codebase_index/extractors/lib_extractor.rb +0 -218
  88. data/lib/codebase_index/extractors/mailer_extractor.rb +0 -269
  89. data/lib/codebase_index/extractors/manager_extractor.rb +0 -188
  90. data/lib/codebase_index/extractors/middleware_extractor.rb +0 -133
  91. data/lib/codebase_index/extractors/migration_extractor.rb +0 -469
  92. data/lib/codebase_index/extractors/model_extractor.rb +0 -988
  93. data/lib/codebase_index/extractors/phlex_extractor.rb +0 -252
  94. data/lib/codebase_index/extractors/policy_extractor.rb +0 -191
  95. data/lib/codebase_index/extractors/poro_extractor.rb +0 -229
  96. data/lib/codebase_index/extractors/pundit_extractor.rb +0 -223
  97. data/lib/codebase_index/extractors/rails_source_extractor.rb +0 -473
  98. data/lib/codebase_index/extractors/rake_task_extractor.rb +0 -343
  99. data/lib/codebase_index/extractors/route_extractor.rb +0 -181
  100. data/lib/codebase_index/extractors/scheduled_job_extractor.rb +0 -331
  101. data/lib/codebase_index/extractors/serializer_extractor.rb +0 -339
  102. data/lib/codebase_index/extractors/service_extractor.rb +0 -217
  103. data/lib/codebase_index/extractors/shared_dependency_scanner.rb +0 -91
  104. data/lib/codebase_index/extractors/shared_utility_methods.rb +0 -281
  105. data/lib/codebase_index/extractors/state_machine_extractor.rb +0 -398
  106. data/lib/codebase_index/extractors/test_mapping_extractor.rb +0 -225
  107. data/lib/codebase_index/extractors/validator_extractor.rb +0 -211
  108. data/lib/codebase_index/extractors/view_component_extractor.rb +0 -311
  109. data/lib/codebase_index/extractors/view_template_extractor.rb +0 -261
  110. data/lib/codebase_index/feedback/gap_detector.rb +0 -89
  111. data/lib/codebase_index/feedback/store.rb +0 -119
  112. data/lib/codebase_index/filename_utils.rb +0 -32
  113. data/lib/codebase_index/flow_analysis/operation_extractor.rb +0 -206
  114. data/lib/codebase_index/flow_analysis/response_code_mapper.rb +0 -154
  115. data/lib/codebase_index/flow_assembler.rb +0 -290
  116. data/lib/codebase_index/flow_document.rb +0 -191
  117. data/lib/codebase_index/flow_precomputer.rb +0 -102
  118. data/lib/codebase_index/formatting/base.rb +0 -30
  119. data/lib/codebase_index/formatting/claude_adapter.rb +0 -98
  120. data/lib/codebase_index/formatting/generic_adapter.rb +0 -56
  121. data/lib/codebase_index/formatting/gpt_adapter.rb +0 -64
  122. data/lib/codebase_index/formatting/human_adapter.rb +0 -78
  123. data/lib/codebase_index/graph_analyzer.rb +0 -374
  124. data/lib/codebase_index/mcp/bootstrapper.rb +0 -96
  125. data/lib/codebase_index/mcp/index_reader.rb +0 -394
  126. data/lib/codebase_index/mcp/renderers/claude_renderer.rb +0 -81
  127. data/lib/codebase_index/mcp/renderers/json_renderer.rb +0 -17
  128. data/lib/codebase_index/mcp/renderers/markdown_renderer.rb +0 -353
  129. data/lib/codebase_index/mcp/renderers/plain_renderer.rb +0 -240
  130. data/lib/codebase_index/mcp/server.rb +0 -961
  131. data/lib/codebase_index/mcp/tool_response_renderer.rb +0 -85
  132. data/lib/codebase_index/model_name_cache.rb +0 -51
  133. data/lib/codebase_index/notion/client.rb +0 -217
  134. data/lib/codebase_index/notion/exporter.rb +0 -219
  135. data/lib/codebase_index/notion/mapper.rb +0 -40
  136. data/lib/codebase_index/notion/mappers/column_mapper.rb +0 -57
  137. data/lib/codebase_index/notion/mappers/migration_mapper.rb +0 -39
  138. data/lib/codebase_index/notion/mappers/model_mapper.rb +0 -161
  139. data/lib/codebase_index/notion/mappers/shared.rb +0 -22
  140. data/lib/codebase_index/notion/rate_limiter.rb +0 -68
  141. data/lib/codebase_index/observability/health_check.rb +0 -79
  142. data/lib/codebase_index/observability/instrumentation.rb +0 -34
  143. data/lib/codebase_index/observability/structured_logger.rb +0 -57
  144. data/lib/codebase_index/operator/error_escalator.rb +0 -81
  145. data/lib/codebase_index/operator/pipeline_guard.rb +0 -92
  146. data/lib/codebase_index/operator/status_reporter.rb +0 -80
  147. data/lib/codebase_index/railtie.rb +0 -38
  148. data/lib/codebase_index/resilience/circuit_breaker.rb +0 -99
  149. data/lib/codebase_index/resilience/index_validator.rb +0 -167
  150. data/lib/codebase_index/resilience/retryable_provider.rb +0 -108
  151. data/lib/codebase_index/retrieval/context_assembler.rb +0 -261
  152. data/lib/codebase_index/retrieval/query_classifier.rb +0 -133
  153. data/lib/codebase_index/retrieval/ranker.rb +0 -277
  154. data/lib/codebase_index/retrieval/search_executor.rb +0 -316
  155. data/lib/codebase_index/retriever.rb +0 -152
  156. data/lib/codebase_index/ruby_analyzer/class_analyzer.rb +0 -170
  157. data/lib/codebase_index/ruby_analyzer/dataflow_analyzer.rb +0 -77
  158. data/lib/codebase_index/ruby_analyzer/fqn_builder.rb +0 -18
  159. data/lib/codebase_index/ruby_analyzer/mermaid_renderer.rb +0 -280
  160. data/lib/codebase_index/ruby_analyzer/method_analyzer.rb +0 -143
  161. data/lib/codebase_index/ruby_analyzer/trace_enricher.rb +0 -143
  162. data/lib/codebase_index/ruby_analyzer.rb +0 -87
  163. data/lib/codebase_index/session_tracer/file_store.rb +0 -104
  164. data/lib/codebase_index/session_tracer/middleware.rb +0 -143
  165. data/lib/codebase_index/session_tracer/redis_store.rb +0 -106
  166. data/lib/codebase_index/session_tracer/session_flow_assembler.rb +0 -254
  167. data/lib/codebase_index/session_tracer/session_flow_document.rb +0 -223
  168. data/lib/codebase_index/session_tracer/solid_cache_store.rb +0 -139
  169. data/lib/codebase_index/session_tracer/store.rb +0 -81
  170. data/lib/codebase_index/storage/graph_store.rb +0 -120
  171. data/lib/codebase_index/storage/metadata_store.rb +0 -196
  172. data/lib/codebase_index/storage/pgvector.rb +0 -195
  173. data/lib/codebase_index/storage/qdrant.rb +0 -205
  174. data/lib/codebase_index/storage/vector_store.rb +0 -167
  175. data/lib/codebase_index/temporal/json_snapshot_store.rb +0 -245
  176. data/lib/codebase_index/temporal/snapshot_store.rb +0 -345
  177. data/lib/codebase_index/token_utils.rb +0 -19
  178. data/lib/codebase_index/version.rb +0 -5
  179. data/lib/generators/codebase_index/install_generator.rb +0 -32
  180. data/lib/generators/codebase_index/pgvector_generator.rb +0 -37
  181. data/lib/generators/codebase_index/templates/add_pgvector_to_codebase_index.rb.erb +0 -15
  182. data/lib/generators/codebase_index/templates/create_codebase_index_tables.rb.erb +0 -43
  183. data/lib/tasks/codebase_index.rake +0 -597
  184. data/lib/tasks/codebase_index_evaluation.rake +0 -115
@@ -1,201 +0,0 @@
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 = source_file_for(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
- # Locate the source file for a channel class.
93
- #
94
- # Convention path first, then introspection via {#resolve_source_location}
95
- # which filters out vendor/node_modules paths.
96
- #
97
- # @param klass [Class] The channel class
98
- # @param name [String] The channel class name
99
- # @return [String, nil]
100
- def source_file_for(klass, name)
101
- return nil unless defined?(Rails) && Rails.respond_to?(:root) && Rails.root
102
-
103
- convention_path = Rails.root.join('app', 'channels', "#{name.underscore}.rb").to_s
104
- return convention_path if File.exist?(convention_path)
105
-
106
- resolve_source_location(klass, app_root: Rails.root.to_s, fallback: nil)
107
- end
108
-
109
- # Read source code from a file path.
110
- #
111
- # @param file_path [String, nil]
112
- # @return [String]
113
- def read_source(file_path)
114
- return '' unless file_path && File.exist?(file_path)
115
-
116
- File.read(file_path)
117
- rescue StandardError
118
- ''
119
- end
120
-
121
- # Build metadata hash for a channel.
122
- #
123
- # @param source [String] Channel source code
124
- # @param own_methods [Array<Symbol>] Methods defined directly on the channel
125
- # @return [Hash]
126
- def build_metadata(source, own_methods)
127
- {
128
- stream_names: detect_stream_names(source),
129
- actions: detect_actions(own_methods),
130
- has_subscribed: own_methods.include?(:subscribed),
131
- has_unsubscribed: own_methods.include?(:unsubscribed),
132
- broadcasts_to: detect_broadcasts(source),
133
- loc: count_loc(source)
134
- }
135
- end
136
-
137
- # Detect stream names from stream_from and stream_for calls.
138
- #
139
- # @param source [String] Channel source code
140
- # @return [Array<String>]
141
- def detect_stream_names(source)
142
- streams = []
143
-
144
- # stream_from "string" or stream_from 'string' (also catches interpolated strings)
145
- streams.concat(source.scan(/stream_from\s+["']([^"']+)["']/).flatten)
146
-
147
- # stream_for model
148
- streams.concat(source.scan(/stream_for\s+(\w+)/).map { |m| "stream_for:#{m[0]}" })
149
-
150
- streams.uniq
151
- end
152
-
153
- # Detect action methods (public instance methods minus lifecycle methods).
154
- #
155
- # @param own_methods [Array<Symbol>] Methods defined directly on the channel
156
- # @return [Array<String>]
157
- def detect_actions(own_methods)
158
- (own_methods - LIFECYCLE_METHODS).map(&:to_s)
159
- end
160
-
161
- # Detect broadcast patterns in source code.
162
- #
163
- # @param source [String] Channel source code
164
- # @return [Array<String>]
165
- def detect_broadcasts(source)
166
- broadcasts = []
167
-
168
- # ActionCable.server.broadcast("channel_name", ...)
169
- broadcasts.concat(source.scan(/ActionCable\.server\.broadcast\(\s*["']([^"']+)["']/).flatten)
170
-
171
- # SomeChannel.broadcast_to(target, ...)
172
- broadcasts.concat(source.scan(/\w+\.broadcast_to\(\s*(\w+)/).map { |m| "broadcast_to:#{m[0]}" })
173
-
174
- broadcasts.uniq
175
- end
176
-
177
- # Count non-blank, non-comment lines.
178
- #
179
- # @param source [String]
180
- # @return [Integer]
181
- def count_loc(source)
182
- return 0 if source.empty?
183
-
184
- source.each_line.count do |line|
185
- stripped = line.strip
186
- !stripped.empty? && !stripped.start_with?('#')
187
- end
188
- end
189
-
190
- # Log a channel extraction error.
191
- #
192
- # @param name [String] Channel class name
193
- # @param error [StandardError]
194
- def log_extraction_error(name, error)
195
- return unless defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
196
-
197
- Rails.logger.error("Failed to extract channel #{name}: #{error.message}")
198
- end
199
- end
200
- end
201
- end
@@ -1,46 +0,0 @@
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
@@ -1,309 +0,0 @@
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