codebase_index 0.2.1 → 0.3.1

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +95 -300
  4. data/exe/codebase-index-mcp +3 -31
  5. data/exe/codebase-index-mcp-http +3 -31
  6. data/lib/codebase_index/ast/method_extractor.rb +3 -8
  7. data/lib/codebase_index/ast/node.rb +28 -0
  8. data/lib/codebase_index/ast/parser.rb +53 -92
  9. data/lib/codebase_index/builder.rb +67 -4
  10. data/lib/codebase_index/cache/cache_middleware.rb +199 -0
  11. data/lib/codebase_index/cache/cache_store.rb +264 -0
  12. data/lib/codebase_index/cache/redis_cache_store.rb +116 -0
  13. data/lib/codebase_index/cache/solid_cache_store.rb +111 -0
  14. data/lib/codebase_index/chunking/semantic_chunker.rb +29 -24
  15. data/lib/codebase_index/console/adapters/good_job_adapter.rb +7 -40
  16. data/lib/codebase_index/console/adapters/job_adapter.rb +68 -0
  17. data/lib/codebase_index/console/adapters/sidekiq_adapter.rb +7 -40
  18. data/lib/codebase_index/console/adapters/solid_queue_adapter.rb +7 -40
  19. data/lib/codebase_index/console/bridge.rb +7 -0
  20. data/lib/codebase_index/console/console_response_renderer.rb +3 -7
  21. data/lib/codebase_index/console/embedded_executor.rb +2 -1
  22. data/lib/codebase_index/console/server.rb +1 -4
  23. data/lib/codebase_index/dependency_graph.rb +28 -19
  24. data/lib/codebase_index/embedding/indexer.rb +18 -8
  25. data/lib/codebase_index/embedding/openai.rb +27 -6
  26. data/lib/codebase_index/embedding/provider.rb +29 -2
  27. data/lib/codebase_index/evaluation/evaluator.rb +5 -12
  28. data/lib/codebase_index/extractor.rb +40 -44
  29. data/lib/codebase_index/extractors/action_cable_extractor.rb +9 -36
  30. data/lib/codebase_index/extractors/callback_analyzer.rb +22 -8
  31. data/lib/codebase_index/extractors/controller_extractor.rb +3 -93
  32. data/lib/codebase_index/extractors/decorator_extractor.rb +7 -14
  33. data/lib/codebase_index/extractors/engine_extractor.rb +20 -1
  34. data/lib/codebase_index/extractors/graphql_extractor.rb +4 -29
  35. data/lib/codebase_index/extractors/job_extractor.rb +11 -6
  36. data/lib/codebase_index/extractors/lib_extractor.rb +0 -31
  37. data/lib/codebase_index/extractors/mailer_extractor.rb +15 -85
  38. data/lib/codebase_index/extractors/manager_extractor.rb +1 -15
  39. data/lib/codebase_index/extractors/model_extractor.rb +20 -53
  40. data/lib/codebase_index/extractors/phlex_extractor.rb +8 -8
  41. data/lib/codebase_index/extractors/policy_extractor.rb +1 -24
  42. data/lib/codebase_index/extractors/poro_extractor.rb +0 -17
  43. data/lib/codebase_index/extractors/serializer_extractor.rb +12 -7
  44. data/lib/codebase_index/extractors/service_extractor.rb +1 -38
  45. data/lib/codebase_index/extractors/shared_utility_methods.rb +183 -1
  46. data/lib/codebase_index/extractors/validator_extractor.rb +3 -17
  47. data/lib/codebase_index/extractors/view_component_extractor.rb +10 -9
  48. data/lib/codebase_index/filename_utils.rb +32 -0
  49. data/lib/codebase_index/flow_analysis/operation_extractor.rb +1 -4
  50. data/lib/codebase_index/formatting/base.rb +0 -10
  51. data/lib/codebase_index/graph_analyzer.rb +1 -1
  52. data/lib/codebase_index/mcp/bootstrapper.rb +58 -0
  53. data/lib/codebase_index/mcp/renderers/markdown_renderer.rb +35 -34
  54. data/lib/codebase_index/mcp/renderers/plain_renderer.rb +29 -29
  55. data/lib/codebase_index/mcp/server.rb +59 -68
  56. data/lib/codebase_index/mcp/tool_response_renderer.rb +23 -0
  57. data/lib/codebase_index/notion/client.rb +2 -2
  58. data/lib/codebase_index/notion/mapper.rb +1 -0
  59. data/lib/codebase_index/notion/mappers/column_mapper.rb +3 -11
  60. data/lib/codebase_index/notion/mappers/model_mapper.rb +20 -23
  61. data/lib/codebase_index/notion/mappers/shared.rb +22 -0
  62. data/lib/codebase_index/observability/health_check.rb +0 -2
  63. data/lib/codebase_index/observability/structured_logger.rb +12 -30
  64. data/lib/codebase_index/operator/pipeline_guard.rb +0 -7
  65. data/lib/codebase_index/resilience/index_validator.rb +3 -21
  66. data/lib/codebase_index/retrieval/context_assembler.rb +19 -7
  67. data/lib/codebase_index/retrieval/query_classifier.rb +14 -12
  68. data/lib/codebase_index/retrieval/ranker.rb +6 -2
  69. data/lib/codebase_index/retrieval/search_executor.rb +8 -19
  70. data/lib/codebase_index/retriever.rb +1 -9
  71. data/lib/codebase_index/ruby_analyzer/class_analyzer.rb +5 -25
  72. data/lib/codebase_index/ruby_analyzer/dataflow_analyzer.rb +6 -7
  73. data/lib/codebase_index/ruby_analyzer/mermaid_renderer.rb +58 -53
  74. data/lib/codebase_index/ruby_analyzer/trace_enricher.rb +11 -7
  75. data/lib/codebase_index/session_tracer/file_store.rb +1 -8
  76. data/lib/codebase_index/session_tracer/redis_store.rb +1 -7
  77. data/lib/codebase_index/session_tracer/session_flow_assembler.rb +4 -13
  78. data/lib/codebase_index/session_tracer/solid_cache_store.rb +1 -7
  79. data/lib/codebase_index/session_tracer/store.rb +14 -0
  80. data/lib/codebase_index/storage/metadata_store.rb +37 -10
  81. data/lib/codebase_index/storage/pgvector.rb +37 -5
  82. data/lib/codebase_index/storage/qdrant.rb +39 -6
  83. data/lib/codebase_index/storage/vector_store.rb +11 -0
  84. data/lib/codebase_index/temporal/snapshot_store.rb +14 -10
  85. data/lib/codebase_index/token_utils.rb +19 -0
  86. data/lib/codebase_index/version.rb +1 -1
  87. data/lib/codebase_index.rb +25 -6
  88. data/lib/tasks/codebase_index.rake +2 -2
  89. metadata +11 -2
