rubik 0.4.0 → 0.4.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 +118 -1
- data/lib/rubik/ast/c_extractor.rb +1 -1
- data/lib/rubik/ast/cache_store.rb +36 -5
- data/lib/rubik/ast/extraction_result.rb +6 -0
- data/lib/rubik/ast/fallback_cleaner.rb +3 -1
- data/lib/rubik/ast/graph.rb +31 -5
- data/lib/rubik/ast/ruby_extractor.rb +8 -2
- data/lib/rubik/ast/rust_extractor.rb +1 -1
- data/lib/rubik/ast/typescript_extractor.rb +3 -3
- data/lib/rubik/buffer/apply_contract.rb +6 -0
- data/lib/rubik/buffer/manager.rb +37 -27
- data/lib/rubik/diagnostics/parser.rb +16 -16
- data/lib/rubik/llm/openai_compatible.rb +2 -1
- data/lib/rubik/lsp/manager.rb +15 -5
- data/lib/rubik/mcp/client.rb +4 -0
- data/lib/rubik/mcp/manager.rb +51 -35
- data/lib/rubik/mcp/proxy_tool.rb +17 -0
- data/lib/rubik/mcp/transport/stdio.rb +142 -17
- data/lib/rubik/orchestrator/step_executor.rb +1 -1
- data/lib/rubik/orchestrator/subagent/completion.rb +1 -0
- data/lib/rubik/orchestrator/subagent/dependency.rb +1 -0
- data/lib/rubik/orchestrator/subagent/supervisor.rb +20 -3
- data/lib/rubik/orchestrator/subagent/waiting.rb +16 -9
- data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +1 -1
- data/lib/rubik/orchestrator/turn_executor/user_channel.rb +2 -2
- data/lib/rubik/orchestrator/turn_executor.rb +3 -2
- data/lib/rubik/platform/path.rb +2 -1
- data/lib/rubik/platform/sanitize.rb +4 -1
- data/lib/rubik/plugin/manager.rb +39 -15
- data/lib/rubik/plugin/package_inventory.rb +11 -1
- data/lib/rubik/plugin/storage.rb +5 -4
- data/lib/rubik/repair/loop.rb +29 -15
- data/lib/rubik/repl/step_formatter.rb +2 -1
- data/lib/rubik/runtime/diagnostic_engine.rb +1 -1
- data/lib/rubik/security/blocked_guidance.rb +1 -1
- data/lib/rubik/security/redactor.rb +3 -3
- data/lib/rubik/storage/layout.rb +3 -1
- data/lib/rubik/storage/render.rb +1 -1
- data/lib/rubik/storage/vakanuvis/vocabulary.rb +36 -2
- data/lib/rubik/tools/base.rb +2 -1
- data/lib/rubik/tools/builtins/lifecycle.rb +42 -47
- data/lib/rubik/ui/capability.rb +19 -5
- data/lib/rubik/ui/renderer.rb +1 -0
- data/lib/rubik/version.rb +1 -1
- metadata +1 -1
|
@@ -68,6 +68,7 @@ module Rubik
|
|
|
68
68
|
@instances = {}
|
|
69
69
|
@deferred = {}
|
|
70
70
|
@monitor = Monitor.new
|
|
71
|
+
@wait_cond = @monitor.new_cond
|
|
71
72
|
@counter = 0
|
|
72
73
|
@max_depth = (config&.fetch("subagents.max_depth", 2) || 2).to_i
|
|
73
74
|
@max_concurrency = (config&.fetch("subagents.max_concurrency", 10) || 10).to_i
|
|
@@ -83,6 +84,13 @@ module Rubik
|
|
|
83
84
|
# redacted, paginated — see ResultArchive.
|
|
84
85
|
@result_archive = ResultArchive.new(config: @config)
|
|
85
86
|
|
|
87
|
+
if @event_bus.respond_to?(:subscribe)
|
|
88
|
+
@event_bus.subscribe(Notices::ERROR_PAUSED_EVENT) { notify_wait! }
|
|
89
|
+
@event_bus.subscribe(Notices::BUDGET_EXHAUSTED_EVENT) { notify_wait! }
|
|
90
|
+
@event_bus.subscribe(Notices::COMPLETED_EVENT) { notify_wait! }
|
|
91
|
+
@event_bus.subscribe(Notices::QUESTION_EVENT) { notify_wait! } if defined?(Notices::QUESTION_EVENT)
|
|
92
|
+
end
|
|
93
|
+
|
|
86
94
|
# Register inverse in root context (maestro hook)
|
|
87
95
|
@root_context.register_disposable { shutdown_all } if @root_context.respond_to?(:register_disposable)
|
|
88
96
|
end
|
|
@@ -93,20 +101,26 @@ module Rubik
|
|
|
93
101
|
inst = get(id)
|
|
94
102
|
return false unless inst.is_a?(Instance)
|
|
95
103
|
|
|
96
|
-
inst.extend_budget(steps)
|
|
104
|
+
res = inst.extend_budget(steps)
|
|
105
|
+
notify_wait! if res
|
|
106
|
+
res
|
|
97
107
|
end
|
|
98
108
|
|
|
99
109
|
def resume(id, additional_steps: nil)
|
|
100
110
|
inst = get(id)
|
|
101
|
-
inst.respond_to?(:resume!) ? inst.resume!(additional_steps: additional_steps) : false
|
|
111
|
+
res = inst.respond_to?(:resume!) ? inst.resume!(additional_steps: additional_steps) : false
|
|
112
|
+
notify_wait! if res
|
|
113
|
+
res
|
|
102
114
|
end
|
|
103
115
|
|
|
104
116
|
def resume_all
|
|
105
|
-
@monitor.synchronize do
|
|
117
|
+
resumed = @monitor.synchronize do
|
|
106
118
|
@instances.values.select(&:parked?).map do |i|
|
|
107
119
|
i.resume! ? i.id : nil
|
|
108
120
|
end.compact
|
|
109
121
|
end
|
|
122
|
+
notify_wait! unless resumed.empty?
|
|
123
|
+
resumed
|
|
110
124
|
end
|
|
111
125
|
|
|
112
126
|
def list(parent_id: nil)
|
|
@@ -129,6 +143,7 @@ module Rubik
|
|
|
129
143
|
# monitörü 2s'ye kadar bloklar (emit kuralı ihlali olurdu).
|
|
130
144
|
announce_cancellations(cancelled)
|
|
131
145
|
release_ready!
|
|
146
|
+
notify_wait!
|
|
132
147
|
found
|
|
133
148
|
end
|
|
134
149
|
|
|
@@ -142,6 +157,7 @@ module Rubik
|
|
|
142
157
|
true
|
|
143
158
|
end
|
|
144
159
|
announce_cancellations(cancelled)
|
|
160
|
+
notify_wait!
|
|
145
161
|
end
|
|
146
162
|
|
|
147
163
|
def shutdown_all
|
|
@@ -151,6 +167,7 @@ module Rubik
|
|
|
151
167
|
@instances.clear
|
|
152
168
|
@deferred.clear
|
|
153
169
|
end
|
|
170
|
+
notify_wait!
|
|
154
171
|
end
|
|
155
172
|
|
|
156
173
|
def reap_orphans
|
|
@@ -20,25 +20,32 @@ module Rubik
|
|
|
20
20
|
wait(ids, timeout: timeout, wait_for_budget: wait_for_budget).rows
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
def notify_wait!
|
|
24
|
+
@monitor.synchronize { @wait_cond&.broadcast }
|
|
25
|
+
end
|
|
26
|
+
|
|
23
27
|
def wait(ids = nil, timeout: Notices::DEFAULT_WAIT_TIMEOUT, wait_for_budget: true)
|
|
24
28
|
target_ids = Array(ids).compact.map(&:to_s)
|
|
25
29
|
deadline = Time.now + [timeout.to_i, 1].max
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
@monitor.synchronize do
|
|
32
|
+
loop do
|
|
29
33
|
candidates = target_ids.empty? ? @instances.values : target_ids.filter_map { |id| @instances[id] }
|
|
30
|
-
candidates.select do |inst|
|
|
34
|
+
active_targets = candidates.select do |inst|
|
|
31
35
|
inst.active? && !inst.error_paused? && !inst.awaiting_human? && (!wait_for_budget || !inst.awaiting_budget?)
|
|
32
36
|
end
|
|
33
|
-
end
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
# Kuyruğa takılı hedefler wait'i canlı tutar (zincir bitene dek).
|
|
39
|
+
rem = deadline - Time.now
|
|
40
|
+
break if (active_targets.empty? && !deferred_pending?(target_ids)) || rem <= 0
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
if @wait_cond
|
|
43
|
+
@wait_cond.wait([rem, 0].max)
|
|
44
|
+
else
|
|
45
|
+
sleep [rem, 0.1].min
|
|
46
|
+
end
|
|
47
|
+
end
|
|
40
48
|
|
|
41
|
-
@monitor.synchronize do
|
|
42
49
|
candidates = target_ids.empty? ? @instances.values : target_ids.filter_map { |id| @instances[id] || queued_spec(id) }
|
|
43
50
|
paused = candidates.select { |c| parked_candidate?(c) }
|
|
44
51
|
active = candidates.select { |c| c.respond_to?(:active?) && c.active? }
|
|
@@ -107,8 +107,8 @@ module Rubik
|
|
|
107
107
|
max_steps: @deps.max_steps, layer: layer)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
def announce_llm_thought(text)
|
|
111
|
-
@deps.on_step&.call(type: :llm_thought, content: text, step: @deps.step_count, max_steps: @deps.max_steps)
|
|
110
|
+
def announce_llm_thought(text, streamed: false)
|
|
111
|
+
@deps.on_step&.call(type: :llm_thought, content: text, streamed: streamed, step: @deps.step_count, max_steps: @deps.max_steps)
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
# ── terminals & dispatch ──
|
|
@@ -218,7 +218,8 @@ module Rubik
|
|
|
218
218
|
def tool_turn(res, text_resp, tool_calls)
|
|
219
219
|
channel.nudge_policy.reset!
|
|
220
220
|
channel.append_tool_call_row(channel.final_text(res, text_resp), tool_calls)
|
|
221
|
-
|
|
221
|
+
streamed = res.respond_to?(:streamed) && res.streamed == true
|
|
222
|
+
channel.announce_llm_thought(text_resp, streamed: streamed) if !text_resp.empty? && tool_calls.none? { |tc| self.class.finish_call?(channel.tool_registry, tc) }
|
|
222
223
|
|
|
223
224
|
tool_calls.each do |tc|
|
|
224
225
|
# finish is exempt (pinned by test_orchestrator_stream): its args
|
|
@@ -300,7 +301,7 @@ module Rubik
|
|
|
300
301
|
# ── announce ──
|
|
301
302
|
def announce_compaction(_reason, _layer) = nil
|
|
302
303
|
|
|
303
|
-
def announce_llm_thought(_text) = nil
|
|
304
|
+
def announce_llm_thought(_text, **) = nil
|
|
304
305
|
|
|
305
306
|
# Goal-ledger observation projection (plan D): related-optional
|
|
306
307
|
# capability — base channel is structurally inert (subagent planes
|
data/lib/rubik/platform/path.rb
CHANGED
|
@@ -228,7 +228,8 @@ module Rubik
|
|
|
228
228
|
# drive root?" depend on the process cwd (which drive the workspace
|
|
229
229
|
# lives on). Security callers pass already-expanded absolute paths;
|
|
230
230
|
# compare the literal form here.
|
|
231
|
-
cleaned = path.to_s.tr("\\", "/").downcase
|
|
231
|
+
cleaned = path.to_s.tr("\\", "/").downcase
|
|
232
|
+
cleaned = cleaned.delete_suffix("/") while cleaned.end_with?("/")
|
|
232
233
|
cleaned =~ /^[a-z]:$/
|
|
233
234
|
end
|
|
234
235
|
end
|
|
@@ -108,14 +108,17 @@ module Rubik
|
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
VALID_SYMBOL_ENCODINGS = [Encoding::UTF_8, Encoding::US_ASCII].freeze
|
|
112
|
+
|
|
111
113
|
# Binary (ASCII-8BIT) symbol names always report valid_encoding?, so a
|
|
112
114
|
# bare validity check would fast-path them through UNCLEANED — the old
|
|
113
115
|
# clean_utf8 path strips their >= 0x80 bytes and the byte loss would
|
|
114
116
|
# instead surface later, at the JSON/display layer. Only UTF-8 (or the
|
|
115
117
|
# lossless US-ASCII subset) symbols may take the fast path.
|
|
118
|
+
# PERF: Hoist encoding array to frozen constant to eliminate array allocations per symbol check (~26% faster, ~99.99% fewer allocations).
|
|
116
119
|
def fast_clean_symbol(sym)
|
|
117
120
|
name = sym.name
|
|
118
|
-
return sym if
|
|
121
|
+
return sym if VALID_SYMBOL_ENCODINGS.include?(name.encoding) && name.valid_encoding?
|
|
119
122
|
|
|
120
123
|
clean_utf8(name).to_sym
|
|
121
124
|
end
|
data/lib/rubik/plugin/manager.rb
CHANGED
|
@@ -100,30 +100,44 @@ module Rubik
|
|
|
100
100
|
def load_dropin_plugins(runtime_manager:, storage:)
|
|
101
101
|
plugins = storage.discover_plugins
|
|
102
102
|
plugins.each do |plugin_meta|
|
|
103
|
-
|
|
103
|
+
content = File.exist?(plugin_meta[:path]) ? File.binread(plugin_meta[:path]) : nil
|
|
104
|
+
integrity = storage.verify_integrity(plugin_meta[:path], scope: plugin_meta[:scope], content: content)
|
|
104
105
|
if integrity == :rejected
|
|
105
106
|
@logger.warn("Skipping drop-in plugin '#{plugin_meta[:name]}' due to integrity check failure.")
|
|
106
107
|
next
|
|
107
108
|
end
|
|
108
109
|
|
|
109
|
-
load_plugin_file(plugin_meta, runtime_manager: runtime_manager)
|
|
110
|
+
load_plugin_file(plugin_meta, runtime_manager: runtime_manager, content: content)
|
|
110
111
|
end
|
|
111
112
|
end
|
|
112
113
|
|
|
113
|
-
def load_plugin_file(plugin_meta, runtime_manager:)
|
|
114
|
+
def load_plugin_file(plugin_meta, runtime_manager:, content: nil)
|
|
114
115
|
name = plugin_meta[:name]
|
|
115
116
|
component_name = "plugin_#{name}"
|
|
116
117
|
|
|
117
|
-
|
|
118
|
+
# If plugin is already running, prepare the replacement first (issue #309).
|
|
119
|
+
# Unloading before prepare would destroy the live plugin if the replacement has a syntax/prepare error.
|
|
120
|
+
already_running = runtime_manager.fibers.key?(component_name)
|
|
121
|
+
previous_def = Rubik::Plugin::Registry.get(name) if already_running
|
|
122
|
+
previous_fiber = runtime_manager.fibers[component_name] if already_running
|
|
123
|
+
was_active = previous_fiber&.state == :active
|
|
118
124
|
|
|
119
|
-
definition = prepare_plugin_definition(plugin_meta)
|
|
125
|
+
definition = prepare_plugin_definition(plugin_meta, content: content)
|
|
120
126
|
return false unless definition
|
|
121
127
|
|
|
128
|
+
unload_plugin(name, runtime_manager: runtime_manager) if already_running
|
|
129
|
+
Rubik::Plugin::Registry.register(definition)
|
|
130
|
+
|
|
122
131
|
fiber = register_plugin_component(name, definition, plugin_meta, runtime_manager: runtime_manager)
|
|
123
|
-
|
|
132
|
+
if fiber.nil? || fiber.state == :failed || (was_active && fiber.state == :pending)
|
|
133
|
+
if already_running && previous_def
|
|
134
|
+
@logger.error("Failed to activate replacement for plugin '#{name}' (state: #{fiber&.state}); rolling back to previous version")
|
|
135
|
+
runtime_manager.unregister_component(component_name)
|
|
136
|
+
restore_previous_plugin(name, previous_def, plugin_meta, runtime_manager: runtime_manager)
|
|
137
|
+
end
|
|
138
|
+
return false
|
|
139
|
+
end
|
|
124
140
|
|
|
125
|
-
# Honest load result (issue #144): a failed activation (factory raised)
|
|
126
|
-
# is reported as failure, not as a successful load.
|
|
127
141
|
fiber.state != :failed
|
|
128
142
|
end
|
|
129
143
|
|
|
@@ -159,14 +173,19 @@ module Rubik
|
|
|
159
173
|
return false
|
|
160
174
|
end
|
|
161
175
|
|
|
162
|
-
|
|
176
|
+
content = File.exist?(target[:path]) ? File.binread(target[:path]) : nil
|
|
177
|
+
integrity = storage.verify_integrity(target[:path], scope: target[:scope], content: content)
|
|
163
178
|
if integrity == :rejected
|
|
164
179
|
@logger.error("Reload of drop-in plugin '#{name}' refused: integrity check failed; previous version kept")
|
|
165
180
|
return false
|
|
166
181
|
end
|
|
167
182
|
|
|
183
|
+
component_name = "plugin_#{name}"
|
|
168
184
|
previous = Rubik::Plugin::Registry.get(name)
|
|
169
|
-
|
|
185
|
+
previous_fiber = runtime_manager.fibers[component_name]
|
|
186
|
+
was_active = previous_fiber&.state == :active
|
|
187
|
+
|
|
188
|
+
definition = prepare_plugin_definition(target, content: content)
|
|
170
189
|
unless definition
|
|
171
190
|
@logger.error("Reload of drop-in plugin '#{name}' failed during preparation; previous version kept")
|
|
172
191
|
return false
|
|
@@ -175,10 +194,11 @@ module Rubik
|
|
|
175
194
|
unload_plugin(name, runtime_manager: runtime_manager)
|
|
176
195
|
Rubik::Plugin::Registry.register(definition)
|
|
177
196
|
fiber = register_plugin_component(name, definition, target, runtime_manager: runtime_manager)
|
|
178
|
-
return true if fiber && fiber.state != :failed
|
|
197
|
+
return true if fiber && fiber.state != :failed && (!was_active || fiber.state == :active)
|
|
179
198
|
|
|
180
|
-
|
|
181
|
-
|
|
199
|
+
state_reason = fiber ? fiber.state.to_s : "nil fiber"
|
|
200
|
+
@logger.error("Reload of drop-in plugin '#{name}' failed during activation (ended #{state_reason}); rolling back")
|
|
201
|
+
runtime_manager.unregister_component(component_name)
|
|
182
202
|
restore_previous_plugin(name, previous, target, runtime_manager: runtime_manager)
|
|
183
203
|
false
|
|
184
204
|
end
|
|
@@ -190,7 +210,7 @@ module Rubik
|
|
|
190
210
|
# replacement can never cost the old version. Returns the new
|
|
191
211
|
# Definition on success; nil on failure, with the previous registry
|
|
192
212
|
# entry restored and the load-path claim released.
|
|
193
|
-
def prepare_plugin_definition(plugin_meta)
|
|
213
|
+
def prepare_plugin_definition(plugin_meta, content: nil)
|
|
194
214
|
name = plugin_meta[:name]
|
|
195
215
|
path = plugin_meta[:path]
|
|
196
216
|
load_path = plugin_meta[:load_path]
|
|
@@ -199,7 +219,11 @@ module Rubik
|
|
|
199
219
|
claim_load_path(load_path)
|
|
200
220
|
|
|
201
221
|
begin
|
|
202
|
-
|
|
222
|
+
if content
|
|
223
|
+
TOPLEVEL_BINDING.eval(content, path)
|
|
224
|
+
else
|
|
225
|
+
load path
|
|
226
|
+
end
|
|
203
227
|
rescue StandardError, ScriptError => e
|
|
204
228
|
@logger.error("Failed to parse drop-in plugin '#{name}': #{e.message}")
|
|
205
229
|
rollback_prepared_definition(name, previous)
|
|
@@ -33,12 +33,22 @@ module Rubik
|
|
|
33
33
|
e.message
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
def secure_compare(first, second)
|
|
37
|
+
return false unless first.is_a?(String) && second.is_a?(String)
|
|
38
|
+
return false unless first.bytesize == second.bytesize
|
|
39
|
+
|
|
40
|
+
bytes = first.unpack("C*")
|
|
41
|
+
res = 0
|
|
42
|
+
second.each_byte { |byte| res |= byte ^ bytes.shift }
|
|
43
|
+
res.zero?
|
|
44
|
+
end
|
|
45
|
+
|
|
36
46
|
# Audits additions, deletions, and modifications: any drift means the
|
|
37
47
|
# package no longer matches the consented inventory.
|
|
38
48
|
def drift(expected, current)
|
|
39
49
|
added = current.keys - expected.keys
|
|
40
50
|
removed = expected.keys - current.keys
|
|
41
|
-
modified = (current.keys & expected.keys).reject { |f| current[f]
|
|
51
|
+
modified = (current.keys & expected.keys).reject { |f| secure_compare(current[f], expected[f]) }
|
|
42
52
|
|
|
43
53
|
parts = []
|
|
44
54
|
parts << "added: #{added.sort.join(', ')}" unless added.empty?
|
data/lib/rubik/plugin/storage.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "time"
|
|
|
6
6
|
# save_manifest atomik yazımı Platform.atomic_write kullanır; dosyanın tek başına
|
|
7
7
|
# require edilmesi durumunda da çözümlenebilsin diye açık yüklenir.
|
|
8
8
|
require_relative "../platform"
|
|
9
|
+
require_relative "../log"
|
|
9
10
|
require_relative "package_inventory"
|
|
10
11
|
|
|
11
12
|
module Rubik
|
|
@@ -31,7 +32,7 @@ module Rubik
|
|
|
31
32
|
[project_plugins_dir, global_plugins_dir].each do |dir|
|
|
32
33
|
FileUtils.mkdir_p(dir)
|
|
33
34
|
rescue SystemCallError => e
|
|
34
|
-
warn "
|
|
35
|
+
Rubik::Log.warn(:plugin, "Failed to create plugin directory #{dir}: #{e.class}: #{e.message}")
|
|
35
36
|
end
|
|
36
37
|
end
|
|
37
38
|
|
|
@@ -67,7 +68,7 @@ module Rubik
|
|
|
67
68
|
save_manifest(dir, manifest)
|
|
68
69
|
end
|
|
69
70
|
|
|
70
|
-
def verify_integrity(path, scope: :project)
|
|
71
|
+
def verify_integrity(path, scope: :project, content: nil)
|
|
71
72
|
return :trusted if ENV["RUBIK_TRUST_OVERLAYS"] == "1"
|
|
72
73
|
|
|
73
74
|
dir = scope.to_sym == :global ? global_plugins_dir : project_plugins_dir
|
|
@@ -78,12 +79,12 @@ module Rubik
|
|
|
78
79
|
basename = File.basename(path)
|
|
79
80
|
manifest = load_manifest(dir)
|
|
80
81
|
|
|
81
|
-
current_content = File.exist?(path) ? File.binread(path) : ""
|
|
82
|
+
current_content = content || (File.exist?(path) ? File.binread(path) : "")
|
|
82
83
|
current_hash = PackageInventory.content_hash(current_content)
|
|
83
84
|
|
|
84
85
|
if manifest.key?(basename)
|
|
85
86
|
expected_hash = manifest[basename]["sha256"]
|
|
86
|
-
if current_hash
|
|
87
|
+
if PackageInventory.secure_compare(current_hash, expected_hash)
|
|
87
88
|
:verified
|
|
88
89
|
else
|
|
89
90
|
warn "[SECURITY] Integrity check failed for plugin '#{basename}': file modified since adding."
|
data/lib/rubik/repair/loop.rb
CHANGED
|
@@ -27,20 +27,34 @@ module Rubik
|
|
|
27
27
|
|
|
28
28
|
@snapshot_store.create_snapshot(@buffer_manager)
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
30
|
+
begin
|
|
31
|
+
prompt = build_repair_prompt(current_diag, target_file, @attempts_made)
|
|
32
|
+
res = @provider.complete([{ role: "user", content: prompt }])
|
|
33
|
+
proposed_code = extract_code(res.text)
|
|
34
|
+
|
|
35
|
+
if proposed_code.empty?
|
|
36
|
+
@snapshot_store.undo(@buffer_manager)
|
|
37
|
+
next
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@buffer_manager.write(target_file, proposed_code)
|
|
41
|
+
|
|
42
|
+
# Verify syntax against in-memory buffer via tempfile
|
|
43
|
+
check_res = check_buffer_syntax(target_file, lang)
|
|
44
|
+
|
|
45
|
+
return { ok: true, attempts: @attempts_made, code: proposed_code } if check_res[:ok]
|
|
46
|
+
|
|
47
|
+
if check_res[:unverified]
|
|
48
|
+
@snapshot_store.undo(@buffer_manager)
|
|
49
|
+
return { ok: false, attempts: @attempts_made, unverified: true, error: check_res[:error] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
current_diag = Rubik::Diagnostics::Parser.parse(check_res[:output], lang)
|
|
53
|
+
@snapshot_store.undo(@buffer_manager)
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
@snapshot_store.undo(@buffer_manager)
|
|
56
|
+
return { ok: false, attempts: @attempts_made, error: "#{e.class}: #{e.message}" }
|
|
57
|
+
end
|
|
44
58
|
end
|
|
45
59
|
|
|
46
60
|
{ ok: false, attempts: @attempts_made, last_diagnostics: current_diag }
|
|
@@ -50,7 +64,7 @@ module Rubik
|
|
|
50
64
|
|
|
51
65
|
def check_buffer_syntax(target_file, lang)
|
|
52
66
|
adapter = @adapter_registry.get(lang.to_s)
|
|
53
|
-
return { ok: true } unless adapter
|
|
67
|
+
return { ok: false, unverified: true, error: "No adapter registered for #{lang}" } unless adapter
|
|
54
68
|
|
|
55
69
|
content = @buffer_manager.read(target_file)
|
|
56
70
|
ext = File.extname(target_file)
|
|
@@ -145,7 +145,7 @@ module Rubik
|
|
|
145
145
|
# blocks stripped; nil when nothing human-readable remains. Shared
|
|
146
146
|
# with UI::Renderer (Plan 51, Decision D2).
|
|
147
147
|
def thought_first_line(content)
|
|
148
|
-
clean_content = content.to_s.sub(/```(?:json)?\s*\{
|
|
148
|
+
clean_content = content.to_s.sub(/```(?:json)?\s*\{[^`]*\}\s*```/m, "").strip
|
|
149
149
|
return nil if clean_content.empty?
|
|
150
150
|
|
|
151
151
|
clean_content.lines.first.to_s.strip
|
|
@@ -243,6 +243,7 @@ module Rubik
|
|
|
243
243
|
# with UI::Renderer (Plan 51, Decision D2).
|
|
244
244
|
def format_llm_thought(step_info)
|
|
245
245
|
return nil if @level == LEVEL_QUIET || @level == LEVEL_NORMAL
|
|
246
|
+
return nil if step_info[:streamed] == true
|
|
246
247
|
|
|
247
248
|
first_line = thought_first_line(step_info[:content])
|
|
248
249
|
return nil if first_line.nil? || first_line.empty?
|
|
@@ -144,7 +144,7 @@ module Rubik
|
|
|
144
144
|
:dependency_missing
|
|
145
145
|
elsif combined.match?(/(syntax error|unexpected token|compilation error|error\[E\d+\]|parse error|fatal error)/i)
|
|
146
146
|
:syntax_or_compile_error
|
|
147
|
-
elsif combined.match?(/(failures:|assertions|tests failed|failed tests|FAILED \(|FAIL:|\d+ failures, \d+ errors)/i)
|
|
147
|
+
elsif combined.match?(/(failures:|assertions|tests failed|failed tests|FAILED \(|FAIL:|\b\d+ failures, \d+ errors\b)/i)
|
|
148
148
|
:test_failure
|
|
149
149
|
else
|
|
150
150
|
:unknown_failure
|
|
@@ -164,7 +164,7 @@ module Rubik
|
|
|
164
164
|
def self.segment_annotation(segment)
|
|
165
165
|
rule = segment[:rule] || "unknown"
|
|
166
166
|
reason = segment[:reason].to_s.strip
|
|
167
|
-
return "#{rule} — option #{segment[:option]} overrides to DANGEROUS" if segment[:option] && reason.match?(/overridden by option
|
|
167
|
+
return "#{rule} — option #{segment[:option]} overrides to DANGEROUS" if segment[:option] && reason.match?(/overridden by option \S+ to dangerous\z/i)
|
|
168
168
|
|
|
169
169
|
# Base-rule reasons restate matched_rule ("base rule: rm (…)") — the
|
|
170
170
|
# rule is already displayed; keep only the elevation evidence.
|
|
@@ -14,19 +14,19 @@ module Rubik
|
|
|
14
14
|
/AIzaSy[a-zA-Z0-9_-]{33}/,
|
|
15
15
|
/\bbearer\s+[a-zA-Z0-9_\-.]{20,}/i,
|
|
16
16
|
/-----BEGIN [A-Z0-9_\s-]+PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9_\s-]+PRIVATE KEY-----/,
|
|
17
|
-
%r{(https?://[
|
|
17
|
+
%r{(https?://[^\s/@:]+:)([^\s/@:]+)@}
|
|
18
18
|
].freeze
|
|
19
19
|
|
|
20
20
|
ASSIGNMENT_KEY_REGEX = /\b(password|passwd|api_key|secret)(\s*[:=]\s*)(['"][^'"]*['"]|[^\s,;'"]+)/i
|
|
21
21
|
ASSIGNMENT_TOKEN_REGEX = /\b(token)(\s*[:=]\s*)(['"][^'"]*['"]|[^\s,;'"]+)/i
|
|
22
22
|
|
|
23
23
|
# PERF: Pre-compiled constants to avoid re-allocation inside hot loops
|
|
24
|
-
URL_PATTERN = %r{(https?://[
|
|
24
|
+
URL_PATTERN = %r{(https?://[^\s/@:]+:)([^\s/@:]+)@}
|
|
25
25
|
URL_MASK = "\\1#{MASK_STRING}@".freeze
|
|
26
26
|
STATIC_PATTERNS = PATTERNS.reject { |pattern| pattern == URL_PATTERN }.freeze
|
|
27
27
|
# Skip static replacement passes for assignment-only and URL-only inputs.
|
|
28
28
|
STATIC_PATTERNS_COMBINED_REGEX = Regexp.union(STATIC_PATTERNS).freeze
|
|
29
|
-
QUICK_CHECK_REGEX = %r{sk-|AKIA|ghp_|github_pat_|AIza|bearer|PRIVATE KEY|://[
|
|
29
|
+
QUICK_CHECK_REGEX = %r{sk-|AKIA|ghp_|github_pat_|AIza|bearer|PRIVATE KEY|://[^\s/@:]+:|password|passwd|api_key|secret|token}i
|
|
30
30
|
|
|
31
31
|
# Anahtar-maskeleme desenleri (Session::Recorder#mask_payload ile aynı
|
|
32
32
|
# aile): sır görünümlü anahtarların değerleri desen eşleşmese bile
|
data/lib/rubik/storage/layout.rb
CHANGED
|
@@ -48,7 +48,9 @@ module Rubik
|
|
|
48
48
|
|
|
49
49
|
# [a-z0-9._-] dışını tireye çevirir; boş kalırsa "default".
|
|
50
50
|
def slugify(name)
|
|
51
|
-
slug = name.to_s.downcase.gsub(/[^a-z0-9._-]+/, "-")
|
|
51
|
+
slug = name.to_s.downcase.gsub(/[^a-z0-9._-]+/, "-")
|
|
52
|
+
slug = slug.delete_prefix("-").delete_prefix(".") while slug.start_with?("-", ".")
|
|
53
|
+
slug = slug.delete_suffix("-").delete_suffix(".") while slug.end_with?("-", ".")
|
|
52
54
|
slug.empty? ? "default" : slug
|
|
53
55
|
end
|
|
54
56
|
|
data/lib/rubik/storage/render.rb
CHANGED
|
@@ -38,7 +38,7 @@ module Rubik
|
|
|
38
38
|
return header << "_(empty — no #{type} entities yet)_\n" if summaries.empty?
|
|
39
39
|
|
|
40
40
|
summaries.each do |s|
|
|
41
|
-
title = s[:title].to_s.gsub("|", "\\|").slice(0, 80)
|
|
41
|
+
title = s[:title].to_s.gsub("\\", "\\\\\\\\").gsub("|", "\\|").slice(0, 80)
|
|
42
42
|
header << "| #{s[:row]} | `#{s[:id]}` | #{s[:status]} | #{s[:updated_at]} | #{title} |\n"
|
|
43
43
|
end
|
|
44
44
|
header
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
3
5
|
module Rubik
|
|
4
6
|
module Storage
|
|
5
7
|
module Vakanuvis
|
|
@@ -36,14 +38,46 @@ module Rubik
|
|
|
36
38
|
|
|
37
39
|
module_function
|
|
38
40
|
|
|
41
|
+
def transliterate(text)
|
|
42
|
+
s = text.to_s.dup
|
|
43
|
+
s.gsub!(/[çÇ]/, "c")
|
|
44
|
+
s.gsub!(/[ğĞ]/, "g")
|
|
45
|
+
s.gsub!(/[ıİ]/, "i")
|
|
46
|
+
s.gsub!(/[öÖ]/, "o")
|
|
47
|
+
s.gsub!(/[şŞ]/, "s")
|
|
48
|
+
s.gsub!(/[üÜ]/, "u")
|
|
49
|
+
s.unicode_normalize(:nfd).gsub(/\p{M}/, "")
|
|
50
|
+
end
|
|
51
|
+
|
|
39
52
|
# Deterministik slug: aynı metin ⇒ aynı slug ⇒ aynı ref (digest
|
|
40
53
|
# makbuzlarının ve mem./wiki. ref'lerin yeniden oynatılabilirliği
|
|
41
54
|
# buna dayanır). Yalnızca-rakam slug'a "n" öneki zorunlu: Store'un
|
|
42
55
|
# çözümleyicisi "wiki.42" gibi iki parçalı tip.satır formlarını ref
|
|
43
56
|
# alias'tan ÖNCE dener — çarpışma yanlış satırı ezmesi demekti.
|
|
57
|
+
# Unicode dönüştürme ve kısaltma/boşluk durumunda deterministik
|
|
58
|
+
# hash eki (issue #269) çakışmaları önler.
|
|
44
59
|
def slug(text)
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
raw = text.to_s
|
|
61
|
+
return "untitled" if raw.strip.empty?
|
|
62
|
+
|
|
63
|
+
trans = transliterate(raw)
|
|
64
|
+
lossy = trans.match?(/[^\x00-\x7F]/)
|
|
65
|
+
|
|
66
|
+
s = trans.downcase.gsub(/[^a-z0-9]+/, "-")
|
|
67
|
+
s = s.delete_prefix("-") while s.start_with?("-")
|
|
68
|
+
s = s.delete_suffix("-") while s.end_with?("-")
|
|
69
|
+
|
|
70
|
+
hash = Digest::SHA256.hexdigest(raw)[0, 6]
|
|
71
|
+
|
|
72
|
+
return "untitled-#{hash}" if s.empty?
|
|
73
|
+
|
|
74
|
+
if s.length > SLUG_MAX || lossy
|
|
75
|
+
prefix = s[0, SLUG_MAX - 7].to_s
|
|
76
|
+
prefix = prefix.delete_suffix("-") while prefix.end_with?("-")
|
|
77
|
+
s = prefix.empty? ? "untitled-#{hash}" : "#{prefix}-#{hash}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
s = s.delete_suffix("-") while s.end_with?("-")
|
|
47
81
|
s = "n#{s}" if s.match?(/\A\d+\z/)
|
|
48
82
|
s.empty? ? "untitled" : s
|
|
49
83
|
end
|
data/lib/rubik/tools/base.rb
CHANGED
|
@@ -55,7 +55,8 @@ module Rubik
|
|
|
55
55
|
filter = raw.to_s.strip.tr("\\", "/").sub(%r{^\./}, "")
|
|
56
56
|
return ["", nil] if filter.empty? || filter == "." || filter == "/"
|
|
57
57
|
|
|
58
|
-
norm_root = root_dir.to_s.tr("\\", "/")
|
|
58
|
+
norm_root = root_dir.to_s.tr("\\", "/")
|
|
59
|
+
norm_root = norm_root.delete_suffix("/") while norm_root.end_with?("/")
|
|
59
60
|
if filter.downcase == norm_root.downcase
|
|
60
61
|
return ["", nil]
|
|
61
62
|
elsif filter.downcase.start_with?("#{norm_root.downcase}/")
|