rubik 0.1.1 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13f2d00bb2c72a92d48741df2cb3c3e84fc970a9ce3e4e844722afa58ef59599
4
- data.tar.gz: f3314db467118bbd56aa79625f954f5c9e76611c56b5eb5e467d442fa440aa63
3
+ metadata.gz: a3548e6b569bb11a7c6c6349bca95300d93cd375c65860095ff18d362cd289ed
4
+ data.tar.gz: 207e5ed22d5180f7b219b546d0322ac2f597703dec1157ec44da48feb899ab1e
5
5
  SHA512:
6
- metadata.gz: a5c29f5af7119d6b9189e37d39390c6dd2463caa1fd870e49022b92a0ed72481b227616000a84a2e201f26c7a65009f7fcb05ac6591db6ca1b9ecae3f81bc6d4
7
- data.tar.gz: 3ab1188c4607f7dbcbe4011111723f530555052497154eaea6c8d47b09dc927bfcfa43b50b88869f2dc8bc1e402c7589a569d59ad584d29b991bfff765292d08
6
+ metadata.gz: a6b485c11a72537a00fbe813889f74b92e18edaa1445ec59bc3834bf6e56c377cc60ac628f059878e6d3fc0a1e27d4f63fa1f6161bac84513af4b3640a37c4b6
7
+ data.tar.gz: 228ce20578fc055bde12ffe0883343f21b74a9b499e718c1e953fc40c9e5054d3c21a27f5e3b986890efba08d3ae23ec77a37498fd197e37a3449de5524d0943
data/ATTRIBUTION.md CHANGED
@@ -54,6 +54,15 @@ SOFTWARE.
54
54
  - **Spatial Composability:** Reactive Coeffect Dependency DAG ($\Sigma \vdash C : \tau$) implemented in [`Rubik::Runtime::ComponentFiber`](lib/rubik/runtime/fiber.rb) and [`Rubik::Runtime::Manager`](lib/rubik/runtime/manager.rb).
55
55
  - Zero-downtime hot reloading, atomic rollback on activation failure, and leak-free lifecycle management.
56
56
 
