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,117 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'yaml'
4
-
5
- module CodebaseIndex
6
- module Extractors
7
- # I18nExtractor handles internationalization locale file extraction.
8
- #
9
- # Parses YAML files from `config/locales/` to extract translation keys,
10
- # locale information, and key structure. Each locale file becomes one
11
- # ExtractedUnit.
12
- #
13
- # @example
14
- # extractor = I18nExtractor.new
15
- # units = extractor.extract_all
16
- # en = units.find { |u| u.identifier == "en.yml" }
17
- #
18
- class I18nExtractor
19
- # Directories to scan for locale files
20
- I18N_DIRECTORIES = %w[
21
- config/locales
22
- ].freeze
23
-
24
- def initialize
25
- @directories = I18N_DIRECTORIES.map { |d| Rails.root.join(d) }
26
- .select(&:directory?)
27
- end
28
-
29
- # Extract all locale files
30
- #
31
- # @return [Array<ExtractedUnit>] List of i18n units
32
- def extract_all
33
- @directories.flat_map do |dir|
34
- Dir[dir.join('**/*.yml')].filter_map do |file|
35
- extract_i18n_file(file)
36
- end
37
- end
38
- end
39
-
40
- # Extract a single locale file
41
- #
42
- # @param file_path [String] Path to the YAML locale file
43
- # @return [ExtractedUnit, nil] The extracted unit or nil on failure
44
- def extract_i18n_file(file_path)
45
- source = File.read(file_path)
46
- data = YAML.safe_load(source, permitted_classes: [Symbol, Date, Time, Regexp])
47
-
48
- return nil unless data.is_a?(Hash) && data.any?
49
-
50
- identifier = build_identifier(file_path)
51
- locale = data.keys.first
52
-
53
- unit = ExtractedUnit.new(
54
- type: :i18n,
55
- identifier: identifier,
56
- file_path: file_path
57
- )
58
-
59
- unit.namespace = locale
60
- unit.source_code = source
61
- unit.metadata = build_metadata(data, locale)
62
- unit.dependencies = []
63
-
64
- unit
65
- rescue StandardError => e
66
- Rails.logger.error("Failed to extract i18n #{file_path}: #{e.message}")
67
- nil
68
- end
69
-
70
- private
71
-
72
- # Build a readable identifier from the file path.
73
- #
74
- # @param file_path [String] Absolute path
75
- # @return [String] Relative identifier like "en.yml" or "models/en.yml"
76
- def build_identifier(file_path)
77
- relative = file_path.sub("#{Rails.root}/", '')
78
- relative.sub(%r{^config/locales/}, '')
79
- end
80
-
81
- # Build metadata for the locale file.
82
- #
83
- # @param data [Hash] Parsed YAML data
84
- # @param locale [String] The locale key (e.g., "en")
85
- # @return [Hash]
86
- def build_metadata(data, locale)
87
- locale_data = data[locale] || {}
88
- key_paths = flatten_keys(locale_data)
89
-
90
- {
91
- locale: locale,
92
- key_count: key_paths.size,
93
- top_level_keys: locale_data.is_a?(Hash) ? locale_data.keys : [],
94
- key_paths: key_paths
95
- }
96
- end
97
-
98
- # Flatten a nested hash into dot-notation key paths.
99
- #
100
- # @param hash [Hash] Nested hash to flatten
101
- # @param prefix [String] Current key prefix
102
- # @return [Array<String>] Flattened key paths
103
- def flatten_keys(hash, prefix = '')
104
- return ["#{prefix}(leaf)"] unless hash.is_a?(Hash)
105
-
106
- hash.flat_map do |key, value|
107
- full_key = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
108
- if value.is_a?(Hash)
109
- flatten_keys(value, full_key)
110
- else
111
- [full_key]
112
- end
113
- end
114
- end
115
- end
116
- end
117
- end
@@ -1,374 +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
- # JobExtractor handles ActiveJob and Sidekiq job extraction.
9
- #
10
- # Background jobs are critical for understanding async behavior.
11
- # They often perform important business logic that would otherwise
12
- # be unclear from just looking at models and controllers.
13
- #
14
- # We extract:
15
- # - Queue configuration
16
- # - Retry/error handling configuration
17
- # - Arguments (the job's interface)
18
- # - What the job calls (dependencies)
19
- # - What triggers this job (reverse lookup via dependencies)
20
- #
21
- # @example
22
- # extractor = JobExtractor.new
23
- # units = extractor.extract_all
24
- # order_job = units.find { |u| u.identifier == "ProcessOrderJob" }
25
- #
26
- class JobExtractor
27
- include SharedUtilityMethods
28
- include SharedDependencyScanner
29
-
30
- # Directories to scan for jobs
31
- JOB_DIRECTORIES = %w[
32
- app/jobs
33
- app/workers
34
- app/sidekiq
35
- ].freeze
36
-
37
- def initialize
38
- @directories = JOB_DIRECTORIES.map { |d| Rails.root.join(d) }
39
- .select(&:directory?)
40
- end
41
-
42
- # Extract all jobs in the application
43
- #
44
- # @return [Array<ExtractedUnit>] List of job units
45
- def extract_all
46
- units = []
47
-
48
- # File-based discovery (catches everything)
49
- @directories.each do |dir|
50
- Dir[dir.join('**/*.rb')].each do |file|
51
- unit = extract_job_file(file)
52
- units << unit if unit
53
- end
54
- end
55
-
56
- # Also try class-based discovery for ActiveJob
57
- if defined?(ApplicationJob)
58
- seen = units.to_set(&:identifier)
59
- ApplicationJob.descendants.each do |job_class|
60
- next if seen.include?(job_class.name)
61
-
62
- unit = extract_job_class(job_class)
63
- if unit
64
- units << unit
65
- seen << unit.identifier
66
- end
67
- end
68
- end
69
-
70
- units.compact
71
- end
72
-
73
- # Extract a job from its file
74
- #
75
- # @param file_path [String] Path to the job file
76
- # @return [ExtractedUnit, nil] The extracted unit
77
- def extract_job_file(file_path)
78
- source = File.read(file_path)
79
- class_name = extract_class_name(file_path, source)
80
-
81
- return nil unless class_name
82
- return nil unless job_file?(source)
83
-
84
- unit = ExtractedUnit.new(
85
- type: :job,
86
- identifier: class_name,
87
- file_path: file_path
88
- )
89
-
90
- unit.namespace = extract_namespace(class_name)
91
- unit.source_code = annotate_source(source, class_name)
92
- unit.metadata = extract_metadata_from_source(source, class_name)
93
- unit.dependencies = extract_dependencies(source, class_name)
94
-
95
- unit
96
- rescue StandardError => e
97
- Rails.logger.error("Failed to extract job #{file_path}: #{e.message}")
98
- nil
99
- end
100
-
101
- # Extract a job from its class (runtime introspection)
102
- #
103
- # @param job_class [Class] The job class
104
- # @return [ExtractedUnit, nil] The extracted unit
105
- def extract_job_class(job_class)
106
- return nil if job_class.name.nil?
107
-
108
- file_path = source_file_for(job_class)
109
- source = file_path && File.exist?(file_path) ? File.read(file_path) : ''
110
-
111
- unit = ExtractedUnit.new(
112
- type: :job,
113
- identifier: job_class.name,
114
- file_path: file_path
115
- )
116
-
117
- unit.namespace = extract_namespace(job_class.name)
118
- unit.source_code = annotate_source(source, job_class.name)
119
- unit.metadata = extract_metadata_from_class(job_class, source)
120
- unit.dependencies = extract_dependencies(source, job_class.name)
121
-
122
- unit
123
- rescue StandardError => e
124
- Rails.logger.error("Failed to extract job #{job_class.name}: #{e.message}")
125
- nil
126
- end
127
-
128
- private
129
-
130
- # ──────────────────────────────────────────────────────────────────────
131
- # Class Discovery
132
- # ──────────────────────────────────────────────────────────────────────
133
-
134
- def extract_class_name(file_path, source)
135
- # Try to extract from source
136
- return ::Regexp.last_match(1) if source =~ /^\s*class\s+([\w:]+)/
137
-
138
- # Fall back to convention
139
- file_path
140
- .sub("#{Rails.root}/", '')
141
- .sub(%r{^app/(jobs|workers|sidekiq)/}, '')
142
- .sub('.rb', '')
143
- .camelize
144
- end
145
-
146
- def job_file?(source)
147
- # Check if this looks like a job/worker file
148
- source.match?(/< ApplicationJob/) ||
149
- source.match?(/< ActiveJob::Base/) ||
150
- source.match?(/include Sidekiq::Worker/) ||
151
- source.match?(/include Sidekiq::Job/) ||
152
- source.match?(/def perform/)
153
- end
154
-
155
- # Locate the source file for a job class.
156
- #
157
- # Convention path first, then introspection via {#resolve_source_location}
158
- # which filters out vendor/node_modules paths.
159
- #
160
- # @param job_class [Class]
161
- # @return [String, nil]
162
- def source_file_for(job_class)
163
- convention_path = Rails.root.join("app/jobs/#{job_class.name.underscore}.rb").to_s
164
- return convention_path if File.exist?(convention_path)
165
-
166
- resolve_source_location(job_class, app_root: Rails.root.to_s, fallback: convention_path)
167
- end
168
-
169
- # ──────────────────────────────────────────────────────────────────────
170
- # Source Annotation
171
- # ──────────────────────────────────────────────────────────────────────
172
-
173
- def annotate_source(source, class_name)
174
- job_type = detect_job_type(source)
175
- queue = extract_queue(source)
176
-
177
- <<~ANNOTATION
178
- # ╔═══════════════════════════════════════════════════════════════════════╗
179
- # ║ Job: #{class_name.ljust(62)}║
180
- # ║ Type: #{job_type.to_s.ljust(61)}║
181
- # ║ Queue: #{(queue || 'default').ljust(60)}║
182
- # ╚═══════════════════════════════════════════════════════════════════════╝
183
-
184
- #{source}
185
- ANNOTATION
186
- end
187
-
188
- def detect_job_type(source)
189
- return :sidekiq if source.match?(/include Sidekiq::(Worker|Job)/)
190
- return :active_job if source.match?(/< (ApplicationJob|ActiveJob::Base)/)
191
- return :good_job if source.match?(/include GoodJob/)
192
- return :delayed_job if source.match?(/delay|handle_asynchronously/)
193
-
194
- :unknown
195
- end
196
-
197
- def extract_queue(source)
198
- # ActiveJob style
199
- return ::Regexp.last_match(1) if source =~ /queue_as\s+[:"'](\w+)/
200
-
201
- # Sidekiq style
202
- return ::Regexp.last_match(1) if source =~ /sidekiq_options.*queue:\s*[:"'](\w+)/
203
-
204
- nil
205
- end
206
-
207
- # ──────────────────────────────────────────────────────────────────────
208
- # Metadata Extraction (from source)
209
- # ──────────────────────────────────────────────────────────────────────
210
-
211
- def extract_metadata_from_source(source, class_name)
212
- {
213
- job_type: detect_job_type(source),
214
- queue: extract_queue(source),
215
-
216
- # Configuration
217
- sidekiq_options: extract_sidekiq_options(source),
218
- retry_config: extract_retry_config(source),
219
- concurrency_controls: extract_concurrency(source),
220
-
221
- # Interface
222
- perform_params: extract_perform_params(source),
223
- scheduled: source.match?(/perform_later|perform_in|perform_at/),
224
-
225
- # Error handling
226
- discard_on: extract_discard_on(source),
227
- retry_on: extract_retry_on(source),
228
-
229
- # Callbacks
230
- callbacks: extract_callbacks(source),
231
-
232
- # Job chaining
233
- enqueues_jobs: extract_enqueued_jobs(source, class_name),
234
-
235
- # Metrics
236
- loc: source.lines.count { |l| l.strip.present? && !l.strip.start_with?('#') }
237
- }
238
- end
239
-
240
- def extract_metadata_from_class(job_class, source)
241
- base_metadata = extract_metadata_from_source(source, job_class.name)
242
-
243
- # Enhance with runtime introspection if available
244
- base_metadata[:queue] ||= job_class.queue_name if job_class.respond_to?(:queue_name)
245
-
246
- base_metadata[:sidekiq_options] = job_class.sidekiq_options_hash if job_class.respond_to?(:sidekiq_options_hash)
247
-
248
- base_metadata
249
- end
250
-
251
- def extract_sidekiq_options(source)
252
- options = {}
253
-
254
- if source =~ /sidekiq_options\s+(.+)/
255
- opts_str = ::Regexp.last_match(1)
256
- opts_str.scan(/(\w+):\s*([^,\n]+)/) do |key, value|
257
- options[key.to_sym] = value.strip
258
- end
259
- end
260
-
261
- options
262
- end
263
-
264
- def extract_retry_config(source)
265
- config = {}
266
-
267
- # ActiveJob retry_on
268
- source.scan(/retry_on\s+(\w+)(?:,\s*wait:\s*([^,\n]+))?(?:,\s*attempts:\s*(\d+))?/) do |error, wait, attempts|
269
- config[:retry_on] ||= []
270
- config[:retry_on] << {
271
- error: error,
272
- wait: wait,
273
- attempts: attempts&.to_i
274
- }
275
- end
276
-
277
- # Sidekiq retries
278
- config[:sidekiq_retries] = ::Regexp.last_match(1) if source =~ /sidekiq_options.*retry:\s*(\d+|false|true)/
279
-
280
- config
281
- end
282
-
283
- def extract_concurrency(source)
284
- controls = {}
285
-
286
- # Sidekiq unique jobs
287
- controls[:unique_for] = ::Regexp.last_match(1).to_i if source =~ /unique_for:\s*(\d+)/
288
-
289
- # Sidekiq rate limiting
290
- controls[:rate_limit] = ::Regexp.last_match(1) if source =~ /rate_limit:\s*\{([^}]+)\}/
291
-
292
- controls
293
- end
294
-
295
- def extract_perform_params(source)
296
- return [] unless source =~ /def\s+perform\s*\(([^)]*)\)/
297
-
298
- params_str = ::Regexp.last_match(1)
299
- params = []
300
-
301
- params_str.scan(/(\*?\*?\w+)(?:\s*=\s*([^,]+))?/) do |name, default|
302
- params << {
303
- name: name.gsub(/^\*+/, ''),
304
- splat: if name.start_with?('**')
305
- :double
306
- else
307
- (name.start_with?('*') ? :single : nil)
308
- end,
309
- has_default: !default.nil?
310
- }
311
- end
312
-
313
- params
314
- end
315
-
316
- def extract_discard_on(source)
317
- source.scan(/discard_on\s+(\w+(?:::\w+)*)/).flatten
318
- end
319
-
320
- def extract_retry_on(source)
321
- source.scan(/retry_on\s+(\w+(?:::\w+)*)/).flatten
322
- end
323
-
324
- def extract_callbacks(source)
325
- callbacks = []
326
-
327
- %w[before_enqueue after_enqueue before_perform after_perform around_perform].each do |cb|
328
- source.scan(/#{cb}\s+(?::(\w+)|do)/) do |method|
329
- callbacks << { type: cb, method: method&.first }
330
- end
331
- end
332
-
333
- callbacks
334
- end
335
-
336
- # ──────────────────────────────────────────────────────────────────────
337
- # Dependency Extraction
338
- # ──────────────────────────────────────────────────────────────────────
339
-
340
- def extract_dependencies(source, current_class_name = nil)
341
- # Scan standard dep types individually (not scan_common_dependencies) so we can
342
- # handle job deps with the richer :job_enqueue via and self-reference exclusion.
343
- deps = scan_model_dependencies(source)
344
- deps.concat(scan_service_dependencies(source))
345
- deps.concat(scan_mailer_dependencies(source))
346
-
347
- # Job-to-job dependencies with specific :job_enqueue via and self-reference exclusion
348
- extract_enqueued_jobs(source, current_class_name).each do |job_name|
349
- deps << { type: :job, target: job_name, via: :job_enqueue }
350
- end
351
-
352
- # External services
353
- if source.match?(/HTTParty|Faraday|RestClient|Net::HTTP/)
354
- deps << { type: :external, target: :http_api, via: :code_reference }
355
- end
356
-
357
- deps << { type: :infrastructure, target: :redis, via: :code_reference } if source.match?(/Redis\.current|REDIS/)
358
-
359
- deps.uniq { |d| [d[:type], d[:target]] }
360
- end
361
-
362
- # Scan source for job class enqueue calls and return the list of enqueued job names.
363
- #
364
- # @param source [String] The job source code
365
- # @param current_class_name [String, nil] The current job class name (excluded from results)
366
- # @return [Array<String>] Unique list of enqueued job class names
367
- def extract_enqueued_jobs(source, current_class_name = nil)
368
- pattern = /(\w+Job)\.(?:perform_later|perform_async|perform_in|perform_at|set\b)/
369
- job_names = source.scan(pattern).flatten.uniq
370
- job_names.reject { |name| name == current_class_name }
371
- end
372
- end
373
- end
374
- end