rubik 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ATTRIBUTION.md +1 -1
- data/CHANGELOG.md +118 -1
- data/lib/rubik/ast/cache_store.rb +69 -5
- data/lib/rubik/ast/extraction_result.rb +6 -0
- data/lib/rubik/ast/graph.rb +32 -5
- data/lib/rubik/ast/ruby_extractor.rb +8 -2
- data/lib/rubik/buffer/apply_contract.rb +6 -0
- data/lib/rubik/buffer/manager.rb +37 -27
- data/lib/rubik/config/defaults.rb +7 -1
- 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/budget_manager.rb +2 -0
- data/lib/rubik/orchestrator/loop.rb +1 -0
- data/lib/rubik/orchestrator/shell_deps.rb +4 -0
- data/lib/rubik/orchestrator/turn_executor/user_channel.rb +2 -0
- 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/prompt/doctrine.rb +10 -15
- data/lib/rubik/prompt/tool_teachings.rb +29 -35
- data/lib/rubik/repair/loop.rb +29 -15
- data/lib/rubik/repl/commands/agent_context.rb +1 -0
- data/lib/rubik/repl/commands/system.rb +2 -0
- data/lib/rubik/repl/turn_recorder.rb +3 -0
- data/lib/rubik/security/redactor.rb +13 -9
- data/lib/rubik/storage/vakanuvis/gate.rb +13 -8
- data/lib/rubik/storage/vakanuvis/harness.rb +115 -112
- data/lib/rubik/storage/vakanuvis/orientation.rb +277 -0
- data/lib/rubik/storage/vakanuvis/service.rb +5 -1
- data/lib/rubik/storage/vakanuvis/vocabulary.rb +33 -2
- data/lib/rubik/storage/vakanuvis.rb +8 -1
- data/lib/rubik/tools/builtins/lifecycle.rb +42 -47
- data/lib/rubik/tools/builtins/vakanuvis.rb +13 -15
- data/lib/rubik/ui/capability.rb +19 -5
- data/lib/rubik/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a55adca9a39a813ea7620e63018dc98ae7c902142e9a1fa1dc5afe92637ef88
|
|
4
|
+
data.tar.gz: 72d7ca21a839051017fd515fe63a0cc99573054ec3a23c5fd179a6e37c623460
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4b426b159392a724d6e66d96dfd09db13cee8882eec7f3dcf0afaaf3d683325d3a74731f0e34abf43e77c1f1655d850cecd2912e324e16da9ad84bd74c1eb71
|
|
7
|
+
data.tar.gz: 36090da4466990229e789cf5912cf880bf4193eb300f8823abc7a1cefed4bf6253aff44774ff9d18e19ebbfb3f874d7c70434d7ab72692ba474bd9e6ba337abf
|
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
|
|
124
|
+
| **`minitest`** | Ryan Davis & seattlerb | MIT | Fast, modular test suite powering Rubik's 4,000+ 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,6 +6,121 @@ 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
|
+
## [0.5.0] - 2026-10-31
|
|
10
|
+
|
|
11
|
+
A vakanüvis minor: the memory system gains a harness-owned auto-digest
|
|
12
|
+
loop and a deterministic topics orientation injected at session start
|
|
13
|
+
and after compaction, the prompt doctrine's recall/record contradiction
|
|
14
|
+
is closed with a single storage write path, duck-typed AST cache stores
|
|
15
|
+
stop breaking `save_to_cache`, and the security redactor's clean-text
|
|
16
|
+
path skips its intermediate allocations.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Harness-owned auto-digest of touched topics (#346, #348):** topics
|
|
21
|
+
recorded or updated since the last digest are tracked in the Harness
|
|
22
|
+
and auto-digested at goal completion and queued (spawn-free) at
|
|
23
|
+
shutdown, gated by the `subagents.vakanuvis.auto_digest` knob
|
|
24
|
+
(default on); per-topic error isolation re-arms failed or refused
|
|
25
|
+
topics for the next flush instead of losing them, the drain window
|
|
26
|
+
between store write and touched-topic tracking is closed with one
|
|
27
|
+
monitor section, and shutdown always joins/kills the digest worker
|
|
28
|
+
with an honest dmesg listing parked topics.
|
|
29
|
+
- **Deterministic topics orientation (#343):** session start and budget
|
|
30
|
+
compaction inject a compact, deterministic overview of the memory
|
|
31
|
+
topics so the model is oriented without a recall round-trip.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **Keywords-in-query contradiction closed (#342):** the vakanüvis
|
|
36
|
+
teaching now separates the recall and record directions, stops
|
|
37
|
+
telling the model to add Keywords to recall queries, renders a single
|
|
38
|
+
write-path storage variant when vakanüvis is active, and folds the
|
|
39
|
+
doctrine down to two core duties with the duplicated
|
|
40
|
+
`goal_storage_rule` wording removed (SSOT).
|
|
41
|
+
- **Duck-typed AST cache stores (#347):** `save_to_cache` probes
|
|
42
|
+
`store.method(:save).parameters` once and dispatches deterministically
|
|
43
|
+
instead of rescuing `ArgumentError` (which swallowed genuine errors
|
|
44
|
+
and double-fired side-effecting stores); 1-arity stores get the
|
|
45
|
+
snapshot without statuses plus a warn naming the store class, while
|
|
46
|
+
stores declaring the canonical kwarg signature keep the full-map
|
|
47
|
+
delivery — including in the clean-graph case.
|
|
48
|
+
|
|
49
|
+
### Performance
|
|
50
|
+
|
|
51
|
+
- **Redactor clean-text fast path (#337):** `Redactor.redact` no longer
|
|
52
|
+
allocates an intermediate scrub/dup string before testing
|
|
53
|
+
`QUICK_CHECK_REGEX` on valid-encoding clean input.
|
|
54
|
+
|
|
55
|
+
## [0.4.2] - 2026-10-29
|
|
56
|
+
|
|
57
|
+
A reliability patch: plugin file loads become transactional with
|
|
58
|
+
timing-safe integrity checks, MCP transport and handshake lifecycles
|
|
59
|
+
shed their remaining deadlocks and wedged-boot races, buffer apply
|
|
60
|
+
failures report faithfully, the repair loop can no longer return
|
|
61
|
+
unverified code as success, and vakanüvis slugs stop colliding across
|
|
62
|
+
scripts and long topics.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- **MCP transport lifecycle deadlocks closed (#308):** server stderr is
|
|
67
|
+
drained on a dedicated thread with a bounded diagnostic tail (also
|
|
68
|
+
attached to `TimeoutError` messages), a dead transport incarnation is
|
|
69
|
+
reaped on re-open instead of leaking its file descriptors and waiter,
|
|
70
|
+
and a fast async boot can no longer park a dead thread handle that
|
|
71
|
+
permanently wedged the sync-boot gate after `stop_all`.
|
|
72
|
+
- **AST persistent cache no longer pins stale symbols (#308):** a failed
|
|
73
|
+
re-index is excluded from the cache snapshot so its protected stale
|
|
74
|
+
symbols are never validated under the current file's fresh digest;
|
|
75
|
+
per-entry extraction status is persisted and validated on load
|
|
76
|
+
(cache schema version 2, backward compatible with existing caches).
|
|
77
|
+
- **Plugin file loads are transactional (#334):** `load_plugin_file`
|
|
78
|
+
prepares the replacement before touching the running plugin so a
|
|
79
|
+
failed re-add no longer bricks the live plugin; reloading a previously
|
|
80
|
+
`:active` plugin that lands in `:pending` rolls back to the last
|
|
81
|
+
working version; the manifest TOCTOU window is closed by evaluating
|
|
82
|
+
the in-memory verified content via `TOPLEVEL_BINDING.eval`; manifest
|
|
83
|
+
SHA-256 checks use timing-safe `secure_compare`; and directory
|
|
84
|
+
creation failures are always reported with `Rubik::Log.warn`.
|
|
85
|
+
- **MCP handshake and boot races closed (#336):** the manager's
|
|
86
|
+
handshake lock is split so a slow connect no longer serializes
|
|
87
|
+
unrelated state reads; async boots carry epoch-tagged guards that
|
|
88
|
+
discard a superseded connection instead of adopting it; the stdio
|
|
89
|
+
transport gains a synchronized reaper and a bounded `stderr_tail`
|
|
90
|
+
diagnostic; and rollback paths log the client they stopped.
|
|
91
|
+
- **Repair loop no longer reports unverified code as success (#335):**
|
|
92
|
+
a missing diagnostics adapter, an empty LLM proposal, or an exception
|
|
93
|
+
mid-repair now undoes the buffer snapshot and returns `ok: false`
|
|
94
|
+
(flagging `unverified:` when syntax could not be checked) instead of
|
|
95
|
+
writing broken code; LSP rollback only restores clients that are
|
|
96
|
+
alive and running.
|
|
97
|
+
- **Buffer apply failures report faithfully (#333):** `apply!` commits
|
|
98
|
+
`last_applied` and `last_conflicts` in an `ensure` block so files
|
|
99
|
+
already written to disk before a failure are remembered and
|
|
100
|
+
`clean_buffer_message` no longer hides them; garbage-skipped edits
|
|
101
|
+
are tracked in `last_skipped`/`ApplyFailedError.skipped` and surfaced
|
|
102
|
+
in `ApplyContract.failure_lines` and tool outputs; and conflict event
|
|
103
|
+
payloads are frozen copies, closing the live-array aliasing mutation.
|
|
104
|
+
- **Vakanüvis slugs no longer collide across scripts and topics
|
|
105
|
+
(#332):** slug generation Unicode-folds and transliterates Turkish
|
|
106
|
+
characters (ç, ğ, ı, ö, ş, ü) and appends a deterministic 6-character
|
|
107
|
+
SHA-256 suffix when normalization is lossy, when the topic exceeds the
|
|
108
|
+
40-char limit, or when punctuation-only input yields the untitled
|
|
109
|
+
fallback — distinct topics can no longer resolve to the same storage
|
|
110
|
+
key.
|
|
111
|
+
- **UI capability detection unwraps capture streams (#331):**
|
|
112
|
+
`Capability.resolve_mode`, `color?`, and `spinner?` unwrap `TeeIO`
|
|
113
|
+
targets so a live terminal wrapped in a capture stream renders styled
|
|
114
|
+
dialogs and notices again; non-empty truthy `CLICOLOR_FORCE` values
|
|
115
|
+
(e.g. `true`, `yes`) now force ANSI color while `NO_COLOR` keeps
|
|
116
|
+
precedence.
|
|
117
|
+
|
|
118
|
+
### Performance
|
|
119
|
+
|
|
120
|
+
- **Prism `CallNode` filter lists hoisted to frozen Sets (#307):** the
|
|
121
|
+
Ruby extractor's hot walk no longer allocates two temporary Arrays per
|
|
122
|
+
call node (~140k fewer heap objects per 10k file extractions).
|
|
123
|
+
|
|
9
124
|
## [0.4.1] - 2026-10-29
|
|
10
125
|
|
|
11
126
|
A hardening patch: CodeQL-driven fixes remove ReDoS-prone regular
|
|
@@ -892,6 +1007,8 @@ installation section.
|
|
|
892
1007
|
- Context: serialized index mutations, snapshot-outside-lock reconcile,
|
|
893
1008
|
streaming-first truncation.
|
|
894
1009
|
|
|
895
|
-
[Unreleased]: https://github.com/ozgurulukir/rubik/compare/v0.
|
|
1010
|
+
[Unreleased]: https://github.com/ozgurulukir/rubik/compare/v0.5.0...HEAD
|
|
1011
|
+
[0.5.0]: https://github.com/ozgurulukir/rubik/compare/v0.4.2...v0.5.0
|
|
1012
|
+
[0.4.2]: https://github.com/ozgurulukir/rubik/compare/v0.4.1...v0.4.2
|
|
896
1013
|
[0.4.1]: https://github.com/ozgurulukir/rubik/compare/v0.4.0...v0.4.1
|
|
897
1014
|
[0.1.0]: https://github.com/ozgurulukir/rubik/releases/tag/v0.1.0
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "digest"
|
|
6
6
|
require_relative "symbol"
|
|
7
|
+
require_relative "extraction_result"
|
|
7
8
|
require_relative "../platform"
|
|
8
9
|
|
|
9
10
|
module Rubik
|
|
@@ -44,9 +45,11 @@ module Rubik
|
|
|
44
45
|
File.join(@home_dir, ".rubik", "cache", project_hash, "ast_index.json")
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
def save(file_symbols_hash)
|
|
48
|
+
def save(file_symbols_hash, statuses: {})
|
|
48
49
|
return false unless file_symbols_hash.is_a?(Hash)
|
|
49
50
|
|
|
51
|
+
statuses = {} unless statuses.is_a?(Hash)
|
|
52
|
+
|
|
50
53
|
data = {
|
|
51
54
|
"version" => SCHEMA_VERSION,
|
|
52
55
|
"last_scan" => Time.now.to_i,
|
|
@@ -84,12 +87,17 @@ module Rubik
|
|
|
84
87
|
|
|
85
88
|
# Bodies are intentionally NOT persisted: they dominate the cache
|
|
86
89
|
# size and are re-extracted lazily from source via Symbol#body.
|
|
90
|
+
# `status` carries the issue-#147 extraction status so the
|
|
91
|
+
# loaded graph can keep telling "parsed clean" from "partial" —
|
|
92
|
+
# entries predating the field read as :ok, the only status they
|
|
93
|
+
# could ever have been saved under.
|
|
87
94
|
data["files"][rel_path] = {
|
|
88
95
|
"language" => lang,
|
|
89
96
|
"mtime" => stat.mtime.to_i,
|
|
90
97
|
"mtime_f" => stat.mtime.to_f,
|
|
91
98
|
"size" => stat.size,
|
|
92
99
|
"digest" => digest,
|
|
100
|
+
"status" => (statuses[file_path] || :ok).to_s,
|
|
93
101
|
"symbols" => syms.map { |sym| sym.to_h(include_body: false) }
|
|
94
102
|
}
|
|
95
103
|
end
|
|
@@ -102,15 +110,56 @@ module Rubik
|
|
|
102
110
|
false
|
|
103
111
|
end
|
|
104
112
|
|
|
113
|
+
# Dispatch a cache save across duck-typed stores. The canonical #save
|
|
114
|
+
# contract is (hash, statuses: {}) with a FULL statuses map (every
|
|
115
|
+
# snapshot file present, :ok or not), but gem consumers may hand the
|
|
116
|
+
# graph a narrower store. The signature is probed instead of rescuing
|
|
117
|
+
# ArgumentError around the call — a genuine ArgumentError raised inside
|
|
118
|
+
# a store's save must surface, and a failing save must never be
|
|
119
|
+
# silently retried single-arg (double side effects).
|
|
120
|
+
#
|
|
121
|
+
# Classification: a `statuses:` keyword (or **kwargs) means the kwarg
|
|
122
|
+
# call; otherwise the store is treated as 1-arity and non-ok statuses
|
|
123
|
+
# are dropped with a warn (such entries reload as :ok). When the
|
|
124
|
+
# signature cannot be read at all — save resolves through
|
|
125
|
+
# method_missing, or the object has no save — the canonical kwarg call
|
|
126
|
+
# is kept: the call itself surfaces any problem, exactly as the
|
|
127
|
+
# pre-probe code did. respond_to_missing?-backed proxies report
|
|
128
|
+
# [[:rest]] and are therefore treated as 1-arity: behavioral probing
|
|
129
|
+
# would risk double invocation.
|
|
130
|
+
def self.save_via(store, snapshot, statuses)
|
|
131
|
+
accepts_statuses = begin
|
|
132
|
+
store.method(:save).parameters.any? do |param|
|
|
133
|
+
kind, name = param
|
|
134
|
+
kind == :keyrest || ([:key, :keyreq].include?(kind) && name == :statuses)
|
|
135
|
+
end
|
|
136
|
+
rescue NameError
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
return store.save(snapshot, statuses: statuses) if accepts_statuses != false
|
|
140
|
+
|
|
141
|
+
non_ok = statuses.count { |_, status| status != :ok }
|
|
142
|
+
Rubik::Log.warn(:ast, "#{store.class}#save cannot carry extraction statuses; #{non_ok} file(s) will reload as :ok") if non_ok.positive?
|
|
143
|
+
store.save(snapshot)
|
|
144
|
+
end
|
|
145
|
+
|
|
105
146
|
def load
|
|
147
|
+
load_with_statuses[0]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Same entries as #load, paired with the persisted per-file extraction
|
|
151
|
+
# status keyed by the same expanded paths (issue #147 across the cache
|
|
152
|
+
# boundary: a cache-loaded file must not report "never attempted").
|
|
153
|
+
def load_with_statuses
|
|
106
154
|
target_path = cache_path
|
|
107
|
-
return {} unless File.exist?(target_path)
|
|
155
|
+
return [{}, {}] unless File.exist?(target_path)
|
|
108
156
|
|
|
109
157
|
content = File.read(target_path)
|
|
110
158
|
data = JSON.parse(content)
|
|
111
|
-
return {} unless data.is_a?(Hash) && data["version"] == SCHEMA_VERSION && data["files"].is_a?(Hash)
|
|
159
|
+
return [{}, {}] unless data.is_a?(Hash) && data["version"] == SCHEMA_VERSION && data["files"].is_a?(Hash)
|
|
112
160
|
|
|
113
161
|
result = {}
|
|
162
|
+
statuses = {}
|
|
114
163
|
data["files"].each do |rel_path, cached_info|
|
|
115
164
|
full_path = File.expand_path(rel_path, @root_dir)
|
|
116
165
|
next unless valid_entry?(full_path, cached_info)
|
|
@@ -132,14 +181,29 @@ module Rubik
|
|
|
132
181
|
)
|
|
133
182
|
end
|
|
134
183
|
result[full_path] = symbols
|
|
184
|
+
raw_status = cached_info["status"]
|
|
185
|
+
# Entries predating the field had exactly one possible status;
|
|
186
|
+
# anything else unreadable is corrupt data, not a status — warn
|
|
187
|
+
# and fall back instead of materializing an arbitrary symbol that
|
|
188
|
+
# the symbols tool would report and save_to_cache would honor.
|
|
189
|
+
statuses[full_path] = if raw_status.nil?
|
|
190
|
+
:ok
|
|
191
|
+
else
|
|
192
|
+
status = raw_status.is_a?(String) ? raw_status.to_sym : nil
|
|
193
|
+
unless ExtractionResult::STATUSES.include?(status)
|
|
194
|
+
Rubik::Log.warn(:ast, "unknown cached extraction status #{raw_status.inspect} for #{rel_path}; treating as ok")
|
|
195
|
+
status = :ok
|
|
196
|
+
end
|
|
197
|
+
status
|
|
198
|
+
end
|
|
135
199
|
end
|
|
136
200
|
|
|
137
|
-
result
|
|
201
|
+
[result, statuses]
|
|
138
202
|
rescue JSON::ParserError, Errno::ENOENT, Errno::EACCES, TypeError, NoMethodError
|
|
139
203
|
# Corrupt-but-valid JSON shapes raise TypeError/NoMethodError inside
|
|
140
204
|
# the entry loop; EACCES covers an unreadable cache file. All mean
|
|
141
205
|
# "cache unusable" → rebuild from scratch.
|
|
142
|
-
{}
|
|
206
|
+
[{}, {}]
|
|
143
207
|
end
|
|
144
208
|
|
|
145
209
|
def valid_entry?(file_path, cached_info)
|
|
@@ -33,5 +33,11 @@ module Rubik
|
|
|
33
33
|
[:ok, :partial].include?(status)
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
|
+
|
|
37
|
+
class ExtractionResult
|
|
38
|
+
# The closed status vocabulary, shared with persistence seams: the AST
|
|
39
|
+
# cache persists it per entry and validates against it on load.
|
|
40
|
+
STATUSES = [:ok, :partial, :failed, :unavailable].freeze
|
|
41
|
+
end
|
|
36
42
|
end
|
|
37
43
|
end
|
data/lib/rubik/ast/graph.rb
CHANGED
|
@@ -89,7 +89,7 @@ module Rubik
|
|
|
89
89
|
def load_from_cache(cache_store)
|
|
90
90
|
return 0 unless cache_store
|
|
91
91
|
|
|
92
|
-
loaded_files = cache_store.
|
|
92
|
+
loaded_files, statuses = cache_store.load_with_statuses
|
|
93
93
|
return 0 if loaded_files.nil? || loaded_files.empty?
|
|
94
94
|
|
|
95
95
|
@mutex.synchronize do
|
|
@@ -99,6 +99,7 @@ module Rubik
|
|
|
99
99
|
lang = CacheStore::LANGUAGE_MAP[ext] || "code"
|
|
100
100
|
|
|
101
101
|
@file_symbols[file_path] = syms.dup
|
|
102
|
+
@extraction_status[file_path] = statuses[file_path] || :ok
|
|
102
103
|
syms.each do |sym|
|
|
103
104
|
@symbols[sym.name] = sym
|
|
104
105
|
q_key = "#{lang}:#{file_path}##{sym.name}"
|
|
@@ -113,12 +114,33 @@ module Rubik
|
|
|
113
114
|
def save_to_cache(cache_store)
|
|
114
115
|
return false unless cache_store
|
|
115
116
|
|
|
116
|
-
snapshot =
|
|
117
|
-
|
|
117
|
+
snapshot = nil
|
|
118
|
+
statuses = nil
|
|
119
|
+
skipped_failed = 0
|
|
120
|
+
@mutex.synchronize do
|
|
121
|
+
snapshot = @file_symbols.each_with_object({}) do |(file_path, symbols), copy|
|
|
122
|
+
# Failed-extraction honesty across the cache boundary: a :failed
|
|
123
|
+
# file's in-memory symbols are protected stale data or
|
|
124
|
+
# untrustworthy best-effort fallback. Persisting them would let
|
|
125
|
+
# CacheStore re-stat the CURRENT file and pin them under a fresh
|
|
126
|
+
# digest, making the staleness permanent — skip them and let the
|
|
127
|
+
# next boot re-extract.
|
|
128
|
+
if _get_extraction_status_unlocked(file_path) == :failed
|
|
129
|
+
skipped_failed += 1
|
|
130
|
+
next
|
|
131
|
+
end
|
|
132
|
+
|
|
118
133
|
copy[file_path] = symbols.dup
|
|
119
134
|
end
|
|
135
|
+
statuses = {}
|
|
136
|
+
snapshot.each_key do |file_path|
|
|
137
|
+
statuses[file_path] = _get_extraction_status_unlocked(file_path) || :ok
|
|
138
|
+
end
|
|
120
139
|
end
|
|
121
|
-
|
|
140
|
+
skipped = "cache snapshot excluded #{skipped_failed} file(s) with failed extractions; they re-extract on next boot"
|
|
141
|
+
Rubik::Log.warn(:ast, skipped) if skipped_failed.positive?
|
|
142
|
+
|
|
143
|
+
CacheStore.save_via(cache_store, snapshot, statuses)
|
|
122
144
|
end
|
|
123
145
|
|
|
124
146
|
def smart_context_for(symbol_name)
|
|
@@ -179,7 +201,6 @@ module Rubik
|
|
|
179
201
|
Rubik::Log.warn(:ast, "extraction failed for #{file_path} (#{result.detail}); keeping previously indexed symbols")
|
|
180
202
|
return existing
|
|
181
203
|
end
|
|
182
|
-
Rubik::Log.warn(:ast, "extraction failed for #{file_path} (#{result.detail}); no symbols indexed")
|
|
183
204
|
end
|
|
184
205
|
|
|
185
206
|
_remove_file_unlocked(file_path)
|
|
@@ -188,6 +209,12 @@ module Rubik
|
|
|
188
209
|
lang = CacheStore::LANGUAGE_MAP[ext] || "code"
|
|
189
210
|
|
|
190
211
|
extracted = result.symbols
|
|
212
|
+
if result.status == :failed
|
|
213
|
+
# Truthful accounting: the fallback symbols ARE indexed when
|
|
214
|
+
# nothing pre-existing needed protection — even when there are
|
|
215
|
+
# none, the log must not claim otherwise.
|
|
216
|
+
Rubik::Log.warn(:ast, "extraction failed for #{file_path} (#{result.detail}); indexing #{extracted.length} best-effort fallback symbols")
|
|
217
|
+
end
|
|
191
218
|
extracted.each do |sym|
|
|
192
219
|
@symbols[sym.name] = sym
|
|
193
220
|
q_key = "#{lang}:#{file_path}##{sym.name}"
|
|
@@ -8,6 +8,10 @@ module Rubik
|
|
|
8
8
|
module AST
|
|
9
9
|
class RubyExtractor
|
|
10
10
|
EXCLUDED_CALLS = Set.new(["def", "if", "unless", "case", "return", "raise", "puts", "p", "print", "require"]).freeze
|
|
11
|
+
# PERF: Hoist require and excluded call lists to frozen Sets to avoid per-node Array allocations in hot AST walks.
|
|
12
|
+
REQUIRE_CALLS = Set.new(["require", "require_relative"]).freeze
|
|
13
|
+
EXCLUDED_PRISM_CALLS = Set.new(["require", "require_relative", "raise", "puts", "p", "print"]).freeze
|
|
14
|
+
|
|
11
15
|
begin
|
|
12
16
|
require "prism"
|
|
13
17
|
PRISM_AVAILABLE = true
|
|
@@ -119,7 +123,8 @@ module Rubik
|
|
|
119
123
|
|
|
120
124
|
when Prism::CallNode
|
|
121
125
|
name = node.name.to_s
|
|
122
|
-
|
|
126
|
+
# PERF: Fast Set lookup avoids Array allocation on every CallNode
|
|
127
|
+
if REQUIRE_CALLS.include?(name)
|
|
123
128
|
line_num = node.location.start_line
|
|
124
129
|
args_node = node.arguments&.arguments
|
|
125
130
|
arg = args_node&.first&.slice.to_s
|
|
@@ -153,7 +158,8 @@ module Rubik
|
|
|
153
158
|
operator = curr.call_operator || "."
|
|
154
159
|
cname = "#{receiver}#{operator}#{cname}"
|
|
155
160
|
end
|
|
156
|
-
|
|
161
|
+
# PERF: Fast Set lookup avoids Array allocation per call site in method body
|
|
162
|
+
calls << cname unless EXCLUDED_PRISM_CALLS.include?(cname)
|
|
157
163
|
end
|
|
158
164
|
queue.concat(curr.compact_child_nodes) if curr.respond_to?(:compact_child_nodes)
|
|
159
165
|
end
|
|
@@ -38,6 +38,12 @@ module Rubik
|
|
|
38
38
|
lines << " • Failed: #{failure.failed}#{cause ? " (#{cause.class}: #{cause.message})" : " (#{failure.kind})"}"
|
|
39
39
|
lines << " • Still dirty — a retry writes only these: #{failure.pending.join(', ')}" unless failure.pending.empty?
|
|
40
40
|
lines << " • Skipped conflicts (disk changed externally): #{failure.conflicts.join(', ')}" unless failure.conflicts.empty?
|
|
41
|
+
if failure.respond_to?(:skipped) && !failure.skipped.empty?
|
|
42
|
+
skipped_desc = failure.skipped.map do |s|
|
|
43
|
+
s.is_a?(Hash) ? "#{s[:path]} (#{s[:reason]})" : s.to_s
|
|
44
|
+
end.join(", ")
|
|
45
|
+
lines << " • Skipped invalid paths (anti-injection/garbage): #{skipped_desc}"
|
|
46
|
+
end
|
|
41
47
|
if sync
|
|
42
48
|
annotation = sync_annotation(failure, sync)
|
|
43
49
|
lines << annotation if annotation
|
data/lib/rubik/buffer/manager.rb
CHANGED
|
@@ -21,20 +21,22 @@ module Rubik
|
|
|
21
21
|
# the underlying error. `pending` lists this run's entries that are
|
|
22
22
|
# still dirty (a retry writes only those).
|
|
23
23
|
class ApplyFailedError < StandardError
|
|
24
|
-
attr_reader :kind, :applied, :failed, :pending, :conflicts
|
|
24
|
+
attr_reader :kind, :applied, :failed, :pending, :conflicts, :skipped
|
|
25
25
|
|
|
26
|
-
def initialize(message, kind:, applied: [], failed: nil, pending: [], conflicts: [])
|
|
26
|
+
def initialize(message, kind:, applied: [], failed: nil, pending: [], conflicts: [], skipped: [])
|
|
27
27
|
super(message)
|
|
28
28
|
@kind = kind
|
|
29
29
|
@applied = applied
|
|
30
30
|
@failed = failed
|
|
31
31
|
@pending = pending
|
|
32
32
|
@conflicts = conflicts
|
|
33
|
+
@skipped = skipped
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
class Manager
|
|
37
|
-
attr_reader :root_dir, :dirty_files, :additional_roots, :rw_roots, :allowed_external_dirs, :rw_allowed_dirs,
|
|
38
|
+
attr_reader :root_dir, :dirty_files, :additional_roots, :rw_roots, :allowed_external_dirs, :rw_allowed_dirs,
|
|
39
|
+
:guard, :last_conflicts, :last_skipped
|
|
38
40
|
attr_accessor :allow_external_writes, :config
|
|
39
41
|
|
|
40
42
|
def initialize(root_dir: Dir.pwd, additional_roots: [], allowed_external_dirs: [], allow_external_writes: false,
|
|
@@ -52,7 +54,8 @@ module Rubik
|
|
|
52
54
|
@buffer = {}
|
|
53
55
|
@dirty_files = {}
|
|
54
56
|
@original_hashes = {}
|
|
55
|
-
@last_conflicts = []
|
|
57
|
+
@last_conflicts = [].freeze
|
|
58
|
+
@last_skipped = [].freeze
|
|
56
59
|
@last_applied = nil
|
|
57
60
|
end
|
|
58
61
|
|
|
@@ -317,25 +320,35 @@ module Rubik
|
|
|
317
320
|
@monitor.synchronize do
|
|
318
321
|
applied_paths = []
|
|
319
322
|
conflicts = []
|
|
323
|
+
skipped = []
|
|
320
324
|
keys_to_apply = determine_keys_to_apply(target_paths)
|
|
321
325
|
effective_policy = (policy || external_modification_policy).to_s.downcase
|
|
322
326
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
327
|
+
begin
|
|
328
|
+
keys_to_apply.each_with_index do |rel_path, index|
|
|
329
|
+
next unless @dirty_files.key?(rel_path)
|
|
326
330
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
end
|
|
331
|
+
unless validate_and_clean_garbage(rel_path)
|
|
332
|
+
skipped << { path: rel_path, reason: :path_validator_rejected }
|
|
333
|
+
next
|
|
334
|
+
end
|
|
332
335
|
|
|
333
|
-
|
|
336
|
+
if external_conflict?(rel_path) && effective_policy != "overwrite"
|
|
337
|
+
conflicts << rel_path
|
|
338
|
+
@event_bus&.emit(:buffer_apply_conflict, paths: conflicts.dup.freeze, path: rel_path)
|
|
339
|
+
next
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
apply_single_entry(rel_path, index: index, keys_to_apply: keys_to_apply,
|
|
343
|
+
applied_paths: applied_paths, conflicts: conflicts, skipped: skipped)
|
|
344
|
+
end
|
|
345
|
+
ensure
|
|
346
|
+
@last_conflicts = conflicts.dup.freeze
|
|
347
|
+
@last_skipped = skipped.dup.freeze
|
|
348
|
+
@last_applied = { at: Time.now.utc, paths: applied_paths.dup.freeze } unless applied_paths.empty?
|
|
334
349
|
end
|
|
335
350
|
|
|
336
|
-
@
|
|
337
|
-
@last_applied = { at: Time.now.utc, paths: applied_paths.dup.freeze } unless applied_paths.empty?
|
|
338
|
-
@event_bus&.emit(:buffer_applied, paths: applied_paths) unless applied_paths.empty?
|
|
351
|
+
@event_bus&.emit(:buffer_applied, paths: applied_paths.dup.freeze) unless applied_paths.empty?
|
|
339
352
|
applied_paths
|
|
340
353
|
end
|
|
341
354
|
end
|
|
@@ -353,7 +366,8 @@ module Rubik
|
|
|
353
366
|
@buffer.clear
|
|
354
367
|
@dirty_files.clear
|
|
355
368
|
@original_hashes.clear
|
|
356
|
-
@last_conflicts.
|
|
369
|
+
@last_conflicts = [].freeze
|
|
370
|
+
@last_skipped = [].freeze
|
|
357
371
|
@last_applied = nil
|
|
358
372
|
end
|
|
359
373
|
end
|
|
@@ -410,7 +424,7 @@ module Rubik
|
|
|
410
424
|
end
|
|
411
425
|
end
|
|
412
426
|
|
|
413
|
-
def apply_single_entry(rel_path, index:, keys_to_apply:, applied_paths:, conflicts:)
|
|
427
|
+
def apply_single_entry(rel_path, index:, keys_to_apply:, applied_paths:, conflicts:, skipped:)
|
|
414
428
|
full_path = File.expand_path(rel_path, @root_dir)
|
|
415
429
|
|
|
416
430
|
# TOCTOU close: re-verify containment immediately before the
|
|
@@ -422,15 +436,11 @@ module Rubik
|
|
|
422
436
|
containment_kind, = containment_root(full_path)
|
|
423
437
|
unless containment_kind
|
|
424
438
|
@event_bus&.emit(:buffer_containment_blocked, path: rel_path)
|
|
425
|
-
# Commit what this run has seen so "apply! rewrites
|
|
426
|
-
# last_conflicts on every run" holds for readers (AutoApply's
|
|
427
|
-
# report_conflicts) even on the failure path.
|
|
428
|
-
@last_conflicts = conflicts
|
|
429
439
|
raise apply_failure(
|
|
430
440
|
"Write blocked: '#{rel_path}' resolves outside all allowed roots at apply time " \
|
|
431
441
|
"(possible symlink swap since buffering). Re-verify the workspace and retry.",
|
|
432
442
|
kind: :containment_blocked, applied: applied_paths, failed: rel_path,
|
|
433
|
-
pending: pending_after(keys_to_apply, index), conflicts: conflicts
|
|
443
|
+
pending: pending_after(keys_to_apply, index), conflicts: conflicts, skipped: skipped
|
|
434
444
|
)
|
|
435
445
|
end
|
|
436
446
|
|
|
@@ -439,13 +449,12 @@ module Rubik
|
|
|
439
449
|
begin
|
|
440
450
|
Rubik::Platform.atomic_write(full_path, content)
|
|
441
451
|
rescue StandardError => e
|
|
442
|
-
@last_conflicts = conflicts
|
|
443
452
|
raise apply_failure(
|
|
444
453
|
"Apply failed for '#{rel_path}': #{e.class}: #{e.message}; " \
|
|
445
454
|
"#{applied_paths.empty? ? 'no file was written' : "already written: #{applied_paths.join(', ')}"}; " \
|
|
446
455
|
"#{pending_after(keys_to_apply, index).size} file(s) still dirty",
|
|
447
456
|
kind: :write_failed, applied: applied_paths, failed: rel_path,
|
|
448
|
-
pending: pending_after(keys_to_apply, index), conflicts: conflicts
|
|
457
|
+
pending: pending_after(keys_to_apply, index), conflicts: conflicts, skipped: skipped
|
|
449
458
|
), cause: e
|
|
450
459
|
end
|
|
451
460
|
|
|
@@ -494,11 +503,12 @@ module Rubik
|
|
|
494
503
|
|
|
495
504
|
# Build the structured partial-apply error with frozen report copies —
|
|
496
505
|
# a reader must see the state at raise time, not later mutations.
|
|
497
|
-
def apply_failure(message, kind:, applied:, failed:, pending:, conflicts:)
|
|
506
|
+
def apply_failure(message, kind:, applied:, failed:, pending:, conflicts:, skipped: [])
|
|
498
507
|
ApplyFailedError.new(
|
|
499
508
|
message, kind: kind,
|
|
500
509
|
applied: applied.dup.freeze, failed: failed,
|
|
501
|
-
pending: pending.dup.freeze, conflicts: conflicts.dup.freeze
|
|
510
|
+
pending: pending.dup.freeze, conflicts: conflicts.dup.freeze,
|
|
511
|
+
skipped: skipped.dup.freeze
|
|
502
512
|
)
|
|
503
513
|
end
|
|
504
514
|
|
|
@@ -166,15 +166,21 @@ module Rubik
|
|
|
166
166
|
# annal + wiki derleyicisi. enabled: harness kur/kurulama; distiller:
|
|
167
167
|
# Düzlem 2 LLM doğumu (bütçe: flush_events x max_steps); model: nil =
|
|
168
168
|
# root provider; retention: supersede|purge; root_turns: kök-turn annal'ı.
|
|
169
|
+
# auto_digest: goal bitişinde/kapanışta dokunulan konuların hedefli damıtımı (issue #341).
|
|
169
170
|
"vakanuvis" => {
|
|
170
171
|
"enabled" => true,
|
|
171
172
|
"distiller" => true,
|
|
173
|
+
"auto_digest" => true,
|
|
172
174
|
"max_steps" => 8,
|
|
173
175
|
"flush_events" => 10,
|
|
174
176
|
"max_digest_jobs" => 100,
|
|
175
177
|
"model" => nil,
|
|
176
178
|
"retention" => "supersede",
|
|
177
|
-
"root_turns" => true
|
|
179
|
+
"root_turns" => true,
|
|
180
|
+
# Soğuk-başlangıç ve sıkıştırma sonrası yönelim enjeksiyonu (issue #340):
|
|
181
|
+
# sistem istemi şişirilmeden kök oturuma kısa mem.* özeti eklenir.
|
|
182
|
+
"orientation" => true,
|
|
183
|
+
"orientation_max_topics" => 10
|
|
178
184
|
},
|
|
179
185
|
# Pruner (repl/pruner): yarı deterministik KÖK pencere temizleyicisi,
|
|
180
186
|
# DEFAULT ON (kullanıcı kararı 2026-10-10; kill-switch: enabled=false).
|