57
+ ### SKILL.state (Structured Execution State)
58
+ - **Paper:** *"SKILL.state: Scalable Long-Horizon Agent Skills"* ([arXiv:2608.26263](https://arxiv.org/abs/2608.26263))
59
+ - **Authors:** Sanket Badhe, Priyanka Tiwari, Jonghyun Chung
60
+ - **Rubik Implementation:**
61
+ - The structured goal ledger ([`orchestrator/goal_ledger/`](lib/rubik/orchestrator/goal_ledger/), 2026-10-10): the `!goal` loop replaces append-only conversation memory with an explicit, mutable, rev-guarded execution state Σ (objective, tasks, findings, decisions, open questions, environment notes) — the model sees the current state, not an ever-growing history.
62
+ - Validated JSON-patch intake (RFC 6902 subset) with the `:syntax` / `:schema` / `:conflict` rejection taxonomy and bounded-retry model feedback (`GoalLedger::Patch` / `Schema` / `Validator`).
63
+ - The paper's world-drift recovery, coded as tri-state FS re-verification: declarative `verify` assertions are re-checked at step boundaries; a failed assertion flips the task to `stale` with an internal rev bump, so the `:conflict` the model then hits IS the drift signal (`GoalLedger::Verifier` / `VerifySpec` / `DriftNote`).
64
+ - Observation projection: banked tool results compact into per-row `<absorbed_into_ledger>` tombstones while the last K turns stay raw (`GoalLedger::ObservationProjection`).
65
+
57
66
  ---
58
67
 
59
68
  ## 🌳 3. AST Parsing & Multi-Language Ecosystem
@@ -112,7 +121,7 @@ SOFTWARE.
112
121
  | **`strings-ansi`** | Piotr Murach | MIT | ANSI-aware string truncation and alignment for REPL formatters. |
113
122
  | **`reline`** | Hiroshi SHIBATA & Ruby Team | Ruby License / BSD-2-Clause | Line-editing, history navigation, autocompletion hooks, and headless terminal adaptation. |
114
123
  | **`prism`** | Kevin Newton & Ruby Core Team | MIT | High-performance Ruby parsing and AST extraction ([`Rubik::AST::RubyExtractor`](lib/rubik/ast/ruby_extractor.rb)). |
115
- | **`minitest`** | Ryan Davis & seattlerb | MIT | Fast, modular test suite powering Rubik's 1,690+ unit, integration, and contract tests. |
124
+ | **`minitest`** | Ryan Davis & seattlerb | MIT | Fast, modular test suite powering Rubik's 3,400+ unit, integration, and contract tests. |
116
125
  | **`rubocop`** | Bozhidar Batsov & contributors | MIT | Static code analysis, style enforcement, and architectural hygiene gates. |
117
126
  | **`benchmark`, `logger`, `pstore`, `reline`, `fiddle`** | Ruby core / rubygems teams | Ruby License / BSD-2-Clause | Ruby stdlib gems pinned explicitly in `rubik.gemspec` as they are unbundled from the default set in Ruby 3.4/3.5. |
118
127
 
data/CHANGELOG.md CHANGED
@@ -10,8 +10,66 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
10
10
 
11
11
  Nothing yet.
12
12
 
13
+ ## [0.1.3] - 2026-10-12
14
+
15
+ A robustness release straight from a field incident: the `!goal` loop
16
+ no longer strands state when the provider fails mid-goal, and the
17
+ redactor gets measurably cheaper on secret-free input.
18
+
19
+ ### Fixed
20
+
21
+ - **`!goal` survives provider errors (#105):** an api read-timeout
22
+ `ClientError` escaped the goal handler (only `Interrupt` was
23
+ rescued), leaving `GoalState` stuck in `:running`, the ledger
24
+ unfinalized and the session silent while the batch spinner kept
25
+ cycling with the cursor hidden. One `fail_goal` channel now serves
26
+ the `:error` return plus `ClientError`/`StandardError` rescues
27
+ (`Interrupt` still means `:stopped`); both command-dispatch branches
28
+ kill the leaked spinner instead of masking the real failure; and
29
+ goal steps record their tool calls to the session like free-text
30
+ turns.
31
+
32
+ ### Changed
33
+
34
+ - **Redactor short-circuits secret-free lines (#104):** pre-compiled
35
+ quick-check constants plus a trigger check before scrubbing cut
36
+ allocations by ~68% on a mixed redact workload (172,940 -> 54,864
37
+ objects per 20x2000-line pass) with byte-for-byte output parity;
38
+ invalid-UTF-8 input stays total (masked/scrubbed) instead of raising
39
+ ArgumentError.
40
+
41
+ ## [0.1.2] - 2026-10-11
42
+
43
+
44
+ The Anthropic correctness release: the provider now speaks the modern
45
+ extended-thinking contract, so pinning any current Claude model works
46
+ instead of failing with HTTP 400.
47
+
48
+ ### Fixed
49
+
50
+ - **Anthropic provider speaks the modern thinking contract:** the
51
+ default model pin moves from the stale `claude-3-7-sonnet-20250219`
52
+ to `claude-sonnet-5-5`, and the extended-thinking payload follows the
53
+ model line ? 4.6+ and the 5.x/Fable models get the adaptive form
54
+ (`thinking: {type: "adaptive"}` with an `output_config` effort),
55
+ 3-7-sonnet through the 4.5 line keep the `budget_tokens` math, and
56
+ pre-thinking lines get nothing attached. 0.1.1 sent the legacy form
57
+ to every Anthropic model, which Claude 4.7+ rejects with HTTP 400.
58
+
59
+ ### Changed
60
+
61
+ - Extended thinking auto-enables on every thinking-capable Anthropic
62
+ model line (previously only claude-3-7-sonnet); `thinking: false`
63
+ still disables it everywhere and `thinking_budget_ratio` shapes the
64
+ legacy form only. Requests that carry thinking still force
65
+ `temperature` to 1.0.
66
+ - README quick start documents the native Anthropic provider
67
+ (`ANTHROPIC_API_KEY` + `!model anthropic/<model>`) next to the
68
+ OpenAI-compatible example.
69
+
13
70
  ## [0.1.1] - 2026-10-11
14
71
 
72
+
15
73
  The Linux surface release: CI now proves Linux on Ruby 3.4 and 4.0
16
74
  (`linux-test` is a required check), and the host seams that surface
17
75
  exposed are fixed. Ships the structured goal ledger, the default-on
data/README.md CHANGED
@@ -11,7 +11,7 @@ English | [Türkçe](README.tr.md)
11
11
  [![Protocol](https://img.shields.io/badge/protocol-AGENTS.md-blue.svg)](AGENTS.md)
12
12
  [![Commands Guide](https://img.shields.io/badge/docs-COMMANDS.md-yellow.svg)](COMMANDS.md)
13
13
  [![Config Guide](https://img.shields.io/badge/docs-CONFIG.md-green.svg)](CONFIG.md)
14
- [![Test Suite](https://img.shields.io/badge/tests-3202%20runs%20%7C%20100%25%20passed-brightgreen.svg)]()
14
+ [![Test Suite](https://img.shields.io/badge/tests-3403%20runs%20%7C%20100%25%20passed-brightgreen.svg)]()
15
15
 
16
16
  ---
17
17
 
@@ -31,6 +31,12 @@ Requires **Ruby 3.4+**:
31
31
  gem install rubik
32
32
  ```
33
33
 
34
+ The installer puts the `rubik` launcher into RubyGems' bin directory (already on your `PATH`), so you can start it from any project directory:
35
+
36
+ ```bash
37
+ rubik
38
+ ```
39
+
34
40
  or with Bundler:
35
41
 
36
42
  ```ruby
@@ -73,6 +79,9 @@ ln -sf $(pwd)/bin/rubik ~/.local/bin/rubik
73
79
  rubik
74
80
  ```
75
81
 
82
+ > [!NOTE]
83
+ > **The gem and a clone can coexist.** On POSIX the symlink always runs your clone: `bin/rubik` resolves `lib/` from its own real path, so a `git pull` is live on the very next launch — even while the `rubik` gem stays installed. The gem-installed `rubik` keeps serving the last released version until you run `gem update rubik`. On Windows, whichever `bin` folder comes first in `PATH` wins — `where.exe rubik` shows the winner.
84
+
76
85
  #### 🪟 Windows (PowerShell / Command Prompt)
77
86
 
78
87
  Rubik ships with built-in `.cmd`, `.bat`, and `.ps1` wrappers for Windows:
@@ -105,6 +114,12 @@ rubik > !provider add deepseek openai_compatible https://api.deepseek.com/v1/cha
105
114
  rubik > !model deepseek/deepseek-coder
106
115
  ```
107
116
 
117
+ Anthropic talks over its **native Messages API** (not an OpenAI shim): the `anthropic` provider is registered out of the box — set `ANTHROPIC_API_KEY` and pick a model:
118
+
119
+ ```bash
120
+ rubik > !model anthropic/claude-sonnet-5-5
121
+ ```
122
+
108
123
  ### First Session Example
109
124
 
110
125
  ```text
@@ -9,7 +9,32 @@ module Rubik
9
9
  class AnthropicProvider < BaseProvider
10
10
  attr_reader :name, :model, :formatter, :headers, :api_key, :api_key_env
11
11
 
12
- def initialize(name: nil, model: "claude-3-7-sonnet-20250219", formatter: AnthropicFormatter.new, headers: {},
12
+ # Extended-thinking support by model line (Anthropic docs, 2026-10):
13
+ # :adaptive — 4.6+ and the 5.x/Fable lines: `thinking: {type: "adaptive"}`
14
+ # with an `output_config` effort; the legacy budget_tokens form is
15
+ # REJECTED with a 400 from 4.7 onward.
16
+ # :legacy — claude-3-7-sonnet through the 4.5 line: the manual
17
+ # `thinking: {type: "enabled", budget_tokens: N}` form is the only
18
+ # accepted mode.
19
+ # nil — 3.5-and-earlier lines accept no thinking at all, and
20
+ # unrecognized names (custom aliases/proxies) get no auto thinking.
21
+ def self.thinking_mode_for(model_name)
22
+ s = model_name.to_s
23
+ return :legacy if s.include?("claude-3-7-sonnet")
24
+ return nil if s.match?(/\Aclaude-[23]/) # number-first 2.x/3.x lines (3-7 handled above)
25
+
26
+ # Single-digit captures: Anthropic versions are one digit per segment,
27
+ # so a snapshot date suffix (claude-sonnet-4-20250514) can never be
28
+ # swallowed as the minor version.
29
+ version = s.match(/\Aclaude-(?:sonnet|opus|haiku|fable)-(\d)(?:-(\d))?/)
30
+ return nil unless version
31
+
32
+ major = version[1].to_i
33
+ minor = version[2].to_i
34
+ major > 4 || (major == 4 && minor >= 6) ? :adaptive : :legacy
35
+ end
36
+
37
+ def initialize(name: nil, model: "claude-sonnet-5-5", formatter: AnthropicFormatter.new, headers: {},
13
38
  api_key: nil, api_key_env: nil)
14
39
  super()
15
40
  @name = name.to_s.downcase
@@ -170,14 +195,25 @@ module Rubik
170
195
  thinking_opt = opts[:thinking]
171
196
  thinking_disabled = [false, :disabled].include?(thinking_opt)
172
197
  ratio = (opts[:thinking_budget_ratio] || 0.5).to_f
198
+ # Thinking is on for every model line that supports it unless
199
+ # explicitly disabled; thinking_budget_ratio only shapes the legacy
200
+ # budget form — adaptive models reason as they see fit.
201
+ mode = self.class.thinking_mode_for(target_model)
173
202
 
174
203
  if thinking_opt.is_a?(Hash)
175
204
  payload[:thinking] = thinking_opt
176
- elsif !thinking_disabled && (thinking_opt == true || target_model.to_s.include?("claude-3-7-sonnet"))
177
- if max_tokens > 2048
178
- budget = [(max_tokens * ratio).to_i, 1024].max
179
- budget = [budget, max_tokens - 1024].min
180
- payload[:thinking] = { type: "enabled", budget_tokens: budget } if budget >= 1024
205
+ elsif !thinking_disabled && (thinking_opt == true || mode)
206
+ if mode == :legacy
207
+ if max_tokens > 2048
208
+ budget = [(max_tokens * ratio).to_i, 1024].max
209
+ budget = [budget, max_tokens - 1024].min
210
+ payload[:thinking] = { type: "enabled", budget_tokens: budget } if budget >= 1024
211
+ end
212
+ else
213
+ # Adaptive models reject the legacy form outright, and for
214
+ # unrecognized names adaptive is the modern-generation default.
215
+ payload[:thinking] = { type: "adaptive" }
216
+ payload[:output_config] = { effort: "high" }
181
217
  end
182
218
  end
183
219
 
@@ -38,7 +38,7 @@ module Rubik
38
38
  @providers = {
39
39
  "ollama" => OllamaProvider.new(model: "qwen2.5-coder:7b", headers: global_headers),
40
40
  "openai" => OpenAIProvider.new(model: "gpt-4o", headers: global_headers),
41
- "anthropic" => AnthropicProvider.new(model: "claude-3-7-sonnet-20250219", headers: global_headers)
41
+ "anthropic" => AnthropicProvider.new(model: "claude-sonnet-5-5", headers: global_headers)
42
42
  }
43
43
  @models_catalog = {}
44
44
  load_from_config(config) if config
@@ -82,7 +82,7 @@ module Rubik
82
82
  end
83
83
  if @providers["anthropic"].instance_of?(AnthropicProvider)
84
84
  @providers["anthropic"] = AnthropicProvider.new(
85
- model: @providers["anthropic"]&.model || "claude-3-7-sonnet-20250219",
85
+ model: @providers["anthropic"]&.model || "claude-sonnet-5-5",
86
86
  headers: global_headers
87
87
  )
88
88
  end
@@ -132,7 +132,7 @@ module Rubik
132
132
  when "anthropic"
133
133
  AnthropicProvider.new(
134
134
  name: p_name,
135
- model: default_model || "claude-3-7-sonnet-20250219",
135
+ model: default_model || "claude-sonnet-5-5",
136
136
  # Honor the same api_key / api_key_env that
137
137
  # openai-compatible specs already forward;
138
138
  # otherwise Anthropic auth fails silently.
@@ -257,7 +257,7 @@ module Rubik
257
257
  when "anthropic"
258
258
  anthropic_headers = (custom.respond_to?(:headers) ? custom.headers : nil) || global_headers
259
259
  AnthropicProvider.new(
260
- model: model_name || "claude-3-7-sonnet-20250219",
260
+ model: model_name || "claude-sonnet-5-5",
261
261
  headers: anthropic_headers,
262
262
  api_key: custom.respond_to?(:api_key) ? custom.api_key : nil,
263
263
  api_key_env: custom.respond_to?(:api_key_env) ? custom.api_key_env : nil
@@ -127,6 +127,16 @@ module Rubik
127
127
  # Adım sayacı rubik://goal/state'e aksın: olaylar bir adım
128
128
  # içinde tekrarlanabilir, GoalState monotoniktir (zararsız).
129
129
  goal_state.update_steps(step_info[:step], step_info[:max_steps]) if step_info[:step].is_a?(Integer)
130
+ # Session teşhis edilebilirliği (field incident 2026-10-12):
131
+ # saatler süren !goal koşuları session dosyasına tek satır
132
+ # düşürüyordu — free-text turn simetrisinde tool adımları
133
+ # kayıtçıya akar; render ve LLM geçmişi değişmez.
134
+ case step_info[:type]&.to_sym
135
+ when :tool_call
136
+ ctx.recorder&.record_tool_call(step_info[:tool], step_info[:args], call_id: step_info[:call_id])
137
+ when :tool_result
138
+ ctx.recorder&.record_tool_result(step_info[:tool], step_info[:output], status: step_info[:status] || :ok, call_id: step_info[:call_id])
139
+ end
130
140
  ctx.render_step_event(step_info)
131
141
  }
132
142
  )
@@ -143,8 +153,28 @@ module Rubik
143
153
  end
144
154
 
145
155
  # Pruner apply seam: hedef döngüsü de aynı sınırda tohumlanır —
146
- # aşamalı plan varsa goal promptundan ÖNCE uygulanır.
156
+ # aşamalı budama planı varsa goal promptundan ÖNCE uygulanır.
147
157
  ctx.apply_staged_prune_plan!
158
+
159
+ # Tek hata kanalı (field incident 2026-10-12): eskiden yalnızca
160
+ # normal :error dönüşü başarısızlık eşlemesini yapıyordu; loop'tan
161
+ # kaçan bir ClientError (sağlayıcı read timeout) goal'u kayıtsız
162
+ # 'running' bırakıyor, ledger terminal görmiyor ve session
163
+ # dosyası sessiz kalıyordu. Rescue dalları da aynı kapanış
164
+ # sırasını buradan çalıştırır. Kayıtçı '!goal ...' user_turn'unu
165
+ # komut girişinde çoktan yazdı — assistant_response olmadan
166
+ # session restore EŞLEŞMEMİŞ bir 'Goal: ...' turu hidrate ederdi.
167
+ # Adım sayısı: loop'un truncation hatası :steps'siz döner —
168
+ # GoalState monotonik sayacı otoriter yedek.
169
+ fail_goal = lambda do |reason, tokens: nil, steps: nil|
170
+ sanitized = Rubik::Platform.safe_utf8(reason.to_s)
171
+ steps_used = steps || goal_state.step_count
172
+ goal_state.fail!(sanitized, tokens: tokens)
173
+ finalize_ledger.call(:failed)
174
+ ctx.out.puts "\n❌ Goal could not be completed: #{sanitized}"
175
+ record_goal_pair.call("[Goal attempt ended after #{steps_used} steps: #{sanitized}]", tokens: tokens)
176
+ end
177
+
148
178
  ctx.active_loop = orchestrator
149
179
  begin
150
180
  res = orchestrator.run(goal_text, goal_mode: true, conversation_history: ctx.conversation_history)
@@ -170,6 +200,16 @@ module Rubik
170
200
  finalize_ledger.call(:stopped)
171
201
  record_goal_pair.call("[Goal attempt interrupted after #{goal_state.step_count} steps]")
172
202
  raise
203
+ rescue Rubik::LLM::ClientError => e
204
+ # execute_free_text simetrisi: sağlayıcı hatası (read timeout,
205
+ # HTTP 4xx) kaçarsa da goal terminal görür. Yeniden yükseltmez —
206
+ # spinner temizliği dispatch katmanına (execute_command) aittir.
207
+ # next: rescue sonrası akış res'siz `if res[:ok]`'a düşmesin.
208
+ fail_goal.call(e.message)
209
+ next
210
+ rescue StandardError => e
211
+ fail_goal.call("#{e.class}: #{e.message}")
212
+ next
173
213
  ensure
174
214
  ctx.active_loop = nil
175
215
  end
@@ -197,17 +237,10 @@ module Rubik
197
237
  ctx.out.puts "\nTip: Use `!diff` to review changes and `!apply` to commit to disk." if ctx.buffer_manager.dirty?
198
238
  end
199
239
  else
200
- # Başarısızlık eşlemesi: kayıtçı '!goal ...' user_turn'unu komut
201
- # girişinde çoktan yazdı — assistant_response olmadan session
202
- # restore EŞLEŞMEMİŞ bir 'Goal: ...' turu hidrate ederdi.
203
- # Adım sayısı: loop'un truncation hatası :steps'siz döner —
204
- # GoalState monotonik sayacı otoriter yedek.
205
- sanitized = Rubik::Platform.safe_utf8(res[:error].to_s)
206
- steps_used = res[:steps] || goal_state.step_count
207
- goal_state.fail!(sanitized, tokens: res[:tokens])
208
- finalize_ledger.call(:failed)
209
- ctx.out.puts "\n❌ Goal could not be completed: #{sanitized}"
210
- record_goal_pair.call("[Goal attempt ended after #{steps_used} steps: #{sanitized}]", tokens: res[:tokens])
240
+ # Başarısızlık eşlemesi yukarıdaki tek hata kanalından akar:
241
+ # GoalState → ledger finalize → kayıtçı çifti, istisna yoluyla
242
+ # aynı sırayla.
243
+ fail_goal.call(res[:error], tokens: res[:tokens], steps: res[:steps])
211
244
  end
212
245
  end
213
246
  end
@@ -56,6 +56,11 @@ module Rubik
56
56
  rescue Interrupt
57
57
  Rubik::REPL::InterruptHandler.restore_console_after_interrupt
58
58
  @output.puts "\n⚠️ [Rubik] Komut kullanıcı tarafından iptal edildi (^C)."
59
+ ensure
60
+ # !goal dahil lifecycle komutları tracked dal ile aynı spinner
61
+ # garantisini taşır (field incident 2026-10-12: !goal'ten kaçan
62
+ # ClientError'da öksüz spinner terminali kilitli gösterdi).
63
+ kill_command_spinner!
59
64
  end
60
65
  else
61
66
  captured = StringIO.new
@@ -70,7 +75,7 @@ module Rubik
70
75
  @output.puts "\n⚠️ [Rubik] Komut kullanıcı tarafından iptal edildi (^C)."
71
76
  ensure
72
77
  # A command (!plan/!goal) may leave a live spinner if it died mid-wait.
73
- @ui_renderer&.spinner&.kill
78
+ kill_command_spinner!
74
79
  end
75
80
  output_text = captured.string.strip
76
81
  if !cmd_interrupted && !output_text.empty?
@@ -297,11 +302,7 @@ module Rubik
297
302
  rescue StandardError => e
298
303
  warn "[rubik] markdown tail flush failed: #{e.class}: #{e.message}"
299
304
  end
300
- begin
301
- renderer&.spinner&.kill
302
- rescue StandardError => e
303
- warn "[rubik] spinner cleanup failed: #{e.class}: #{e.message}"
304
- end
305
+ kill_spinner!(renderer&.spinner)
305
306
  flush_subagent_notices! if respond_to?(:flush_subagent_notices!)
306
307
  # Session review B3: notices stranded in the ended loop's inbox never
307
308
  # reached the model channel and owed no wake — an idle maestro learned
@@ -318,6 +319,21 @@ module Rubik
318
319
  end
319
320
  reap_dead_kernel_units! if respond_to?(:reap_dead_kernel_units!)
320
321
  end
322
+
323
+ # Shared spinner teardown. Cleanup must never mask the in-flight
324
+ # exception (AGENTS.md §5): a spinner whose tick thread died holding
325
+ # the mutex raises ThreadError from synchronize — report it to stderr
326
+ # and continue so the real failure surfaces.
327
+ def kill_spinner!(spinner)
328
+ spinner&.kill
329
+ rescue StandardError => e
330
+ warn "[rubik] spinner cleanup failed: #{e.class}: #{e.message}"
331
+ end
332
+
333
+ # Command-dispatch spinner cleanup (both branches).
334
+ def kill_command_spinner!
335
+ kill_spinner!(@ui_renderer&.spinner)
336
+ end
321
337
  end
322
338
  end
323
339
  end
@@ -20,6 +20,11 @@ module Rubik
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
+ # PERF: Pre-compiled constants to avoid re-allocation inside hot loops
24
+ URL_PATTERN = %r{(https?://[^:]+:)([^@]+)@}
25
+ URL_MASK = "\\1#{MASK_STRING}@".freeze
26
+ QUICK_CHECK_REGEX = %r{sk-|AKIA|ghp_|github_pat_|AIza|bearer|PRIVATE KEY|://[^:]+:|password|passwd|api_key|secret|token}i
27
+
23
28
  # Anahtar-maskeleme desenleri (Session::Recorder#mask_payload ile aynı
24
29
  # aile): sır görünümlü anahtarların değerleri desen eşleşmese bile
25
30
  # maskelenir; sayaç anahtarları (tokens, max_tokens) istisnadır.
@@ -33,10 +38,17 @@ module Rubik
33
38
  # scrub: redact ham geçmişe (tool çıktısı, prompt) dokunur — geçersiz
34
39
  # UTF-8 gsub!'ı ArgumentError ile düşürür; total olmak maskenin
35
40
  # kendisi bir arıza kaynağı olmasın demektir.
36
- result = text.dup.scrub
41
+ result = text.valid_encoding? ? text.dup : text.scrub
42
+
43
+ # PERF: kısa devre — sır tetikleyicisi yoksa 11 gsub turu atlanır.
44
+ # Tetik testi scrub'lanmış sonuç üstünde yapılır: match? ham geçersiz
45
+ # UTF-8'de ArgumentError düşürür; scrub sonrası metin hep geçerlidir
46
+ # ve desenler zaten bu metne uygulanıyordu, dolayısıyla eşdeğerdir.
47
+ return result unless result.match?(QUICK_CHECK_REGEX)
48
+
37
49
  PATTERNS.each do |pattern|
38
- if pattern == %r{(https?://[^:]+:)([^@]+)@}
39
- result.gsub!(pattern, "\\1#{MASK_STRING}@")
50
+ if pattern == URL_PATTERN
51
+ result.gsub!(pattern, URL_MASK)
40
52
  else
41
53
  result.gsub!(pattern, MASK_STRING)
42
54
  end
data/lib/rubik/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubik
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
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.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Özgür Ulukır