rubik 0.3.4 → 0.4.0
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 +105 -2
- data/lib/rubik/adapter/process.rb +11 -8
- data/lib/rubik/ast/go_extractor.rb +9 -2
- data/lib/rubik/ast/graph.rb +20 -1
- data/lib/rubik/ast/graph_extensions.rb +89 -1
- data/lib/rubik/ast/python_extractor.rb +5 -5
- data/lib/rubik/ast/rust_extractor.rb +9 -2
- data/lib/rubik/ast/typescript_extractor.rb +13 -7
- data/lib/rubik/buffer/manager.rb +78 -66
- data/lib/rubik/config/defaults.rb +1 -0
- data/lib/rubik/context/git_lister.rb +3 -1
- data/lib/rubik/diagnostics/parser.rb +53 -1
- data/lib/rubik/llm/anthropic.rb +5 -2
- data/lib/rubik/llm/openai_compatible.rb +83 -68
- data/lib/rubik/llm/registry.rb +81 -74
- data/lib/rubik/lsp/client.rb +320 -96
- data/lib/rubik/lsp/manager.rb +83 -12
- data/lib/rubik/lsp/server_command.rb +102 -0
- data/lib/rubik/orchestrator/lsp_feedback.rb +20 -4
- data/lib/rubik/orchestrator/step_executor.rb +138 -154
- data/lib/rubik/orchestrator/subagent/spawn_transaction.rb +60 -31
- data/lib/rubik/prompt/token_estimator.rb +16 -12
- data/lib/rubik/repl/commands/agent_context.rb +126 -104
- data/lib/rubik/repl/commands/agent_execution.rb +83 -68
- data/lib/rubik/repl/commands/agent_models.rb +112 -87
- data/lib/rubik/repl/commands/buffer.rb +122 -88
- data/lib/rubik/repl/commands/mcp.rb +120 -92
- data/lib/rubik/repl/commands/session.rb +90 -59
- data/lib/rubik/repl/commands/workspace.rb +110 -94
- data/lib/rubik/repl/shell.rb +10 -1
- data/lib/rubik/runtime/discovery.rb +12 -6
- data/lib/rubik/security/classifier/classifier.rb +3 -1
- data/lib/rubik/storage/vakanuvis/distill_gate.rb +6 -2
- data/lib/rubik/storage/vakanuvis/gate.rb +37 -16
- data/lib/rubik/storage/vakanuvis/harness.rb +36 -21
- data/lib/rubik/storage/vakanuvis/memory_steward.rb +12 -6
- data/lib/rubik/tools/builtins/lifecycle.rb +34 -7
- data/lib/rubik/tools/builtins/lsp.rb +36 -8
- data/lib/rubik/tools/truncator.rb +21 -8
- data/lib/rubik/version.rb +1 -1
- metadata +2 -1
data/lib/rubik/repl/shell.rb
CHANGED
|
@@ -92,7 +92,16 @@ module Rubik
|
|
|
92
92
|
files.each do |rel_file|
|
|
93
93
|
@context_index.update_file(rel_file)
|
|
94
94
|
full_path = File.join(root_d, rel_file)
|
|
95
|
-
File.exist?(full_path)
|
|
95
|
+
if File.exist?(full_path)
|
|
96
|
+
@ast_graph.index_file(full_path)
|
|
97
|
+
if @lsp_manager&.enabled?
|
|
98
|
+
content = Rubik::Platform.read_text(full_path)
|
|
99
|
+
@lsp_manager.sync_buffer_file(rel_file, content)
|
|
100
|
+
@lsp_manager.did_save_file(rel_file, content)
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
@ast_graph.remove_file(full_path)
|
|
104
|
+
end
|
|
96
105
|
end
|
|
97
106
|
@ast_graph.save_to_cache(@cache_store) if @cache_store
|
|
98
107
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "shellwords"
|
|
4
4
|
require_relative "../platform"
|
|
5
|
+
require_relative "../lsp/server_command"
|
|
5
6
|
|
|
6
7
|
module Rubik
|
|
7
8
|
module Runtime
|
|
@@ -179,22 +180,27 @@ module Rubik
|
|
|
179
180
|
end
|
|
180
181
|
|
|
181
182
|
def binary_available?(cmd, root_dir: nil)
|
|
182
|
-
|
|
183
|
+
argv = Rubik::LSP::ServerCommand.argv(cmd)
|
|
184
|
+
return false if argv.empty?
|
|
183
185
|
|
|
184
|
-
bin =
|
|
185
|
-
if
|
|
186
|
-
full_path = File.expand_path(bin, root_dir)
|
|
186
|
+
bin = argv.first
|
|
187
|
+
if bin.include?("/") || (Rubik::Platform.windows? && bin.include?("\\"))
|
|
188
|
+
full_path = File.expand_path(bin, root_dir || Dir.pwd)
|
|
187
189
|
return true if Rubik::Platform.executable_file?(full_path)
|
|
190
|
+
|
|
191
|
+
return false
|
|
188
192
|
end
|
|
189
193
|
|
|
190
|
-
!Rubik::Platform.which(
|
|
194
|
+
!Rubik::Platform.which(bin).nil?
|
|
195
|
+
rescue ArgumentError
|
|
196
|
+
false
|
|
191
197
|
end
|
|
192
198
|
|
|
193
199
|
def probe_lsp(lang, dir = nil, root_dir: nil, custom_cmd: nil)
|
|
194
200
|
effective_root = root_dir || dir || Dir.pwd
|
|
195
201
|
lang_key = lang.to_s.downcase
|
|
196
202
|
candidates = []
|
|
197
|
-
candidates << custom_cmd
|
|
203
|
+
candidates << custom_cmd if custom_cmd && !Rubik::LSP::ServerCommand.argv(custom_cmd).empty?
|
|
198
204
|
candidates.concat(lsp_candidates(lang_key))
|
|
199
205
|
candidates.uniq!
|
|
200
206
|
|
|
@@ -22,6 +22,8 @@ module Rubik
|
|
|
22
22
|
DEV_PATHS = ["/dev/null", "/dev/stdin", "/dev/stdout", "/dev/stderr", "/dev/fd/"].freeze
|
|
23
23
|
WRITE_OPERATORS = [">", ">>", "2>", "&>", ">&", "2>>", "*>"].freeze
|
|
24
24
|
READ_OPERATORS = ["<"].freeze
|
|
25
|
+
# PERF: Hoist home token array to avoid per-path array allocations in system_path? check.
|
|
26
|
+
HOME_TOKENS = ["~", "~/", "$HOME", "${HOME}"].freeze
|
|
25
27
|
|
|
26
28
|
def self.default_database
|
|
27
29
|
@default_database ||= CommandDatabase.new
|
|
@@ -351,7 +353,7 @@ module Rubik
|
|
|
351
353
|
|
|
352
354
|
return true if p =~ %r{\A/(?:\*|\.+)?/?\z}
|
|
353
355
|
return true if p =~ %r{\A[a-zA-Z]:(?:[\\/]|\*|\.+)?\z}i
|
|
354
|
-
return true if
|
|
356
|
+
return true if HOME_TOKENS.include?(p)
|
|
355
357
|
|
|
356
358
|
if p =~ %r{\A([a-zA-Z]):[/\\](.*)\z}
|
|
357
359
|
parts = []
|
|
@@ -238,6 +238,10 @@ module Rubik
|
|
|
238
238
|
# talebi olarak sunar. Tümü geçerse sayfanın eski gap notu
|
|
239
239
|
# supersede edilir (öz-onarım kapanışı). Düzlem 1 sayfa metnini
|
|
240
240
|
# ASLA düzenlemez — düzeltme daima bir sonraki derlemeye aittir.
|
|
241
|
+
def active_recall_hits(store, **)
|
|
242
|
+
store.search_ranked(**).reject { |record| record["status"] == Vocabulary::STATUS_SUPERSEDED }
|
|
243
|
+
end
|
|
244
|
+
|
|
241
245
|
def run_recall_smoke(gate, proposal, page_id)
|
|
242
246
|
checks = proposal.fetch("checks", [])
|
|
243
247
|
return if checks.empty?
|
|
@@ -247,8 +251,8 @@ module Rubik
|
|
|
247
251
|
# koşulmalı — ilk RECALL_LIMIT kayıt. 6. sıradaki sayfa maestro'ya
|
|
248
252
|
# görünmüyorsa iska sayılmalı (öz-onarım döngüsü kör kalmasın).
|
|
249
253
|
missed = checks.reject do |check|
|
|
250
|
-
gate.store
|
|
251
|
-
|
|
254
|
+
active_recall_hits(gate.store, query: check, type: Harness::RECALL_TYPES)
|
|
255
|
+
.first(Harness::RECALL_LIMIT).any? { |r| r["id"] == page_id }
|
|
252
256
|
end
|
|
253
257
|
if missed.empty?
|
|
254
258
|
resolved = gate.store.get(type: "note", id: gap_ref)
|
|
@@ -21,10 +21,15 @@ module Rubik
|
|
|
21
21
|
# supervisor, event_bus ve sözlü girdiler dışarıdan verilir —
|
|
22
22
|
# Harness'e ASLA geri dokunmaz.
|
|
23
23
|
class Gate
|
|
24
|
+
DEFAULT_MAX_DIGEST_JOBS = 100
|
|
24
25
|
attr_reader :store, :supervisor, :dmesg, :session_slug, :max_steps, :model
|
|
25
26
|
|
|
26
27
|
def initialize(store:, max_steps:, session_slug:, supervisor: nil, dmesg: nil,
|
|
27
|
-
event_bus: nil, enabled: true, model: nil)
|
|
28
|
+
event_bus: nil, enabled: true, model: nil, max_digest_jobs: DEFAULT_MAX_DIGEST_JOBS)
|
|
29
|
+
raise ArgumentError, "max_digest_jobs must be a positive integer" unless max_digest_jobs.is_a?(Integer) && max_digest_jobs.positive?
|
|
30
|
+
|
|
31
|
+
@max_digest_jobs = max_digest_jobs
|
|
32
|
+
@digest_targets = {} # accepted targets retain their slot through harvest/retry
|
|
28
33
|
@store = store
|
|
29
34
|
@supervisor = supervisor
|
|
30
35
|
@dmesg = dmesg
|
|
@@ -37,7 +42,7 @@ module Rubik
|
|
|
37
42
|
@distillers = {}
|
|
38
43
|
@lock = Mutex.new
|
|
39
44
|
# v2 §4-V5: kirli işareti — son başarılı derlemeden bu yana KALICI
|
|
40
|
-
# içerik (memory+wiki+note
|
|
45
|
+
# içerik (memory+wiki+note içerik ayak izi) değişmediyse toplu
|
|
41
46
|
# damıtıcı doğmaz. Turn satırları ayak izini değiştirmez: saf
|
|
42
47
|
# turn akışında derleme boşa yakılmaz.
|
|
43
48
|
@compile_footprint = nil
|
|
@@ -54,19 +59,22 @@ module Rubik
|
|
|
54
59
|
target = Vocabulary.wiki_ref(query)
|
|
55
60
|
# v2 §4-V5: aynı hedefli iş zaten kuyrukta ya da uçuştaysa makbuz
|
|
56
61
|
# 'already queued' varyantına düşer — çift doğum yok.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
busy
|
|
62
|
+
admission = @lock.synchronize do
|
|
63
|
+
next :duplicate if @digest_targets.key?(target)
|
|
64
|
+
next :full if @digest_targets.size >= @max_digest_jobs
|
|
65
|
+
|
|
66
|
+
@pending_digests << { query: query, target: target, transcript_uris: source_transcript_uris }
|
|
67
|
+
@digest_targets[target] = true
|
|
68
|
+
:accepted
|
|
65
69
|
end
|
|
66
|
-
if
|
|
70
|
+
if admission == :duplicate
|
|
67
71
|
return "Digest already queued for '#{target}' (#{AnnalWriter.one_line(query, 60)}): the chronicler is " \
|
|
68
72
|
"compiling it — recall it later with vakanuvis(action: 'recall', query: '...')."
|
|
69
73
|
end
|
|
74
|
+
if admission == :full
|
|
75
|
+
return "Digest not accepted for '#{target}': capacity #{@max_digest_jobs} is full. " \
|
|
76
|
+
"Existing jobs are preserved; retry after a digest finishes."
|
|
77
|
+
end
|
|
70
78
|
|
|
71
79
|
spawned = spawn_job!(batch: false)
|
|
72
80
|
DistillGate.receipt(query: query, target: target, spawned: !spawned.nil?)
|
|
@@ -122,6 +130,7 @@ module Rubik
|
|
|
122
130
|
# gerçek değeri yaz (future harvest sembolleri sessiz kalmasın).
|
|
123
131
|
Vakanuvis.log_via(@dmesg, :warn, "distiller ended with unexpected outcome=#{outcome.inspect} — dropped, store untouched")
|
|
124
132
|
end
|
|
133
|
+
@lock.synchronize { @digest_targets.delete(job[:target]) } unless [:incomplete, :crashed].include?(outcome)
|
|
125
134
|
outcome
|
|
126
135
|
end
|
|
127
136
|
|
|
@@ -213,7 +222,7 @@ module Rubik
|
|
|
213
222
|
end
|
|
214
223
|
|
|
215
224
|
def restore_job(job)
|
|
216
|
-
@pending_digests.
|
|
225
|
+
@pending_digests.push(job) if job[:target]
|
|
217
226
|
end
|
|
218
227
|
|
|
219
228
|
def confirm(inst_id, job)
|
|
@@ -224,11 +233,23 @@ module Rubik
|
|
|
224
233
|
@lock.synchronize { @compile_footprint.nil? || @compile_footprint != footprint }
|
|
225
234
|
end
|
|
226
235
|
|
|
227
|
-
# Kalıcı içerik ayak izi:
|
|
228
|
-
#
|
|
229
|
-
#
|
|
236
|
+
# Kalıcı içerik ayak izi: sabit id + içerik hash'i + durum.
|
|
237
|
+
# Annal'ın yalnız turn satırları ayıklanır; diğer not değişiklikleri
|
|
238
|
+
# ve mevcut kart/sayfa güncellemeleri derleyiciyi yeniden kirletir.
|
|
230
239
|
def footprint
|
|
231
|
-
["memory", "wiki", "note"].
|
|
240
|
+
["memory", "wiki", "note"].flat_map do |type|
|
|
241
|
+
@store.list(type: type).sort_by(&:id).map do |summary|
|
|
242
|
+
record = @store.get(type: type, id: summary.id)
|
|
243
|
+
next [type, summary.id, nil] unless record
|
|
244
|
+
|
|
245
|
+
hash = record["hash"]
|
|
246
|
+
if type == "note" && (Vocabulary::ANNAL_TAGS - Array(record["tags"])).empty?
|
|
247
|
+
body = record["body"].to_s.lines.grep_v(/\A- \[\d+\] turn .*\(event=turn\.\d+\.[^)]*\)\s*\z/).join.rstrip
|
|
248
|
+
hash = Digest::SHA256.hexdigest([record["title"], body, record["tags"], record["links"]].to_json)
|
|
249
|
+
end
|
|
250
|
+
[type, summary.id, hash, record["status"]]
|
|
251
|
+
end
|
|
252
|
+
end
|
|
232
253
|
end
|
|
233
254
|
|
|
234
255
|
# Kaynak disiplini (v2 §4-V4): yalnız status=completed transcript
|
|
@@ -63,7 +63,9 @@ module Rubik
|
|
|
63
63
|
# (kurucu enjeksiyonu — gate Harness'e geri dokunmaz).
|
|
64
64
|
@gate = DistillGate::Gate.new(store: @store, supervisor: @supervisor, dmesg: @dmesg,
|
|
65
65
|
event_bus: @event_bus, enabled: @distiller_enabled,
|
|
66
|
-
max_steps: @max_steps, model: @model, session_slug: @session_slug
|
|
66
|
+
max_steps: @max_steps, model: @model, session_slug: @session_slug,
|
|
67
|
+
max_digest_jobs: fetch_cfg("subagents.vakanuvis.max_digest_jobs",
|
|
68
|
+
DistillGate::Gate::DEFAULT_MAX_DIGEST_JOBS))
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
class << self
|
|
@@ -90,9 +92,13 @@ module Rubik
|
|
|
90
92
|
# 2 saniyede durmazsa (yavaş bir flush/spawn içinde) kill gelir ve
|
|
91
93
|
# işlenemeyen olay sayısı dmesg'e düşer — sessiz kayıp yok.
|
|
92
94
|
def shutdown
|
|
93
|
-
@monitor.synchronize
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
@monitor.synchronize do
|
|
96
|
+
return if @stopped
|
|
97
|
+
|
|
98
|
+
@stopped = true
|
|
99
|
+
@busy += 1 # STOP işaretinin de işleniş sayılır
|
|
100
|
+
@queue.push(STOP)
|
|
101
|
+
end
|
|
96
102
|
@worker&.join(2)
|
|
97
103
|
if @worker&.alive?
|
|
98
104
|
Vakanuvis.log_via(@dmesg, :warn, "worker did not stop in 2s — killing with #{queue_size} event(s) unprocessed")
|
|
@@ -114,17 +120,19 @@ module Rubik
|
|
|
114
120
|
# ---- olay giriş kapıları (kworker thread'inde çağrılır) -----------
|
|
115
121
|
|
|
116
122
|
def enqueue_event(kind, payload)
|
|
117
|
-
return false if stopped?
|
|
118
|
-
|
|
119
123
|
unless payload.is_a?(Hash)
|
|
120
124
|
Vakanuvis.log_via(@dmesg, :warn, "discarded non-hash #{kind} event (#{payload.class})")
|
|
121
125
|
return false
|
|
122
126
|
end
|
|
123
127
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
@monitor.synchronize do
|
|
129
|
+
return false if @stopped
|
|
130
|
+
|
|
131
|
+
drop_oldest! if @queue.size >= @max_queue
|
|
132
|
+
@busy += 1
|
|
133
|
+
@queue.push([kind, payload])
|
|
134
|
+
true
|
|
135
|
+
end
|
|
128
136
|
end
|
|
129
137
|
|
|
130
138
|
def queue_size = @queue.size
|
|
@@ -142,7 +150,7 @@ module Rubik
|
|
|
142
150
|
|
|
143
151
|
allowed = Array(types).map(&:to_s)
|
|
144
152
|
allowed = RECALL_TYPES if allowed.empty?
|
|
145
|
-
hits = @store
|
|
153
|
+
hits = DistillGate.active_recall_hits(@store, query: q, type: allowed).first(RECALL_LIMIT)
|
|
146
154
|
unless hits.empty?
|
|
147
155
|
lines = hits.map { |r| recall_line(r) }
|
|
148
156
|
header = "Recalled #{hits.size} entit#{hits.size == 1 ? 'y' : 'ies'} for '#{AnnalWriter.one_line(q, 80)}':"
|
|
@@ -186,8 +194,8 @@ module Rubik
|
|
|
186
194
|
"recall shows compiled pages; a submitted digest appears here until it compiles."
|
|
187
195
|
end
|
|
188
196
|
|
|
189
|
-
"Digest queue: #{targets[:queued].size} queued (#{targets[:queued]
|
|
190
|
-
"#{targets[:in_flight].size} in flight (#{targets[:in_flight]
|
|
197
|
+
"Digest queue: #{targets[:queued].size} queued (#{digest_target_summary(targets[:queued])}), " \
|
|
198
|
+
"#{targets[:in_flight].size} in flight (#{digest_target_summary(targets[:in_flight])})."
|
|
191
199
|
end
|
|
192
200
|
|
|
193
201
|
# Soğuk-başlangıç yönelimi (issue #99 P2): ilk sorgu sözcük dağı
|
|
@@ -214,6 +222,12 @@ module Rubik
|
|
|
214
222
|
|
|
215
223
|
private
|
|
216
224
|
|
|
225
|
+
def digest_target_summary(targets)
|
|
226
|
+
shown = targets.first(10)
|
|
227
|
+
shown << "+#{targets.size - 10} targets" if targets.size > 10
|
|
228
|
+
shown.join(", ")
|
|
229
|
+
end
|
|
230
|
+
|
|
217
231
|
# v2 §4-V2 + issue #99 P2: AND boşluğu tek tip körlük değil — en
|
|
218
232
|
# yakın kısmi eşleşmeler dürüstçe "(fallback)" damgasıyla sunulur
|
|
219
233
|
# (gerçek eşleşme değildir), transcript izleri kapsam ipucu olarak
|
|
@@ -226,9 +240,8 @@ module Rubik
|
|
|
226
240
|
# Süpersede edilmiş sayılar "en yakın kısmi eşleşme" olarak
|
|
227
241
|
# sunulmaz (review M1) — emekli bir sayfayı okumaya davet etmek,
|
|
228
242
|
# issue #100'ün "iki kopya, biri bozuk" bulgusunun aynısıdır.
|
|
229
|
-
relaxed = @store
|
|
230
|
-
|
|
231
|
-
.first(RECALL_FALLBACK_LIMIT)
|
|
243
|
+
relaxed = DistillGate.active_recall_hits(@store, query: q, type: allowed, require_all: false)
|
|
244
|
+
.first(RECALL_FALLBACK_LIMIT)
|
|
232
245
|
transcripts = @store.search(query: q, type: "transcript")
|
|
233
246
|
if relaxed.empty? && transcripts.empty?
|
|
234
247
|
return "No chronicled knowledge matches '#{AnnalWriter.one_line(q, 80)}'. Recall is keyword-AND: retry with " \
|
|
@@ -319,13 +332,15 @@ module Rubik
|
|
|
319
332
|
end
|
|
320
333
|
|
|
321
334
|
def handle_subagent(payload)
|
|
322
|
-
# Önce kronik
|
|
323
|
-
#
|
|
324
|
-
# katman LLM katmanının hatasından zarar görmez.
|
|
335
|
+
# Önce kronik, sonra hasat; iki düzlemin hataları birbirini
|
|
336
|
+
# engellemez. Annal yazılamasa da digest tamamlanır/geri kuyruğa alınır.
|
|
325
337
|
seq = next_seq
|
|
326
338
|
line = AnnalWriter.subagent_line(seq, payload)
|
|
327
|
-
|
|
328
|
-
|
|
339
|
+
begin
|
|
340
|
+
append_session([[event_key(payload), line]])
|
|
341
|
+
ensure
|
|
342
|
+
@gate.completion_event(payload)
|
|
343
|
+
end
|
|
329
344
|
maintain if flush_due?
|
|
330
345
|
end
|
|
331
346
|
|
|
@@ -136,10 +136,12 @@ module Rubik
|
|
|
136
136
|
# `storage put type=memory` ile açılmış) satırları disipline
|
|
137
137
|
# sokar — başlıktan slug türet, en yeni satıra mem.<slug> ref'i
|
|
138
138
|
# bağla (bir sonraki record() artık O satırı günceller), aynı
|
|
139
|
-
# slug'a düşen eski kopyaları supersede et.
|
|
139
|
+
# slug'a düşen eski kopyaları supersede et. Mevcut canonical ref
|
|
140
|
+
# her zaman korunur; kaçak kopyalar onun lehine emekli edilir. Deterministik: sıra
|
|
140
141
|
# ve seçim yalnızca [row] üzerinden.
|
|
141
142
|
def sweep_rogue_memory(store)
|
|
142
|
-
|
|
143
|
+
bindings = store.refs.select { |ref, _id| Vocabulary.memory_ref?(ref) }
|
|
144
|
+
bound_ids = bindings.values
|
|
143
145
|
rows = store.list(type: "memory").reject do |s|
|
|
144
146
|
bound_ids.include?(s.id) || s.status == Vocabulary::STATUS_SUPERSEDED
|
|
145
147
|
end
|
|
@@ -147,11 +149,15 @@ module Rubik
|
|
|
147
149
|
|
|
148
150
|
swept = 0
|
|
149
151
|
rows.group_by { |s| Vocabulary.slug(s.title) }.each do |slug, group|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
ref = Vocabulary.memory_ref(slug)
|
|
153
|
+
winner_id = bindings[ref]
|
|
154
|
+
unless winner_id
|
|
155
|
+
winner_id = group.max_by(&:row).id
|
|
156
|
+
store.set_ref(ref, winner_id)
|
|
157
|
+
end
|
|
158
|
+
group.reject { |s| s.id == winner_id }.each do |s|
|
|
153
159
|
rogue = store.get(type: "memory", id: s.id)
|
|
154
|
-
Vakanuvis.supersede_record(store, rogue, by:
|
|
160
|
+
Vakanuvis.supersede_record(store, rogue, by: winner_id) if rogue
|
|
155
161
|
end
|
|
156
162
|
swept += group.size
|
|
157
163
|
end
|
|
@@ -112,8 +112,12 @@ module Rubik
|
|
|
112
112
|
description: "Language or service identifier (e.g., 'ruby', 'python', 'rust', 'go', 'zig', 'c', 'bash')"
|
|
113
113
|
},
|
|
114
114
|
proposed_command: {
|
|
115
|
-
type: "string",
|
|
116
|
-
|
|
115
|
+
oneOf: [{ type: "string" },
|
|
116
|
+
{ type: "object", properties: { executable: { type: "string" },
|
|
117
|
+
args: { type: "array", items: { type: "string" } } },
|
|
118
|
+
required: ["executable", "args"], additionalProperties: false },
|
|
119
|
+
{ type: "array", items: { type: "string" }, minItems: 1 }],
|
|
120
|
+
description: "Replacement command: a legacy string or, for LSP, {executable, args} / argv array. Omit to probe automatically."
|
|
117
121
|
},
|
|
118
122
|
probe_args: {
|
|
119
123
|
type: "array", items: { type: "string" },
|
|
@@ -142,7 +146,14 @@ module Rubik
|
|
|
142
146
|
def execute(args = {})
|
|
143
147
|
target = (args["target"] || args[:target]).to_s.downcase.strip
|
|
144
148
|
lang = (args["language_or_type"] || args[:language_or_type]).to_s.downcase.strip
|
|
145
|
-
|
|
149
|
+
raw_command = args["proposed_command"] || args[:proposed_command]
|
|
150
|
+
if raw_command.is_a?(Hash) || raw_command.is_a?(Array)
|
|
151
|
+
return ToolOutput.error("Structured command argv is supported only for LSP repairs.") unless target == "lsp"
|
|
152
|
+
|
|
153
|
+
proposed_cmd = raw_command
|
|
154
|
+
else
|
|
155
|
+
proposed_cmd = raw_command.to_s.strip
|
|
156
|
+
end
|
|
146
157
|
auto_probe = if args["auto_probe"].nil?
|
|
147
158
|
args[:auto_probe].nil? || args[:auto_probe]
|
|
148
159
|
else
|
|
@@ -152,7 +163,7 @@ module Rubik
|
|
|
152
163
|
return ToolOutput.error("Target and language_or_type must be provided.") if target.empty? || lang.empty?
|
|
153
164
|
|
|
154
165
|
# 1. Automatic Probing if proposed_cmd is not specified or auto_probe is requested
|
|
155
|
-
if proposed_cmd
|
|
166
|
+
if !command_provided?(proposed_cmd) && auto_probe
|
|
156
167
|
candidate = case target
|
|
157
168
|
when "lsp"
|
|
158
169
|
Rubik::Runtime::Discovery.probe_lsp(lang, root_dir: @root_dir)
|
|
@@ -179,7 +190,7 @@ module Rubik
|
|
|
179
190
|
end
|
|
180
191
|
end
|
|
181
192
|
|
|
182
|
-
return ToolOutput.error("No command provided or discovered for #{target} (#{lang}).")
|
|
193
|
+
return ToolOutput.error("No command provided or discovered for #{target} (#{lang}).") unless command_provided?(proposed_cmd)
|
|
183
194
|
|
|
184
195
|
# 2. Pre-flight Verification in Isolated Sub-Context (ctx.extend())
|
|
185
196
|
root_ctx = @runtime_manager&.context || Rubik::Runtime::Context.new
|
|
@@ -313,11 +324,11 @@ module Rubik
|
|
|
313
324
|
end
|
|
314
325
|
|
|
315
326
|
if @config.nil? && ["lsp", "test_runner", "linter"].include?(target)
|
|
316
|
-
return "⚠️ Promoted '#{proposed_cmd}' for #{target} (#{lang}) — no config is attached, " \
|
|
327
|
+
return "⚠️ Promoted '#{display_command(proposed_cmd)}' for #{target} (#{lang}) — no config is attached, " \
|
|
317
328
|
"so the command is active for this session only and was not saved."
|
|
318
329
|
end
|
|
319
330
|
|
|
320
|
-
"✨ Successfully verified and promoted runtime service for #{target} (#{lang}): '#{proposed_cmd}'. " \
|
|
331
|
+
"✨ Successfully verified and promoted runtime service for #{target} (#{lang}): '#{display_command(proposed_cmd)}'. " \
|
|
321
332
|
"Service is now active in the live context."
|
|
322
333
|
end
|
|
323
334
|
|
|
@@ -326,6 +337,22 @@ module Rubik
|
|
|
326
337
|
def verify_binary(target, lang, proposed_cmd, sub_ctx, probe_args: nil)
|
|
327
338
|
Rubik::Runtime::RepairProbe.new(root_dir: @root_dir).verify(target, lang, proposed_cmd, sub_ctx, probe_args: probe_args)
|
|
328
339
|
end
|
|
340
|
+
|
|
341
|
+
def command_provided?(command)
|
|
342
|
+
if command.is_a?(String)
|
|
343
|
+
!command.strip.empty?
|
|
344
|
+
elsif command.is_a?(Hash) || command.is_a?(Array)
|
|
345
|
+
!Rubik::LSP::ServerCommand.argv(command).empty?
|
|
346
|
+
else
|
|
347
|
+
false
|
|
348
|
+
end
|
|
349
|
+
rescue ArgumentError
|
|
350
|
+
false
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def display_command(command)
|
|
354
|
+
command.is_a?(Hash) || command.is_a?(Array) ? Rubik::LSP::ServerCommand.display(command) : command.to_s
|
|
355
|
+
end
|
|
329
356
|
end
|
|
330
357
|
|
|
331
358
|
class FinishTool < BaseTool
|
|
@@ -39,16 +39,32 @@ module Rubik
|
|
|
39
39
|
@lsp_manager.sync_buffer_file_if_needed(path, bm) if bm && @lsp_manager.respond_to?(:sync_buffer_file_if_needed)
|
|
40
40
|
|
|
41
41
|
if path.empty?
|
|
42
|
-
all = @lsp_manager.
|
|
43
|
-
total_issues = all.values.
|
|
42
|
+
all = @lsp_manager.all_diagnostic_snapshots
|
|
43
|
+
total_issues = all.values.sum { |snapshot| snapshot[:diagnostics].size }
|
|
44
|
+
pending = all.values.count { |snapshot| snapshot[:state] == :pending }
|
|
45
|
+
unversioned = all.values.count { |snapshot| snapshot[:state] == :unversioned }
|
|
44
46
|
if total_issues.zero?
|
|
45
|
-
|
|
47
|
+
if pending.positive?
|
|
48
|
+
"LSP analysis pending for #{pending} workspace file(s); no clean result is available yet."
|
|
49
|
+
elsif unversioned.positive?
|
|
50
|
+
"No diagnostics were reported for #{unversioned} workspace file(s), but the server omitted document versions; freshness is unverified."
|
|
51
|
+
elsif all.empty?
|
|
52
|
+
"No active LSP diagnostics have been published for open workspace documents."
|
|
53
|
+
else
|
|
54
|
+
"No LSP diagnostics were reported for #{all.size} analyzed workspace file(s)."
|
|
55
|
+
end
|
|
46
56
|
else
|
|
47
57
|
lines = ["=== Active Workspace LSP Diagnostics (#{total_issues} issue(s)) ==="]
|
|
48
|
-
all.each do |fpath,
|
|
58
|
+
all.each do |fpath, snapshot|
|
|
59
|
+
diags = snapshot[:diagnostics]
|
|
49
60
|
next if diags.empty?
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
freshness = if snapshot[:state] == :pending
|
|
63
|
+
"PENDING; previous results may be stale"
|
|
64
|
+
else
|
|
65
|
+
(snapshot[:state] == :unversioned ? "VERSION UNVERIFIED" : "current")
|
|
66
|
+
end
|
|
67
|
+
lines << "\nFile: #{fpath} (#{diags.size}; #{freshness}):"
|
|
52
68
|
diags.each do |d|
|
|
53
69
|
sev = d["severity"] == 1 ? "ERROR" : "WARN"
|
|
54
70
|
start_line = (d.dig("range", "start", "line") || 0) + 1
|
|
@@ -58,11 +74,23 @@ module Rubik
|
|
|
58
74
|
lines.join("\n")
|
|
59
75
|
end
|
|
60
76
|
else
|
|
61
|
-
|
|
77
|
+
snapshot = @lsp_manager.diagnostics_snapshot_for(path)
|
|
78
|
+
diags = snapshot[:diagnostics]
|
|
62
79
|
if diags.empty?
|
|
63
|
-
|
|
80
|
+
message = case snapshot[:state]
|
|
81
|
+
when :pending then "LSP analysis is pending for '#{path}'; no clean result is available yet."
|
|
82
|
+
when :unversioned then "No LSP diagnostics were reported for '#{path}', but the server omitted document versions; freshness is unverified."
|
|
83
|
+
when :fresh then "No LSP diagnostics or errors for '#{path}'."
|
|
84
|
+
else "No LSP diagnostic result is available for '#{path}'."
|
|
85
|
+
end
|
|
86
|
+
append_buffer_tip(message, path)
|
|
64
87
|
else
|
|
65
|
-
|
|
88
|
+
freshness = if snapshot[:state] == :pending
|
|
89
|
+
"pending; previous results may be stale"
|
|
90
|
+
else
|
|
91
|
+
(snapshot[:state] == :unversioned ? "version unverified" : "current")
|
|
92
|
+
end
|
|
93
|
+
lines = ["=== LSP Diagnostics for #{path} (#{diags.size} issue(s); #{freshness}) ==="]
|
|
66
94
|
diags.each do |d|
|
|
67
95
|
sev = case d["severity"]
|
|
68
96
|
when 1 then "ERROR"
|
|
@@ -41,7 +41,10 @@ module Rubik
|
|
|
41
41
|
when :head_only
|
|
42
42
|
kept_lines = lines.first(max_l)
|
|
43
43
|
kept_str = kept_lines.join
|
|
44
|
-
|
|
44
|
+
if kept_str.bytesize > max_c
|
|
45
|
+
kept_str = kept_str.byteslice(0, max_c)
|
|
46
|
+
kept_str = kept_str.scrub("") unless kept_str.valid_encoding?
|
|
47
|
+
end
|
|
45
48
|
kept_line_count = kept_str.empty? ? 0 : kept_str.lines(sep).size
|
|
46
49
|
omitted_lines = [lines.size - kept_line_count, 0].max
|
|
47
50
|
omitted_chars = [str.bytesize - kept_str.bytesize, 0].max
|
|
@@ -51,7 +54,10 @@ module Rubik
|
|
|
51
54
|
when :tail_only
|
|
52
55
|
kept_lines = lines.last(max_l)
|
|
53
56
|
kept_str = kept_lines.join
|
|
54
|
-
|
|
57
|
+
if kept_str.bytesize > max_c
|
|
58
|
+
kept_str = kept_str.byteslice(-max_c, max_c)
|
|
59
|
+
kept_str = kept_str.scrub("") unless kept_str.valid_encoding?
|
|
60
|
+
end
|
|
55
61
|
kept_line_count = kept_str.empty? ? 0 : kept_str.lines(sep).size
|
|
56
62
|
omitted_lines = [lines.size - kept_line_count, 0].max
|
|
57
63
|
omitted_chars = [str.bytesize - kept_str.bytesize, 0].max
|
|
@@ -62,7 +68,10 @@ module Rubik
|
|
|
62
68
|
if max_l <= 1
|
|
63
69
|
kept_lines = lines.first(max_l)
|
|
64
70
|
kept_str = kept_lines.join
|
|
65
|
-
|
|
71
|
+
if kept_str.bytesize > max_c
|
|
72
|
+
kept_str = kept_str.byteslice(0, max_c)
|
|
73
|
+
kept_str = kept_str.scrub("") unless kept_str.valid_encoding?
|
|
74
|
+
end
|
|
66
75
|
kept_line_count = kept_str.empty? ? 0 : kept_str.lines(sep).size
|
|
67
76
|
omitted_lines = [lines.size - kept_line_count, 0].max
|
|
68
77
|
omitted_chars = [str.bytesize - kept_str.bytesize, 0].max
|
|
@@ -93,8 +102,14 @@ module Rubik
|
|
|
93
102
|
head_str = head_lines.join
|
|
94
103
|
tail_str = tail_lines.join
|
|
95
104
|
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
if head_str.bytesize > head_chars
|
|
106
|
+
head_str = head_str.byteslice(0, head_chars)
|
|
107
|
+
head_str = head_str.scrub("") unless head_str.valid_encoding?
|
|
108
|
+
end
|
|
109
|
+
if tail_str.bytesize > tail_chars
|
|
110
|
+
tail_str = tail_str.byteslice(-tail_chars, tail_chars)
|
|
111
|
+
tail_str = tail_str.scrub("") unless tail_str.valid_encoding?
|
|
112
|
+
end
|
|
98
113
|
|
|
99
114
|
omitted_lines = if lines.size == 1
|
|
100
115
|
0
|
|
@@ -106,9 +121,7 @@ module Rubik
|
|
|
106
121
|
omitted_chars = [str.bytesize - (head_str.bytesize + tail_str.bytesize), 0].max
|
|
107
122
|
notice = "\n\n... [Truncated: #{omitted_lines} lines / #{omitted_chars} chars omitted to preserve token budget] ...\n\n"
|
|
108
123
|
|
|
109
|
-
|
|
110
|
-
tail_final = tail_str.valid_encoding? ? tail_str : tail_str.scrub("")
|
|
111
|
-
{ text: "#{head_final}#{notice}#{tail_final}", truncated: true,
|
|
124
|
+
{ text: "#{head_str}#{notice}#{tail_str}", truncated: true,
|
|
112
125
|
omitted_lines: omitted_lines, omitted_chars: omitted_chars }
|
|
113
126
|
end
|
|
114
127
|
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Özgür Ulukır
|
|
@@ -322,6 +322,7 @@ files:
|
|
|
322
322
|
- lib/rubik/lsp.rb
|
|
323
323
|
- lib/rubik/lsp/client.rb
|
|
324
324
|
- lib/rubik/lsp/manager.rb
|
|
325
|
+
- lib/rubik/lsp/server_command.rb
|
|
325
326
|
- lib/rubik/mcp.rb
|
|
326
327
|
- lib/rubik/mcp/boot_lifecycle.rb
|
|
327
328
|
- lib/rubik/mcp/client.rb
|