rubik 0.3.5 → 0.4.1

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/ATTRIBUTION.md +1 -1
  3. data/CHANGELOG.md +99 -3
  4. data/lib/rubik/ast/c_extractor.rb +1 -1
  5. data/lib/rubik/ast/fallback_cleaner.rb +3 -1
  6. data/lib/rubik/ast/graph.rb +20 -1
  7. data/lib/rubik/ast/graph_extensions.rb +89 -1
  8. data/lib/rubik/ast/python_extractor.rb +5 -5
  9. data/lib/rubik/ast/rust_extractor.rb +1 -1
  10. data/lib/rubik/ast/typescript_extractor.rb +3 -3
  11. data/lib/rubik/diagnostics/parser.rb +65 -13
  12. data/lib/rubik/llm/openai_compatible.rb +2 -1
  13. data/lib/rubik/llm/registry.rb +81 -74
  14. data/lib/rubik/lsp/manager.rb +15 -5
  15. data/lib/rubik/orchestrator/step_executor.rb +139 -155
  16. data/lib/rubik/orchestrator/subagent/completion.rb +1 -0
  17. data/lib/rubik/orchestrator/subagent/dependency.rb +1 -0
  18. data/lib/rubik/orchestrator/subagent/supervisor.rb +20 -3
  19. data/lib/rubik/orchestrator/subagent/waiting.rb +16 -9
  20. data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +1 -1
  21. data/lib/rubik/orchestrator/turn_executor/user_channel.rb +2 -2
  22. data/lib/rubik/orchestrator/turn_executor.rb +3 -2
  23. data/lib/rubik/platform/path.rb +2 -1
  24. data/lib/rubik/platform/sanitize.rb +4 -1
  25. data/lib/rubik/prompt/token_estimator.rb +16 -12
  26. data/lib/rubik/repl/step_formatter.rb +2 -1
  27. data/lib/rubik/runtime/diagnostic_engine.rb +1 -1
  28. data/lib/rubik/security/blocked_guidance.rb +1 -1
  29. data/lib/rubik/security/classifier/classifier.rb +3 -1
  30. data/lib/rubik/security/redactor.rb +3 -3
  31. data/lib/rubik/storage/layout.rb +3 -1
  32. data/lib/rubik/storage/render.rb +1 -1
  33. data/lib/rubik/storage/vakanuvis/vocabulary.rb +5 -2
  34. data/lib/rubik/tools/base.rb +2 -1
  35. data/lib/rubik/tools/truncator.rb +21 -8
  36. data/lib/rubik/ui/renderer.rb +1 -0
  37. data/lib/rubik/version.rb +1 -1
  38. metadata +1 -1
@@ -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*\{.*?\}\s*```/m, "").strip
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 .+ to dangerous\z/i)
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.
@@ -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 ["~", "~/", "$HOME", "${HOME}"].include?(p)
356
+ return true if HOME_TOKENS.include?(p)
355
357
 
356
358
  if p =~ %r{\A([a-zA-Z]):[/\\](.*)\z}
357
359
  parts = []
@@ -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|://[^:]+:|password|passwd|api_key|secret|token}i
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
@@ -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._-]+/, "-").gsub(/\A[-.]+|[-.]+\z/, "")
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
 
@@ -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
@@ -42,8 +42,11 @@ module Rubik
42
42
  # çözümleyicisi "wiki.42" gibi iki parçalı tip.satır formlarını ref
43
43
  # alias'tan ÖNCE dener — çarpışma yanlış satırı ezmesi demekti.
44
44
  def slug(text)
45
- s = text.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-+|-+\z/, "")
46
- s = s[0, SLUG_MAX].to_s.gsub(/\A-+|-+\z/, "")
45
+ s = text.to_s.downcase.gsub(/[^a-z0-9]+/, "-")
46
+ s = s.delete_prefix("-") while s.start_with?("-")
47
+ s = s.delete_suffix("-") while s.end_with?("-")
48
+ s = s[0, SLUG_MAX].to_s
49
+ s = s.delete_suffix("-") while s.end_with?("-")
47
50
  s = "n#{s}" if s.match?(/\A\d+\z/)
48
51
  s.empty? ? "untitled" : s
49
52
  end
@@ -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("\\", "/").sub(%r{/+\z}, "")
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}/")
@@ -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
- kept_str = kept_str.byteslice(0, max_c) if kept_str.bytesize > max_c
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
- kept_str = kept_str.byteslice(-max_c, max_c) if kept_str.bytesize > max_c
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
- kept_str = kept_str.byteslice(0, max_c) if kept_str.bytesize > max_c
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
- head_str = head_str.byteslice(0, head_chars) if head_str.bytesize > head_chars
97
- tail_str = tail_str.byteslice(-tail_chars, tail_chars) if tail_str.bytesize > tail_chars
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
- head_final = head_str.valid_encoding? ? head_str : head_str.scrub("")
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
@@ -172,6 +172,7 @@ module Rubik
172
172
  def render_llm_thought(step_info)
173
173
  @spinner.stop
174
174
  return nil if level < Rubik::REPL::StepFormatter::LEVEL_VERBOSE
175
+ return nil if step_info[:streamed] == true
175
176
 
176
177
  first_line = @legacy.thought_first_line(step_info[:content])
177
178
  return nil if first_line.nil? || first_line.empty?
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.3.5"
4
+ VERSION = "0.4.1"
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.3.5
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Özgür Ulukır