granita 0.0.2-aarch64-linux → 0.0.4-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +33 -0
- data/include/granita_graph.h +56 -0
- data/lib/granita/graph.rb +152 -0
- data/lib/granita/query.rb +145 -0
- data/lib/granita/version.rb +1 -1
- data/lib/granita.rb +2 -0
- data/libexec/granita_graph.so +4 -0
- data/libexec/sorbet +2 -2
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b33f8759d6ec6a83591a4d88ee834c7be6d862990e081bc9579bb3aa4499868b
|
|
4
|
+
data.tar.gz: 782a66757a36cd6f8fc619bca524277065bbfea6562dcf78f61b8c44b4b49a90
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 07c5437c70e3bc9434bafe3e1a1d8b26fb6072149e98b5d9f902f2bc350e7bab9f1939bf1b38669c83a3673ec87dbdbaa7e4e7e8d792fd39374311cb6a169ee5
|
|
7
|
+
data.tar.gz: da11cbc755206de9c2841871fafd68751f99e88c1957b7ce9ca1740476286031ed2244b29903af31d5272489aafc054c55e64347ef7dac60335e51a6c3337b07
|
data/README.md
CHANGED
|
@@ -14,4 +14,37 @@ Granita is in beta and is meant as a drop-in replacement for `srb tc`.
|
|
|
14
14
|
granita # typecheck the project described by sorbet/config
|
|
15
15
|
granita --lsp # run the language server
|
|
16
16
|
|
|
17
|
+
## Semantic graph API
|
|
18
|
+
|
|
19
|
+
The native graph API runs Granita's semantic pipeline and exposes an immutable query snapshot:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "granita"
|
|
23
|
+
|
|
24
|
+
graph = Granita::Graph.index(Dir["lib/**/*.rb"])
|
|
25
|
+
|
|
26
|
+
graph.documents.each { |document| puts document.path }
|
|
27
|
+
graph.definitions.each { |definition| puts definition.full_name }
|
|
28
|
+
graph.declarations.each { |declaration| puts declaration.full_name }
|
|
29
|
+
graph.constant_references.each { |reference| puts reference.name }
|
|
30
|
+
graph.calls.each do |call|
|
|
31
|
+
puts "#{call.receiver_types.join(' | ')}##{call.name} -> #{call.target_ids.join(', ')}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
product = graph["Shop::Product"]
|
|
35
|
+
puts product.definition_ids
|
|
36
|
+
|
|
37
|
+
result = graph.cypher(<<~CYPHER)
|
|
38
|
+
MATCH (call:Call)-[:TARGETS]->(method:Method)
|
|
39
|
+
WHERE call.receiver_type CONTAINS 'Shop::Product'
|
|
40
|
+
RETURN call.name, method.name
|
|
41
|
+
CYPHER
|
|
42
|
+
result.rows.each { |row| puts row }
|
|
43
|
+
puts result.render(:table)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For editor buffers, generated source, or non-UTF-8 Ruby, use `Granita::Graph.from_sources("path.rb" => source)`; source bytes are preserved. Calls are present for typed and untyped files. Inferred calls expose `receiver_types` and resolved `target_ids`; uninferred or unresolved calls leave both arrays empty.
|
|
47
|
+
|
|
48
|
+
Locations contain zero-based, end-exclusive byte offsets. `Definition#location` covers the full construct and `#declaration_location` identifies its header/name. Declarations from the bundled RBI payload that are referenced by workspace code have `external == true`. `Granita::Query.parse` allows a parsed read-only Cypher query to be reused across graph snapshots, and `Granita::Query.schema(:json)` describes the available labels, properties, and relationships.
|
|
49
|
+
|
|
17
50
|
Set `SRB_SORBET_EXE` to run a different typechecker binary.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#ifndef GRANITA_GRAPH_H
|
|
2
|
+
#define GRANITA_GRAPH_H
|
|
3
|
+
|
|
4
|
+
#include <stddef.h>
|
|
5
|
+
|
|
6
|
+
#ifdef __cplusplus
|
|
7
|
+
extern "C" {
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
/** Opaque, immutable semantic graph. */
|
|
11
|
+
typedef struct GranitaGraph GranitaGraph;
|
|
12
|
+
/** Opaque, reusable parsed Cypher query. */
|
|
13
|
+
typedef struct GranitaQuery GranitaQuery;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Builds a graph from UTF-8 JSON. Accepted inputs are:
|
|
17
|
+
* {"paths":["lib/a.rb", "lib/b.rb"]}
|
|
18
|
+
* {"sources":[{"path":"a.rb", "source":[99,108,97,115,115,32,65,59,32,101,110,100]}]}
|
|
19
|
+
*
|
|
20
|
+
* Returns NULL on failure and stores an allocated error in *error_out when error_out is non-NULL.
|
|
21
|
+
*/
|
|
22
|
+
GranitaGraph *granita_graph_new_json(const char *input, char **error_out);
|
|
23
|
+
void granita_graph_free(GranitaGraph *graph);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns an allocated UTF-8 JSON string for one collection. `collection` is one of
|
|
27
|
+
* "documents", "definitions", "declarations", "constant_references", "calls", or "all".
|
|
28
|
+
* Returns NULL on failure and stores an allocated error in *error_out when non-NULL.
|
|
29
|
+
*/
|
|
30
|
+
char *granita_graph_query_json(const GranitaGraph *graph, const char *collection, char **error_out);
|
|
31
|
+
/** Parses a read-only Cypher query. */
|
|
32
|
+
GranitaQuery *granita_query_parse(const char *query, char **error_out);
|
|
33
|
+
void granita_query_free(GranitaQuery *query);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Executes a parsed Cypher query and returns a JSON array of row objects. Returned node values
|
|
37
|
+
* include stable `id`, `label`, and `name` fields.
|
|
38
|
+
*/
|
|
39
|
+
char *granita_query_execute_json(
|
|
40
|
+
const GranitaGraph *graph,
|
|
41
|
+
const GranitaQuery *query,
|
|
42
|
+
char **error_out
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/** Returns the queryable schema. `format` is "json" or "table". */
|
|
46
|
+
char *granita_query_schema(const char *format, char **error_out);
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/** Frees strings returned by this API, including errors. */
|
|
50
|
+
void granita_string_free(char *string);
|
|
51
|
+
|
|
52
|
+
#ifdef __cplusplus
|
|
53
|
+
}
|
|
54
|
+
#endif
|
|
55
|
+
|
|
56
|
+
#endif
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle/import"
|
|
4
|
+
require "json"
|
|
5
|
+
require "rbconfig"
|
|
6
|
+
|
|
7
|
+
module Granita
|
|
8
|
+
class Graph
|
|
9
|
+
Document = Struct.new(:id, :path, :definition_ids, :constant_reference_ids, :call_ids, keyword_init: true)
|
|
10
|
+
Definition = Struct.new(:id, :declaration_id, :kind, :name, :full_name, :location, :declaration_location, :line, keyword_init: true)
|
|
11
|
+
Declaration = Struct.new(:id, :kind, :name, :full_name, :owner_id, :definition_ids, :constant_reference_ids, :visibility, :external, keyword_init: true)
|
|
12
|
+
ConstantReference = Struct.new(:id, :name, :target_id, :location, keyword_init: true)
|
|
13
|
+
Call = Struct.new(:id, :name, :receiver_types, :target_ids, :location, keyword_init: true)
|
|
14
|
+
Location = Struct.new(:document_id, :start, :end, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
module Native
|
|
17
|
+
extend Fiddle::Importer
|
|
18
|
+
|
|
19
|
+
extension = RbConfig::CONFIG.fetch("DLEXT")
|
|
20
|
+
default = File.expand_path("../../libexec/granita_graph.#{extension}", __dir__)
|
|
21
|
+
dlload ENV.fetch("GRANITA_GRAPH_LIBRARY", default)
|
|
22
|
+
|
|
23
|
+
extern "void *granita_graph_new_json(const char *, void *)"
|
|
24
|
+
extern "void granita_graph_free(void *)"
|
|
25
|
+
extern "char *granita_graph_query_json(void *, const char *, void *)"
|
|
26
|
+
extern "void *granita_query_parse(const char *, void *)"
|
|
27
|
+
extern "void granita_query_free(void *)"
|
|
28
|
+
extern "char *granita_query_execute_json(void *, void *, void *)"
|
|
29
|
+
extern "char *granita_query_schema(const char *, void *)"
|
|
30
|
+
extern "void granita_string_free(void *)"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
def index(paths)
|
|
35
|
+
new(paths: Array(paths).map { |path| File.expand_path(path) })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def from_sources(sources)
|
|
39
|
+
entries = sources.map { |path, source| {path: path.to_s, source: source.to_s.b.bytes} }
|
|
40
|
+
new(sources: entries)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private :new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize(input)
|
|
47
|
+
error = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
|
|
48
|
+
error[0, Fiddle::SIZEOF_VOIDP] = [0].pack(pointer_pack)
|
|
49
|
+
address = Native.granita_graph_new_json(JSON.generate(input), error)
|
|
50
|
+
raise_native_error(error) if address.to_i.zero?
|
|
51
|
+
|
|
52
|
+
@pointer = Fiddle::Pointer.new(address.to_i, 0, Native["granita_graph_free"])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def documents
|
|
56
|
+
query("documents").map { |row| Document.new(**symbolize(row)) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def definitions
|
|
60
|
+
query("definitions").map do |row|
|
|
61
|
+
values = record(row)
|
|
62
|
+
values[:declaration_location] = Location.new(**symbolize(values.fetch(:declaration_location)))
|
|
63
|
+
Definition.new(**values)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def declarations
|
|
68
|
+
query("declarations").map { |row| Declaration.new(**symbolize(row)) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def constant_references
|
|
72
|
+
query("constant_references").map { |row| ConstantReference.new(**record(row)) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def calls
|
|
76
|
+
query("calls").map { |row| Call.new(**record(row)) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def declaration(name)
|
|
80
|
+
declarations.find { |declaration| declaration.full_name == name || declaration.full_name == name.delete_prefix("::") }
|
|
81
|
+
end
|
|
82
|
+
alias_method :[], :declaration
|
|
83
|
+
def cypher(query)
|
|
84
|
+
Granita::Query.parse(query).run(self)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def native_handle
|
|
91
|
+
@pointer
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def resolve_query_node(encoded)
|
|
95
|
+
kind, id = encoded.split(":", 2)
|
|
96
|
+
id = Integer(id, exception: false)
|
|
97
|
+
return unless id
|
|
98
|
+
|
|
99
|
+
@query_node_indexes ||= {}
|
|
100
|
+
records = case kind
|
|
101
|
+
when "doc"
|
|
102
|
+
@query_node_indexes[:documents] ||= documents.to_h { |record| [record.id, record] }
|
|
103
|
+
when "def"
|
|
104
|
+
@query_node_indexes[:definitions] ||= definitions.to_h { |record| [record.id, record] }
|
|
105
|
+
when "decl"
|
|
106
|
+
@query_node_indexes[:declarations] ||= declarations.to_h { |record| [record.id, record] }
|
|
107
|
+
when "call"
|
|
108
|
+
@query_node_indexes[:calls] ||= calls.to_h { |record| [record.id, record] }
|
|
109
|
+
else
|
|
110
|
+
return
|
|
111
|
+
end
|
|
112
|
+
records[id]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def query(collection)
|
|
116
|
+
error = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
|
|
117
|
+
error[0, Fiddle::SIZEOF_VOIDP] = [0].pack(pointer_pack)
|
|
118
|
+
result = Native.granita_graph_query_json(@pointer, collection, error)
|
|
119
|
+
raise_native_error(error) if result.to_i.zero?
|
|
120
|
+
|
|
121
|
+
pointer = Fiddle::Pointer.new(result.to_i)
|
|
122
|
+
json = pointer.to_s
|
|
123
|
+
Native.granita_string_free(pointer)
|
|
124
|
+
JSON.parse(json)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def record(row)
|
|
128
|
+
values = symbolize(row)
|
|
129
|
+
values[:location] = Location.new(**symbolize(values.fetch(:location)))
|
|
130
|
+
values
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def symbolize(hash)
|
|
134
|
+
hash.transform_keys!(&:to_sym)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def raise_native_error(error)
|
|
138
|
+
address = error[0, Fiddle::SIZEOF_VOIDP].unpack1(pointer_pack)
|
|
139
|
+
message = "Granita graph operation failed"
|
|
140
|
+
unless address.zero?
|
|
141
|
+
pointer = Fiddle::Pointer.new(address)
|
|
142
|
+
message = pointer.to_s
|
|
143
|
+
Native.granita_string_free(pointer)
|
|
144
|
+
end
|
|
145
|
+
raise ArgumentError, message
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def pointer_pack
|
|
149
|
+
"J"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "graph"
|
|
4
|
+
|
|
5
|
+
module Granita
|
|
6
|
+
class Query
|
|
7
|
+
class SyntaxError < ArgumentError; end
|
|
8
|
+
class ExecutionError < StandardError; end
|
|
9
|
+
|
|
10
|
+
class Result
|
|
11
|
+
attr_reader :rows
|
|
12
|
+
|
|
13
|
+
def initialize(rows, raw_rows)
|
|
14
|
+
@rows = rows.freeze
|
|
15
|
+
@raw_rows = raw_rows.freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render(format = :table)
|
|
19
|
+
case format.to_sym
|
|
20
|
+
when :json
|
|
21
|
+
JSON.generate(@raw_rows)
|
|
22
|
+
when :table
|
|
23
|
+
render_table
|
|
24
|
+
else
|
|
25
|
+
raise ArgumentError, "unknown query output format `#{format}`"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def render_table
|
|
32
|
+
return "" if @raw_rows.empty?
|
|
33
|
+
|
|
34
|
+
columns = @raw_rows.flat_map(&:keys).uniq
|
|
35
|
+
values = @raw_rows.map do |row|
|
|
36
|
+
columns.map { |column| display(row[column]) }
|
|
37
|
+
end
|
|
38
|
+
widths = columns.each_index.map do |index|
|
|
39
|
+
([columns[index].length] + values.map { |row| row[index].length }).max
|
|
40
|
+
end
|
|
41
|
+
lines = []
|
|
42
|
+
lines << columns.each_index.map { |index| columns[index].ljust(widths[index]) }.join(" | ")
|
|
43
|
+
lines << widths.map { |width| "-" * width }.join("-+-")
|
|
44
|
+
values.each do |row|
|
|
45
|
+
lines << row.each_index.map { |index| row[index].ljust(widths[index]) }.join(" | ")
|
|
46
|
+
end
|
|
47
|
+
lines << ""
|
|
48
|
+
lines << "#{values.length} #{values.length == 1 ? "row" : "rows"}"
|
|
49
|
+
"#{lines.join("\n")}\n"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def display(value)
|
|
53
|
+
case value
|
|
54
|
+
when Hash
|
|
55
|
+
value["name"] || JSON.generate(value)
|
|
56
|
+
when Array
|
|
57
|
+
"[#{value.map { |item| display(item) }.join(", ")}]"
|
|
58
|
+
when nil
|
|
59
|
+
""
|
|
60
|
+
else
|
|
61
|
+
value.to_s
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class << self
|
|
67
|
+
def parse(source)
|
|
68
|
+
address = call_native(SyntaxError) do |error|
|
|
69
|
+
Graph::Native.granita_query_parse(source.to_s, error)
|
|
70
|
+
end
|
|
71
|
+
new(address)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def schema(format = :table)
|
|
75
|
+
address = call_native(ArgumentError) do |error|
|
|
76
|
+
Graph::Native.granita_query_schema(format.to_s, error)
|
|
77
|
+
end
|
|
78
|
+
take_string(address)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def call_native(error_class)
|
|
84
|
+
error = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
|
|
85
|
+
error[0, Fiddle::SIZEOF_VOIDP] = [0].pack("J")
|
|
86
|
+
address = yield(error)
|
|
87
|
+
return address unless address.to_i.zero?
|
|
88
|
+
|
|
89
|
+
pointer_address = error[0, Fiddle::SIZEOF_VOIDP].unpack1("J")
|
|
90
|
+
message = "Granita query operation failed"
|
|
91
|
+
unless pointer_address.zero?
|
|
92
|
+
pointer = Fiddle::Pointer.new(pointer_address)
|
|
93
|
+
message = pointer.to_s
|
|
94
|
+
Graph::Native.granita_string_free(pointer)
|
|
95
|
+
end
|
|
96
|
+
raise error_class, message
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def take_string(address)
|
|
100
|
+
pointer = Fiddle::Pointer.new(address.to_i)
|
|
101
|
+
value = pointer.to_s
|
|
102
|
+
Graph::Native.granita_string_free(pointer)
|
|
103
|
+
value
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private :new
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def initialize(address)
|
|
110
|
+
@pointer = Fiddle::Pointer.new(address.to_i, 0, Graph::Native["granita_query_free"])
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def run(graph)
|
|
114
|
+
address = self.class.__send__(:call_native, ExecutionError) do |error|
|
|
115
|
+
Graph::Native.granita_query_execute_json(
|
|
116
|
+
graph.__send__(:native_handle),
|
|
117
|
+
@pointer,
|
|
118
|
+
error,
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
raw_rows = JSON.parse(self.class.__send__(:take_string, address))
|
|
122
|
+
rows = raw_rows.map do |row|
|
|
123
|
+
row.transform_values { |value| convert_value(graph, value) }
|
|
124
|
+
end
|
|
125
|
+
Result.new(rows, raw_rows)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def convert_value(graph, value)
|
|
131
|
+
case value
|
|
132
|
+
when Array
|
|
133
|
+
value.map { |item| convert_value(graph, item) }
|
|
134
|
+
when Hash
|
|
135
|
+
if value["id"].is_a?(String) && value["id"].match?(/\A(?:doc|def|decl|call):\d+\z/)
|
|
136
|
+
graph.__send__(:resolve_query_node, value["id"]) || value
|
|
137
|
+
else
|
|
138
|
+
value.transform_values { |item| convert_value(graph, item) }
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
value
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
data/lib/granita/version.rb
CHANGED
data/lib/granita.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "granita/version"
|
|
4
4
|
|
|
5
5
|
module Granita
|
|
6
|
+
autoload :Graph, File.expand_path("granita/graph", __dir__)
|
|
7
|
+
autoload :Query, File.expand_path("granita/query", __dir__)
|
|
6
8
|
# Absolute path to the bundled typechecker binary.
|
|
7
9
|
def self.binary_path
|
|
8
10
|
File.expand_path("../libexec/sorbet", __dir__)
|
data/libexec/sorbet
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/libexec/sorbet
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 60922008 bytes
|
|
4
|
+
sha256: ac6d6a3025048011b933edfeead667a7b2a7656d25edfedd95f9bee015c1dc6c
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: granita
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.4
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Alexandre Terrasa
|
|
@@ -18,8 +18,12 @@ files:
|
|
|
18
18
|
- NOTICE
|
|
19
19
|
- README.md
|
|
20
20
|
- exe/granita
|
|
21
|
+
- include/granita_graph.h
|
|
21
22
|
- lib/granita.rb
|
|
23
|
+
- lib/granita/graph.rb
|
|
24
|
+
- lib/granita/query.rb
|
|
22
25
|
- lib/granita/version.rb
|
|
26
|
+
- libexec/granita_graph.so
|
|
23
27
|
- libexec/sorbet
|
|
24
28
|
homepage: https://github.com/Morriar/granita
|
|
25
29
|
licenses:
|