Class: RailsAiBridge::RubydexAdapter

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_ai_bridge/rubydex_adapter.rb,
lib/rails_ai_bridge/rubydex_adapter/indexer.rb,
lib/rails_ai_bridge/rubydex_adapter/serializer.rb,
lib/rails_ai_bridge/rubydex_adapter/method_counter.rb,
lib/rails_ai_bridge/rubydex_adapter/incremental_indexer.rb

Overview

Wrapper class for Shopify's rubydex semantic analysis API.

Provides a high-level interface for indexing Ruby source files and querying semantic information such as declarations, references, ancestors, and codebase statistics.

Rubydex is an optional dependency. When not installed, all query methods return empty results and RubydexAdapter.available? returns +false+.

Defined Under Namespace

Classes: IncrementalIndexer, Indexer, MethodCounter, Serializer

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root) ⇒ RubydexAdapter

Returns a new instance of RubydexAdapter.

Parameters:

  • root (String)

    the project root directory to index



71
72
73
74
75
76
77
78
79
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 71

def initialize(root)
  @root = root
  @graph = nil
  @indexed = false
  @file_mtimes = {}
  @serializer = Serializer.new(@root)
  @indexer = Indexer.new
  @method_counter = MethodCounter.new(serializer: @serializer)
end

Instance Attribute Details

#graphRubydex::Graph? (readonly)

Returns the underlying rubydex graph instance.

Returns:

  • (Rubydex::Graph, nil)

    the underlying rubydex graph instance



18
19
20
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 18

def graph
  @graph
end

Class Method Details

.available?Boolean

Whether the rubydex gem is installed and loadable.

Returns:

  • (Boolean)

Raises:

  • (LoadError)

    rescued internally, returns false



25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 25

def available?
  if @available.nil?
    @available = begin
      require 'rubydex'
      true
    rescue LoadError
      false
    end
  end

  @available
end

.instance(root = nil) ⇒ RubydexAdapter

Returns a shared adapter instance, building the index if needed. Thread-safe via +Mutex+.

Parameters:

  • root (String) (defaults to: nil)

    the Rails root path to index

Returns:

Raises:

  • (StandardError)

    on indexing failure (graph set to nil, indexed to false)



51
52
53
54
55
56
57
58
59
60
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 51

def instance(root = nil)
  @mutex.synchronize do
    root ||= Rails.root.to_s
    @instance = nil if @instance && @instance_root != root
    @instance ||= begin
      @instance_root = root
      new(root).tap(&:index!)
    end
  end
end

.reset!void

This method returns an undefined value.

Clears the shared instance (useful in tests or when reindexing).



65
66
67
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 65

def reset!
  @mutex.synchronize { @instance = nil }
end

.reset_availability!void

This method returns an undefined value.

Resets the cached availability check (useful in tests).



41
42
43
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 41

def reset_availability!
  @available = nil
end

Instance Method Details

#all_declarationsArray<Hash>

Get all declarations in the graph.

Returns:

  • (Array<Hash>)

    array of declaration summaries

Raises:

  • (StandardError)

    rescued internally, returns empty array



154
155
156
157
158
159
160
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 154

def all_declarations
  return [] unless indexed?

  @graph.declarations.map { |decl| @serializer.declaration_to_hash(decl) }
rescue StandardError
  []
end

#ancestors(name) ⇒ Array<String>

Get ancestors (superclasses/included modules) of a declaration.

Parameters:

  • name (String)

    fully qualified name

Returns:

  • (Array<String>)

    ancestor names

Raises:

  • (StandardError)

    rescued internally, returns empty array



199
200
201
202
203
204
205
206
207
208
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 199

def ancestors(name)
  return [] unless indexed?

  decl = @graph[name]
  return [] unless decl.respond_to?(:ancestors)

  decl.ancestors.map(&:name)
rescue StandardError
  []
end

#codebase_statsHash

High-level codebase statistics from the rubydex index.

Returns:

  • (Hash)

    statistics about the indexed codebase

Raises:

  • (StandardError)

    rescued internally, returns empty hash



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 229

def codebase_stats
  return {} unless indexed?

  declarations = @graph.declarations.to_a
  documents = @graph.documents.to_a

  classes = declarations.select { |d| class_declaration?(d) }
  modules = declarations.select { |d| module_declaration?(d) }

  {
    total_files: documents.size,
    total_declarations: declarations.size,
    total_classes: classes.size,
    total_modules: modules.size,
    total_methods: @method_counter.count(declarations),
    total_constant_references: safe_count(@graph, :constant_references),
    total_method_references: safe_count(@graph, :method_references)
  }
