rubik 0.3.1 → 0.3.2
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 +50 -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/lsp/client.rb +102 -24
- data/lib/rubik/lsp/manager.rb +136 -83
- data/lib/rubik/orchestrator/tool_circuit_breaker.rb +15 -5
- data/lib/rubik/prompt/token_estimator.rb +15 -3
- data/lib/rubik/repl/ast_warmup.rb +35 -4
- data/lib/rubik/repl/shell.rb +1 -1
- 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/lsp/manager.rb
CHANGED
|
@@ -42,6 +42,8 @@ module Rubik
|
|
|
42
42
|
@context = context || runtime_manager&.context
|
|
43
43
|
@clients = {}
|
|
44
44
|
@crash_stats = {}
|
|
45
|
+
@recovery_errors = {}
|
|
46
|
+
@registered_disposable_langs = {}
|
|
45
47
|
@monitor = Monitor.new
|
|
46
48
|
end
|
|
47
49
|
|
|
@@ -93,82 +95,20 @@ module Rubik
|
|
|
93
95
|
def ensure_healthy!(language_or_client)
|
|
94
96
|
return nil unless enabled?
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
return nil unless
|
|
98
|
+
lang = language_for(language_or_client)
|
|
99
|
+
return nil unless lang
|
|
98
100
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
@monitor.synchronize do
|
|
103
|
-
begin
|
|
104
|
-
client.stop
|
|
105
|
-
rescue
|
|
106
|
-
nil
|
|
107
|
-
end
|
|
108
|
-
@clients.delete(lang)
|
|
109
|
-
end
|
|
110
|
-
start_client(lang) unless circuit_open?(lang)
|
|
111
|
-
client = @clients[lang]
|
|
112
|
-
elsif !client.running && @config.fetch("lsp.auto_start", true) && client.available? && !circuit_open?(client.language)
|
|
113
|
-
start_client(client.language)
|
|
114
|
-
client = @clients[client.language]
|
|
101
|
+
@monitor.synchronize do
|
|
102
|
+
preferred = language_or_client if language_or_client.is_a?(Client)
|
|
103
|
+
client_for_unlocked(lang, preferred)
|
|
115
104
|
end
|
|
116
|
-
|
|
117
|
-
client
|
|
118
105
|
end
|
|
119
106
|
|
|
120
107
|
def client_for(language_or_file)
|
|
121
|
-
lang =
|
|
122
|
-
detect_language(language_or_file)
|
|
123
|
-
else
|
|
124
|
-
language_or_file.to_s.downcase
|
|
125
|
-
end
|
|
126
|
-
|
|
108
|
+
lang = language_for(language_or_file)
|
|
127
109
|
return nil unless lang
|
|
128
110
|
|
|
129
|
-
@monitor.synchronize
|
|
130
|
-
if @clients.key?(lang)
|
|
131
|
-
client = @clients[lang]
|
|
132
|
-
if client.running && client.respond_to?(:alive?) && !client.alive?
|
|
133
|
-
record_crash(lang)
|
|
134
|
-
begin
|
|
135
|
-
client.stop
|
|
136
|
-
rescue
|
|
137
|
-
nil
|
|
138
|
-
end
|
|
139
|
-
@clients.delete(lang)
|
|
140
|
-
nil # Will trigger recreate / start below
|
|
141
|
-
else
|
|
142
|
-
client
|
|
143
|
-
end
|
|
144
|
-
elsif enabled?
|
|
145
|
-
configured_cmd = @config.fetch("lsp.servers.#{lang}") || Client::SERVERS[lang]
|
|
146
|
-
client = Client.new(lang, root_dir: @root_dir, server_cmd: configured_cmd)
|
|
147
|
-
unless client.available?
|
|
148
|
-
probed_cmd = Runtime::Discovery.probe_lsp(lang, root_dir: @root_dir, custom_cmd: configured_cmd)
|
|
149
|
-
client = Client.new(lang, root_dir: @root_dir, server_cmd: probed_cmd) if probed_cmd
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
if client.available? && @config.fetch("lsp.auto_start", true) && !circuit_open?(lang)
|
|
153
|
-
if client.start
|
|
154
|
-
reset_circuit!(lang)
|
|
155
|
-
effective_ctx = @context || @runtime_manager&.context
|
|
156
|
-
if effective_ctx
|
|
157
|
-
effective_ctx.provide("lsp:#{lang}", client)
|
|
158
|
-
effective_ctx.register_disposable { client.stop if client.running }
|
|
159
|
-
end
|
|
160
|
-
else
|
|
161
|
-
record_crash(lang)
|
|
162
|
-
begin
|
|
163
|
-
client.stop
|
|
164
|
-
rescue StandardError
|
|
165
|
-
nil
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
@clients[lang] = client
|
|
170
|
-
end
|
|
171
|
-
end
|
|
111
|
+
@monitor.synchronize { client_for_unlocked(lang) }
|
|
172
112
|
end
|
|
173
113
|
|
|
174
114
|
def start_client(lang)
|
|
@@ -204,19 +144,12 @@ module Rubik
|
|
|
204
144
|
@clients[lang_str] = client
|
|
205
145
|
if res
|
|
206
146
|
reset_circuit!(lang_str)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
effective_ctx.provide("lsp:#{lang_str}", client)
|
|
210
|
-
effective_ctx.register_disposable { client.stop if client.running }
|
|
211
|
-
end
|
|
147
|
+
@recovery_errors.delete(lang_str)
|
|
148
|
+
register_client_unlocked(lang_str, client)
|
|
212
149
|
{ ok: true, message: "Started LSP server for '#{lang_str}' (#{client.server_cmd})" }
|
|
213
150
|
else
|
|
214
151
|
record_crash(lang_str)
|
|
215
|
-
|
|
216
|
-
client.stop
|
|
217
|
-
rescue StandardError
|
|
218
|
-
nil
|
|
219
|
-
end
|
|
152
|
+
stop_client_safely(client)
|
|
220
153
|
failure = Runtime::DiagnosticEngine.diagnose(
|
|
221
154
|
cmd: client.server_cmd,
|
|
222
155
|
exit_code: 1,
|
|
@@ -241,13 +174,19 @@ module Rubik
|
|
|
241
174
|
|
|
242
175
|
client.stop
|
|
243
176
|
@clients.delete(lang_str)
|
|
177
|
+
effective_ctx = @context || @runtime_manager&.context
|
|
178
|
+
effective_ctx&.unprovide("lsp:#{lang_str}") if effective_ctx.respond_to?(:unprovide)
|
|
244
179
|
{ ok: true, message: "Stopped LSP server for '#{lang_str}'" }
|
|
245
180
|
end
|
|
246
181
|
end
|
|
247
182
|
|
|
248
183
|
def stop_all
|
|
249
184
|
@monitor.synchronize do
|
|
250
|
-
@
|
|
185
|
+
effective_ctx = @context || @runtime_manager&.context
|
|
186
|
+
@clients.each do |lang_str, client|
|
|
187
|
+
client.stop
|
|
188
|
+
effective_ctx&.unprovide("lsp:#{lang_str}") if effective_ctx.respond_to?(:unprovide)
|
|
189
|
+
end
|
|
251
190
|
@clients.clear
|
|
252
191
|
end
|
|
253
192
|
end
|
|
@@ -317,21 +256,21 @@ module Rubik
|
|
|
317
256
|
|
|
318
257
|
def hover(file_path, line, character)
|
|
319
258
|
client = client_for(file_path)
|
|
320
|
-
return { ok: false, error:
|
|
259
|
+
return { ok: false, error: inactive_client_error(file_path, client) } unless client&.running
|
|
321
260
|
|
|
322
261
|
client.hover(file_path, line, character)
|
|
323
262
|
end
|
|
324
263
|
|
|
325
264
|
def definition(file_path, line, character)
|
|
326
265
|
client = client_for(file_path)
|
|
327
|
-
return { ok: false, error:
|
|
266
|
+
return { ok: false, error: inactive_client_error(file_path, client) } unless client&.running
|
|
328
267
|
|
|
329
268
|
client.definition(file_path, line, character)
|
|
330
269
|
end
|
|
331
270
|
|
|
332
271
|
def references(file_path, line, character)
|
|
333
272
|
client = client_for(file_path)
|
|
334
|
-
return { ok: false, error:
|
|
273
|
+
return { ok: false, error: inactive_client_error(file_path, client) } unless client&.running
|
|
335
274
|
|
|
336
275
|
client.references(file_path, line, character)
|
|
337
276
|
end
|
|
@@ -361,6 +300,120 @@ module Rubik
|
|
|
361
300
|
servers: client_statuses
|
|
362
301
|
}
|
|
363
302
|
end
|
|
303
|
+
|
|
304
|
+
private
|
|
305
|
+
|
|
306
|
+
def language_for(language_or_file)
|
|
307
|
+
if language_or_file.is_a?(Client)
|
|
308
|
+
language_or_file.language.to_s.downcase
|
|
309
|
+
elsif language_or_file.to_s.include?(".")
|
|
310
|
+
detect_language(language_or_file)
|
|
311
|
+
else
|
|
312
|
+
language_or_file.to_s.downcase
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Caller MUST hold @monitor. Dead-client removal and replacement startup
|
|
317
|
+
# happen in one critical section so concurrent requests share one client.
|
|
318
|
+
def client_for_unlocked(lang, preferred = nil)
|
|
319
|
+
stored = @clients[lang]
|
|
320
|
+
client = stored || preferred
|
|
321
|
+
recovering = false
|
|
322
|
+
|
|
323
|
+
if client && client_dead?(client)
|
|
324
|
+
recovering = true
|
|
325
|
+
record_crash(lang)
|
|
326
|
+
stop_client_safely(client)
|
|
327
|
+
@clients.delete(lang) if stored.equal?(client)
|
|
328
|
+
client = nil
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
if recovering && circuit_open?(lang)
|
|
332
|
+
@recovery_errors[lang] = "LSP server restart for '#{lang}' is blocked by the circuit breaker"
|
|
333
|
+
return nil
|
|
334
|
+
end
|
|
335
|
+
if recovering && !@config.fetch("lsp.auto_start", true)
|
|
336
|
+
@recovery_errors[lang] = "LSP server restart for '#{lang}' is disabled"
|
|
337
|
+
return nil
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
return nil unless client || enabled?
|
|
341
|
+
|
|
342
|
+
if client.nil?
|
|
343
|
+
configured_cmd = @config.fetch("lsp.servers.#{lang}") || Client::SERVERS[lang]
|
|
344
|
+
client = Client.new(lang, root_dir: @root_dir, server_cmd: configured_cmd)
|
|
345
|
+
unless client.available?
|
|
346
|
+
probed_cmd = Runtime::Discovery.probe_lsp(lang, root_dir: @root_dir, custom_cmd: configured_cmd)
|
|
347
|
+
client = Client.new(lang, root_dir: @root_dir, server_cmd: probed_cmd) if probed_cmd
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
if client.available? && @config.fetch("lsp.auto_start", true) && !circuit_open?(lang)
|
|
351
|
+
if client.start
|
|
352
|
+
reset_circuit!(lang)
|
|
353
|
+
@recovery_errors.delete(lang)
|
|
354
|
+
register_client_unlocked(lang, client)
|
|
355
|
+
else
|
|
356
|
+
record_crash(lang)
|
|
357
|
+
stop_client_safely(client)
|
|
358
|
+
@recovery_errors[lang] = "LSP server restart failed for '#{lang}'" if recovering
|
|
359
|
+
end
|
|
360
|
+
elsif recovering
|
|
361
|
+
@recovery_errors[lang] = if circuit_open?(lang)
|
|
362
|
+
"LSP server restart for '#{lang}' is blocked by the circuit breaker"
|
|
363
|
+
elsif !@config.fetch("lsp.auto_start", true)
|
|
364
|
+
"LSP server restart for '#{lang}' is disabled"
|
|
365
|
+
else
|
|
366
|
+
"LSP server restart failed for '#{lang}': server unavailable"
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
@clients[lang] = client
|
|
370
|
+
elsif !client.running && @config.fetch("lsp.auto_start", true) &&
|
|
371
|
+
client.available? && !circuit_open?(lang)
|
|
372
|
+
if client.start
|
|
373
|
+
reset_circuit!(lang)
|
|
374
|
+
@recovery_errors.delete(lang)
|
|
375
|
+
register_client_unlocked(lang, client)
|
|
376
|
+
else
|
|
377
|
+
record_crash(lang)
|
|
378
|
+
stop_client_safely(client)
|
|
379
|
+
@recovery_errors[lang] = "LSP server restart failed for '#{lang}'" if recovering
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
client
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def client_dead?(client)
|
|
387
|
+
(client.running && client.respond_to?(:alive?) && !client.alive?) ||
|
|
388
|
+
(client.respond_to?(:crashed?) && client.crashed?)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def register_client_unlocked(lang, client)
|
|
392
|
+
effective_ctx = @context || @runtime_manager&.context
|
|
393
|
+
return unless effective_ctx
|
|
394
|
+
|
|
395
|
+
effective_ctx.provide("lsp:#{lang}", client)
|
|
396
|
+
return if @registered_disposable_langs[lang] == effective_ctx.object_id
|
|
397
|
+
|
|
398
|
+
@registered_disposable_langs[lang] = effective_ctx.object_id
|
|
399
|
+
effective_ctx.register_disposable do
|
|
400
|
+
current = @clients[lang]
|
|
401
|
+
current.stop if current&.running
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def stop_client_safely(client)
|
|
406
|
+
client.stop
|
|
407
|
+
rescue StandardError
|
|
408
|
+
nil
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def inactive_client_error(file_path, client)
|
|
412
|
+
lang = client&.language || detect_language(file_path)
|
|
413
|
+
@monitor.synchronize do
|
|
414
|
+
@recovery_errors[lang] || "LSP not active for '#{file_path}'"
|
|
415
|
+
end
|
|
416
|
+
end
|
|
364
417
|
end
|
|
365
418
|
end
|
|
366
419
|
end
|
|
@@ -35,15 +35,25 @@ module Rubik
|
|
|
35
35
|
when :open
|
|
36
36
|
return false unless cooldown_elapsed?(circuit)
|
|
37
37
|
|
|
38
|
-
#
|
|
38
|
+
# Cooldown elapsed: enter half-open state and grant single probe
|
|
39
39
|
circuit[:state] = :half_open
|
|
40
40
|
circuit[:probing] = true
|
|
41
|
+
circuit[:opened_at] = now
|
|
41
42
|
true
|
|
42
43
|
when :half_open
|
|
43
44
|
if circuit[:probing]
|
|
44
|
-
|
|
45
|
+
# Probe is currently in flight. If probe wedged past cooldown,
|
|
46
|
+
# allow a new probe to prevent permanent deadlock.
|
|
47
|
+
if cooldown_elapsed?(circuit)
|
|
48
|
+
circuit[:opened_at] = now
|
|
49
|
+
true
|
|
50
|
+
else
|
|
51
|
+
false
|
|
52
|
+
end
|
|
45
53
|
else
|
|
46
|
-
|
|
54
|
+
circuit[:probing] = true
|
|
55
|
+
circuit[:opened_at] = now
|
|
56
|
+
true
|
|
47
57
|
end
|
|
48
58
|
else
|
|
49
59
|
true
|
|
@@ -53,7 +63,7 @@ module Rubik
|
|
|
53
63
|
|
|
54
64
|
def record_success(tool_name)
|
|
55
65
|
@monitor.synchronize do
|
|
56
|
-
@tools.delete(tool_name)
|
|
66
|
+
@tools.delete(tool_name.to_s)
|
|
57
67
|
end
|
|
58
68
|
end
|
|
59
69
|
|
|
@@ -76,7 +86,7 @@ module Rubik
|
|
|
76
86
|
private
|
|
77
87
|
|
|
78
88
|
def circuit(tool_name)
|
|
79
|
-
@tools[tool_name] ||= { failures: 0, opened_at: nil, probing: false, state: :closed }
|
|
89
|
+
@tools[tool_name.to_s] ||= { failures: 0, opened_at: nil, probing: false, state: :closed }
|
|
80
90
|
end
|
|
81
91
|
|
|
82
92
|
def cooldown_elapsed?(circuit)
|
|
@@ -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
|
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)
|
|
@@ -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
|