Class: Mxrb::Semantic::Index

Inherits:
Object
  • Object
show all
Defined in:
lib/mxrb/semantic/index.rb

Overview

Builds a semantic index directly from an opened MPR. Ruby is the public API; the CLI only formats these results.

Constant Summary collapse

CACHE_VERSION =
3
CACHE_METADATA_KEYS =
%i[
  allowed_roles bson_type documentation excluded exposed from mark_as_used to
].freeze
FIELD_RELATIONS =
{
  "microflow" => :calls,
  "nanoflow" => :calls,
  "javaaction" => :calls,
  "javascriptaction" => :calls,
  "workflow" => :calls,
  "form" => :opens,
  "page" => :opens,
  "layout" => :uses_layout,
  "snippet" => :uses_snippet,
  "entity" => :uses_entity,
  "attribute" => :uses_attribute,
  "association" => :uses_association,
  "enumeration" => :uses_enumeration
}.freeze
CALL_KINDS =
%i[microflow nanoflow java_action javascript_action workflow].freeze
TOKEN =
/[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*){1,2}/
MEMBER_TOKEN =
/[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*\/[A-Za-z_][A-Za-z0-9_]*/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(project) ⇒ Index

Returns a new instance of Index.



74
75
76
77
78
79
80
81
82
83
84
# File 'lib/mxrb/semantic/index.rb', line 74

def initialize(project)
  @project = project
  reset_state
  @fingerprint = compute_fingerprint
  json = @fingerprint && @project.mpr.read_index_cache(@fingerprint)
  unless json && restore_from_cache(json)
    reset_state
    build
    @project.mpr.write_index_cache(@fingerprint, serialize_to_cache) if @fingerprint
  end
end

Instance Attribute Details

#artifactsObject (readonly)

Returns the value of attribute artifacts.



54
55
56
# File 'lib/mxrb/semantic/index.rb', line 54

def artifacts
  @artifacts
end

#referencesObject (readonly)

Returns the value of attribute references.



54
55
56
# File 'lib/mxrb/semantic/index.rb', line 54

def references
  @references
end

#unresolved_referencesObject (readonly)

Returns the value of attribute unresolved_references.



54
55
56
# File 'lib/mxrb/semantic/index.rb', line 54

def unresolved_references
  @unresolved_references
end

Class Method Details

.fingerprint(project) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/mxrb/semantic/index.rb', line 56

def self.fingerprint(project)
  rows = project.query(
    "SELECT UnitID, ContainerID, ContainmentName, ContentsHash " \
    "FROM Unit ORDER BY UnitID"
  )
  digest = Digest::SHA256.new
  digest << CACHE_VERSION.to_s
  rows.each do |row|
    row.each do |value|
      bytes = value.to_s.b
      digest << [bytes.bytesize].pack("Q>") << bytes
    end
  end
  digest.hexdigest
rescue StandardError
  nil
end

Instance Method Details

#callees_of(source) ⇒ Object



159
160
161
162
163
164
165
# File 'lib/mxrb/semantic/index.rb', line 159

def callees_of(source)
  artifact = resolve_argument(source)
  references_from(artifact)
    .select { _1.relation == :calls }
    .map(&:target)
    .uniq(&:id)
end

#callers_of(target) ⇒ Object



151
152
153
154
155
156
157
# File 'lib/mxrb/semantic/index.rb', line 151

def callers_of(target)
  artifact = resolve_argument(target)
  references_to(artifact)
    .select { _1.relation == :calls }
    .map(&:source)
    .uniq(&:id)
end

#describe(artifact) ⇒ Object



132
133
134
135
136
137
138
139
# File 'lib/mxrb/semantic/index.rb', line 132

def describe(artifact)
  resolved = resolve_argument(artifact)
  ArtifactDetails.new(
    resolved,
    references_to(resolved).freeze,
    references_from(resolved).freeze
  )
end

#find(name, kind: nil) ⇒ Object



86
87
88
89
90
91
92
# File 'lib/mxrb/semantic/index.rb', line 86

def find(name, kind: nil)
  return name if name.is_a?(Artifact)

  matches = @by_name[name.to_s]
  matches = matches.select { _1.kind == kind.to_sym } if kind
  matches.one? ? matches.first : nil
end

#find_all(name) ⇒ Object



94
95
96
# File 'lib/mxrb/semantic/index.rb', line 94

def find_all(name)
  @by_name[name.to_s].dup
end

#impact_of(target, transitive: true) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/mxrb/semantic/index.rb', line 167

def impact_of(target, transitive: true)
  root = resolve_argument(target)
  seen = Set[root.id]
  queue = [root]
  affected = []
  traversed = []

  until queue.empty?
    current = queue.shift
    incoming = references_to(current)
    traversed.concat(incoming)
    incoming.each do |reference|
      source = reference.source
      next if seen.include?(source.id)

      seen << source.id
      affected << source
      queue << source if transitive
    end
  end

  Impact.new(root, affected.freeze, traversed.uniq.freeze)
end

#references_from(source) ⇒ Object



146
147
148
149
# File 'lib/mxrb/semantic/index.rb', line 146

def references_from(source)
  artifact = resolve_argument(source)
  @references.select { _1.source.id == artifact.id }
end

#references_to(target) ⇒ Object



141
142
143
144
# File 'lib/mxrb/semantic/index.rb', line 141

def references_to(target)
  artifact = resolve_argument(target)
  @references.select { _1.target.id == artifact.id }
end

#search(query, kind: nil, module_name: nil) ⇒ Object



98
99
100
101
102
103
104
105
# File 'lib/mxrb/semantic/index.rb', line 98

def search(query, kind: nil, module_name: nil)
  matcher = query.is_a?(Regexp) ? query : /#{Regexp.escape(query.to_s)}/i
  @artifacts.select do |artifact|
    matcher.match?(artifact.qualified_name) &&
      (!kind || artifact.kind == kind.to_sym) &&
      (!module_name || artifact.module_name == module_name.to_s)
  end.sort_by { [_1.module_name.to_s, _1.kind.to_s, _1.qualified_name] }
end

#semantic_search(query, limit: 10, backend: :auto) ⇒ Object Also known as: semantic_search_artifacts

Semantic similarity search with optional sqlite-vec acceleration.



108
109
110
# File 'lib/mxrb/semantic/index.rb', line 108

def semantic_search(query, limit: 10, backend: :auto)
  semantic_search_hits(query, limit:, backend:).map(&:artifact).freeze
end

#semantic_search_hits(query, limit: 10, backend: :auto) ⇒ Object

Raises:

  • (ArgumentError)


113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/mxrb/semantic/index.rb', line 113

def semantic_search_hits(query, limit: 10, backend: :auto)
  size = Integer(limit)
  raise ArgumentError, "semantic search limit must be positive" unless size.positive?

  embedder = semantic_embedder(backend)
  store = vec_store(embedder)
  return ruby_semantic_hits(query, embedder, size) unless store

  begin
    store.rebuild!(@artifacts) unless store.compatible?
    store.search(query, limit: size).filter_map do |hit|
      artifact = @by_id[hit[:id]]
      SemanticSearchHit.new(artifact, cosine_distance(hit[:distance])) if artifact
    end.freeze
  rescue SQLite3::Exception, LoadError
    ruby_semantic_hits(query, embedder, size)
  end
end