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
FORMAT_VERSION =

Bump when the stored shape changes so stale indexes rebuild instead of silently serving degraded data. 2 = Prism spans + call edges.

2

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(project_root:) ⇒ CodebaseIndex

Returns a new instance of CodebaseIndex.



23
24
25
26
27
28
29
# File 'lib/rubyn_code/index/codebase_index.rb', line 23

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.



21
22
23
# File 'lib/rubyn_code/index/codebase_index.rb', line 21

def edges
  @edges
end

#index_pathObject (readonly)

Returns the value of attribute index_path.



21
22
23
# File 'lib/rubyn_code/index/codebase_index.rb', line 21

def index_path
  @index_path
end

#nodesObject (readonly)

Returns the value of attribute nodes.



21
22
23
# File 'lib/rubyn_code/index/codebase_index.rb', line 21

def nodes
  @nodes
end

Instance Method Details

#build!Object

Build the index from scratch (first session).



32
33
34
35
36
37
38
39
40
41
42
# File 'lib/rubyn_code/index/codebase_index.rb', line 32

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

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

#impact_analysis(file_path) ⇒ Object

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



106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/rubyn_code/index/codebase_index.rb', line 106

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.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/rubyn_code/index/codebase_index.rb', line 45

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

  data = JSON.parse(File.read(@index_path))
  return nil unless data['format_version'] == FORMAT_VERSION

  @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.



62
63
64
# File 'lib/rubyn_code/index/codebase_index.rb', line 62

def load_or_build!
  load || build!
end

#query(term) ⇒ Object

Query the index for symbols matching a search term.



97
98
99
100
101
102
103
# File 'lib/rubyn_code/index/codebase_index.rb', line 97

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



145
146
147
148
149
150
151
# File 'lib/rubyn_code/index/codebase_index.rb', line 145

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).



120
121
122
123
124
125
126
127
128
129
# File 'lib/rubyn_code/index/codebase_index.rb', line 120

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.



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

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.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/rubyn_code/index/codebase_index.rb', line 67

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
  prune_call_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!.



84
85
86
87
88
89
90
91
92
93
94
# File 'lib/rubyn_code/index/codebase_index.rb', line 84

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
  prune_call_edges!
  save!
  self
end