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.



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

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.



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

def edges
  @edges
end

#index_pathObject (readonly)

Returns the value of attribute index_path.



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

def index_path
  @index_path
end

#nodesObject (readonly)

Returns the value of attribute nodes.



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

def nodes
  @nodes
end

Instance Method Details

#build!Object

Build the index from scratch (first session).



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

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



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

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.



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

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

  data = JSON.parse(File.read(@index_path))
  @nodes = data['nodes'] || []
  @edges = data['edges'] || []
  @file_mtimes = data['file_mtimes'] || {}
  self
rescue StandardError
  nil
end

#load_or_build!Object

Load if exists, otherwise build from scratch.



53
54
55
# File 'lib/rubyn_code/index/codebase_index.rb', line 53

def load_or_build!
  load || build!
end

#query(term) ⇒ Object

Query the index for symbols matching a search term.



73
74
75
76
77
78
79
# File 'lib/rubyn_code/index/codebase_index.rb', line 73

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



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

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



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

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.



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

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.



58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/rubyn_code/index/codebase_index.rb', line 58

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