@@ -58,17 +58,17 @@ module CodebaseIndex
58
58
  end
59
59
 
60
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'] || []
61
+ query = fetch_key(data, :query)
62
+ count = fetch_key(data, :result_count, 0)
63
+ results = fetch_key(data, :results, [])
64
64
 
65
65
  lines = []
66
66
  lines << "Search: \"#{query}\" (#{count} results)"
67
67
  lines << DIVIDER
68
68
 
69
69
  results.each do |r|
70
- ident = r['identifier'] || r[:identifier]
71
- type = r['type'] || r[:type]
70
+ ident = fetch_key(r, :identifier)
71
+ type = fetch_key(r, :type)
72
72
  lines << " #{ident} (#{type})"
73
73
  end
74
74
 
@@ -84,7 +84,7 @@ module CodebaseIndex
84
84
  end
85
85
 
86
86
  def render_structure(data, **)
87
- manifest = data[:manifest] || data['manifest'] || {}
87
+ manifest = fetch_key(data, :manifest, {})
88
88
  lines = []
89
89
  lines << 'Codebase Structure'
90
90
  lines << DIVIDER
@@ -100,7 +100,7 @@ module CodebaseIndex
100
100
  counts.sort_by { |_k, v| -v }.each { |type, count| lines << " #{type}: #{count}" }
