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,352 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CodebaseIndex
4
+ module MCP
5
+ module Renderers
6
+ # Renders MCP tool responses as pure Markdown.
7
+ # Headers, tables, code blocks, and bullet lists — no JSON structural markers.
8
+ class MarkdownRenderer < ToolResponseRenderer
9
+ # ── lookup ──────────────────────────────────────────────────
10
+
11
+ # @param data [Hash] Unit data from IndexReader#find_unit
12
+ # @return [String] Markdown-formatted unit
13
+ def render_lookup(data, **)
14
+ return 'Unit not found' unless data.is_a?(Hash) && data['identifier']
15
+
16
+ lines = []
17
+ lines << "## #{data['identifier']} (#{data['type']})"
18
+ lines << ''
19
+ lines << "**File:** `#{data['file_path']}`" if data['file_path']
20
+ lines << "**Namespace:** #{data['namespace']}" if data['namespace']
21
+ lines << ''
22
+
23
+ lines << render_metadata_section(data['metadata']) if data['metadata'].is_a?(Hash) && data['metadata'].any?
24
+
25
+ if data['source_code']
26
+ lines << '### Source'
27
+ lines << ''
28
+ lines << '```ruby'
29
+ lines << data['source_code'].chomp
30
+ lines << '```'
31
+ lines << ''
32
+ end
33
+
34
+ if data['dependencies'].is_a?(Array) && data['dependencies'].any?
35
+ lines << '### Dependencies'
36
+ lines << ''
37
+ data['dependencies'].each { |dep| lines << "- #{dep}" }
38
+ lines << ''
39
+ end
40
+
41
+ if data['dependents'].is_a?(Array) && data['dependents'].any?
42
+ lines << '### Dependents'
43
+ lines << ''
44
+ data['dependents'].each { |dep| lines << "- #{dep}" }
45
+ lines << ''
46
+ end
47
+
48
+ lines.join("\n").rstrip
49
+ end
50
+
51
+ # ── search ──────────────────────────────────────────────────
52
+
53
+ # @param data [Hash] Search results with :query, :result_count, :results
54
+ # @return [String] Markdown search results
55
+ def render_search(data, **)
56
+ lines = []
57
+ lines << "## Search: \"#{data[:query] || data['query']}\""
58
+ count = data[:result_count] || data['result_count'] || 0
59
+ lines << ''
60
+ lines << "#{count} result#{'s' unless count == 1} found."
61
+ lines << ''
62
+
63
+ results = data[:results] || data['results'] || []
64
+ results.each do |r|
65
+ ident = r['identifier'] || r[:identifier]
66
+ type = r['type'] || r[:type]
67
+ match = r['match_field'] || r[:match_field]
68
+ line = "- **#{ident}** (#{type})"
69
+ line += " — matched in #{match}" if match
70
+ lines << line
71
+ end
72
+
73
+ lines.join("\n").rstrip
74
+ end
75
+
76
+ # ── dependencies / dependents ───────────────────────────────
77
+
78
+ # @param data [Hash] Traversal result with :root, :nodes, :found
79
+ # @return [String] Markdown dependency tree
80
+ def render_dependencies(data, **)
81
+ render_traversal('Dependencies', data)
82
+ end
83
+
84
+ # @param data [Hash] Traversal result with :root, :nodes, :found
85
+ # @return [String] Markdown dependents tree
86
+ def render_dependents(data, **)
87
+ render_traversal('Dependents', data)
88
+ end
89
+
90
+ # ── structure ───────────────────────────────────────────────
91
+
92
+ # @param data [Hash] Structure data with :manifest and optional :summary
93
+ # @return [String] Markdown structure overview
94
+ def render_structure(data, **)
95
+ manifest = data[:manifest] || data['manifest'] || {}
96
+ lines = []
97
+ lines << '## Codebase Structure'
98
+ lines << ''
99
+
100
+ %w[rails_version ruby_version git_branch git_sha extracted_at].each do |key|
101
+ lines << "- **#{key.tr('_', ' ').capitalize}:** #{manifest[key]}" if manifest[key]
102
+ end
103
+ lines << "- **Total units:** #{manifest['total_units']}" if manifest['total_units']
104
+ lines << ''
105
+
106
+ counts = manifest['counts']
107
+ if counts.is_a?(Hash) && counts.any?
108
+ lines << '| Type | Count |'
109
+ lines << '|------|-------|'
110
+ counts.sort_by { |_k, v| -v }.each do |type, count|
111
+ lines << "| #{type} | #{count} |"
112
+ end
113
+ lines << ''
114
+ end
115
+
116
+ summary = data[:summary] || data['summary']
117
+ if summary
118
+ lines << '### Summary'
119
+ lines << ''
120
+ lines << summary
121
+ end
122
+
123
+ lines.join("\n").rstrip
124
+ end
125
+
126
+ # ── graph_analysis ──────────────────────────────────────────
127
+
128
+ # @param data [Hash] Graph analysis with section arrays and stats
129
+ # @return [String] Markdown graph analysis
130
+ def render_graph_analysis(data, **)
131
+ lines = []
132
+ lines << '## Graph Analysis'
133
+ lines << ''
134
+
135
+ stats = data['stats'] || data[:stats]
136
+ if stats.is_a?(Hash)
137
+ stats.each { |k, v| lines << "- **#{k}:** #{v}" }
138
+ lines << ''
139
+ end
140
+
141
+ %w[orphans dead_ends hubs cycles bridges].each do |section|
142
+ items = data[section] || data[section.to_sym]
143
+ next unless items.is_a?(Array) && items.any?
144
+
145
+ lines << "### #{section.tr('_', ' ').capitalize}"
146
+ lines << ''
147
+ items.each do |item|
148
+ lines << if item.is_a?(Hash)
149
+ "- **#{item['identifier']}** (#{item['type']}) — #{item['dependent_count']} dependents"
150
+ else
151
+ "- #{item}"
152
+ end
153
+ end
154
+
155
+ total_key = "#{section}_total"
156
+ if data[total_key]
157
+ lines << ''
158
+ lines << "_Showing #{items.size} of #{data[total_key]} (truncated)_"
159
+ end
160
+ lines << ''
161
+ end
162
+
163
+ lines.join("\n").rstrip
164
+ end
165
+
166
+ # ── pagerank ────────────────────────────────────────────────
167
+
168
+ # @param data [Hash] PageRank data with :total_nodes and :results
169
+ # @return [String] Markdown table of ranked nodes
170
+ def render_pagerank(data, **)
171
+ lines = []
172
+ lines << '## PageRank Scores'
173
+ lines << ''
174
+ lines << "#{data[:total_nodes] || data['total_nodes']} nodes in graph."
175
+ lines << ''
176
+ lines << '| Rank | Identifier | Type | Score |'
177
+ lines << '|------|-----------|------|-------|'
178
+
179
+ results = data[:results] || data['results'] || []
180
+ results.each_with_index do |r, i|
181
+ ident = r[:identifier] || r['identifier']
182
+ type = r[:type] || r['type']
183
+ score = r[:score] || r['score']
184
+ lines << "| #{i + 1} | #{ident} | #{type} | #{score} |"
185
+ end
186
+
187
+ lines.join("\n").rstrip
188
+ end
189
+
190
+ # ── framework ───────────────────────────────────────────────
191
+
192
+ # @param data [Hash] Framework search results
193
+ # @return [String] Markdown framework results
194
+ def render_framework(data, **)
195
+ lines = []
196
+ keyword = data[:keyword] || data['keyword']
197
+ count = data[:result_count] || data['result_count'] || 0
198
+ lines << "## Framework: \"#{keyword}\""
199
+ lines << ''
200
+ lines << "#{count} result#{'s' unless count == 1} found."
201
+ lines << ''
202
+
203
+ results = data[:results] || data['results'] || []
204
+ results.each do |r|
205
+ ident = r['identifier'] || r[:identifier]
206
+ type = r['type'] || r[:type]
207
+ file = r['file_path'] || r[:file_path]
208
+ line = "- **#{ident}** (#{type})"
209
+ line += " — `#{file}`" if file
210
+ lines << line
211
+ end
212
+
213
+ lines.join("\n").rstrip
214
+ end
215
+
216
+ # ── recent_changes ──────────────────────────────────────────
217
+
218
+ # @param data [Hash] Recent changes with :result_count and :results
219
+ # @return [String] Markdown table of recent changes
220
+ def render_recent_changes(data, **)
221
+ lines = []
222
+ count = data[:result_count] || data['result_count'] || 0
223
+ lines << '## Recent Changes'
224
+ lines << ''
225
+ lines << "#{count} recently modified unit#{'s' unless count == 1}."
226
+ lines << ''
227
+ lines << '| Identifier | Type | Last Modified | Author |'
228
+ lines << '|-----------|------|---------------|--------|'
229
+
230
+ results = data[:results] || data['results'] || []
231
+ results.each do |r|
232
+ ident = r['identifier'] || r[:identifier]
233
+ type = r['type'] || r[:type]
234
+ modified = r['last_modified'] || r[:last_modified] || '-'
235
+ author = r['author'] || r[:author] || '-'
236
+ lines << "| #{ident} | #{type} | #{modified} | #{author} |"
237
+ end
238
+
239
+ lines.join("\n").rstrip
240
+ end
241
+
242
+ # ── Default fallback ────────────────────────────────────────
243
+
244
+ # @param data [Object] Any data
245
+ # @return [String] Markdown-formatted default output
246
+ def render_default(data)
247
+ case data
248
+ when Hash
249
+ render_hash_as_markdown(data)
250
+ when Array
251
+ render_array_as_markdown(data)
252
+ else
253
+ data.to_s
254
+ end
255
+ end
256
+
257
+ private
258
+
259
+ def render_traversal(label, data)
260
+ root = data[:root] || data['root']
261
+ found = data[:found] || data['found']
262
+ nodes = data[:nodes] || data['nodes'] || {}
263
+ message = data[:message] || data['message']
264
+
265
+ lines = []
266
+ lines << "## #{label} of #{root}"
267
+ lines << ''
268
+
269
+ if found == false
270
+ (lines << message) || "Identifier '#{root}' not found in the index."
271
+ return lines.join("\n").rstrip
272
+ end
273
+
274
+ nodes.each do |id, info|
275
+ depth = info['depth'] || info[:depth] || 0
276
+ deps = info['deps'] || info[:deps] || []
277
+ indent = ' ' * depth
278
+ lines << "#{indent}- **#{id}**"
279
+ deps.each { |d| lines << "#{indent} - #{d}" }
280
+ end
281
+
282
+ lines.join("\n").rstrip
283
+ end
284
+
285
+ def render_metadata_section(metadata)
286
+ lines = []
287
+ lines << '### Metadata'
288
+ lines << ''
289
+
290
+ metadata.each do |key, value|
291
+ case value
292
+ when Array
293
+ next if value.empty?
294
+
295
+ lines << "**#{key}:**"
296
+ value.each do |item|
297
+ if item.is_a?(Hash)
298
+ summary = item.map { |k, v| "#{k}: #{v}" }.join(', ')
299
+ lines << " - #{summary}"
300
+ else
301
+ lines << " - #{item}"
302
+ end
303
+ end
304
+ when Hash
305
+ lines << "**#{key}:** #{value.map { |k, v| "#{k}=#{v}" }.join(', ')}"
306
+ else
307
+ lines << "**#{key}:** #{value}"
308
+ end
309
+ end
310
+ lines << ''
311
+ lines.join("\n")
312
+ end
313
+
314
+ def render_hash_as_markdown(hash)
315
+ lines = []
316
+ hash.each do |key, value|
317
+ case value
318
+ when Hash
319
+ lines << "**#{key}:**"
320
+ value.each { |k, v| lines << " - #{k}: #{v}" }
321
+ when Array
322
+ lines << "**#{key}:** #{value.size} items"
323
+ value.first(10).each do |item|
324
+ lines << " - #{item.is_a?(Hash) ? item.values.first(3).join(', ') : item}"
325
+ end
326
+ else
327
+ lines << "**#{key}:** #{value}"
328
+ end
329
+ end
330
+ lines.join("\n")
331
+ end
332
+
333
+ def render_array_as_markdown(array)
334
+ return '_(empty)_' if array.empty?
335
+
336
+ if array.first.is_a?(Hash)
337
+ keys = array.first.keys.first(5)
338
+ lines = []
339
+ lines << "| #{keys.join(' | ')} |"
340
+ lines << "| #{keys.map { '---' }.join(' | ')} |"
341
+ array.each do |row|
342
+ lines << "| #{keys.map { |k| row[k] }.join(' | ')} |"
343
+ end
344
+ lines.join("\n")
345
+ else
346
+ array.map { |item| "- #{item}" }.join("\n")
347
+ end
348
+ end
349
+ end
350
+ end
351
+ end
352
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CodebaseIndex
4
+ module MCP
5
+ module Renderers
6
+ # Renders MCP tool responses as plain text with === dividers.
7
+ # Lightweight fallback format with no markup.
8
+ class PlainRenderer < ToolResponseRenderer
9
+ DIVIDER = ('=' * 60).freeze
10
+
11
+ def render_lookup(data, **)
12
+ return 'Unit not found' unless data.is_a?(Hash) && data['identifier']
13
+
14
+ lines = []
15
+ lines << "#{data['identifier']} (#{data['type']})"
16
+ lines << DIVIDER
17
+ lines << "File: #{data['file_path']}" if data['file_path']
18
+ lines << "Namespace: #{data['namespace']}" if data['namespace']
19
+ lines << ''
20
+
21
+ if data['metadata'].is_a?(Hash) && data['metadata'].any?
22
+ lines << 'Metadata:'
23
+ data['metadata'].each do |key, value|
24
+ case value
25
+ when Array
26
+ next if value.empty?
27
+
28
+ lines << " #{key}:"
29
+ value.each do |item|
30
+ lines << " - #{item.is_a?(Hash) ? item.map { |k, v| "#{k}: #{v}" }.join(', ') : item}"
31
+ end
32
+ when Hash
33
+ lines << " #{key}: #{value.map { |k, v| "#{k}=#{v}" }.join(', ')}"
34
+ else
35
+ lines << " #{key}: #{value}"
36
+ end
37
+ end
38
+ lines << ''
39
+ end
40
+
41
+ if data['source_code']
42
+ lines << 'Source:'
43
+ lines << DIVIDER
44
+ lines << data['source_code'].chomp
45
+ lines << DIVIDER
46
+ lines << ''
47
+ end
48
+
49
+ if data['dependencies'].is_a?(Array) && data['dependencies'].any?
50
+ lines << "Dependencies: #{data['dependencies'].join(', ')}"
51
+ end
52
+
53
+ if data['dependents'].is_a?(Array) && data['dependents'].any?
54
+ lines << "Dependents: #{data['dependents'].join(', ')}"
55
+ end
56
+
57
+ lines.join("\n").rstrip
58
+ end
59
+
60
+ def render_search(data, **)
61
+ query = data[:query] || data['query']
62
+ count = data[:result_count] || data['result_count'] || 0
63
+ results = data[:results] || data['results'] || []
64
+
65
+ lines = []
66
+ lines << "Search: \"#{query}\" (#{count} results)"
67
+ lines << DIVIDER
68
+
69
+ results.each do |r|
70
+ ident = r['identifier'] || r[:identifier]
71
+ type = r['type'] || r[:type]
72
+ lines << " #{ident} (#{type})"
73
+ end
74
+
75
+ lines.join("\n").rstrip
76
+ end
77
+
78
+ def render_dependencies(data, **)
79
+ render_plain_traversal('Dependencies', data)
80
+ end
81
+
82
+ def render_dependents(data, **)
83
+ render_plain_traversal('Dependents', data)
84
+ end
85
+
86
+ def render_structure(data, **)
87
+ manifest = data[:manifest] || data['manifest'] || {}
88
+ lines = []
89
+ lines << 'Codebase Structure'
90
+ lines << DIVIDER
91
+
92
+ %w[rails_version ruby_version git_branch git_sha extracted_at total_units].each do |key|
93
+ lines << " #{key}: #{manifest[key]}" if manifest[key]
94
+ end
95
+
96
+ counts = manifest['counts']
97
+ if counts.is_a?(Hash) && counts.any?
98
+ lines << ''
99
+ lines << 'Unit counts:'
100
+ counts.sort_by { |_k, v| -v }.each { |type, count| lines << " #{type}: #{count}" }
101
+ end
102
+
103
+ summary = data[:summary] || data['summary']
104
+ if summary
105
+ lines << ''
106
+ lines << DIVIDER
107
+ lines << summary
108
+ end
109
+
110
+ lines.join("\n").rstrip
111
+ end
112
+
113
+ def render_graph_analysis(data, **)
114
+ lines = []
115
+ lines << 'Graph Analysis'
116
+ lines << DIVIDER
117
+
118
+ stats = data['stats'] || data[:stats]
119
+ if stats.is_a?(Hash)
120
+ stats.each { |k, v| lines << " #{k}: #{v}" }
121
+ lines << ''
122
+ end
123
+
124
+ %w[orphans dead_ends hubs cycles bridges].each do |section|
125
+ items = data[section] || data[section.to_sym]
126
+ next unless items.is_a?(Array) && items.any?
127
+
128
+ lines << "#{section.tr('_', ' ').upcase}:"
129
+ items.each do |item|
130
+ lines << if item.is_a?(Hash)
131
+ " #{item['identifier']} (#{item['type']}) - #{item['dependent_count']} dependents"
132
+ else
133
+ " #{item}"
134
+ end
135
+ end
136
+
137
+ total_key = "#{section}_total"
138
+ lines << " (showing #{items.size} of #{data[total_key]})" if data[total_key]
139
+ lines << ''
140
+ end
141
+
142
+ lines.join("\n").rstrip
143
+ end
144
+
145
+ def render_pagerank(data, **)
146
+ lines = []
147
+ lines << "PageRank Scores (#{data[:total_nodes] || data['total_nodes']} nodes)"
148
+ lines << DIVIDER
149
+
150
+ results = data[:results] || data['results'] || []
151
+ results.each_with_index do |r, i|
152
+ ident = r[:identifier] || r['identifier']
153
+ type = r[:type] || r['type']
154
+ score = r[:score] || r['score']
155
+ lines << " #{i + 1}. #{ident} (#{type}) - #{score}"
156
+ end
157
+
158
+ lines.join("\n").rstrip
159
+ end
160
+
161
+ def render_framework(data, **)
162
+ keyword = data[:keyword] || data['keyword']
163
+ count = data[:result_count] || data['result_count'] || 0
164
+ results = data[:results] || data['results'] || []
165
+
166
+ lines = []
167
+ lines << "Framework: \"#{keyword}\" (#{count} results)"
168
+ lines << DIVIDER
169
+
170
+ results.each do |r|
171
+ ident = r['identifier'] || r[:identifier]
172
+ type = r['type'] || r[:type]
173
+ lines << " #{ident} (#{type})"
174
+ end
175
+
176
+ lines.join("\n").rstrip
177
+ end
178
+
179
+ def render_recent_changes(data, **)
180
+ count = data[:result_count] || data['result_count'] || 0
181
+ results = data[:results] || data['results'] || []
182
+
183
+ lines = []
184
+ lines << "Recent Changes (#{count} units)"
185
+ lines << DIVIDER
186
+
187
+ results.each do |r|
188
+ ident = r['identifier'] || r[:identifier]
189
+ type = r['type'] || r[:type]
190
+ modified = r['last_modified'] || r[:last_modified] || '-'
191
+ lines << " #{ident} (#{type}) - #{modified}"
192
+ end
193
+
194
+ lines.join("\n").rstrip
195
+ end
196
+
197
+ # @param data [Object] Any data
198
+ # @return [String] Plain text output
199
+ def render_default(data)
200
+ case data
201
+ when Hash
202
+ data.map { |k, v| "#{k}: #{v}" }.join("\n")
203
+ when Array
204
+ data.map { |item| " #{item}" }.join("\n")
205
+ else
206
+ data.to_s
207
+ end
208
+ end
209
+
210
+ private
211
+
212
+ def render_plain_traversal(label, data)
213
+ root = data[:root] || data['root']
214
+ found = data[:found] || data['found']
215
+ nodes = data[:nodes] || data['nodes'] || {}
216
+ message = data[:message] || data['message']
217
+
218
+ lines = []
219
+ lines << "#{label} of #{root}"
220
+ lines << DIVIDER
221
+
222
+ if found == false
223
+ lines << (message || "Identifier '#{root}' not found in the index.")
224
+ return lines.join("\n").rstrip
225
+ end
226
+
227
+ nodes.each do |id, info|
228
+ depth = info['depth'] || info[:depth] || 0
229
+ deps = info['deps'] || info[:deps] || []
230
+ indent = ' ' * (depth + 1)
231
+ lines << "#{indent}#{id}"
232
+ deps.each { |d| lines << "#{indent} -> #{d}" }
233
+ end
234
+
235
+ lines.join("\n").rstrip
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end