Class: RubynCode::Index::CodebaseIndex

Inherits:
Object
  • Object
show all
Defined in:
lib/rubyn_code/index/codebase_index.rb

Overview

Rails-aware codebase index built with Prism (Ruby’s built-in parser). Stores classes, modules, methods, associations, and Rails edges in a JSON file for fast session startup. First build scans all .rb files; incremental updates re-index only changed files.

Constant Summary collapse

INDEX_DIR =

rubocop:disable Metrics/ClassLength – structural summary methods

'.rubyn-code'
INDEX_FILE =
'codebase_index.json'
CHARS_PER_TOKEN =
4

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(project_root:) ⇒ CodebaseIndex

Returns a new instance of CodebaseIndex.



20
21
22
23
24
25
26
# File 'lib/rubyn_code/index/codebase_index.rb', line 20

def initialize(project_root:)
  @project_root = File.expand_path(project_root)
  @index_path = File.join(@project_root, INDEX_DIR, INDEX_FILE)
  @nodes = []   # { type:, name:, file:, line:, params:, visibility: }
  @edges = []   # { from:, to:, relationship: }
  @file_mtimes = {}
end

Instance Attribute Details

#edgesObject (readonly)

Returns the value of attribute edges.



18
19
20
# File 'lib/rubyn_code/index/codebase_index.rb', line 18

def edges
  @edges
end

#index_pathObject (readonly)

Returns the value of attribute index_path.



18
19
20
# File 'lib/rubyn_code/index/codebase_index.rb', line 18

def index_path
  @index_path
end

#nodesObject (readonly)

Returns the value of attribute nodes.



18
19
20
# File 'lib/rubyn_code/index/codebase_index.rb', line 18

def nodes
  @nodes
end

Instance Method Details

#build!Object

Build the index from scratch (first session).



29
30
31
32
33
34
35
36
37
38
# File 'lib/rubyn_code/index/codebase_index.rb', line 29

def build!
  @nodes = []
  @edges = []
  @file_mtimes = {}

  ruby_files.each { |file| index_file(file) }
  extract_rails_edges
  save!
  self
end

#impact_analysis(file_path) ⇒ Object

Find all nodes related to a given file (callers, dependents, specs).



98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/rubyn_code/index/codebase_index.rb', line 98

def impact_analysis(file_path)
  relative = relative_path(file_path)
  direct = @nodes.select { |n| n['file'] == relative }
  names = direct.map { |n| n['name'] }.compact
  related_edges = edges_involving(names)

  {
    definitions: direct,
    relationships: related_edges,
    affected_files: related_edges.flat_map { |e| find_files_for(e) }.uniq
  }
end

#loadObject

Load existing index from disk.



41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/rubyn_code/index/codebase_index.rb', line 41

def load
  return nil unless File.exist?(@index_path)

  data = JSON.parse(File.read(@index_path))
  @nodes = data['nodes'] || []
  # uniq drops duplicate edges accumulated by older versions, which
  # appended tests edges on every update! without dedup.
  @edges = (data['edges'] || []).uniq
  @file_mtimes = data['file_mtimes'] || {}
  self
rescue StandardError
  nil
end

#load_or_build!Object

Load if exists, otherwise build from scratch.



56
57
58
# File 'lib/rubyn_code/index/codebase_index.rb', line 56

def load_or_build!
  load || build!
end

#query(term) ⇒ Object

Query the index for symbols matching a search term.



89
90
91
92
93
94
95
# File 'lib/rubyn_code/index/codebase_index.rb', line 89

def query(term)
  pattern = term.to_s.downcase
  @nodes.select do |node|
    node['name'].to_s.downcase.include?(pattern) ||
      node['file'].to_s.downcase.include?(pattern)
  end
end

#statsObject



137
138
139
140
141
142
143
# File 'lib/rubyn_code/index/codebase_index.rb', line 137

def stats
  {
    files_indexed: @file_mtimes.size,
    nodes: @nodes.size,
    edges: @edges.size
  }
end

#to_prompt_summaryObject

Compact summary for system prompt injection (~200-500 tokens).



112
113
114
115
116
117
118
119
120
121
# File 'lib/rubyn_code/index/codebase_index.rb', line 112

def to_prompt_summary
  counts = node_type_counts
  assoc_count = @edges.count { |e| e['relationship'] == 'association' }

  lines = ['Codebase Index:']
  lines << "  Classes: #{counts['class']}, Methods: #{counts['method']}"
  lines << "  Models: #{counts['model']}, Controllers: #{counts['controller']}, Services: #{counts['service']}"
  lines << "  Associations: #{assoc_count}"
  lines.join("\n")
end

#to_structural_summary(max_tokens: 500) ⇒ Object

Structural map for system prompt: model names with associations, controllers, and service objects. Capped to stay within token budget.



125
126
127
128
129
130
131
132
133
134
135
# File 'lib/rubyn_code/index/codebase_index.rb', line 125

def to_structural_summary(max_tokens: 500)
  budget = max_tokens * CHARS_PER_TOKEN
  lines = ['Codebase Structure:']

  append_model_section(lines)
  append_controller_section(lines)
  append_service_section(lines)
  append_stats_section(lines)

  truncate_to_budget(lines, budget)
end

#update!Object

Incremental update: re-index only files changed since last build.



61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/rubyn_code/index/codebase_index.rb', line 61

def update!
  changed = detect_changed_files
  return self if changed.empty?

  changed.each do |file|
    remove_nodes_for(file)
    index_file(file) if File.exist?(file)
  end

  extract_rails_edges
  save!
  self
end

#update_file!(path) ⇒ Object

Incremental update for a single known-changed file (e.g. after a write_file/edit_file tool call). Avoids the full-tree scan in update!.



77
78
79
80
81
82
83
84
85
86
# File 'lib/rubyn_code/index/codebase_index.rb', line 77

def update_file!(path)
  absolute = File.expand_path(path, @project_root)
  return self unless absolute.start_with?("#{@project_root}/")

  remove_nodes_for(absolute)
  index_file(absolute) if File.exist?(absolute)
  extract_rails_edges
  save!
  self
end