101
101
  end
102
102
 
103
- summary = data[:summary] || data['summary']
103
+ summary = fetch_key(data, :summary)
104
104
  if summary
105
105
  lines << ''
106
106
  lines << DIVIDER
@@ -115,14 +115,14 @@ module CodebaseIndex
115
115
  lines << 'Graph Analysis'
116
116
  lines << DIVIDER
117
117
 
118
- stats = data['stats'] || data[:stats]
118
+ stats = fetch_key(data, :stats)
119
119
  if stats.is_a?(Hash)
120
120
  stats.each { |k, v| lines << " #{k}: #{v}" }
121
121
  lines << ''
122
122
  end
123
123
 
124
124
  %w[orphans dead_ends hubs cycles bridges].each do |section|
125
- items = data[section] || data[section.to_sym]
125
+ items = fetch_key(data, section)
126
126
  next unless items.is_a?(Array) && items.any?
127
127
 
128
128
  lines << "#{section.tr('_', ' ').upcase}:"
@@ -144,14 +144,14 @@ module CodebaseIndex
144
144
 
145
145
  def render_pagerank(data, **)
146
146
  lines = []
147
- lines << "PageRank Scores (#{data[:total_nodes] || data['total_nodes']} nodes)"
147
+ lines << "PageRank Scores (#{fetch_key(data, :total_nodes)} nodes)"
148
148
  lines << DIVIDER
149
149
 
150
- results = data[:results] || data['results'] || []
150
+ results = fetch_key(data, :results, [])
151
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']
152
+ ident = fetch_key(r, :identifier)
153
+ type = fetch_key(r, :type)
154
+ score = fetch_key(r, :score)
155
155
  lines << " #{i + 1}. #{ident} (#{type}) - #{score}"
156
156
  end
157
157
 
@@ -159,17 +159,17 @@ module CodebaseIndex
159
159
  end
160
160
 
161
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'] || []
162
+ keyword = fetch_key(data, :keyword)
163
+ count = fetch_key(data, :result_count, 0)
164
+ results = fetch_key(data, :results, [])
165
165
 
166
166
  lines = []
167
167
  lines << "Framework: \"#{keyword}\" (#{count} results)"
168
168
  lines << DIVIDER
169
169
 
170
170
  results.each do |r|
171
- ident = r['identifier'] || r[:identifier]
172
- type = r['type'] || r[:type]
171
+ ident = fetch_key(r, :identifier)
172
+ type = fetch_key(r, :type)
173
173
  lines << " #{ident} (#{type})"
174
174
  end
175
175
 
@@ -177,17 +177,17 @@ module CodebaseIndex
177
177
  end
178
178
 
179
179
  def render_recent_changes(data, **)
180
- count = data[:result_count] || data['result_count'] || 0
181
- results = data[:results] || data['results'] || []
180
+ count = fetch_key(data, :result_count, 0)
181
+ results = fetch_key(data, :results, [])
182
182
 
183
183
  lines = []
184
184
  lines << "Recent Changes (#{count} units)"
185
185
  lines << DIVIDER
186
186
 
187
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] || '-'
188
+ ident = fetch_key(r, :identifier)
189
+ type = fetch_key(r, :type)
190
+ modified = fetch_key(r, :last_modified) || '-'
191
191
  lines << " #{ident} (#{type}) - #{modified}"
192
192
  end
193
193
 
@@ -210,10 +210,10 @@ module CodebaseIndex
210
210
  private
211
211
 
212
212
  def render_plain_traversal(label, data)
213
- root = data[:root] || data['root']
213
+ root = fetch_key(data, :root)
214
214
  found = data[:found] || data['found']
215
- nodes = data[:nodes] || data['nodes'] || {}
216
- message = data[:message] || data['message']
215
+ nodes = fetch_key(data, :nodes, {})
216
+ message = fetch_key(data, :message)
217
217
 
218
218
  lines = []
219
219
  lines << "#{label} of #{root}"
@@ -225,8 +225,8 @@ module CodebaseIndex
225
225
  end
226
226
 
227
227
  nodes.each do |id, info|
