Class: Msnav::Graph

Inherits:
Object
  • Object
show all
Defined in:
lib/msnav/graph.rb

Overview

In-memory view of the coderag code graph (nodes/edges tables), with the same secondary indexes coderag builds: by-kind, per-(service, file) node lists, and per-file line-interval lists for covering-symbol lookup.

Node data and edge data are the JSON hashes coderag persisted (string keys); an edge's "kind" column is merged back into its data hash, exactly like CodeGraph.load does in Python.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(nodes, out_edges, in_edges) ⇒ Graph

Returns a new instance of Graph.



46
47
48
49
50
51
# File 'lib/msnav/graph.rb', line 46

def initialize(nodes, out_edges, in_edges)
  @nodes = nodes
  @out_edges = out_edges
  @in_edges = in_edges
  build_indexes
end

Instance Attribute Details

#nodesObject (readonly)

Returns the value of attribute nodes.



14
15
16
# File 'lib/msnav/graph.rb', line 14

def nodes
  @nodes
end

Class Method Details

.load(store, workspace_root = nil) ⇒ Object

Load the graph and re-anchor service roots when the same workspace is mounted at a different path than at index time (host <-> container). Each root is rebased only if the rebased directory actually exists — a container that mounts only the data dir keeps the stored host paths and serves them verbatim (the service-scoped devcontainer flow).



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/msnav/graph.rb', line 21

def self.load(store, workspace_root = nil)
  nodes = {}
  out_edges = Hash.new { |h, k| h[k] = [] }
  in_edges = Hash.new { |h, k| h[k] = [] }
  stored_ws = nil
  store.with_conn do |db|
    db.execute("SELECT id, data FROM nodes") do |id, data|
      nodes[id] = JSON.parse(data)
    end
    db.execute("SELECT src, dst, kind, data FROM edges") do |src, dst, kind, data|
      edge = JSON.parse(data)
      edge["kind"] = kind
      nodes[src] ||= {} # edges may reference nodes with no attributes
      nodes[dst] ||= {}
      out_edges[src] << [dst, edge]
      in_edges[dst] << [src, edge]
    end
    row = db.get_first_row("SELECT value FROM meta WHERE key='workspace_root'")
    stored_ws = row && row[0]
  end
  graph = new(nodes, out_edges, in_edges)
  graph.rebase_roots(stored_ws, workspace_root.to_s) if workspace_root
  graph
end

Instance Method Details

#covering_symbol(service, file, line) ⇒ Object

Smallest node whose line span covers line (1-based) in the file's interval list. Ties break toward the later start line, matching the Python bisect walk.



94
95
96
97
98
99
100
101
102
103
104
# File 'lib/msnav/graph.rb', line 94

def covering_symbol(service, file, line)
  spans = @intervals[[service, file]] || []
  best = nil
  spans.reverse_each do |start_line, end_line, node_id|
    next if start_line > line
    next if end_line < line
    size = end_line - start_line
    best = [size, node_id] if best.nil? || size < best[0]
  end
  best && best[1]
end

#endpoints(service = nil) ⇒ Object



85
86
87
88
89
# File 'lib/msnav/graph.rb', line 85

def endpoints(service = nil)
  nodes_of_kind("endpoint").select do |_id, data|
    service.nil? || data["service"] == service
  end
end

#has_node?(node_id) ⇒ Boolean

Returns:

  • (Boolean)


57
58
59
# File 'lib/msnav/graph.rb', line 57

def has_node?(node_id)
  @nodes.key?(node_id)
end

#in_edges(node_id) ⇒ Object

Incoming [src, edge_data] pairs (cross-service references).



69
70
71
# File 'lib/msnav/graph.rb', line 69

def in_edges(node_id)
  @in_edges[node_id] || []
end

#node(node_id) ⇒ Object



53
54
55
# File 'lib/msnav/graph.rb', line 53

def node(node_id)
  @nodes[node_id]
end

#nodes_in_file(service, file) ⇒ Object



77
78
79
# File 'lib/msnav/graph.rb', line 77

def nodes_in_file(service, file)
  (@by_service_file[[service, file]] || []).map { |id| [id, @nodes[id]] }
end

#nodes_of_kind(kind) ⇒ Object



73
74
75
# File 'lib/msnav/graph.rb', line 73

def nodes_of_kind(kind)
  (@by_kind[kind] || []).map { |id| [id, @nodes[id]] }
end

#out(node_id, kinds = nil) ⇒ Object

Outgoing [dst, edge_data] pairs, optionally filtered to edge kinds.



62
63
64
65
66
# File 'lib/msnav/graph.rb', line 62

def out(node_id, kinds = nil)
  edges = @out_edges[node_id] || []
  return edges if kinds.nil?
  edges.select { |_dst, data| kinds.include?(data["kind"]) }
end

#rebase_roots(stored_ws, workspace_root) ⇒ Object

Re-anchor service roots from the workspace root stored at index time to WORKSPACE_ROOT. No-op when they match, the meta key is missing, or the rebased directory does not exist.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/msnav/graph.rb', line 109

def rebase_roots(stored_ws, workspace_root)
  ws = File.expand_path(workspace_root)
  return if stored_ws.nil? || stored_ws.empty?
  return if File.expand_path(stored_ws) == ws
  stored = Pathname.new(File.expand_path(stored_ws))
  services.each do |_id, data|
    root = data["root"]
    next if root.nil? || root.empty?
    begin
      rel = Pathname.new(File.expand_path(root)).relative_path_from(stored).to_s
    rescue ArgumentError
      next
    end
    next if rel.start_with?("..")
    candidate = rel == "." ? ws : File.join(ws, rel)
    data["root"] = candidate if File.directory?(candidate)
  end
  build_indexes
end

#servicesObject



81
82
83
# File 'lib/msnav/graph.rb', line 81

def services
  nodes_of_kind("service")
end