rubik 0.3.1 → 0.3.3
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/ATTRIBUTION.md +1 -1
- data/CHANGELOG.md +79 -0
- data/lib/rubik/ast/cache_store.rb +4 -2
- data/lib/rubik/ast/graph.rb +49 -94
- data/lib/rubik/ast/graph_extensions.rb +202 -0
- data/lib/rubik/ast/ruby_extractor.rb +8 -3
- data/lib/rubik/ast/symbol.rb +7 -0
- data/lib/rubik/ast/treesitter/base_walker.rb +4 -3
- data/lib/rubik/ast/treesitter/c_walker.rb +10 -8
- data/lib/rubik/ast/treesitter/go_walker.rb +4 -2
- data/lib/rubik/ast/treesitter/rust_walker.rb +9 -7
- data/lib/rubik/ast/treesitter/typescript_walker.rb +7 -5
- data/lib/rubik/ast/treesitter/zig_walker.rb +44 -6
- data/lib/rubik/config/defaults.rb +3 -6
- data/lib/rubik/config/store.rb +1 -1
- data/lib/rubik/lsp/client.rb +102 -24
- data/lib/rubik/lsp/manager.rb +136 -83
- data/lib/rubik/orchestrator/subagent/budget.rb +17 -10
- data/lib/rubik/orchestrator/subagent/instance.rb +14 -2
- data/lib/rubik/orchestrator/subagent/journal.rb +56 -13
- data/lib/rubik/orchestrator/subagent/reuse.rb +1 -1
- data/lib/rubik/orchestrator/subagent/run_cycle.rb +8 -5
- data/lib/rubik/orchestrator/subagent/supervisor.rb +7 -1
- data/lib/rubik/orchestrator/tool_circuit_breaker.rb +15 -5
- data/lib/rubik/platform/process.rb +1 -1
- data/lib/rubik/platform/sanitize.rb +4 -4
- data/lib/rubik/platform.rb +2 -2
- data/lib/rubik/prompt/token_estimator.rb +15 -3
- data/lib/rubik/repl/ast_warmup.rb +35 -4
- data/lib/rubik/repl/commands/subagents.rb +12 -11
- data/lib/rubik/repl/interrupt_handler.rb +10 -10
- data/lib/rubik/repl/shell.rb +1 -1
- data/lib/rubik/tools/builtins/subagent.rb +7 -2
- data/lib/rubik/tools/builtins/symbols.rb +56 -20
- data/lib/rubik/tools/symbol_tool_support.rb +113 -0
- data/lib/rubik/version.rb +1 -1
- metadata +3 -1
data/lib/rubik/platform.rb
CHANGED
|
@@ -87,7 +87,7 @@ module Rubik
|
|
|
87
87
|
|
|
88
88
|
def home_dir
|
|
89
89
|
Dir.home
|
|
90
|
-
rescue
|
|
90
|
+
rescue StandardError
|
|
91
91
|
(ENV["USERPROFILE"] || (ENV.fetch("HOMEDRIVE",
|
|
92
92
|
nil) && ENV.fetch("HOMEPATH",
|
|
93
93
|
nil) && "#{ENV.fetch('HOMEDRIVE',
|
|
@@ -96,7 +96,7 @@ module Rubik
|
|
|
96
96
|
|
|
97
97
|
def tmp_dir
|
|
98
98
|
Dir.tmpdir
|
|
99
|
-
rescue
|
|
99
|
+
rescue StandardError
|
|
100
100
|
(ENV["TEMP"] || ENV["TMP"] || "/tmp")
|
|
101
101
|
end
|
|
102
102
|
end
|
|
@@ -34,7 +34,7 @@ module Rubik
|
|
|
34
34
|
|
|
35
35
|
tool_calls = msg[:tool_calls] || msg["tool_calls"]
|
|
36
36
|
if tool_calls && !tool_calls.empty?
|
|
37
|
-
tool_calls_str =
|
|
37
|
+
tool_calls_str = serialize_for_estimation(tool_calls)
|
|
38
38
|
tokens += estimate_text(tool_calls_str)
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -57,8 +57,20 @@ module Rubik
|
|
|
57
57
|
return 0 if schemas.nil?
|
|
58
58
|
return 0 if schemas.respond_to?(:empty?) && schemas.empty?
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
estimate_text(serialize_for_estimation(schemas))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# PERF: Fast-path JSON generation for tool_calls and tool_schemas estimation.
|
|
64
|
+
# Uses direct JSON.generate to avoid deep clean_utf8 traversal, falling back
|
|
65
|
+
# to Rubik::Platform.safe_json_generate only when malformed UTF-8 raises.
|
|
66
|
+
def serialize_for_estimation(obj)
|
|
67
|
+
return Rubik::Platform.clean_utf8(obj) if obj.is_a?(String)
|
|
68
|
+
|
|
69
|
+
begin
|
|
70
|
+
JSON.generate(obj)
|
|
71
|
+
rescue StandardError
|
|
72
|
+
Rubik::Platform.safe_json_generate(obj)
|
|
73
|
+
end
|
|
62
74
|
end
|
|
63
75
|
end
|
|
64
76
|
end
|
|
@@ -22,10 +22,15 @@ module Rubik
|
|
|
22
22
|
# Reset the cooperative-stop flag so repeated warm cycles behave the
|
|
23
23
|
# same as the first (per-turn scopes, MCP reconnects, tool epochs).
|
|
24
24
|
@indexer_stop_requested = false
|
|
25
|
+
warmup_token = @ast_graph.begin_warmup if @ast_graph.respond_to?(:begin_warmup)
|
|
25
26
|
@indexer_thread = Thread.new do
|
|
27
|
+
Thread.current[:rubik_ast_warmup_token] = warmup_token
|
|
26
28
|
perform_warm_ast_graph
|
|
27
29
|
rescue StandardError => e
|
|
30
|
+
@ast_graph.finish_warmup(warmup_token, state: :failed) if warmup_token
|
|
28
31
|
@logger&.warn("AST graph background indexer thread error: #{e.class}: #{e.message}")
|
|
32
|
+
ensure
|
|
33
|
+
Thread.current[:rubik_ast_warmup_token] = nil
|
|
29
34
|
end
|
|
30
35
|
ctx = @runtime_manager&.context || (respond_to?(:context) ? context : nil)
|
|
31
36
|
if ctx.respond_to?(:register_disposable) && ctx != @indexer_disposable_ctx
|
|
@@ -33,7 +38,16 @@ module Rubik
|
|
|
33
38
|
ctx.register_disposable { stop_background_indexer }
|
|
34
39
|
end
|
|
35
40
|
else
|
|
36
|
-
|
|
41
|
+
@indexer_stop_requested = false
|
|
42
|
+
warmup_token = @ast_graph.begin_warmup if @ast_graph.respond_to?(:begin_warmup)
|
|
43
|
+
previous_token = Thread.current[:rubik_ast_warmup_token]
|
|
44
|
+
Thread.current[:rubik_ast_warmup_token] = warmup_token
|
|
45
|
+
begin
|
|
46
|
+
perform_warm_ast_graph
|
|
47
|
+
ensure
|
|
48
|
+
Thread.current[:rubik_ast_warmup_token] = previous_token
|
|
49
|
+
@ast_graph.finish_warmup(warmup_token) if warmup_token
|
|
50
|
+
end
|
|
37
51
|
end
|
|
38
52
|
end
|
|
39
53
|
|
|
@@ -82,6 +96,9 @@ module Rubik
|
|
|
82
96
|
end
|
|
83
97
|
|
|
84
98
|
def perform_warm_ast_graph
|
|
99
|
+
warmup_token = Thread.current[:rubik_ast_warmup_token]
|
|
100
|
+
warmup_token ||= @ast_graph.begin_warmup if @ast_graph.respond_to?(:begin_warmup)
|
|
101
|
+
warmup_state = :ready
|
|
85
102
|
root_d = @buffer_manager&.root_dir || Dir.pwd
|
|
86
103
|
delta = @context_index.incremental_scan
|
|
87
104
|
indexed_files = @ast_graph.file_symbols.keys
|
|
@@ -96,12 +113,21 @@ module Rubik
|
|
|
96
113
|
files_to_index << rel_file if Rubik::AST::Extractor.supported_extensions.include?(ext) && !(indexed_files.include?(full_path) || indexed_files.include?(rel_file))
|
|
97
114
|
end
|
|
98
115
|
|
|
99
|
-
files_to_index.uniq.
|
|
116
|
+
files_to_process = files_to_index.uniq.select do |rel_file|
|
|
117
|
+
ext = File.extname(rel_file).downcase
|
|
118
|
+
Rubik::AST::Extractor.supported_extensions.include?(ext) && File.exist?(File.join(root_d, rel_file))
|
|
119
|
+
end
|
|
120
|
+
total_operations = files_to_process.length + delta[:deleted].length
|
|
121
|
+
@ast_graph.update_warmup(warmup_token, indexed: 0, total: total_operations) if warmup_token
|
|
122
|
+
completed_operations = 0
|
|
123
|
+
|
|
124
|
+
files_to_process.each do |rel_file|
|
|
100
125
|
break if @indexer_stop_requested
|
|
101
126
|
|
|
102
127
|
full_path = File.join(root_d, rel_file)
|
|
103
|
-
|
|
104
|
-
|
|
128
|
+
@ast_graph.index_file(full_path)
|
|
129
|
+
completed_operations += 1
|
|
130
|
+
@ast_graph.update_warmup(warmup_token, indexed: completed_operations, total: total_operations) if warmup_token
|
|
105
131
|
end
|
|
106
132
|
|
|
107
133
|
delta[:deleted].each do |rel_file|
|
|
@@ -109,6 +135,8 @@ module Rubik
|
|
|
109
135
|
|
|
110
136
|
full_path = File.join(root_d, rel_file)
|
|
111
137
|
@ast_graph.remove_file(full_path)
|
|
138
|
+
completed_operations += 1
|
|
139
|
+
@ast_graph.update_warmup(warmup_token, indexed: completed_operations, total: total_operations) if warmup_token
|
|
112
140
|
end
|
|
113
141
|
|
|
114
142
|
return if @indexer_stop_requested
|
|
@@ -120,9 +148,12 @@ module Rubik
|
|
|
120
148
|
@ast_graph.save_to_cache(@cache_store)
|
|
121
149
|
end
|
|
122
150
|
rescue StandardError => e
|
|
151
|
+
warmup_state = :failed
|
|
123
152
|
@logger&.warn("AST graph warm failed: #{e.message}")
|
|
124
153
|
@event_bus&.emit(:ast_warm_failed, error: e.message)
|
|
125
154
|
nil
|
|
155
|
+
ensure
|
|
156
|
+
@ast_graph.finish_warmup(warmup_token, state: warmup_state) if warmup_token
|
|
126
157
|
end
|
|
127
158
|
end
|
|
128
159
|
end
|
|
@@ -113,10 +113,7 @@ module Rubik
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def self.handle_kill(ctx, sup, id)
|
|
116
|
-
if id.to_s.strip.empty?
|
|
117
|
-
ctx.out.puts "Usage: !subagents kill <subagent_id>"
|
|
118
|
-
return
|
|
119
|
-
end
|
|
116
|
+
return ctx.out.puts "Usage: !subagents kill <subagent_id>" if id.to_s.strip.empty?
|
|
120
117
|
|
|
121
118
|
if sup.kill(id)
|
|
122
119
|
ctx.out.puts "Subagent '#{id}' and child tree killed."
|
|
@@ -126,17 +123,21 @@ module Rubik
|
|
|
126
123
|
end
|
|
127
124
|
|
|
128
125
|
def self.handle_extend(ctx, sup, id, steps_arg)
|
|
129
|
-
if id.to_s.strip.empty?
|
|
130
|
-
ctx.out.puts "Usage: !subagents extend <subagent_id> [additional_steps]"
|
|
131
|
-
return
|
|
132
|
-
end
|
|
126
|
+
return ctx.out.puts "Usage: !subagents extend <subagent_id> [additional_steps]" if id.to_s.strip.empty?
|
|
133
127
|
|
|
134
128
|
steps = Rubik::Orchestrator::Subagent::Budget.normalize_grant(steps_arg)
|
|
129
|
+
inst = sup.get(id)
|
|
135
130
|
if sup.extend_budget(id, steps)
|
|
136
|
-
|
|
137
|
-
ctx.out.puts "Extended budget for #{id} by #{steps} steps (total: #{inst.max_steps}
|
|
131
|
+
ceiling = inst.respond_to?(:max_total_steps) ? "/#{inst.max_total_steps}" : ""
|
|
132
|
+
ctx.out.puts "Extended budget for #{id} by #{steps} steps (total: #{inst.max_steps}#{ceiling}, " \
|
|
133
|
+
"state: #{inst.state.to_s.upcase})."
|
|
138
134
|
else
|
|
139
|
-
|
|
135
|
+
msg = if inst.respond_to?(:max_total_steps) && inst.max_steps >= inst.max_total_steps
|
|
136
|
+
"Could not extend budget for #{id}: lifetime step limit of #{inst.max_total_steps} reached."
|
|
137
|
+
else
|
|
138
|
+
"Could not extend budget for #{id} (not found or inactive)."
|
|
139
|
+
end
|
|
140
|
+
ctx.out.puts msg
|
|
140
141
|
end
|
|
141
142
|
end
|
|
142
143
|
|
|
@@ -89,14 +89,14 @@ module Rubik
|
|
|
89
89
|
if get_std_handle
|
|
90
90
|
new_in = begin
|
|
91
91
|
get_std_handle.call(Reline::Windows::STD_INPUT_HANDLE)
|
|
92
|
-
rescue
|
|
92
|
+
rescue StandardError
|
|
93
93
|
nil
|
|
94
94
|
end
|
|
95
95
|
io.instance_variable_set(:@hConsoleInputHandle, new_in) if new_in && new_in != 0
|
|
96
96
|
|
|
97
97
|
new_out = begin
|
|
98
98
|
get_std_handle.call(Reline::Windows::STD_OUTPUT_HANDLE)
|
|
99
|
-
rescue
|
|
99
|
+
rescue StandardError
|
|
100
100
|
nil
|
|
101
101
|
end
|
|
102
102
|
io.instance_variable_set(:@hConsoleHandle, new_out) if new_out && new_out != 0
|
|
@@ -105,14 +105,14 @@ module Rubik
|
|
|
105
105
|
flush_func = io.instance_variable_get(:@FlushConsoleInputBuffer) ||
|
|
106
106
|
begin
|
|
107
107
|
Reline::Windows::Win32API.new("kernel32", "FlushConsoleInputBuffer", ["L"], "I")
|
|
108
|
-
rescue
|
|
108
|
+
rescue StandardError
|
|
109
109
|
nil
|
|
110
110
|
end
|
|
111
111
|
if flush_func
|
|
112
112
|
io.instance_variable_set(:@FlushConsoleInputBuffer, flush_func)
|
|
113
113
|
begin
|
|
114
114
|
flush_func.call(new_in)
|
|
115
|
-
rescue
|
|
115
|
+
rescue StandardError
|
|
116
116
|
nil
|
|
117
117
|
end
|
|
118
118
|
end
|
|
@@ -143,7 +143,7 @@ if defined?(Reline::Windows)
|
|
|
143
143
|
if @hConsoleInputHandle.nil? || @hConsoleInputHandle.zero? || @hConsoleInputHandle == -1
|
|
144
144
|
@hConsoleInputHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
|
|
145
145
|
@GetStdHandle.call(STD_INPUT_HANDLE)
|
|
146
|
-
rescue
|
|
146
|
+
rescue StandardError
|
|
147
147
|
nil
|
|
148
148
|
end
|
|
149
149
|
end
|
|
@@ -156,18 +156,18 @@ if defined?(Reline::Windows)
|
|
|
156
156
|
if [0xFFFFFFFF, 4_294_967_295, -1].include?(wait_res)
|
|
157
157
|
@hConsoleInputHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
|
|
158
158
|
@GetStdHandle.call(STD_INPUT_HANDLE)
|
|
159
|
-
rescue
|
|
159
|
+
rescue StandardError
|
|
160
160
|
nil
|
|
161
161
|
end
|
|
162
162
|
@hConsoleHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
|
|
163
163
|
@GetStdHandle.call(STD_OUTPUT_HANDLE)
|
|
164
|
-
rescue
|
|
164
|
+
rescue StandardError
|
|
165
165
|
nil
|
|
166
166
|
end
|
|
167
167
|
end
|
|
168
168
|
@legacy_console = begin
|
|
169
169
|
getconsolemode.nobits?(ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
|
170
|
-
rescue
|
|
170
|
+
rescue StandardError
|
|
171
171
|
false
|
|
172
172
|
end
|
|
173
173
|
next
|
|
@@ -175,7 +175,7 @@ if defined?(Reline::Windows)
|
|
|
175
175
|
if @GetNumberOfConsoleInputEvents.call(@hConsoleInputHandle, num_of_events).zero? || num_of_events.unpack1("L").zero?
|
|
176
176
|
@hConsoleInputHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
|
|
177
177
|
@GetStdHandle.call(STD_INPUT_HANDLE)
|
|
178
|
-
rescue
|
|
178
|
+
rescue StandardError
|
|
179
179
|
nil
|
|
180
180
|
end
|
|
181
181
|
next
|
|
@@ -185,7 +185,7 @@ if defined?(Reline::Windows)
|
|
|
185
185
|
if @ReadConsoleInputW.call(@hConsoleInputHandle, input_records, 80, read_event).zero?
|
|
186
186
|
@hConsoleInputHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
|
|
187
187
|
@GetStdHandle.call(STD_INPUT_HANDLE)
|
|
188
|
-
rescue
|
|
188
|
+
rescue StandardError
|
|
189
189
|
nil
|
|
190
190
|
end
|
|
191
191
|
else
|
data/lib/rubik/repl/shell.rb
CHANGED
|
@@ -119,7 +119,7 @@ module Rubik
|
|
|
119
119
|
|
|
120
120
|
def search_symbols(query)
|
|
121
121
|
join_indexer_safely(0.2) if @ast_graph.symbols.empty? && @indexer_thread&.alive?
|
|
122
|
-
@ast_graph.
|
|
122
|
+
@ast_graph.search_symbols(query)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
def register_command(name, description:, usage: nil, custom: false, audience: nil, &handler)
|
|
@@ -251,10 +251,15 @@ module Rubik
|
|
|
251
251
|
return ToolOutput.error("Missing required parameter 'conversation_id' for action 'extend_budget'.") if id.empty?
|
|
252
252
|
|
|
253
253
|
steps = Rubik::Orchestrator::Subagent::Budget.normalize_grant(args["steps"] || args[:steps])
|
|
254
|
+
inst = sup.get(id)
|
|
254
255
|
if sup.extend_budget(id, steps)
|
|
255
|
-
|
|
256
|
+
ceiling = inst.respond_to?(:max_total_steps) ? "/#{inst.max_total_steps}" : ""
|
|
257
|
+
"Step budget for subagent '#{id}' successfully extended by #{steps} steps (total budget: #{inst.max_steps}#{ceiling}, " \
|
|
258
|
+
"state: #{inst.state.to_s.upcase})."
|
|
259
|
+
elsif inst.respond_to?(:max_total_steps) && inst.max_steps >= inst.max_total_steps
|
|
260
|
+
ToolOutput.error("Subagent '#{id}' reached its lifetime step limit (#{inst.max_total_steps}); no more steps can be granted.")
|
|
256
261
|
else
|
|
257
|
-
ToolOutput.error("Subagent '#{id}' not found or cannot extend budget (current state: #{
|
|
262
|
+
ToolOutput.error("Subagent '#{id}' not found or cannot extend budget (current state: #{inst&.state || 'not found'}).")
|
|
258
263
|
end
|
|
259
264
|
when "resume"
|
|
260
265
|
id = resolve_id(args)
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../base"
|
|
4
4
|
require_relative "../../coeffects"
|
|
5
|
-
require_relative "../
|
|
6
|
-
require_relative "../diagnostic_reporter"
|
|
7
|
-
require_relative "../fuzzy_matcher"
|
|
5
|
+
require_relative "../symbol_tool_support"
|
|
8
6
|
|
|
9
7
|
module Rubik
|
|
10
8
|
module Tools
|
|
@@ -36,13 +34,22 @@ module Rubik
|
|
|
36
34
|
end
|
|
37
35
|
|
|
38
36
|
def execute(args = {})
|
|
39
|
-
query = (args
|
|
40
|
-
return
|
|
37
|
+
query, error = SymbolToolSupport.string_argument(args, "query", label: "query")
|
|
38
|
+
return error if error
|
|
39
|
+
return ToolOutput.error("Query parameter is required.") if query.nil? || query.strip.empty?
|
|
40
|
+
|
|
41
|
+
query = query.strip
|
|
42
|
+
|
|
43
|
+
smart_context, error = SymbolToolSupport.boolean_argument(args, "smart_context", label: "smart_context")
|
|
44
|
+
return error if error
|
|
41
45
|
|
|
42
46
|
graph = ast_graph
|
|
43
47
|
return ToolOutput.error("AST graph is not available. Run '!context scan' to build the symbol index.") unless graph
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
warming = SymbolToolSupport.warmup_output(graph)
|
|
50
|
+
return warming if warming
|
|
51
|
+
|
|
52
|
+
if smart_context
|
|
46
53
|
ctx = graph.smart_context_for(query)
|
|
47
54
|
return ctx.empty? ? "No smart context found for '#{query}'." : ctx
|
|
48
55
|
end
|
|
@@ -54,14 +61,12 @@ module Rubik
|
|
|
54
61
|
end
|
|
55
62
|
|
|
56
63
|
if matches.empty?
|
|
57
|
-
all_sym_names =
|
|
64
|
+
all_sym_names = SymbolToolSupport.fuzzy_candidate_names(graph)
|
|
58
65
|
candidates = FuzzyMatcher.best_matches(query, all_sym_names)
|
|
59
|
-
DiagnosticReporter.render_symbol_not_found(query, candidate_symbols: candidates)
|
|
66
|
+
ToolOutput.error(DiagnosticReporter.render_symbol_not_found(query, candidate_symbols: candidates))
|
|
60
67
|
else
|
|
61
|
-
matches
|
|
62
|
-
|
|
63
|
-
"Signature: #{sym.signature}\n Calls: #{sym.calls.join(', ')}"
|
|
64
|
-
end.join("\n\n")
|
|
68
|
+
ranked = SymbolToolSupport.rank_matches(matches, query, graph)
|
|
69
|
+
SymbolToolSupport.render_search(query, ranked)
|
|
65
70
|
end
|
|
66
71
|
end
|
|
67
72
|
end
|
|
@@ -95,13 +100,24 @@ module Rubik
|
|
|
95
100
|
end
|
|
96
101
|
|
|
97
102
|
def execute(args = {})
|
|
98
|
-
path = (args
|
|
99
|
-
return
|
|
103
|
+
path, error = SymbolToolSupport.string_argument(args, "file_path", aliases: ["path"], label: "file_path")
|
|
104
|
+
return error if error
|
|
105
|
+
return ToolOutput.error("File path is required.") if path.nil? || path.strip.empty?
|
|
106
|
+
|
|
107
|
+
path = path.strip
|
|
100
108
|
|
|
101
109
|
graph = ast_graph
|
|
102
110
|
return ToolOutput.error("AST graph is not available. Run '!context scan' to build the symbol index.") unless graph
|
|
103
111
|
|
|
104
|
-
|
|
112
|
+
warming = SymbolToolSupport.warmup_output(graph)
|
|
113
|
+
return warming if warming
|
|
114
|
+
|
|
115
|
+
outline = graph.outline_for(path)
|
|
116
|
+
status = graph.extraction_status(path) if graph.respond_to?(:extraction_status)
|
|
117
|
+
return ToolOutput.error("Outline extraction failed for '#{path}'.") if status == :failed
|
|
118
|
+
return "#{outline}\n[Partial extraction: some symbols may be missing.]" if status == :partial
|
|
119
|
+
|
|
120
|
+
outline
|
|
105
121
|
end
|
|
106
122
|
end
|
|
107
123
|
|
|
@@ -134,12 +150,18 @@ module Rubik
|
|
|
134
150
|
end
|
|
135
151
|
|
|
136
152
|
def execute(args = {})
|
|
137
|
-
sym_name = (args
|
|
138
|
-
return
|
|
153
|
+
sym_name, error = SymbolToolSupport.string_argument(args, "symbol_name", aliases: ["symbol"], label: "symbol_name")
|
|
154
|
+
return error if error
|
|
155
|
+
return ToolOutput.error("Symbol name is required.") if sym_name.nil? || sym_name.strip.empty?
|
|
156
|
+
|
|
157
|
+
sym_name = sym_name.strip
|
|
139
158
|
|
|
140
159
|
graph = ast_graph
|
|
141
160
|
return ToolOutput.error("AST graph is not available. Run '!context scan' to build the symbol index.") unless graph
|
|
142
161
|
|
|
162
|
+
warming = SymbolToolSupport.warmup_output(graph)
|
|
163
|
+
return warming if warming
|
|
164
|
+
|
|
143
165
|
callers = graph.callers_of(sym_name)
|
|
144
166
|
if callers.empty?
|
|
145
167
|
"No references or callers found for '#{sym_name}' in AST graph."
|
|
@@ -150,6 +172,8 @@ module Rubik
|
|
|
150
172
|
end
|
|
151
173
|
lines.join("\n")
|
|
152
174
|
end
|
|
175
|
+
rescue Rubik::AST::Graph::AmbiguousSymbolError => e
|
|
176
|
+
ToolOutput.error(e.message)
|
|
153
177
|
end
|
|
154
178
|
end
|
|
155
179
|
|
|
@@ -181,15 +205,27 @@ module Rubik
|
|
|
181
205
|
end
|
|
182
206
|
|
|
183
207
|
def execute(args = {})
|
|
184
|
-
sym_name = (args
|
|
185
|
-
return
|
|
208
|
+
sym_name, error = SymbolToolSupport.string_argument(args, "symbol_name", aliases: ["symbol"], label: "symbol_name")
|
|
209
|
+
return error if error
|
|
210
|
+
return ToolOutput.error("Symbol name is required.") if sym_name.nil? || sym_name.strip.empty?
|
|
211
|
+
|
|
212
|
+
sym_name = sym_name.strip
|
|
186
213
|
|
|
187
214
|
graph = ast_graph
|
|
188
215
|
return ToolOutput.error("AST graph is not available. Run '!context scan' to build the symbol index.") unless graph
|
|
189
216
|
|
|
217
|
+
warming = SymbolToolSupport.warmup_output(graph)
|
|
218
|
+
return warming if warming
|
|
219
|
+
|
|
220
|
+
candidates = graph.symbol_candidates(sym_name) if graph.respond_to?(:symbol_candidates)
|
|
221
|
+
if candidates && candidates.length > 1
|
|
222
|
+
message = "Ambiguous symbol '#{sym_name}'. Use a qualified key. Candidates:\n- #{candidates.join("\n- ")}"
|
|
223
|
+
return ToolOutput.error(message)
|
|
224
|
+
end
|
|
225
|
+
|
|
190
226
|
info = graph.symbol_definition(sym_name)
|
|
191
227
|
unless info
|
|
192
|
-
all_sym_names =
|
|
228
|
+
all_sym_names = SymbolToolSupport.fuzzy_candidate_names(graph)
|
|
193
229
|
candidates = FuzzyMatcher.best_matches(sym_name, all_sym_names)
|
|
194
230
|
return ToolOutput.error(DiagnosticReporter.render_symbol_not_found(sym_name, candidate_symbols: candidates))
|
|
195
231
|
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "output"
|
|
4
|
+
require_relative "diagnostic_reporter"
|
|
5
|
+
require_relative "fuzzy_matcher"
|
|
6
|
+
require_relative "truncator"
|
|
7
|
+
|
|
8
|
+
module Rubik
|
|
9
|
+
module Tools
|
|
10
|
+
module SymbolToolSupport
|
|
11
|
+
def self.string_argument(args, primary, aliases: [], label: primary)
|
|
12
|
+
return [nil, ToolOutput.error("Tool arguments must be an object.")] unless args.is_a?(Hash)
|
|
13
|
+
|
|
14
|
+
values = [primary, *aliases].flat_map do |key|
|
|
15
|
+
[key.to_s, key.to_sym].uniq.filter_map { |actual| args[actual] if args.key?(actual) }
|
|
16
|
+
end
|
|
17
|
+
values.compact!
|
|
18
|
+
return [nil, nil] if values.empty?
|
|
19
|
+
return [nil, ToolOutput.error("#{label} must be a string.")] unless values.all?(String)
|
|
20
|
+
|
|
21
|
+
[values.first, nil]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.boolean_argument(args, key, label: key)
|
|
25
|
+
return [nil, ToolOutput.error("Tool arguments must be an object.")] unless args.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
values = [key.to_s, key.to_sym].uniq.filter_map { |actual| args[actual] if args.key?(actual) }
|
|
28
|
+
values.compact!
|
|
29
|
+
return [false, nil] if values.empty?
|
|
30
|
+
return [nil, ToolOutput.error("#{label} must be a boolean.")] unless values.all? { |value| [true, false].include?(value) }
|
|
31
|
+
|
|
32
|
+
[values.first, nil]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.warmup_output(graph)
|
|
36
|
+
return nil unless graph.respond_to?(:warmup_status)
|
|
37
|
+
|
|
38
|
+
status = graph.warmup_status
|
|
39
|
+
return nil unless status
|
|
40
|
+
|
|
41
|
+
state = status[:state] || status["state"]
|
|
42
|
+
if [:warming, "warming"].include?(state)
|
|
43
|
+
indexed = status[:indexed] || status["indexed"]
|
|
44
|
+
total = status[:total] || status["total"]
|
|
45
|
+
progress = if indexed.is_a?(Numeric) && total.is_a?(Numeric)
|
|
46
|
+
" (#{indexed}/#{total} files indexed)"
|
|
47
|
+
else
|
|
48
|
+
""
|
|
49
|
+
end
|
|
50
|
+
ToolOutput.error("AST index warming; retry this request shortly#{progress}.")
|
|
51
|
+
elsif [:failed, "failed"].include?(state)
|
|
52
|
+
ToolOutput.error("AST index warmup failed; retry after running '!context scan'.")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.rank_matches(matches, query, graph)
|
|
57
|
+
symbol_map = graph.respond_to?(:symbols) ? graph.symbols : nil
|
|
58
|
+
qualified = symbol_map.respond_to?(:qualified_symbols) ? symbol_map.qualified_symbols : {}
|
|
59
|
+
keys_by_id = {}.compare_by_identity
|
|
60
|
+
qualified.sort_by { |key, _symbol| key }.each do |(key, symbol)|
|
|
61
|
+
keys_by_id[symbol] ||= key
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
matches.each_with_index.sort_by do |symbol, index|
|
|
65
|
+
name = symbol.name.to_s.downcase
|
|
66
|
+
lowered_query = query.downcase
|
|
67
|
+
rank = if name == lowered_query
|
|
68
|
+
0
|
|
69
|
+
elsif name.start_with?(lowered_query)
|
|
70
|
+
1
|
|
71
|
+
else
|
|
72
|
+
2
|
|
73
|
+
end
|
|
74
|
+
key = keys_by_id[symbol] || "#{symbol.file}##{symbol.name}"
|
|
75
|
+
[rank, key, index]
|
|
76
|
+
end.map(&:first)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.render_search(query, matches)
|
|
80
|
+
shown_count = [matches.length, 50].min
|
|
81
|
+
loop do
|
|
82
|
+
omitted = matches.length - shown_count
|
|
83
|
+
lines = ["Found #{matches.length} matching symbol(s) for '#{query}':"]
|
|
84
|
+
matches.first(shown_count).each do |sym|
|
|
85
|
+
lines << "• #{sym.kind.to_s.upcase} #{sym.name} (#{sym.file}:#{sym.line_start}-#{sym.line_end})"
|
|
86
|
+
lines << " Signature: #{sym.signature}"
|
|
87
|
+
lines << " Calls: #{sym.calls.join(', ')}"
|
|
88
|
+
end
|
|
89
|
+
lines << "... #{omitted} additional matches omitted." if omitted.positive?
|
|
90
|
+
|
|
91
|
+
truncated = Truncator.truncate(lines.join("\n"))
|
|
92
|
+
return truncated[:text] if !truncated[:truncated] || shown_count <= 1
|
|
93
|
+
|
|
94
|
+
shown_count -= 1
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.fuzzy_candidate_names(graph)
|
|
99
|
+
symbols = graph.respond_to?(:symbols) ? graph.symbols : nil
|
|
100
|
+
return [] unless symbols
|
|
101
|
+
|
|
102
|
+
names = if symbols.respond_to?(:values)
|
|
103
|
+
symbols.values.filter_map { |sym| sym.name if sym.respond_to?(:name) }.uniq
|
|
104
|
+
else
|
|
105
|
+
[]
|
|
106
|
+
end
|
|
107
|
+
return names unless names.empty?
|
|
108
|
+
|
|
109
|
+
symbols.respond_to?(:keys) ? symbols.keys : []
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/rubik/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubik
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Özgür Ulukır
|
|
@@ -249,6 +249,7 @@ files:
|
|
|
249
249
|
- lib/rubik/ast/fallback_cleaner.rb
|
|
250
250
|
- lib/rubik/ast/go_extractor.rb
|
|
251
251
|
- lib/rubik/ast/graph.rb
|
|
252
|
+
- lib/rubik/ast/graph_extensions.rb
|
|
252
253
|
- lib/rubik/ast/python_extractor.rb
|
|
253
254
|
- lib/rubik/ast/ruby_extractor.rb
|
|
254
255
|
- lib/rubik/ast/rust_extractor.rb
|
|
@@ -757,6 +758,7 @@ files:
|
|
|
757
758
|
- lib/rubik/tools/registry.rb
|
|
758
759
|
- lib/rubik/tools/result.rb
|
|
759
760
|
- lib/rubik/tools/services.rb
|
|
761
|
+
- lib/rubik/tools/symbol_tool_support.rb
|
|
760
762
|
- lib/rubik/tools/truncator.rb
|
|
761
763
|
- lib/rubik/tools/uri_resolver.rb
|
|
762
764
|
- lib/rubik/ui.rb
|