228
- depth = info['depth'] || info[:depth] || 0
229
- deps = info['deps'] || info[:deps] || []
228
+ depth = fetch_key(info, :depth) || 0
229
+ deps = fetch_key(info, :deps, [])
230
230
  indent = ' ' * (depth + 1)
231
231
  lines << "#{indent}#{id}"
232
232
  deps.each { |d| lines << "#{indent} -> #{d}" }
@@ -48,8 +48,16 @@ module CodebaseIndex
48
48
 
49
49
  define_lookup_tool(server, reader, respond, renderer)
50
50
  define_search_tool(server, reader, respond, renderer)
51
- define_dependencies_tool(server, reader, respond, renderer)
52
- define_dependents_tool(server, reader, respond, renderer)
51
+ define_traversal_tool(server, reader, respond, renderer,
52
+ name: 'dependencies',
53
+ description: 'Traverse forward dependencies of a unit (what it depends on). Returns a BFS tree with depth.',
54
+ reader_method: :traverse_dependencies,
55
+ render_key: :dependencies)
56
+ define_traversal_tool(server, reader, respond, renderer,
57
+ name: 'dependents',
58
+ description: 'Traverse reverse dependencies of a unit (what depends on it). Returns a BFS tree with depth.',
59
+ reader_method: :traverse_dependents,
60
+ render_key: :dependents)
53
61
  define_structure_tool(server, reader, respond, renderer)
54
62
  define_graph_analysis_tool(server, reader, respond, renderer)
55
63
  define_pagerank_tool(server, reader, respond, renderer)
@@ -89,7 +97,38 @@ module CodebaseIndex
89
97
  end
90
98
  end
91
99
 
100
+ # Coerce a value to an Array. Wraps a single String in an Array;
101
+ # leaves existing Arrays and nil unchanged.
102
+ #
103
+ # @param value [String, Array, nil] The input value
104
+ # @return [Array, nil]
105
+ def coerce_array(value)
106
+ value.is_a?(String) ? [value] : value
107
+ end
108
+
109
+ # Apply offset+limit pagination to a single section key within a container hash.
110
+ # Adds `_total`, `_truncated`, and `_offset` metadata keys when truncating.
111
+ #
112
+ # @param container [Hash] The hash to mutate
113
+ # @param key [String] The section key whose value is an Array
114
+ # @param limit [Integer, nil] Max items to retain; nil means no limit
115
+ # @param offset [Integer] Items to skip from the front
116
+ # @return [void]
117
+ def paginate_section(container, key, limit, offset)
118
+ original = container[key]
119
+ return unless original.is_a?(Array)
120
+
121
+ sliced = offset.positive? ? original.drop(offset) : original
122
+ container[key] = limit ? truncate_section(sliced, limit) : sliced
123
+ if original.size > offset + (limit || original.size)
124
+ container["#{key}_total"] = original.size
125
+ container["#{key}_truncated"] = true
126
+ end
127
+ container["#{key}_offset"] = offset if offset.positive?
128
+ end
129
+
92
130
  def define_lookup_tool(server, reader, respond, renderer)
131
+ coerce = method(:coerce_array)
93
132
  server.define_tool(
94
133
  name: 'lookup',
95
134
  description: 'Look up a code unit by its exact identifier. Returns full source code, metadata, ' \
@@ -108,7 +147,7 @@ module CodebaseIndex
108
147
  required: ['identifier']
109
148
  }
110
149
  ) do |identifier:, server_context:, include_source: nil, sections: nil|
111
- sections = [sections] if sections.is_a?(String)
150
+ sections = coerce.call(sections)
112
151
  unit = reader.find_unit(identifier)
113
152
  if unit
114
153
  always_include = %w[type identifier file_path namespace]
@@ -126,6 +165,7 @@ module CodebaseIndex
126
165
  end
127
166
 
128
167
  def define_search_tool(server, reader, respond, renderer)
168
+ coerce = method(:coerce_array)
129
169
  server.define_tool(
130
170
  name: 'search',
131
171
  description: 'Search code units by pattern. Matches against identifiers by default; can also search source_code and metadata fields.',
@@ -145,8 +185,8 @@ module CodebaseIndex
145
185
  required: ['query']
146
186
  }