rescue StandardError
  {}
end

#constant_referencesArray<Hash>

Get all constant references across the codebase.

Returns:

  • (Array<Hash>)

    constant reference details

Raises:

  • (StandardError)

    rescued internally, returns empty array



214
215
216
217
218
219
220
221
222
223
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 214

def constant_references
  return [] unless indexed?

  @graph.constant_references.map do |ref|
    { name: ref.respond_to?(:name) ? ref.name : ref.to_s,
      location: ref.respond_to?(:location) ? @serializer.format_location(ref.location) : nil }.compact
  end
rescue StandardError
  []
end

#descendants(name) ⇒ Array<String>

Get descendants (subclasses/includers) of a declaration.

Parameters:

  • name (String)

    fully qualified name

Returns:

  • (Array<String>)

    descendant names

Raises:

  • (StandardError)

    rescued internally, returns empty array



183
184
185
186
187
188
189
190
191
192
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 183

def descendants(name)
  return [] unless indexed?

  decl = @graph[name]
  return [] unless decl.respond_to?(:descendants)

  decl.descendants.map(&:name)
rescue StandardError
  []
end

#file_declarations(path) ⇒ Array<Hash>

Get declarations defined in a specific file.

Parameters:

  • path (String)

    relative or absolute file path

Returns:

  • (Array<Hash>)

    declarations found in the file

Raises:

  • (StandardError)

    rescued internally, returns empty array



167
168
169
170
171
172
173
174
175
176
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 167

def file_declarations(path)
  return [] unless indexed?

  doc = @graph.documents.find { |d| d.uri.end_with?(path) || d.uri == path }
  return [] unless doc

  doc.definitions.map { |defn| @serializer.definition_to_hash(defn) }
rescue StandardError
  []
end

#get_declaration(name) ⇒ Hash?

Get a declaration by its fully qualified name.

Parameters:

  • name (String)

    fully qualified name (e.g. "Foo::Bar")

Returns:

  • (Hash, nil)

    declaration details or nil if not found

Raises:

  • (StandardError)

    rescued internally, returns nil



139
140
141
142
143
144
145
146
147
148
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 139

def get_declaration(name)
  return nil unless indexed?

  decl = @graph[name]
  return nil unless decl

  @serializer.detailed_declaration_to_hash(decl)
rescue StandardError
  nil
end

#index!void

This method returns an undefined value.

Builds the rubydex graph index for the project. No-op if rubydex is not available or already indexed.

Raises:

  • (StandardError)

    rescued internally, sets indexed to false



86
87
88
89
90
91
92
93
94
95
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 86

def index!
  return if @indexed || !self.class.available?

  result = IncrementalIndexer.call(:build, root: @root, **indexer_options)
  handle_index_result(result, :index!)
rescue StandardError => error
  log_warning('rubydex.indexing_failed', error.message, error.backtrace)
  @graph = nil
  @indexed = false
end

#indexed?Boolean

Whether the graph has been successfully indexed.

Returns:

  • (Boolean)


114
115
116
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 114

def indexed?
  @indexed && @graph.present?
end

#reindex!void

This method returns an undefined value.

Re-indexes only changed files since the last index. Falls back to a full rebuild when changes exceed the threshold.



101
102
103
104
105
106
107
108
109
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 101

def reindex!
  return unless @indexed && self.class.available?

  result = IncrementalIndexer.call(:reindex, root: @root, graph: @graph, file_mtimes: @file_mtimes,
                                             **indexer_options)
  handle_index_result(result, :reindex!)
rescue StandardError => error
  log_warning('rubydex.reindex_failed', error.message, error.backtrace)
end

#search(query, max_results: 20) ⇒ Array<Hash>

Search declarations by name using rubydex fuzzy search.

Parameters:

  • query (String)

    search query (e.g. "User", "Foo#bar")

  • max_results (Integer) (defaults to: 20)

    maximum number of results to return

Returns:

  • (Array<Hash>)

    array of declaration summaries

Raises:

  • (StandardError)

    rescued internally, returns empty array



124
125
126
127
128
129
130
131
132
# File 'lib/rails_ai_bridge/rubydex_adapter.rb', line 124

def search(query, max_results: 20)
  return [] unless indexed?

  results = @graph.search(query)
  results.first(max_results).map { |decl| @serializer.declaration_to_hash(decl) }
rescue StandardError => error
  log_warning('rubydex.search_failed', error.message, error.backtrace)
  []
end