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.
- checksums.yaml +4 -4
- data/ATTRIBUTION.md +1 -1
- data/CHANGELOG.md +99 -3
- data/lib/rubik/ast/c_extractor.rb +1 -1
- data/lib/rubik/ast/fallback_cleaner.rb +3 -1
- 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 +1 -1
- data/lib/rubik/ast/typescript_extractor.rb +3 -3
- data/lib/rubik/diagnostics/parser.rb +65 -13
- data/lib/rubik/llm/openai_compatible.rb +2 -1
- data/lib/rubik/llm/registry.rb +81 -74
- data/lib/rubik/lsp/manager.rb +15 -5
- data/lib/rubik/orchestrator/step_executor.rb +139 -155
- 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/prompt/token_estimator.rb +16 -12
- 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/classifier/classifier.rb +3 -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 +5 -2
- data/lib/rubik/tools/base.rb +2 -1
- data/lib/rubik/tools/truncator.rb +21 -8
- data/lib/rubik/ui/renderer.rb +1 -0
- data/lib/rubik/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15cadac070995d7b8d1ac155c6ddfe14c4e4418e596efe6b734af6cb9240f6b4
|
|
4
|
+
data.tar.gz: 21b3376be313a5133ca84180a36770b0c344495b12ed3c00390715d8dd1658eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9edcdfa2830b03dc8d9a5345d8215a5204ba96d4aa5ef93ca0742633a02ff089f2dda9fab638d4a42a489f1b166f74c9a11ecc5f64b3f602b7cb1cbbdd09a5a
|
|
7
|
+
data.tar.gz: 2b478a0aa15376eaac5636c9d6e7f8d5dfac5534394153958d6417ecf3547dbb9ae654d7c59e5d92df6a84e2038d869145a3ba2b7c8dbdc62472106c7727c33d
|
data/ATTRIBUTION.md
CHANGED
|
@@ -121,7 +121,7 @@ SOFTWARE.
|
|
|
121
121
|
| **`strings-ansi`** | Piotr Murach | MIT | ANSI-aware string truncation and alignment for REPL formatters. |
|
|
122
122
|
| **`reline`** | Hiroshi SHIBATA & Ruby Team | Ruby License / BSD-2-Clause | Line-editing, history navigation, autocompletion hooks, and headless terminal adaptation. |
|
|
123
123
|
| **`prism`** | Kevin Newton & Ruby Core Team | MIT | High-performance Ruby parsing and AST extraction ([`Rubik::AST::RubyExtractor`](lib/rubik/ast/ruby_extractor.rb)). |
|
|
124
|
-
| **`minitest`** | Ryan Davis & seattlerb | MIT | Fast, modular test suite powering Rubik's 3,
|
|
124
|
+
| **`minitest`** | Ryan Davis & seattlerb | MIT | Fast, modular test suite powering Rubik's 3,900+ unit, integration, and contract tests. |
|
|
125
125
|
| **`rubocop`** | Bozhidar Batsov & contributors | MIT | Static code analysis, style enforcement, and architectural hygiene gates. |
|
|
126
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. |
|
|
127
127
|
|
data/CHANGELOG.md
CHANGED
|
@@ -6,9 +6,104 @@ commits, 2026-08-23 → 2026-10-09, see `git log`).
|
|
|
6
6
|
|
|
7
7
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## [0.4.1] - 2026-10-29
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
A hardening patch: CodeQL-driven fixes remove ReDoS-prone regular
|
|
12
|
+
expressions and tighten output escaping, LSP client loss retracts its
|
|
13
|
+
coeffects symmetrically, and subagent waits wake reactively instead of
|
|
14
|
+
polling.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **CodeQL scanning alerts resolved — ReDoS, escaping, and workflow
|
|
19
|
+
permissions:** polynomial-time regular expressions in AST extractors,
|
|
20
|
+
the diagnostics parser, storage slug generation, and path helpers are
|
|
21
|
+
replaced with deterministic string operations or bounded alternation;
|
|
22
|
+
`Render.listing` escapes backslashes before pipes in Markdown tables;
|
|
23
|
+
and the CI workflow's default `GITHUB_TOKEN` is floored to
|
|
24
|
+
`contents: read`.
|
|
25
|
+
- **Streamed thought duplication in REPL suppressed (#287):** when an LLM turn
|
|
26
|
+
with tool calls streams preliminary text live to the terminal, `TurnExecutor`
|
|
27
|
+
and UI renderers no longer reprint the identical thought line prefixed with
|
|
28
|
+
`💭` at verbose/debug log levels.
|
|
29
|
+
- **LSP client loss retracts the `lsp:<lang>` coeffect symmetrically
|
|
30
|
+
(#306):** a new `unregister_client_unlocked` runs on every client-removal
|
|
31
|
+
path — `stop_client`, `stop_all`, crash recovery, and dead-client
|
|
32
|
+
detection — so the context `unprovide` pairs with the original `provide`
|
|
33
|
+
and `registered_disposable_langs` tracking is cleared. Crash paths
|
|
34
|
+
previously leaked the coeffect and its dispose hook, leaving stale
|
|
35
|
+
services behind after a server died.
|
|
36
|
+
|
|
37
|
+
### Performance
|
|
38
|
+
|
|
39
|
+
- **Subagent waiters wake reactively via ConditionVariable (#299):** the
|
|
40
|
+
wait loop parks on a monitor-owned condition variable instead of polling
|
|
41
|
+
every 100 ms; every settle, pause, unpark, and launch transition
|
|
42
|
+
(completion, `release_ready!`, kill, resume, budget/error/question
|
|
43
|
+
events) broadcasts the wakeup. The sleep fallback keeps standalone
|
|
44
|
+
`WaitSupervisor` includers working.
|
|
45
|
+
- **LSP diagnostics aggregation merges outside the monitor (#294):**
|
|
46
|
+
`all_diagnostics` and `all_diagnostic_snapshots` collect client hashes
|
|
47
|
+
under the monitor and bulk-merge outside it, dropping the per-client
|
|
48
|
+
merge from the locked section; the result is always a fresh hash.
|
|
49
|
+
- **`compact.map` chains fused into `filter_map` (#290, #291, #297,
|
|
50
|
+
#302):** single-pass collection replaces the intermediate arrays in
|
|
51
|
+
`WaitSupervisor#wait`, subagent dependency normalization,
|
|
52
|
+
`dangerous_system_roots`, and `ConversationId.normalize_list`, with the
|
|
53
|
+
nil-skip semantics of the originals preserved exactly.
|
|
54
|
+
|
|
55
|
+
## [0.4.0] - 2026-10-27
|
|
56
|
+
|
|
57
|
+
A diagnostics-reach minor: the diagnostics engine gains native
|
|
58
|
+
parsers for Python tracebacks, TypeScript/JavaScript compiler
|
|
59
|
+
output, and C++ error streams, alongside an AST-graph correctness
|
|
60
|
+
restore, multibyte-safe truncation, and hot-path performance
|
|
61
|
+
streamlines.
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- **Diagnostics parser speaks Python, TypeScript/JavaScript, and
|
|
66
|
+
C++ (#286):** Python tracebacks (`File "...", line N` headers
|
|
67
|
+
paired with exception lines) parse into structured entries with
|
|
68
|
+
syntax-vs-type categorization; tsc-style `file(line,col): error
|
|
69
|
+
TSxxxx:` and `file:line:col: error` streams parse with error codes
|
|
70
|
+
and columns; C++ compilers route through the existing zig/c
|
|
71
|
+
parser. Unrecognized output still falls back to the generic
|
|
72
|
+
parser.
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- **AST plain-symbol lookups on file removal restored (#280, #282):**
|
|
77
|
+
PR #281's squash merge accidentally reverted the #280 work; the
|
|
78
|
+
reviewed restoration is back on main. Removing a file from the AST
|
|
79
|
+
graph again repairs only the plain symbol entries it owned, and
|
|
80
|
+
`SymbolMap` completes Hash parity (single-yield `#each`,
|
|
81
|
+
`#to_h` applied once, `#dig` raising `TypeError` on non-diggable
|
|
82
|
+
intermediates), with removal/parity edge cases covered by tests.
|
|
83
|
+
- **Truncator output is valid UTF-8 at every step (#284):** byteslice
|
|
84
|
+
cutting multibyte characters midway left invalid byte sequences in
|
|
85
|
+
intermediate results and undercounted `omitted_chars`; sliced
|
|
86
|
+
substrings are now scrubbed immediately in all strategies, so
|
|
87
|
+
line counts and character statistics stay accurate.
|
|
88
|
+
|
|
89
|
+
### Performance
|
|
90
|
+
|
|
91
|
+
- **Python `extract_calls` is cheaper (#278):** the string scan and
|
|
92
|
+
filtering path is streamlined, cutting work in hot call-extraction.
|
|
93
|
+
- **TokenEstimator message field evaluation streamlined (#279):**
|
|
94
|
+
per-message field access in token estimation is evaluated more
|
|
95
|
+
cheaply.
|
|
96
|
+
- **Security classifier avoids per-call allocations (#283):** the
|
|
97
|
+
home-token list in `Engine#system_path?` is hoisted into a frozen
|
|
98
|
+
constant, removing an array allocation on every system-path check.
|
|
99
|
+
|
|
100
|
+
### Changed
|
|
101
|
+
|
|
102
|
+
- **No-behavior-change decompositions (#281, #285):**
|
|
103
|
+
`StepExecutor#execute_registered_tool` is split into private
|
|
104
|
+
helpers, with the file kept under the 350-line hygiene cap, and
|
|
105
|
+
the LLM registry's `register_provider_spec` extracts
|
|
106
|
+
`build_provider_instance` and `register_models_catalog` helpers.
|
|
12
107
|
|
|
13
108
|
## [0.3.5] - 2026-10-24
|
|
14
109
|
|
|
@@ -797,5 +892,6 @@ installation section.
|
|
|
797
892
|
- Context: serialized index mutations, snapshot-outside-lock reconcile,
|
|
798
893
|
streaming-first truncation.
|
|
799
894
|
|
|
800
|
-
[Unreleased]: https://github.com/ozgurulukir/rubik/compare/v0.1
|
|
895
|
+
[Unreleased]: https://github.com/ozgurulukir/rubik/compare/v0.4.1...HEAD
|
|
896
|
+
[0.4.1]: https://github.com/ozgurulukir/rubik/compare/v0.4.0...v0.4.1
|
|
801
897
|
[0.1.0]: https://github.com/ozgurulukir/rubik/releases/tag/v0.1.0
|
|
@@ -51,7 +51,7 @@ module Rubik
|
|
|
51
51
|
signature: line.split("{").first.to_s.strip,
|
|
52
52
|
body_loader: body_loader_for(line_num, end_line)
|
|
53
53
|
)
|
|
54
|
-
elsif (match = stripped.match(/^([A-Za-z0-9_
|
|
54
|
+
elsif (match = stripped.match(/^([A-Za-z0-9_*]+(?:\s+[A-Za-z0-9_*]+)*)\s+([A-Za-z0-9_]+)\s*\((.*?)\)\s*\{/))
|
|
55
55
|
fn_name = match[2]
|
|
56
56
|
unless ["if", "for", "while", "switch"].include?(fn_name)
|
|
57
57
|
end_line = find_closing_brace(i)
|
|
@@ -134,7 +134,9 @@ module Rubik
|
|
|
134
134
|
return true if k.negative?
|
|
135
135
|
return true if REGEX_PRECEDERS.include?(raw[k])
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
word_start = k
|
|
138
|
+
word_start -= 1 while word_start >= 0 && raw[word_start].match?(/\w/)
|
|
139
|
+
REGEX_KEYWORDS.include?(raw[(word_start + 1)..k])
|
|
138
140
|
end
|
|
139
141
|
|
|
140
142
|
def blank_symbol_keywords(line)
|
data/lib/rubik/ast/graph.rb
CHANGED
|
@@ -216,10 +216,29 @@ module Rubik
|
|
|
216
216
|
lang = CacheStore::LANGUAGE_MAP[ext] || "code"
|
|
217
217
|
|
|
218
218
|
existing.each do |sym|
|
|
219
|
-
@symbols.delete(sym.name) if @symbols[sym.name] == sym
|
|
220
219
|
@qualified_symbols.delete("#{lang}:#{file_path}##{sym.name}")
|
|
221
220
|
@qualified_symbols.delete("#{lang}:#{sym.file}##{sym.name}") if sym.file
|
|
222
221
|
end
|
|
222
|
+
|
|
223
|
+
removed = {}.compare_by_identity
|
|
224
|
+
existing.each { |sym| removed[sym] = true }
|
|
225
|
+
existing.map(&:name).uniq.each do |name|
|
|
226
|
+
next unless removed.key?(@symbols[name])
|
|
227
|
+
|
|
228
|
+
replacement = _find_remaining_symbol_unlocked(name)
|
|
229
|
+
replacement ? @symbols[name] = replacement : @symbols.delete(name)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# The plain map keeps the most recently indexed definition of a name, so
|
|
234
|
+
# fall back to the most recently indexed file that still defines it.
|
|
235
|
+
def _find_remaining_symbol_unlocked(name)
|
|
236
|
+
@file_symbols.reverse_each do |_file_path, syms|
|
|
237
|
+
syms.reverse_each do |sym|
|
|
238
|
+
return sym if sym.name == name
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
nil
|
|
223
242
|
end
|
|
224
243
|
|
|
225
244
|
def _get_file_symbols_unlocked(file_path)
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Rubik
|
|
4
4
|
module AST
|
|
5
|
-
class SymbolMap
|
|
5
|
+
class SymbolMap < Hash
|
|
6
|
+
FETCH_DEFAULT = Object.new.freeze
|
|
7
|
+
|
|
6
8
|
def values
|
|
7
9
|
qualified_values = @qualified_symbols.sort_by { |key, _symbol| key }.map(&:last)
|
|
8
10
|
qualified_ids = {}.compare_by_identity
|
|
@@ -18,6 +20,92 @@ module Rubik
|
|
|
18
20
|
def empty?
|
|
19
21
|
super && @qualified_symbols.empty?
|
|
20
22
|
end
|
|
23
|
+
|
|
24
|
+
def fetch(key, default = FETCH_DEFAULT)
|
|
25
|
+
if key?(key)
|
|
26
|
+
self[key]
|
|
27
|
+
elsif block_given?
|
|
28
|
+
yield(key)
|
|
29
|
+
elsif !default.equal?(FETCH_DEFAULT)
|
|
30
|
+
default
|
|
31
|
+
else
|
|
32
|
+
raise KeyError, "key not found: #{key.inspect}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def include?(key)
|
|
37
|
+
key?(key)
|
|
38
|
+
end
|
|
39
|
+
alias member? include?
|
|
40
|
+
|
|
41
|
+
def keys
|
|
42
|
+
(super + @qualified_symbols.keys).uniq
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def each(&)
|
|
46
|
+
return enum_for(:each) unless block_given?
|
|
47
|
+
|
|
48
|
+
combined = (Hash.instance_method(:to_a).bind_call(self) + @qualified_symbols.to_a).uniq(&:first)
|
|
49
|
+
combined.each(&)
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
alias each_pair each
|
|
53
|
+
|
|
54
|
+
def each_key(&)
|
|
55
|
+
return enum_for(:each_key) unless block_given?
|
|
56
|
+
|
|
57
|
+
keys.each(&)
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def each_value(&)
|
|
62
|
+
return enum_for(:each_value) unless block_given?
|
|
63
|
+
|
|
64
|
+
values.each(&)
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def assoc(key)
|
|
69
|
+
return nil unless key?(key)
|
|
70
|
+
|
|
71
|
+
[key, self[key]]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def to_h(&)
|
|
75
|
+
h = Hash.instance_method(:to_h).bind_call(self)
|
|
76
|
+
@qualified_symbols.each { |k, v| h[k] ||= v }
|
|
77
|
+
if block_given?
|
|
78
|
+
h.each_with_object({}) do |(k, v), acc|
|
|
79
|
+
nk, nv = yield(k, v)
|
|
80
|
+
acc[nk] = nv
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
h
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def dig(key, *rest)
|
|
88
|
+
val = self[key]
|
|
89
|
+
return val if rest.empty? || val.nil?
|
|
90
|
+
return val.dig(*rest) if val.respond_to?(:dig)
|
|
91
|
+
|
|
92
|
+
raise TypeError, "#{val.class} does not have #dig method"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def slice(*keys_to_slice)
|
|
96
|
+
plain = {}
|
|
97
|
+
qual = {}
|
|
98
|
+
plain_key_check = Hash.instance_method(:key?).bind(self)
|
|
99
|
+
plain_fetch = Hash.instance_method(:[]).bind(self)
|
|
100
|
+
keys_to_slice.each do |k|
|
|
101
|
+
if plain_key_check.call(k)
|
|
102
|
+
plain[k] = plain_fetch.call(k)
|
|
103
|
+
elsif @qualified_symbols.key?(k)
|
|
104
|
+
qual[k] = @qualified_symbols[k]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
SymbolMap.new(plain, qual)
|
|
108
|
+
end
|
|
21
109
|
end
|
|
22
110
|
|
|
23
111
|
class Graph
|
|
@@ -86,12 +86,12 @@ module Rubik
|
|
|
86
86
|
|
|
87
87
|
private
|
|
88
88
|
|
|
89
|
+
# PERF: Uses zero-capture lookahead, scan with uniq, and delete_if filtering
|
|
90
|
+
# to eliminate Set instantiation and block allocations (~12% faster, ~26% fewer allocations).
|
|
89
91
|
def extract_calls(body, self_name)
|
|
90
|
-
calls =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
end
|
|
94
|
-
calls.to_a
|
|
92
|
+
calls = body.scan(/\b[A-Za-z0-9_]+(?=\s*\()/).uniq
|
|
93
|
+
calls.delete_if { |c| c == self_name || EXCLUDED_KEYWORDS.include?(c) }
|
|
94
|
+
calls
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
def find_block_end(start_idx, base_indent)
|
|
@@ -62,7 +62,7 @@ module Rubik
|
|
|
62
62
|
elsif (match = stripped.match(/^impl(?:\s*<.*?>)?\s+([A-Za-z0-9_]+(?:\s+for\s+[A-Za-z0-9_]+)?)/))
|
|
63
63
|
current_impl = match[1].split(" for ").last.to_s.strip
|
|
64
64
|
impl_end_line = find_closing_brace(i)
|
|
65
|
-
elsif (match = stripped.match(/^(?:pub(?:\(crate\))?\s+)?(?:async\s+)?fn\s+([A-Za-z0-9_]+)
|
|
65
|
+
elsif (match = stripped.match(/^(?:pub(?:\(crate\))?\s+)?(?:async\s+)?fn\s+([A-Za-z0-9_]+)(?:<[^>]*>)?\s*\((.*?)\)/))
|
|
66
66
|
fn_name = match[1]
|
|
67
67
|
full_name = current_impl ? "#{current_impl}::#{fn_name}" : fn_name
|
|
68
68
|
end_line = stripped.end_with?(";") ? line_num : find_closing_brace(i)
|
|
@@ -166,7 +166,7 @@ module Rubik
|
|
|
166
166
|
end
|
|
167
167
|
|
|
168
168
|
def parse_function(line, stripped, line_num, idx)
|
|
169
|
-
if (match = stripped.match(/^(?:export\s+(?:default\s+)?)?(?:async\s+)?function
|
|
169
|
+
if (match = stripped.match(/^(?:export\s+(?:default\s+)?)?(?:async\s+)?function(?:\s*\*\s*|\s*)(?:([A-Za-z0-9_$]+)\s*)?\(/))
|
|
170
170
|
fn_name = match[1] || "default"
|
|
171
171
|
end_line = stripped.include?("{") ? find_closing_brace(idx) : find_statement_end(idx)
|
|
172
172
|
body_for_calls = extract_lines(line_num, end_line)
|
|
@@ -185,7 +185,7 @@ module Rubik
|
|
|
185
185
|
next_i = end_line > line_num ? end_line - 1 : idx
|
|
186
186
|
[sym, next_i]
|
|
187
187
|
elsif (match = stripped.match(
|
|
188
|
-
/^(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)(?:\s*:\s*[^=]
|
|
188
|
+
/^(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)(?:\s*:\s*[^=]*?[^=\s])?\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z0-9_$]+)\s*=>/
|
|
189
189
|
)) ||
|
|
190
190
|
(match = stripped.match(/^(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*(?:async\s*)?function\b/))
|
|
191
191
|
fn_name = match[1]
|
|
@@ -210,7 +210,7 @@ module Rubik
|
|
|
210
210
|
|
|
211
211
|
def parse_method(line, stripped, line_num, idx, current_class)
|
|
212
212
|
return unless (match = stripped.match(
|
|
213
|
-
/^(?:(?:public|private|protected|static|async|get|set)\s+)*([A-Za-z0-9_$]+)\s*\([^)]*\)(?:\s*:\s*[^{]
|
|
213
|
+
/^(?:(?:public|private|protected|static|async|get|set)\s+)*([A-Za-z0-9_$]+)\s*\([^)]*\)(?:\s*:\s*[^{]*?[^{\s])?\s*\{/
|
|
214
214
|
))
|
|
215
215
|
|
|
216
216
|
method_name = match[1]
|
|
@@ -21,8 +21,12 @@ module Rubik
|
|
|
21
21
|
parse_go(text)
|
|
22
22
|
when :rust
|
|
23
23
|
parse_rust(text)
|
|
24
|
-
when :zig, :c
|
|
24
|
+
when :zig, :c, :cpp, :"c++", :cxx
|
|
25
25
|
parse_zig_c(text)
|
|
26
|
+
when :python, :py
|
|
27
|
+
parse_python(text)
|
|
28
|
+
when :typescript, :javascript, :ts, :js
|
|
29
|
+
parse_ts_js(text)
|
|
26
30
|
else
|
|
27
31
|
parse_generic(text)
|
|
28
32
|
end
|
|
@@ -49,11 +53,11 @@ module Rubik
|
|
|
49
53
|
def parse_go(text)
|
|
50
54
|
entries = []
|
|
51
55
|
text.each_line do |line|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
next unless (m = line.match(/^([^:]+):(\d+):(\d+):(.*)$/))
|
|
57
|
+
|
|
58
|
+
msg = m[4].strip
|
|
59
|
+
cat = msg.start_with?("undefined:") ? :undefined_ref : :syntax
|
|
60
|
+
entries << Entry.new(file: m[1], line: m[2], col: m[3], message: msg, category: cat, raw_text: line)
|
|
57
61
|
end
|
|
58
62
|
entries
|
|
59
63
|
end
|
|
@@ -63,9 +67,9 @@ module Rubik
|
|
|
63
67
|
current_error = nil
|
|
64
68
|
|
|
65
69
|
text.each_line do |line|
|
|
66
|
-
if (m = line.match(/^error\[(E\d+)\]
|
|
67
|
-
current_error = { code: m[1], message: m[2], category: :type }
|
|
68
|
-
elsif current_error && (m = line.match(/-->\s*([
|
|
70
|
+
if (m = line.match(/^error\[(E\d+)\]:(.*)$/))
|
|
71
|
+
current_error = { code: m[1], message: m[2].strip, category: :type }
|
|
72
|
+
elsif current_error && (m = line.match(/-->\s*([^:\s]+):(\d+):(\d+)/))
|
|
69
73
|
entries << Entry.new(
|
|
70
74
|
file: m[1],
|
|
71
75
|
line: m[2],
|
|
@@ -84,20 +88,68 @@ module Rubik
|
|
|
84
88
|
def parse_zig_c(text)
|
|
85
89
|
entries = []
|
|
86
90
|
text.each_line do |line|
|
|
87
|
-
next unless (m = line.match(/^([^:]+):(\d+):(\d+):\s*error
|
|
91
|
+
next unless (m = line.match(/^([^:]+):(\d+):(\d+):\s*error:(.*)$/))
|
|
88
92
|
|
|
89
|
-
msg = m[4]
|
|
93
|
+
msg = m[4].strip
|
|
90
94
|
cat = msg.include?("undeclared identifier") || msg.include?("use of undeclared") ? :undefined_ref : :syntax
|
|
91
95
|
entries << Entry.new(file: m[1], line: m[2], col: m[3], message: msg, category: cat, raw_text: line)
|
|
92
96
|
end
|
|
93
97
|
entries
|
|
94
98
|
end
|
|
95
99
|
|
|
100
|
+
def parse_python(text)
|
|
101
|
+
entries = []
|
|
102
|
+
current_file = nil
|
|
103
|
+
current_line = nil
|
|
104
|
+
|
|
105
|
+
text.each_line do |line|
|
|
106
|
+
if (m = line.match(/File "([^"]+)", line (\d+)(?:, in .*)?/))
|
|
107
|
+
current_file = m[1]
|
|
108
|
+
current_line = m[2].to_i
|
|
109
|
+
elsif (m = line.match(/^([A-Za-z0-9_]+Error|Exception):(.*)$/))
|
|
110
|
+
cat = m[1] =~ /\A(?:Syntax|Indentation|Tab)Error\z/ ? :syntax : :type
|
|
111
|
+
entries << Entry.new(
|
|
112
|
+
file: current_file,
|
|
113
|
+
line: current_line,
|
|
114
|
+
message: "#{m[1]}: #{m[2].strip}",
|
|
115
|
+
category: cat,
|
|
116
|
+
raw_text: line
|
|
117
|
+
)
|
|
118
|
+
current_file = nil
|
|
119
|
+
current_line = nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
entries.empty? ? parse_generic(text) : entries
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_ts_js(text)
|
|
126
|
+
entries = []
|
|
127
|
+
text.each_line do |line|
|
|
128
|
+
next unless (m = line.match(%r{^((?:[a-zA-Z]:[\\/])?[^(:\s]+)(?:\((\d+),(\d+)\)|:(\d+):(\d+))(?::\s*-\s*|:\s*|\s*-\s*|\s+)error(?:\s+(TS\d+))?:?(.*)$}i))
|
|
129
|
+
|
|
130
|
+
file = m[1]
|
|
131
|
+
line_num = (m[2] || m[4]).to_i
|
|
132
|
+
col_num = (m[3] || m[5]).to_i
|
|
133
|
+
code = m[6]
|
|
134
|
+
msg = m[7].strip
|
|
135
|
+
entries << Entry.new(
|
|
136
|
+
file: file,
|
|
137
|
+
line: line_num,
|
|
138
|
+
col: col_num,
|
|
139
|
+
code: code,
|
|
140
|
+
message: msg,
|
|
141
|
+
category: :syntax,
|
|
142
|
+
raw_text: line
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
entries.empty? ? parse_generic(text) : entries
|
|
146
|
+
end
|
|
147
|
+
|
|
96
148
|
def parse_generic(text)
|
|
97
149
|
entries = []
|
|
98
150
|
text.each_line do |line|
|
|
99
|
-
if (m = line.match(/^([^:\s]+):(\d+):(?:\d+:)
|
|
100
|
-
entries << Entry.new(file: m[1], line: m[2], message: m[3], category: :unknown, raw_text: line)
|
|
151
|
+
if (m = line.match(/^([^:\s]+):(\d+):(?:\d+:)?(.*)$/))
|
|
152
|
+
entries << Entry.new(file: m[1], line: m[2], message: m[3].strip, category: :unknown, raw_text: line)
|
|
101
153
|
end
|
|
102
154
|
end
|
|
103
155
|
entries
|
|
@@ -14,7 +14,8 @@ module Rubik
|
|
|
14
14
|
raw = url.to_s.strip
|
|
15
15
|
return raw if raw.empty?
|
|
16
16
|
|
|
17
|
-
clean = raw
|
|
17
|
+
clean = raw
|
|
18
|
+
clean = clean.delete_suffix("/") while clean.end_with?("/")
|
|
18
19
|
return clean if clean.end_with?("/chat/completions") || clean.end_with?("/completions")
|
|
19
20
|
|
|
20
21
|
if clean == "https://api.deepseek.com"
|
data/lib/rubik/llm/registry.rb
CHANGED
|
@@ -98,87 +98,15 @@ module Rubik
|
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def register_provider_spec(p_name, p_spec)
|
|
101
|
-
type = p_spec["type"].to_s.downcase
|
|
102
101
|
endpoint = p_spec["endpoint"] || p_spec["host"] || p_spec["url"]
|
|
103
|
-
api_key_env = p_spec["api_key_env"]
|
|
104
|
-
api_key = p_spec["api_key"]
|
|
105
102
|
global_headers = extract_global_headers(@config)
|
|
106
103
|
spec_headers = p_spec["headers"] || {}
|
|
107
104
|
headers = global_headers.merge(spec_headers)
|
|
108
105
|
models = p_spec["models"] || []
|
|
109
106
|
default_model = p_spec["default_model"] || (models.is_a?(Array) && models.first.is_a?(Hash) ? models.first["name"] : models.first)
|
|
110
107
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
OpenAICompatibleProvider.new(
|
|
114
|
-
name: p_name,
|
|
115
|
-
endpoint: endpoint || "https://api.openai.com/v1/chat/completions",
|
|
116
|
-
api_key: api_key,
|
|
117
|
-
api_key_env: api_key_env,
|
|
118
|
-
headers: headers,
|
|
119
|
-
model: default_model || "gpt-4o",
|
|
120
|
-
default_context_limit: spec_limit(p_spec, :context),
|
|
121
|
-
default_max_output_tokens: spec_limit(p_spec, :output),
|
|
122
|
-
tool_native: p_spec.fetch("tool_native", true)
|
|
123
|
-
)
|
|
124
|
-
when "ollama"
|
|
125
|
-
OllamaProvider.new(
|
|
126
|
-
name: p_name,
|
|
127
|
-
endpoint: endpoint || "http://localhost:11434",
|
|
128
|
-
model: default_model || "qwen2.5-coder:7b",
|
|
129
|
-
num_ctx: p_spec["num_ctx"],
|
|
130
|
-
headers: headers
|
|
131
|
-
)
|
|
132
|
-
when "anthropic"
|
|
133
|
-
AnthropicProvider.new(
|
|
134
|
-
name: p_name,
|
|
135
|
-
model: default_model || "claude-sonnet-5-5",
|
|
136
|
-
# Honor the same api_key / api_key_env that
|
|
137
|
-
# openai-compatible specs already forward;
|
|
138
|
-
# otherwise Anthropic auth fails silently.
|
|
139
|
-
api_key: api_key,
|
|
140
|
-
api_key_env: api_key_env,
|
|
141
|
-
headers: headers
|
|
142
|
-
)
|
|
143
|
-
else
|
|
144
|
-
# Default fallback to openai_compatible
|
|
145
|
-
OpenAICompatibleProvider.new(
|
|
146
|
-
name: p_name,
|
|
147
|
-
endpoint: endpoint || "http://localhost:8000/v1/chat/completions",
|
|
148
|
-
api_key: api_key,
|
|
149
|
-
api_key_env: api_key_env,
|
|
150
|
-
headers: headers,
|
|
151
|
-
model: default_model || "default",
|
|
152
|
-
default_context_limit: spec_limit(p_spec, :context),
|
|
153
|
-
default_max_output_tokens: spec_limit(p_spec, :output),
|
|
154
|
-
tool_native: p_spec.fetch("tool_native", true)
|
|
155
|
-
)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
@providers[p_name] = provider_instance
|
|
159
|
-
|
|
160
|
-
# Register models in catalog
|
|
161
|
-
return unless models.is_a?(Array)
|
|
162
|
-
|
|
163
|
-
models.each do |m_entry|
|
|
164
|
-
if m_entry.is_a?(Hash)
|
|
165
|
-
m_name = m_entry["name"]
|
|
166
|
-
c_limit = m_entry["context_limit"] || m_entry["context_window"] || 32_768
|
|
167
|
-
@models_catalog["#{p_name}/#{m_name}"] = {
|
|
168
|
-
provider: p_name,
|
|
169
|
-
name: m_name,
|
|
170
|
-
context_limit: c_limit.to_i,
|
|
171
|
-
endpoint: endpoint
|
|
172
|
-
}
|
|
173
|
-
elsif m_entry.is_a?(String)
|
|
174
|
-
@models_catalog["#{p_name}/#{m_entry}"] = {
|
|
175
|
-
provider: p_name,
|
|
176
|
-
name: m_entry,
|
|
177
|
-
context_limit: 32_768,
|
|
178
|
-
endpoint: endpoint
|
|
179
|
-
}
|
|
180
|
-
end
|
|
181
|
-
end
|
|
108
|
+
@providers[p_name] = build_provider_instance(p_name, p_spec, headers, default_model, endpoint)
|
|
109
|
+
register_models_catalog(p_name, models, endpoint)
|
|
182
110
|
end
|
|
183
111
|
|
|
184
112
|
def register(provider_name, provider_instance)
|
|
@@ -331,6 +259,85 @@ module Rubik
|
|
|
331
259
|
end
|
|
332
260
|
result
|
|
333
261
|
end
|
|
262
|
+
|
|
263
|
+
private
|
|
264
|
+
|
|
265
|
+
def build_provider_instance(p_name, p_spec, headers, default_model, endpoint)
|
|
266
|
+
type = p_spec["type"].to_s.downcase
|
|
267
|
+
api_key_env = p_spec["api_key_env"]
|
|
268
|
+
api_key = p_spec["api_key"]
|
|
269
|
+
|
|
270
|
+
case type
|
|
271
|
+
when "openai_compatible", "openai"
|
|
272
|
+
OpenAICompatibleProvider.new(
|
|
273
|
+
name: p_name,
|
|
274
|
+
endpoint: endpoint || "https://api.openai.com/v1/chat/completions",
|
|
275
|
+
api_key: api_key,
|
|
276
|
+
api_key_env: api_key_env,
|
|
277
|
+
headers: headers,
|
|
278
|
+
model: default_model || "gpt-4o",
|
|
279
|
+
default_context_limit: spec_limit(p_spec, :context),
|
|
280
|
+
default_max_output_tokens: spec_limit(p_spec, :output),
|
|
281
|
+
tool_native: p_spec.fetch("tool_native", true)
|
|
282
|
+
)
|
|
283
|
+
when "ollama"
|
|
284
|
+
OllamaProvider.new(
|
|
285
|
+
name: p_name,
|
|
286
|
+
endpoint: endpoint || "http://localhost:11434",
|
|
287
|
+
model: default_model || "qwen2.5-coder:7b",
|
|
288
|
+
num_ctx: p_spec["num_ctx"],
|
|
289
|
+
headers: headers
|
|
290
|
+
)
|
|
291
|
+
when "anthropic"
|
|
292
|
+
AnthropicProvider.new(
|
|
293
|
+
name: p_name,
|
|
294
|
+
model: default_model || "claude-sonnet-5-5",
|
|
295
|
+
# Honor the same api_key / api_key_env that
|
|
296
|
+
# openai-compatible specs already forward;
|
|
297
|
+
# otherwise Anthropic auth fails silently.
|
|
298
|
+
api_key: api_key,
|
|
299
|
+
api_key_env: api_key_env,
|
|
300
|
+
headers: headers
|
|
301
|
+
)
|
|
302
|
+
else
|
|
303
|
+
# Default fallback to openai_compatible
|
|
304
|
+
OpenAICompatibleProvider.new(
|
|
305
|
+
name: p_name,
|
|
306
|
+
endpoint: endpoint || "http://localhost:8000/v1/chat/completions",
|
|
307
|
+
api_key: api_key,
|
|
308
|
+
api_key_env: api_key_env,
|
|
309
|
+
headers: headers,
|
|
310
|
+
model: default_model || "default",
|
|
311
|
+
default_context_limit: spec_limit(p_spec, :context),
|
|
312
|
+
default_max_output_tokens: spec_limit(p_spec, :output),
|
|
313
|
+
tool_native: p_spec.fetch("tool_native", true)
|
|
314
|
+
)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def register_models_catalog(p_name, models, endpoint)
|
|
319
|
+
return unless models.is_a?(Array)
|
|
320
|
+
|
|
321
|
+
models.each do |m_entry|
|
|
322
|
+
if m_entry.is_a?(Hash)
|
|
323
|
+
m_name = m_entry["name"]
|
|
324
|
+
c_limit = m_entry["context_limit"] || m_entry["context_window"] || 32_768
|
|
325
|
+
@models_catalog["#{p_name}/#{m_name}"] = {
|
|
326
|
+
provider: p_name,
|
|
327
|
+
name: m_name,
|
|
328
|
+
context_limit: c_limit.to_i,
|
|
329
|
+
endpoint: endpoint
|
|
330
|
+
}
|
|
331
|
+
elsif m_entry.is_a?(String)
|
|
332
|
+
@models_catalog["#{p_name}/#{m_entry}"] = {
|
|
333
|
+
provider: p_name,
|
|
334
|
+
name: m_entry,
|
|
335
|
+
context_limit: 32_768,
|
|
336
|
+
endpoint: endpoint
|
|
337
|
+
}
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
334
341
|
end
|
|
335
342
|
end
|
|
336
343
|
end
|