147
187
  ) do |query:, server_context:, types: nil, fields: nil, limit: nil|
148
- types = [types] if types.is_a?(String)
149
- fields = [fields] if fields.is_a?(String)
188
+ types = coerce.call(types)
189
+ fields = coerce.call(fields)
150
190
  results = reader.search(
151
191
  query,
152
192
  types: types,
@@ -161,10 +201,11 @@ module CodebaseIndex
161
201
  end
162
202
  end
163
203
 
164
- def define_dependencies_tool(server, reader, respond, renderer)
204
+ def define_traversal_tool(server, reader, respond, renderer, name:, description:, reader_method:, render_key:)
205
+ coerce = method(:coerce_array)
165
206
  server.define_tool(
166
- name: 'dependencies',
167
- description: 'Traverse forward dependencies of a unit (what it depends on). Returns a BFS tree with depth.',
207
+ name: name,
208
+ description: description,
168
209
  input_schema: {
169
210
  properties: {
170
211
  identifier: { type: 'string', description: 'Unit identifier to start from' },
@@ -177,47 +218,13 @@ module CodebaseIndex
177
218
  required: ['identifier']
178
219
  }
179
220
  ) do |identifier:, server_context:, depth: nil, types: nil|
180
- types = [types] if types.is_a?(String)
181
- result = reader.traverse_dependencies(
182
- identifier,
183
- depth: depth || 2,
184
- types: types
185
- )
221
+ types = coerce.call(types)
222
+ result = reader.send(reader_method, identifier, depth: depth || 2, types: types)
186
223
  if result[:found] == false
187
224
  result[:message] =
188
225
  "Identifier '#{identifier}' not found in the index. Use 'search' to find valid identifiers."
189
226
  end
190
- respond.call(renderer.render(:dependencies, result))
191
- end
192
- end
193
-
194
- def define_dependents_tool(server, reader, respond, renderer)
195
- server.define_tool(
196
- name: 'dependents',
197
- description: 'Traverse reverse dependencies of a unit (what depends on it). Returns a BFS tree with depth.',
198
- input_schema: {
199
- properties: {
200
- identifier: { type: 'string', description: 'Unit identifier to start from' },
201
- depth: { type: 'integer', description: 'Maximum traversal depth (default: 2)' },
202
- types: {
203
- type: 'array', items: { type: 'string' },
204
- description: 'Filter to these types'
205
- }
206
- },
207
- required: ['identifier']
208
- }
209
- ) do |identifier:, server_context:, depth: nil, types: nil|
210
- types = [types] if types.is_a?(String)
211
- result = reader.traverse_dependents(
212
- identifier,
213
- depth: depth || 2,
214
- types: types
215
- )
216
- if result[:found] == false
217
- result[:message] =
218
- "Identifier '#{identifier}' not found in the index. Use 'search' to find valid identifiers."
219
- end
220
- respond.call(renderer.render(:dependents, result))
227
+ respond.call(renderer.render(render_key, result))
221
228
  end
222
229
  end
223
230
 
@@ -241,7 +248,7 @@ module CodebaseIndex
241
248
  end
242
249
 
243
250
  def define_graph_analysis_tool(server, reader, respond, renderer)
244
- truncate = method(:truncate_section)
251
+ paginate = method(:paginate_section)
245
252
  server.define_tool(
246
253
  name: 'graph_analysis',
247
254
  description: 'Get structural analysis of the dependency graph: orphans, dead ends, hubs, cycles, and bridges.',
@@ -265,16 +272,7 @@ module CodebaseIndex
265
272
  if limit || effective_offset.positive?
266
273
  truncated = data.dup
267
274
  %w[orphans dead_ends hubs cycles bridges].each do |key|
268
- next unless truncated[key].is_a?(Array)
269
-
270
- original = truncated[key]
271
- sliced = effective_offset.positive? ? original.drop(effective_offset) : original
272
- truncated[key] = limit ? truncate.call(sliced, limit) : sliced
273
- if original.size > effective_offset + (limit || original.size)
274
- truncated["#{key}_total"] = original.size
275
- truncated["#{key}_truncated"] = true
276
- end
277
- truncated["#{key}_offset"] = effective_offset if effective_offset.positive?
275
+ paginate.call(truncated, key, limit, effective_offset)
278
276
  end
279
277
  truncated
280
278
  else
@@ -282,16 +280,7 @@ module CodebaseIndex
282
280
  end
283
281
  else
284
282
  single = { section => data[section], 'stats' => data['stats'] }
285
- if data[section].is_a?(Array) && (limit || effective_offset.positive?)
286
- original = data[section]
287
- sliced = effective_offset.positive? ? original.drop(effective_offset) : original
288
- single[section] = limit ? truncate.call(sliced, limit) : sliced
289
- if original.size > effective_offset + (limit || original.size)
290
- single["#{section}_total"] = original.size
291
- single["#{section}_truncated"] = true
292
- end
293
- single["#{section}_offset"] = effective_offset if effective_offset.positive?
294
- end
283
+ paginate.call(single, section, limit, effective_offset) if limit || effective_offset.positive?
295
284
  single
296
285
  end
297
286
 
@@ -300,6 +289,7 @@ module CodebaseIndex
300
289
  end
301
290
 
302
291
  def define_pagerank_tool(server, reader, respond, renderer)
292
+ coerce = method(:coerce_array)
303
293
  server.define_tool(
304
294
  name: 'pagerank',
305
295
  description: 'Get PageRank importance scores for code units. Higher scores indicate more structurally important nodes.',
@@ -313,7 +303,7 @@ module CodebaseIndex
313
303
  }
314
304
  }
315
305
  ) do |server_context:, limit: nil, types: nil|
316
- types = [types] if types.is_a?(String)
306
+ types = coerce.call(types)
317
307
  scores = reader.dependency_graph.pagerank
318
308
  graph_data = reader.raw_graph_data
319
309
  nodes = graph_data['nodes'] || {}
@@ -362,6 +352,7 @@ module CodebaseIndex
362
352
  end
363
353
 
364
354
  def define_recent_changes_tool(server, reader, respond, renderer)
355
+ coerce = method(:coerce_array)
365
356
  server.define_tool(
366
357
  name: 'recent_changes',
367
358
  description: 'List recently modified code units sorted by git last_modified timestamp. ' \
@@ -376,7 +367,7 @@ module CodebaseIndex
376
367
  }
377
368
  }
