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,917 @@
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
+ # GraphQLExtractor handles graphql-ruby type and mutation extraction.
9
+ #
10
+ # GraphQL schemas are rich in structure — types, fields, arguments,
11
+ # resolvers, and mutations form a typed API layer over the domain.
12
+ # We extract these with runtime introspection when available (via
13
+ # `GraphQL::Schema.types`) and fall back to file-based discovery
14
+ # when the schema isn't fully loadable.
15
+ #
16
+ # We extract:
17
+ # - Object types, input types, enum types, interface types, union types, scalar types
18
+ # - Mutations and their arguments/return fields
19
+ # - Query fields and resolvers
20
+ # - Standalone resolver classes
21
+ # - Field-level metadata (types, descriptions, complexity, arguments)
22
+ # - Authorization patterns (authorized?, pundit, cancan)
23
+ # - Dependencies on models, services, jobs, and other GraphQL types
24
+ #
25
+ # @example
26
+ # extractor = GraphQLExtractor.new
27
+ # units = extractor.extract_all
28
+ # user_type = units.find { |u| u.identifier == "Types::UserType" }
29
+ #
30
+ class GraphQLExtractor
31
+ include SharedUtilityMethods
32
+ include SharedDependencyScanner
33
+
34
+ # Standard directory for graphql-ruby applications
35
+ GRAPHQL_DIRECTORY = 'app/graphql'
36
+
37
+ # Token threshold for chunking large types
38
+ CHUNK_THRESHOLD = 1500
39
+
40
+ def initialize
41
+ @graphql_dir = defined?(Rails) ? Rails.root.join(GRAPHQL_DIRECTORY) : nil
42
+ @schema_class = find_schema_class
43
+ @runtime_types = load_runtime_types
44
+ end
45
+
46
+ # Extract all GraphQL types, mutations, queries, and resolvers
47
+ #
48
+ # Returns an empty array if graphql-ruby is not installed or
49
+ # no GraphQL files are found.
50
+ #
51
+ # @return [Array<ExtractedUnit>] List of GraphQL units
52
+ def extract_all
53
+ return [] unless graphql_available?
54
+
55
+ units = []
56
+ seen_identifiers = Set.new
57
+
58
+ # First pass: runtime introspection (most accurate)
59
+ if @runtime_types.any?
60
+ @runtime_types.each_value do |type_class|
61
+ unit = extract_from_runtime_type(type_class)
62
+ next unless unit
63
+ next if seen_identifiers.include?(unit.identifier)
64
+
65
+ seen_identifiers << unit.identifier
66
+ units << unit
67
+ end
68
+ end
69
+
70
+ # Second pass: file-based discovery (catches everything)
71
+ if @graphql_dir&.directory?
72
+ Dir[@graphql_dir.join('**/*.rb')].each do |file_path|
73
+ unit = extract_graphql_file(file_path)
74
+ next unless unit
75
+ next if seen_identifiers.include?(unit.identifier)
76
+
77
+ seen_identifiers << unit.identifier
78
+ units << unit
79
+ end
80
+ end
81
+
82
+ units.compact
83
+ end
84
+
85
+ # Extract a single GraphQL file
86
+ #
87
+ # @param file_path [String] Absolute path to a .rb file in app/graphql/
88
+ # @return [ExtractedUnit, nil] The extracted unit, or nil if the file
89
+ # does not contain a recognizable GraphQL class
90
+ def extract_graphql_file(file_path)
91
+ source = File.read(file_path)
92
+ class_name = extract_class_name(file_path, source)
93
+
94
+ return nil unless class_name
95
+ return nil unless graphql_class?(source)
96
+
97
+ unit_type = classify_unit_type(file_path, source)
98
+ runtime_class = class_name.safe_constantize
99
+
100
+ unit = ExtractedUnit.new(
101
+ type: unit_type,
102
+ identifier: class_name,
103
+ file_path: file_path
104
+ )
105
+
106
+ unit.namespace = extract_namespace(class_name)
107
+ unit.source_code = build_annotated_source(source, class_name, unit_type, runtime_class)
108
+ unit.metadata = build_metadata(source, class_name, unit_type, runtime_class)
109
+ unit.dependencies = extract_dependencies(source, class_name)
110
+ unit.chunks = build_chunks(unit, runtime_class) if unit.needs_chunking?(threshold: CHUNK_THRESHOLD)
111
+
112
+ unit
113
+ rescue StandardError => e
114
+ Rails.logger.error("Failed to extract GraphQL file #{file_path}: #{e.message}") if defined?(Rails)
115
+ nil
116
+ end
117
+
118
+ private
119
+
120
+ # ──────────────────────────────────────────────────────────────────────
121
+ # Schema and Runtime Discovery
122
+ # ──────────────────────────────────────────────────────────────────────
123
+
124
+ # Check if graphql-ruby is available at runtime
125
+ #
126
+ # @return [Boolean]
127
+ def graphql_available?
128
+ return false unless defined?(GraphQL::Schema)
129
+ return false unless @graphql_dir&.directory? || @schema_class
130
+
131
+ true
132
+ end
133
+
134
+ # Find the application's schema class (descendant of GraphQL::Schema)
135
+ #
136
+ # @return [Class, nil]
137
+ def find_schema_class
138
+ return nil unless defined?(GraphQL::Schema)
139
+
140
+ GraphQL::Schema.descendants.find do |klass|
141
+ klass.name && !klass.name.start_with?('GraphQL::')
142
+ end
143
+ rescue StandardError
144
+ nil
145
+ end
146
+
147
+ # Load types from the runtime schema for introspection
148
+ #
149
+ # @return [Hash{String => Class}] Map of type name to type class
150
+ def load_runtime_types
151
+ return {} unless @schema_class
152
+
153
+ types = {}
154
+ @schema_class.types.each do |name, type_class|
155
+ # Skip built-in introspection types
156
+ next if name.start_with?('__')
157
+ next unless type_class.respond_to?(:name) && type_class.name
158
+
159
+ types[name] = type_class
160
+ end
161
+
162
+ types
163
+ rescue StandardError
164
+ {}
165
+ end
166
+
167
+ # ──────────────────────────────────────────────────────────────────────
168
+ # Runtime Type Extraction
169
+ # ──────────────────────────────────────────────────────────────────────
170
+
171
+ # Extract a unit from a runtime-loaded GraphQL type class
172
+ #
173
+ # @param type_class [Class] A graphql-ruby type class
174
+ # @return [ExtractedUnit, nil]
175
+ def extract_from_runtime_type(type_class)
176
+ return nil unless type_class.respond_to?(:name) && type_class.name
177
+ # Skip anonymous or internal graphql-ruby classes
178
+ return nil if type_class.name.start_with?('GraphQL::')
179
+
180
+ file_path = source_file_for_class(type_class)
181
+ source = file_path && File.exist?(file_path) ? File.read(file_path) : ''
182
+ unit_type = classify_runtime_type(type_class)
183
+
184
+ unit = ExtractedUnit.new(
185
+ type: unit_type,
186
+ identifier: type_class.name,
187
+ file_path: file_path
188
+ )
189
+
190
+ unit.namespace = extract_namespace(type_class.name)
191
+ unit.source_code = build_annotated_source(source, type_class.name, unit_type, type_class)
192
+ unit.metadata = build_metadata(source, type_class.name, unit_type, type_class)
193
+ unit.dependencies = extract_dependencies(source, type_class.name)
194
+ unit.chunks = build_chunks(unit, type_class) if unit.needs_chunking?(threshold: CHUNK_THRESHOLD)
195
+
196
+ unit
197
+ rescue StandardError => e
198
+ Rails.logger.error("Failed to extract GraphQL type #{type_class.name}: #{e.message}") if defined?(Rails)
199
+ nil
200
+ end
201
+
202
+ # Determine the source file for a runtime-loaded class, validating that
203
+ # paths are within Rails.root to avoid returning graphql gem internals.
204
+ #
205
+ # Uses a multi-tier strategy matching the model extractor's pattern.
206
+ #
207
+ # @param klass [Class]
208
+ # @return [String] Absolute path to the source file
209
+ def source_file_for_class(klass)
210
+ app_root = Rails.root.to_s
211
+ convention_path = Rails.root.join("#{GRAPHQL_DIRECTORY}/#{klass.name.underscore}.rb").to_s
212
+
213
+ # Tier 1: Instance methods defined directly on this class
214
+ klass.instance_methods(false).each do |method_name|
215
+ loc = klass.instance_method(method_name).source_location&.first
216
+ return loc if loc&.start_with?(app_root)
217
+ end
218
+
219
+ # Tier 2: Singleton methods defined on this class
220
+ klass.singleton_methods(false).each do |method_name|
221
+ loc = klass.method(method_name).source_location&.first
222
+ return loc if loc&.start_with?(app_root)
223
+ end
224
+
225
+ # Tier 3: Convention path if file exists
226
+ return convention_path if File.exist?(convention_path)
227
+
228
+ # Tier 4: const_source_location (Ruby 3.0+)
229
+ if Object.respond_to?(:const_source_location)
230
+ loc = Object.const_source_location(klass.name)&.first
231
+ return loc if loc&.start_with?(app_root)
232
+ end
233
+
234
+ # Tier 5: Always return convention path — never a gem path
235
+ convention_path
236
+ rescue StandardError
237
+ Rails.root.join("#{GRAPHQL_DIRECTORY}/#{klass.name.underscore}.rb").to_s
238
+ end
239
+
240
+ # ──────────────────────────────────────────────────────────────────────
241
+ # Classification
242
+ # ──────────────────────────────────────────────────────────────────────
243
+
244
+ # Classify unit type from a runtime type class
245
+ #
246
+ # @param type_class [Class]
247
+ # @return [Symbol]
248
+ def classify_runtime_type(type_class)
249
+ if defined?(GraphQL::Schema::Mutation) && type_class < GraphQL::Schema::Mutation
250
+ :graphql_mutation
251
+ elsif defined?(GraphQL::Schema::Resolver) && type_class < GraphQL::Schema::Resolver
252
+ :graphql_resolver
253
+ elsif defined?(GraphQL::Schema::Enum) && type_class < GraphQL::Schema::Enum
254
+ :graphql_type
255
+ elsif defined?(GraphQL::Schema::Union) && type_class < GraphQL::Schema::Union
256
+ :graphql_type
257
+ elsif defined?(GraphQL::Schema::Interface) && type_class.is_a?(Module) && type_class.respond_to?(:fields)
258
+ :graphql_type
259
+ elsif defined?(GraphQL::Schema::InputObject) && type_class < GraphQL::Schema::InputObject
260
+ :graphql_type
261
+ elsif defined?(GraphQL::Schema::Scalar) && type_class < GraphQL::Schema::Scalar
262
+ :graphql_type
263
+ elsif defined?(GraphQL::Schema::Object) && type_class < GraphQL::Schema::Object
264
+ # Check if this is the Query root type
265
+ if @schema_class.respond_to?(:query) && @schema_class.query == type_class
266
+ :graphql_query
267
+ else
268
+ :graphql_type
269
+ end
270
+ else
271
+ :graphql_type
272
+ end
273
+ end
274
+
275
+ # Classify unit type from file path and source content
276
+ #
277
+ # @param file_path [String]
278
+ # @param source [String]
279
+ # @return [Symbol]
280
+ def classify_unit_type(file_path, source)
281
+ return :graphql_mutation if file_path.include?('/mutations/')
282
+ return :graphql_resolver if file_path.include?('/resolvers/')
283
+
284
+ return :graphql_mutation if source.match?(/< (GraphQL::Schema::Mutation|Mutations::Base|BaseMutation)/)
285
+
286
+ return :graphql_resolver if source.match?(/< (GraphQL::Schema::Resolver|Resolvers::Base|BaseResolver)/)
287
+
288
+ # Query type is usually the root query object
289
+ return :graphql_query if file_path.match?(/query_type\.rb$/) || source.match?(/class QueryType\b/)
290
+
291
+ :graphql_type
292
+ end
293
+
294
+ # Check if a source file contains a graphql-ruby class
295
+ #
296
+ # @param source [String]
297
+ # @return [Boolean]
298
+ def graphql_class?(source)
299
+ source.match?(/< GraphQL::Schema::(Object|InputObject|Enum|Union|Scalar|Mutation|Resolver|Interface|RelayClassicMutation)/) ||
300
+ source.match?(/< (Types::Base\w+|Base(Type|InputObject|Enum|Union|Scalar|Mutation|Resolver|Interface))/) ||
301
+ source.match?(/< (Mutations::Base|Resolvers::Base)/) ||
302
+ source.match?(/include GraphQL::Schema::Interface/) ||
303
+ (source.include?('field :') && source.match?(/< .*Type\b/))
304
+ end
305
+
306
+ # ──────────────────────────────────────────────────────────────────────
307
+ # Class Name and Namespace
308
+ # ──────────────────────────────────────────────────────────────────────
309
+
310
+ # Extract the fully-qualified class name from source or file path
311
+ #
312
+ # @param file_path [String]
313
+ # @param source [String]
314
+ # @return [String, nil]
315
+ def extract_class_name(file_path, source)
316
+ # Build from nested module/class declarations
317
+ modules = source.scan(/^\s*(?:module|class)\s+([\w:]+)/).flatten
318
+ return nil if modules.empty?
319
+
320
+ # If first token is a fully-qualified name, use it directly
321
+ return modules.first if modules.first.include?('::')
322
+
323
+ # Otherwise join the nesting
324
+ modules.join('::')
325
+ rescue StandardError
326
+ # Fall back to convention from file path
327
+ return nil unless defined?(Rails)
328
+
329
+ file_path
330
+ .sub("#{Rails.root.join(GRAPHQL_DIRECTORY)}/", '')
331
+ .sub('.rb', '')
332
+ .camelize
333
+ end
334
+
335
+ # ──────────────────────────────────────────────────────────────────────
336
+ # Source Annotation
337
+ # ──────────────────────────────────────────────────────────────────────
338
+
339
+ # Build annotated source with a descriptive header
340
+ #
341
+ # @param source [String] Raw file contents
342
+ # @param class_name [String]
343
+ # @param unit_type [Symbol]
344
+ # @param runtime_class [Class, nil]
345
+ # @return [String]
346
+ def build_annotated_source(source, class_name, unit_type, runtime_class)
347
+ field_count = count_fields(source, runtime_class)
348
+ argument_count = count_arguments(source, runtime_class)
349
+
350
+ type_label = format_type_label(unit_type)
351
+
352
+ <<~ANNOTATION
353
+ # ╔═══════════════════════════════════════════════════════════════════════╗
354
+ # ║ #{type_label}: #{class_name.ljust(71 - type_label.length - 4)}║
355
+ # ║ Fields: #{field_count.to_s.ljust(4)} | Arguments: #{argument_count.to_s.ljust(42)}║
356
+ # ╚═══════════════════════════════════════════════════════════════════════╝
357
+
358
+ #{source}
359
+ ANNOTATION
360
+ end
361
+
362
+ # Human-readable label for unit type
363
+ #
364
+ # @param unit_type [Symbol]
365
+ # @return [String]
366
+ def format_type_label(unit_type)
367
+ case unit_type
368
+ when :graphql_mutation then 'GraphQL Mutation'
369
+ when :graphql_query then 'GraphQL Query'
370
+ when :graphql_resolver then 'GraphQL Resolver'
371
+ else 'GraphQL Type'
372
+ end
373
+ end
374
+
375
+ # ──────────────────────────────────────────────────────────────────────
376
+ # Metadata Extraction
377
+ # ──────────────────────────────────────────────────────────────────────
378
+
379
+ # Build comprehensive metadata for a GraphQL unit
380
+ #
381
+ # @param source [String]
382
+ # @param class_name [String]
383
+ # @param unit_type [Symbol]
384
+ # @param runtime_class [Class, nil]
385
+ # @return [Hash]
386
+ def build_metadata(source, _class_name, _unit_type, runtime_class)
387
+ {
388
+ # GraphQL classification
389
+ graphql_kind: detect_graphql_kind(source, runtime_class),
390
+ parent_class: extract_parent_class(source),
391
+
392
+ # Fields and arguments
393
+ fields: extract_fields(source, runtime_class),
394
+ arguments: extract_arguments(source, runtime_class),
395
+
396
+ # Interfaces and connections
397
+ interfaces: extract_interfaces(source, runtime_class),
398
+ connections: extract_connections(source),
399
+
400
+ # Resolver info
401
+ resolver_classes: extract_resolver_references(source),
402
+
403
+ # Authorization
404
+ authorization: extract_authorization(source),
405
+
406
+ # Complexity
407
+ complexity: extract_complexity(source),
408
+
409
+ # Enum values (if applicable)
410
+ enum_values: extract_enum_values(source, runtime_class),
411
+
412
+ # Union members (if applicable)
413
+ union_members: extract_union_members(source, runtime_class),
414
+
415
+ # Metrics
416
+ field_count: count_fields(source, runtime_class),
417
+ argument_count: count_arguments(source, runtime_class),
418
+ loc: source.lines.count { |l| l.strip.length.positive? && !l.strip.start_with?('#') }
419
+ }
420
+ end
421
+
422
+ # Detect what kind of GraphQL construct this is
423
+ #
424
+ # @param source [String]
425
+ # @param runtime_class [Class, nil]
426
+ # @return [Symbol]
427
+ def detect_graphql_kind(source, runtime_class)
428
+ if runtime_class
429
+ return :enum if defined?(GraphQL::Schema::Enum) && runtime_class < GraphQL::Schema::Enum
430
+ return :union if defined?(GraphQL::Schema::Union) && runtime_class < GraphQL::Schema::Union
431
+ return :input_object if defined?(GraphQL::Schema::InputObject) && runtime_class < GraphQL::Schema::InputObject
432
+ return :scalar if defined?(GraphQL::Schema::Scalar) && runtime_class < GraphQL::Schema::Scalar
433
+ return :mutation if defined?(GraphQL::Schema::Mutation) && runtime_class < GraphQL::Schema::Mutation
434
+ return :resolver if defined?(GraphQL::Schema::Resolver) && runtime_class < GraphQL::Schema::Resolver
435
+ return :interface if runtime_class.is_a?(Module) && defined?(GraphQL::Schema::Interface) && runtime_class.respond_to?(:included_modules) && runtime_class.included_modules.any? do |m|
436
+ m.name&.include?('GraphQL::Schema::Interface')
437
+ end
438
+ return :object if defined?(GraphQL::Schema::Object) && runtime_class < GraphQL::Schema::Object
439
+ end
440
+
441
+ # Fall back to source analysis
442
+ return :enum if source.match?(/< .*Enum\b/) || source.match?(/value\s+["']/)
443
+ return :union if source.match?(/< .*Union\b/) || source.match?(/possible_types\s/)
444
+ return :input_object if source.match?(/< .*InputObject\b/)
445
+ return :scalar if source.match?(/< .*Scalar\b/)
446
+ return :mutation if source.match?(/< .*(Mutation|RelayClassicMutation)\b/)
447
+ return :resolver if source.match?(/< .*Resolver\b/)
448
+ return :interface if source.match?(/include GraphQL::Schema::Interface/)
449
+
450
+ :object
451
+ end
452
+
453
+ # Extract the parent class name from source
454
+ #
455
+ # @param source [String]
456
+ # @return [String, nil]
457
+ def extract_parent_class(source)
458
+ match = source.match(/class\s+\w+\s*<\s*([\w:]+)/)
459
+ match ? match[1] : nil
460
+ end
461
+
462
+ # Extract field definitions from source and/or runtime
463
+ #
464
+ # @param source [String]
465
+ # @param runtime_class [Class, nil]
466
+ # @return [Array<Hash>]
467
+ def extract_fields(source, runtime_class)
468
+ # Prefer runtime introspection when available
469
+ if runtime_class.respond_to?(:fields) && runtime_class.fields.any?
470
+ return extract_fields_from_runtime(runtime_class)
471
+ end
472
+
473
+ extract_fields_from_source(source)
474
+ end
475
+
476
+ # Extract fields via runtime reflection
477
+ #
478
+ # @param runtime_class [Class]
479
+ # @return [Array<Hash>]
480
+ def extract_fields_from_runtime(runtime_class)
481
+ runtime_class.fields.map do |name, field|
482
+ field_hash = {
483
+ name: name,
484
+ type: field.type.to_type_signature,
485
+ description: field.description,
486
+ null: field_nullable?(field)
487
+ }
488
+
489
+ # Arguments on the field
490
+ if field.respond_to?(:arguments) && field.arguments.any?
491
+ field_hash[:arguments] = field.arguments.map do |arg_name, arg|
492
+ {
493
+ name: arg_name,
494
+ type: arg.type.to_type_signature,
495
+ required: arg.type.non_null?,
496
+ description: arg.description
497
+ }
498
+ end
499
+ end
500
+
501
+ # Resolver class
502
+ field_hash[:resolver_class] = field.resolver.name if field.respond_to?(:resolver) && field.resolver
503
+
504
+ # Complexity
505
+ field_hash[:complexity] = field.complexity if field.respond_to?(:complexity) && field.complexity
506
+
507
+ field_hash
508
+ end
509
+ rescue StandardError
510
+ extract_fields_from_source('')
511
+ end
512
+
513
+ # Check if a field is nullable
514
+ #
515
+ # @param field [GraphQL::Schema::Field]
516
+ # @return [Boolean]
517
+ def field_nullable?(field)
518
+ !field.type.non_null?
519
+ rescue StandardError
520
+ true
521
+ end
522
+
523
+ # Extract fields by parsing source text
524
+ #
525
+ # @param source [String]
526
+ # @return [Array<Hash>]
527
+ def extract_fields_from_source(source)
528
+ fields = []
529
+
530
+ # Match: field :name, Type, null: true/false, description: "..."
531
+ source.scan(/field\s+:(\w+)(?:,\s*(\S+?))?(?:,\s*(.+?))?(?:\s+do\s*$|\s*$)/m) do |name, type, rest|
532
+ field_hash = { name: name, type: type }
533
+
534
+ if rest
535
+ field_hash[:null] = !rest.include?('null: false')
536
+ desc_match = rest.match(/description:\s*["']([^"']+)["']/)
537
+ field_hash[:description] = desc_match[1] if desc_match
538
+ resolver_match = rest.match(/resolver:\s*([\w:]+)/)
539
+ field_hash[:resolver_class] = resolver_match[1] if resolver_match
540
+ end
541
+
542
+ fields << field_hash
543
+ end
544
+
545
+ fields
546
+ end
547
+
548
+ # Extract argument definitions
549
+ #
550
+ # @param source [String]
551
+ # @param runtime_class [Class, nil]
552
+ # @return [Array<Hash>]
553
+ def extract_arguments(source, runtime_class)
554
+ # Prefer runtime introspection
555
+ if runtime_class.respond_to?(:arguments) && runtime_class.arguments.any?
556
+ return extract_arguments_from_runtime(runtime_class)
557
+ end
558
+
559
+ extract_arguments_from_source(source)
560
+ end
561
+
562
+ # Extract arguments via runtime reflection
563
+ #
564
+ # @param runtime_class [Class]
565
+ # @return [Array<Hash>]
566
+ def extract_arguments_from_runtime(runtime_class)
567
+ runtime_class.arguments.map do |name, arg|
568
+ {
569
+ name: name,
570
+ type: arg.type.to_type_signature,
571
+ required: arg.type.non_null?,
572
+ description: arg.description
573
+ }
574
+ end
575
+ rescue StandardError
576
+ []
577
+ end
578
+
579
+ # Extract arguments by parsing source text
580
+ #
581
+ # @param source [String]
582
+ # @return [Array<Hash>]
583
+ def extract_arguments_from_source(source)
584
+ args = []
585
+
586
+ source.scan(/argument\s+:(\w+)(?:,\s*(\S+?))?(?:,\s*(.+?))?$/) do |name, type, rest|
587
+ arg_hash = { name: name, type: type }
588
+
589
+ if rest
590
+ arg_hash[:required] = rest.include?('required: true')
591
+ desc_match = rest.match(/description:\s*["']([^"']+)["']/)
592
+ arg_hash[:description] = desc_match[1] if desc_match
593
+ end
594
+
595
+ args << arg_hash
596
+ end
597
+
598
+ args
599
+ end
600
+
601
+ # Extract interface implementations
602
+ #
603
+ # @param source [String]
604
+ # @param runtime_class [Class, nil]
605
+ # @return [Array<String>]
606
+ def extract_interfaces(source, runtime_class)
607
+ if runtime_class.respond_to?(:interfaces) && runtime_class.interfaces.any?
608
+ return runtime_class.interfaces.filter_map(&:name)
609
+ end
610
+
611
+ source.scan(/implements\s+([\w:]+)/).flatten
612
+ rescue StandardError
613
+ source.scan(/implements\s+([\w:]+)/).flatten
614
+ end
615
+
616
+ # Extract connection type references
617
+ #
618
+ # @param source [String]
619
+ # @return [Array<String>]
620
+ def extract_connections(source)
621
+ # field :items, Types::ItemType.connection_type
622
+ connections = source.scan(/([\w:]+)\.connection_type/).flatten.map do |type|
623
+ type
624
+ end
625
+
626
+ # connection_type_class ConnectionType
627
+ source.scan(/connection_type_class\s+([\w:]+)/).flatten.each do |type|
628
+ connections << type
629
+ end
630
+
631
+ connections.uniq
632
+ end
633
+
634
+ # Extract references to standalone resolver classes
635
+ #
636
+ # @param source [String]
637
+ # @return [Array<String>]
638
+ def extract_resolver_references(source)
639
+ source.scan(/resolver:\s*([\w:]+)/).flatten.uniq
640
+ end
641
+
642
+ # Detect authorization patterns
643
+ #
644
+ # @param source [String]
645
+ # @return [Hash]
646
+ def extract_authorization(source)
647
+ auth = {}
648
+
649
+ auth[:has_authorized_method] = source.match?(/def\s+(?:self\.)?authorized\?/) || false
650
+ auth[:pundit] = source.match?(/PolicyFinder|policy_class|authorize!?\s/) || false
651
+ auth[:cancan] = source.match?(/can\?|authorize!\s|CanCan|Ability/) || false
652
+ auth[:custom_guard] = source.match?(/def\s+(?:self\.)?(?:visible\?|scope_items|ready\?)/) || false
653
+
654
+ auth
655
+ end
656
+
657
+ # Extract field complexity settings
658
+ #
659
+ # @param source [String]
660
+ # @return [Array<Hash>]
661
+ def extract_complexity(source)
662
+ complexities = []
663
+
664
+ source.scan(/field\s+:(\w+).*?complexity:\s*(\d+|->.*?(?:end|\}))/m) do |name, value|
665
+ complexities << { field: name, complexity: value.strip }
666
+ end
667
+
668
+ # Max complexity on schema level
669
+ if source.match?(/max_complexity\s+(\d+)/)
670
+ complexities << { field: :schema, complexity: ::Regexp.last_match(1).to_i }
671
+ end
672
+
673
+ complexities
674
+ end
675
+
676
+ # Extract enum values (for enum types)
677
+ #
678
+ # @param source [String]
679
+ # @param runtime_class [Class, nil]
680
+ # @return [Array<Hash>]
681
+ def extract_enum_values(source, runtime_class)
682
+ if runtime_class.respond_to?(:values) && runtime_class.values.is_a?(Hash)
683
+ return runtime_class.values.map do |name, value_obj|
684
+ {
685
+ name: name,
686
+ value: value_obj.respond_to?(:value) ? value_obj.value : name,
687
+ description: value_obj.respond_to?(:description) ? value_obj.description : nil
688
+ }
689
+ end
690
+ end
691
+
692
+ # Parse from source
693
+ values = []
694
+ source.scan(/value\s+["'](\w+)["'](?:.*?description:\s*["']([^"']+)["'])?/) do |name, desc|
695
+ values << { name: name, description: desc }
696
+ end
697
+
698
+ values
699
+ rescue StandardError
700
+ []
701
+ end
702
+
703
+ # Extract union member types
704
+ #
705
+ # @param source [String]
706
+ # @param runtime_class [Class, nil]
707
+ # @return [Array<String>]
708
+ def extract_union_members(source, runtime_class)
709
+ if runtime_class.respond_to?(:possible_types) && runtime_class.possible_types.any?
710
+ return runtime_class.possible_types.filter_map(&:name)
711
+ end
712
+
713
+ source.scan(/possible_types\s+(.+)$/).flatten.flat_map do |types_str|
714
+ types_str.scan(/([\w:]+)/).flatten
715
+ end
716
+ rescue StandardError
717
+ []
718
+ end
719
+
720
+ # ──────────────────────────────────────────────────────────────────────
721
+ # Field Counting Helpers
722
+ # ──────────────────────────────────────────────────────────────────────
723
+
724
+ # Count fields from runtime or source
725
+ #
726
+ # @param source [String]
727
+ # @param runtime_class [Class, nil]
728
+ # @return [Integer]
729
+ def count_fields(source, runtime_class)
730
+ if runtime_class.respond_to?(:fields)
731
+ runtime_class.fields.size
732
+ else
733
+ source.scan(/^\s*field\s+:/).size
734
+ end
735
+ rescue StandardError
736
+ source.scan(/^\s*field\s+:/).size
737
+ end
738
+
739
+ # Count arguments from runtime or source
740
+ #
741
+ # @param source [String]
742
+ # @param runtime_class [Class, nil]
743
+ # @return [Integer]
744
+ def count_arguments(source, runtime_class)
745
+ if runtime_class.respond_to?(:arguments)
746
+ runtime_class.arguments.size
747
+ else
748
+ source.scan(/^\s*argument\s+:/).size
749
+ end
750
+ rescue StandardError
751
+ source.scan(/^\s*argument\s+:/).size
752
+ end
753
+
754
+ # ──────────────────────────────────────────────────────────────────────
755
+ # Dependency Extraction
756
+ # ──────────────────────────────────────────────────────────────────────
757
+
758
+ # Extract all dependencies from source text
759
+ #
760
+ # Uses pattern scanning (not AR descendant iteration) to avoid O(n^2).
761
+ #
762
+ # @param source [String]
763
+ # @return [Array<Hash>]
764
+ def extract_dependencies(source, identifier = nil)
765
+ # Other GraphQL type references (Types::*), excluding self-references
766
+ deps = source.scan(/Types::\w+/).uniq.filter_map do |type_ref|
767
+ next if type_ref == identifier
768
+
769
+ { type: :graphql_type, target: type_ref, via: :type_reference }
770
+ end
771
+
772
+ # Model references: scan for capitalized constants that look like model names.
773
+ # GraphQL uses its own pattern (not ModelNameCache) to avoid O(n^2).
774
+ source.scan(/\b([A-Z][a-z]\w*)\.(?:find|where|find_by|create|new|first|last|all|count|exists\?|destroy|update|pluck|select|order|limit|includes|joins|preload|eager_load)\b/).flatten.uniq.each do |model_ref|
775
+ deps << { type: :model, target: model_ref, via: :code_reference }
776
+ end
777
+
778
+ source.scan(/\b([A-Z][a-z][a-zA-Z]*)\b/).flatten.uniq.each do |const_ref|
779
+ if const_ref.match?(/\A(Types|Mutations|Resolvers|GraphQL|Base|String|Integer|Float|Boolean|Array|Hash|Set|Struct|Module|Class|Object|ID|Int|ISO8601)\z/)
780
+ next
781
+ end
782
+ next if deps.any? { |d| d[:target] == const_ref }
783
+
784
+ if source.match?(/\b#{Regexp.escape(const_ref)}\.(?:find|where|find_by|create|new|first|last|all)\b/)
785
+ deps << { type: :model, target: const_ref, via: :code_reference }
786
+ end
787
+ end
788
+
789
+ deps.concat(scan_service_dependencies(source))
790
+ deps.concat(scan_job_dependencies(source))
791
+ deps.concat(scan_mailer_dependencies(source))
792
+
793
+ # Resolver dependencies (standalone resolver classes referenced in fields)
794
+ source.scan(/resolver:\s*([\w:]+)/).flatten.uniq.each do |resolver|
795
+ deps << { type: :graphql_resolver, target: resolver, via: :field_resolver }
796
+ end
797
+
798
+ deps.uniq { |d| [d[:type], d[:target]] }
799
+ end
800
+
801
+ # ──────────────────────────────────────────────────────────────────────
802
+ # Chunking
803
+ # ──────────────────────────────────────────────────────────────────────
804
+
805
+ # Build semantic chunks for large GraphQL types
806
+ #
807
+ # @param unit [ExtractedUnit]
808
+ # @param runtime_class [Class, nil]
809
+ # @return [Array<Hash>]
810
+ def build_chunks(unit, _runtime_class)
811
+ chunks = []
812
+
813
+ # Summary chunk: overview with field list
814
+ chunks << build_summary_chunk(unit)
815
+
816
+ # Field-group chunks for types with many fields
817
+ fields = unit.metadata[:fields] || []
818
+ if fields.size > 10
819
+ fields.each_slice(10).with_index do |field_group, idx|
820
+ chunks << build_field_group_chunk(unit, field_group, idx)
821
+ end
822
+ end
823
+
824
+ # Arguments chunk for mutations/resolvers
825
+ arguments = unit.metadata[:arguments] || []
826
+ chunks << build_arguments_chunk(unit, arguments) if arguments.any?
827
+
828
+ chunks
829
+ end
830
+
831
+ # Build a summary chunk with high-level type information
832
+ #
833
+ # @param unit [ExtractedUnit]
834
+ # @return [Hash]
835
+ def build_summary_chunk(unit)
836
+ meta = unit.metadata
837
+ fields = meta[:fields] || []
838
+ field_names = fields.map { |f| f[:name] }.compact
839
+
840
+ interfaces = meta[:interfaces] || []
841
+ auth = meta[:authorization] || {}
842
+
843
+ auth_summary = []
844
+ auth_summary << 'authorized?' if auth[:has_authorized_method]
845
+ auth_summary << 'pundit' if auth[:pundit]
846
+ auth_summary << 'cancan' if auth[:cancan]
847
+
848
+ {
849
+ chunk_type: :summary,
850
+ identifier: "#{unit.identifier}:summary",
851
+ content: <<~SUMMARY,
852
+ # #{unit.identifier} - #{format_type_label(unit.type)} Summary
853
+
854
+ Kind: #{meta[:graphql_kind]}
855
+ Parent: #{meta[:parent_class] || 'unknown'}
856
+ Fields: #{field_names.join(', ').presence || 'none'}
857
+ Interfaces: #{interfaces.join(', ').presence || 'none'}
858
+ Authorization: #{auth_summary.join(', ').presence || 'none'}
859
+ SUMMARY
860
+ metadata: { parent: unit.identifier, purpose: :overview }
861
+ }
862
+ end
863
+
864
+ # Build a chunk for a group of fields
865
+ #
866
+ # @param unit [ExtractedUnit]
867
+ # @param field_group [Array<Hash>]
868
+ # @param group_index [Integer]
869
+ # @return [Hash]
870
+ def build_field_group_chunk(unit, field_group, group_index)
871
+ lines = field_group.map do |f|
872
+ parts = ["field :#{f[:name]}"]
873
+ parts << f[:type] if f[:type]
874
+ parts << "(#{f[:description]})" if f[:description]
875
+ parts.join(', ')
876
+ end
877
+
878
+ {
879
+ chunk_type: :fields,
880
+ identifier: "#{unit.identifier}:fields_#{group_index}",
881
+ content: <<~FIELDS,
882
+ # #{unit.identifier} - Fields (group #{group_index})
883
+
884
+ #{lines.join("\n")}
885
+ FIELDS
886
+ metadata: { parent: unit.identifier, purpose: :fields, group_index: group_index }
887
+ }
888
+ end
889
+
890
+ # Build a chunk for arguments
891
+ #
892
+ # @param unit [ExtractedUnit]
893
+ # @param arguments [Array<Hash>]
894
+ # @return [Hash]
895
+ def build_arguments_chunk(unit, arguments)
896
+ lines = arguments.map do |a|
897
+ parts = ["argument :#{a[:name]}"]
898
+ parts << a[:type] if a[:type]
899
+ parts << 'required' if a[:required]
900
+ parts << "(#{a[:description]})" if a[:description]
901
+ parts.join(', ')
902
+ end
903
+
904
+ {
905
+ chunk_type: :arguments,
906
+ identifier: "#{unit.identifier}:arguments",
907
+ content: <<~ARGS,
908
+ # #{unit.identifier} - Arguments
909
+
910
+ #{lines.join("\n")}
911
+ ARGS
912
+ metadata: { parent: unit.identifier, purpose: :arguments }
913
+ }
914
+ end
915
+ end
916
+ end
917
+ end