378
369
  ) do |server_context:, limit: nil, types: nil|
379
- types = [types] if types.is_a?(String)
370
+ types = coerce.call(types)
380
371
  results = reader.recent_changes(limit: limit || 10, types: types)
381
372
  respond.call(renderer.render(:recent_changes, {
382
373
  result_count: results.size,
@@ -57,6 +57,29 @@ module CodebaseIndex
57
57
  def render_default(data)
58
58
  raise NotImplementedError, "#{self.class}#render_default must be implemented"
59
59
  end
60
+
61
+ private
62
+
63
+ # Fetch a value from a hash by symbol or string key, falling back to a default.
64
+ #
65
+ # Handles data hashes that may use either symbol or string keys (e.g., data
66
+ # assembled from JSON parsing vs. direct Hash literals).
67
+ #
68
+ # @param data [Hash] The source hash
69
+ # @param key [Symbol, String] The key to look up
70
+ # @param default [Object] Value to return when key is absent (default: nil)
71
+ # @return [Object]
72
+ def fetch_key(data, key, default = nil)
73
+ sym_key = key.to_sym
74
+ str_key = key.to_s
75
+ if data.key?(sym_key)
76
+ data[sym_key]
77
+ elsif data.key?(str_key)
78
+ data[str_key]
79
+ else
80
+ default
81
+ end
82
+ end
60
83
  end
61
84
  end
62
85
  end
@@ -129,7 +129,7 @@ module CodebaseIndex
129
129
  retries = 0
130
130
 
131
131
  loop do
132
- response = execute_with_retry(method, path, body, retries)
132
+ response = execute_with_retry(method, path, body)
133
133
 
134
134
  return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
135
135
 
@@ -148,7 +148,7 @@ module CodebaseIndex
148
148
  #
149
149
  # @return [Net::HTTPResponse]
150
150
  # @raise [CodebaseIndex::Error] on persistent network failures
151
- def execute_with_retry(method, path, body, _retries)
151
+ def execute_with_retry(method, path, body)
152
152
  attempts = 0
153
153
  begin
154
154
  @rate_limiter.throttle { execute_http(method, path, body) }
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'mappers/shared'
3
4
  require_relative 'mappers/model_mapper'
4
5
  require_relative 'mappers/column_mapper'
5
6
  require_relative 'mappers/migration_mapper'
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'shared'
4
+
3
5
  module CodebaseIndex
4
6
  module Notion
5
7
  module Mappers
@@ -13,7 +15,7 @@ module CodebaseIndex
13
15
  # properties = mapper.map(column, model_identifier: "User", validations: [...], parent_page_id: "page-123")
14
16
  #
15
17
  class ColumnMapper
16
- MAX_RICH_TEXT_LENGTH = 2000
18
+ include Shared
17
19
 
18
20
  # Map a single column to Notion Columns page properties.
19
21
  #
@@ -49,16 +51,6 @@ module CodebaseIndex
49
51
 
50
52
  matched.map { |v| v['type'] }.join(', ')
51
53
  end
52
-
53
- # Build a Notion rich_text property.
54
- #
55
- # @param text [String]
56
- # @return [Hash]
57
- def rich_text_property(text)
58
- content = text.to_s
59
- content = "#{content[0...1997]}..." if content.length > MAX_RICH_TEXT_LENGTH
60
- { rich_text: [{ text: { content: content } }] }
61
- end
62
54
  end
63
55
  end
64
56
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'shared'
4
+
3
5
  module CodebaseIndex
4
6
  module Notion
5
7
  module Mappers
@@ -14,7 +16,7 @@ module CodebaseIndex
14
16
  # client.create_page(database_id: db_id, properties: properties)
15
17
  #
16
18
  class ModelMapper
17
- MAX_RICH_TEXT_LENGTH = 2000
19
+ include Shared
18
20
 
19
21
  # Map a model unit to Notion Data Models page properties.
20
22
  #
@@ -83,9 +85,7 @@ module CodebaseIndex
83
85
 
84
86
  # @return [String]
85
87
  def format_associations(associations)
86
- return 'None' if associations.nil? || associations.empty?
87
-
88
- associations.map { |a| format_single_association(a) }.join("\n")
88
+ format_list(associations) { |items| items.map { |a| format_single_association(a) }.join("\n") }
89
89
  end
90
90
 
91
91
  # @return [String]
@@ -99,18 +99,16 @@ module CodebaseIndex
99
99
 
100
100
  # @return [String]
101
101
  def format_validations(validations)
102
- return 'None' if validations.nil? || validations.empty?
103
-
104
- validations.group_by { |v| v['attribute'] }.map do |attr, vals|
105
- "#{attr}: #{vals.map { |v| v['type'] }.join(', ')}"
106
- end.join("\n")
102
+ format_list(validations) do |items|
103
+ items.group_by { |v| v['attribute'] }.map do |attr, vals|
104
+ "#{attr}: #{vals.map { |v| v['type'] }.join(', ')}"
105
+ end.join("\n")
106
+ end
107
107
  end
108
108
 
109
109
  # @return [String]
110
110
  def format_callbacks(callbacks)
111
- return 'None' if callbacks.nil? || callbacks.empty?
112
-
113
- callbacks.map { |callback| format_single_callback(callback) }.join("\n")
111
+ format_list(callbacks) { |items| items.map { |callback| format_single_callback(callback) }.join("\n") }
114
112
  end
115
113
 
116
114
  # @return [String]
@@ -135,16 +133,12 @@ module CodebaseIndex
135
133
 
136
134
  # @return [String]
137
135
  def format_scopes(scopes)
138
- return 'None' if scopes.nil? || scopes.empty?
139
-
140
- scopes.map { |s| s['name'] }.join(', ')
136
+ format_list(scopes) { |items| items.map { |s| s['name'] }.join(', ') }
141
137
  end
142
138
 
143
139
  # @return [String]
144
140
  def format_dependencies(dependencies)
145
- return 'None' if dependencies.nil? || dependencies.empty?
146
-
147
- dependencies.map { |dep| "#{dep['target']} (via #{dep['via']})" }.join(', ')
141
+ format_list(dependencies) { |items| items.map { |dep| "#{dep['target']} (via #{dep['via']})" }.join(', ') }
148
142
  end
149
143
 
150
144
  # @return [Hash]
@@ -152,11 +146,14 @@ module CodebaseIndex
152
146
  { title: [{ text: { content: text } }] }
153
147
  end
154
148
 
155
- # @return [Hash]
156
- def rich_text_property(text)
157
- content = text.to_s
158
- content = "#{content[0...1997]}..." if content.length > MAX_RICH_TEXT_LENGTH
159
- { rich_text: [{ text: { content: content } }] }
149
+ # Return 'None' for nil/empty lists; otherwise yield items to a formatting block.
150
+ #
151
+ # @param items [Array, nil]
152
+ # @return [String]
153
+ def format_list(items)
154
+ return 'None' if items.nil? || items.empty?
155
+
156
+ yield items
160
157
  end
161
158
  end
162
159
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CodebaseIndex
4
+ module Notion
5
+ module Mappers
6
+ # Shared helpers for Notion mapper classes.
7
+ module Shared
8
+ MAX_RICH_TEXT_LENGTH = 2000
9
+
10
+ # Build a Notion rich_text property, truncating to API limits.
11
+ #
12
+ # @param text [String]
13
+ # @return [Hash]
14
+ def rich_text_property(text)
15
+ content = text.to_s
16
+ content = "#{content[0...1997]}..." if content.length > MAX_RICH_TEXT_LENGTH
17
+ { rich_text: [{ text: { content: content } }] }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -73,8 +73,6 @@ module CodebaseIndex
73
73
  else
74
74
  :error
75
75
  end
76
- rescue StandardError
77
- :error
78
76
  end
79
77
  end
80
78
  end
@@ -22,36 +22,18 @@ module CodebaseIndex
22
22
  @output = output
23
23
  end
24
24
 
25
- # Log at info level.
26
- #
27
- # @param event [String] Event name
28
- # @param data [Hash] Additional structured data
29
- def info(event, **data)
30
- write_entry('info', event, data)
31
- end
32
-
33
- # Log at warn level.
34
- #
35
- # @param event [String] Event name
36
- # @param data [Hash] Additional structured data
37
- def warn(event, **data)
38
- write_entry('warn', event, data)
39
- end
40
-
41
- # Log at error level.
42
- #
43
- # @param event [String] Event name
44
- # @param data [Hash] Additional structured data
45
- def error(event, **data)
46
- write_entry('error', event, data)
47
- end
48
-
49
- # Log at debug level.
50
- #
51
- # @param event [String] Event name
52
- # @param data [Hash] Additional structured data
53
- def debug(event, **data)
54
- write_entry('debug', event, data)
25
+ # @!method info(event, **data)
26
+ # Log at info level.
27
+ # @param event [String] Event name
28
+ # @param data [Hash] Additional structured data
29
+ # @!method warn(event, **data)
30
+ # Log at warn level.
31
+ # @!method error(event, **data)
32
+ # Log at error level.
33
+ # @!method debug(event, **data)
34
+ # Log at debug level.
35
+ %w[info warn error debug].each do |level|
36
+ define_method(level) { |event, **data| write_entry(level, event, data) }
55
37
  end
56
38
 
57
39
  private
@@ -87,13 +87,6 @@ module CodebaseIndex
87
87
  rescue JSON::ParserError
88
88
  {}
89
89
  end
90
-
91
- # @param state [Hash]
92
- # @return [void]
93
- def write_state(state)
94
- FileUtils.mkdir_p(@state_dir)
95
- File.write(@state_path, JSON.generate(state))
96
- end
97
90
  end
98
91
  end
99
92
  end