codebase_index 0.2.1 → 0.3.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/CHANGELOG.md +60 -0
- data/README.md +95 -300
- data/exe/codebase-index-mcp +3 -31
- data/exe/codebase-index-mcp-http +3 -31
- data/lib/codebase_index/ast/method_extractor.rb +3 -8
- data/lib/codebase_index/ast/node.rb +28 -0
- data/lib/codebase_index/ast/parser.rb +53 -92
- data/lib/codebase_index/builder.rb +67 -4
- data/lib/codebase_index/cache/cache_middleware.rb +199 -0
- data/lib/codebase_index/cache/cache_store.rb +264 -0
- data/lib/codebase_index/cache/redis_cache_store.rb +116 -0
- data/lib/codebase_index/cache/solid_cache_store.rb +111 -0
- data/lib/codebase_index/chunking/semantic_chunker.rb +29 -24
- data/lib/codebase_index/console/adapters/good_job_adapter.rb +7 -40
- data/lib/codebase_index/console/adapters/job_adapter.rb +68 -0
- data/lib/codebase_index/console/adapters/sidekiq_adapter.rb +7 -40
- data/lib/codebase_index/console/adapters/solid_queue_adapter.rb +7 -40
- data/lib/codebase_index/console/bridge.rb +7 -0
- data/lib/codebase_index/console/console_response_renderer.rb +3 -7
- data/lib/codebase_index/console/embedded_executor.rb +2 -1
- data/lib/codebase_index/console/server.rb +1 -4
- data/lib/codebase_index/dependency_graph.rb +28 -19
- data/lib/codebase_index/embedding/indexer.rb +18 -8
- data/lib/codebase_index/embedding/openai.rb +27 -6
- data/lib/codebase_index/embedding/provider.rb +29 -2
- data/lib/codebase_index/evaluation/evaluator.rb +5 -12
- data/lib/codebase_index/extractor.rb +40 -44
- data/lib/codebase_index/extractors/action_cable_extractor.rb +9 -36
- data/lib/codebase_index/extractors/callback_analyzer.rb +22 -8
- data/lib/codebase_index/extractors/controller_extractor.rb +3 -93
- data/lib/codebase_index/extractors/decorator_extractor.rb +7 -14
- data/lib/codebase_index/extractors/engine_extractor.rb +20 -1
- data/lib/codebase_index/extractors/graphql_extractor.rb +4 -29
- data/lib/codebase_index/extractors/job_extractor.rb +11 -6
- data/lib/codebase_index/extractors/lib_extractor.rb +0 -31
- data/lib/codebase_index/extractors/mailer_extractor.rb +15 -85
- data/lib/codebase_index/extractors/manager_extractor.rb +1 -15
- data/lib/codebase_index/extractors/model_extractor.rb +20 -53
- data/lib/codebase_index/extractors/phlex_extractor.rb +8 -8
- data/lib/codebase_index/extractors/policy_extractor.rb +1 -24
- data/lib/codebase_index/extractors/poro_extractor.rb +0 -17
- data/lib/codebase_index/extractors/serializer_extractor.rb +12 -7
- data/lib/codebase_index/extractors/service_extractor.rb +1 -38
- data/lib/codebase_index/extractors/shared_utility_methods.rb +183 -1
- data/lib/codebase_index/extractors/validator_extractor.rb +3 -17
- data/lib/codebase_index/extractors/view_component_extractor.rb +10 -9
- data/lib/codebase_index/filename_utils.rb +32 -0
- data/lib/codebase_index/flow_analysis/operation_extractor.rb +1 -4
- data/lib/codebase_index/formatting/base.rb +0 -10
- data/lib/codebase_index/graph_analyzer.rb +1 -1
- data/lib/codebase_index/mcp/bootstrapper.rb +58 -0
- data/lib/codebase_index/mcp/renderers/markdown_renderer.rb +35 -34
- data/lib/codebase_index/mcp/renderers/plain_renderer.rb +29 -29
- data/lib/codebase_index/mcp/server.rb +59 -68
- data/lib/codebase_index/mcp/tool_response_renderer.rb +23 -0
- data/lib/codebase_index/notion/client.rb +2 -2
- data/lib/codebase_index/notion/mapper.rb +1 -0
- data/lib/codebase_index/notion/mappers/column_mapper.rb +3 -11
- data/lib/codebase_index/notion/mappers/model_mapper.rb +20 -23
- data/lib/codebase_index/notion/mappers/shared.rb +22 -0
- data/lib/codebase_index/observability/health_check.rb +0 -2
- data/lib/codebase_index/observability/structured_logger.rb +12 -30
- data/lib/codebase_index/operator/pipeline_guard.rb +0 -7
- data/lib/codebase_index/resilience/index_validator.rb +3 -21
- data/lib/codebase_index/retrieval/context_assembler.rb +19 -7
- data/lib/codebase_index/retrieval/query_classifier.rb +14 -12
- data/lib/codebase_index/retrieval/ranker.rb +6 -2
- data/lib/codebase_index/retrieval/search_executor.rb +8 -19
- data/lib/codebase_index/retriever.rb +1 -9
- data/lib/codebase_index/ruby_analyzer/class_analyzer.rb +5 -25
- data/lib/codebase_index/ruby_analyzer/dataflow_analyzer.rb +6 -7
- data/lib/codebase_index/ruby_analyzer/mermaid_renderer.rb +58 -53
- data/lib/codebase_index/ruby_analyzer/trace_enricher.rb +11 -7
- data/lib/codebase_index/session_tracer/file_store.rb +1 -8
- data/lib/codebase_index/session_tracer/redis_store.rb +1 -7
- data/lib/codebase_index/session_tracer/session_flow_assembler.rb +4 -13
- data/lib/codebase_index/session_tracer/solid_cache_store.rb +1 -7
- data/lib/codebase_index/session_tracer/store.rb +14 -0
- data/lib/codebase_index/storage/metadata_store.rb +37 -10
- data/lib/codebase_index/storage/pgvector.rb +37 -5
- data/lib/codebase_index/storage/qdrant.rb +39 -6
- data/lib/codebase_index/storage/vector_store.rb +11 -0
- data/lib/codebase_index/temporal/snapshot_store.rb +14 -10
- data/lib/codebase_index/token_utils.rb +19 -0
- data/lib/codebase_index/version.rb +1 -1
- data/lib/codebase_index.rb +25 -6
- data/lib/tasks/codebase_index.rake +2 -2
- metadata +11 -2
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'logger'
|
|
6
|
+
|
|
7
|
+
module CodebaseIndex
|
|
8
|
+
module Cache
|
|
9
|
+
# Default TTLs (in seconds) for each cache domain.
|
|
10
|
+
#
|
|
11
|
+
# Embedding vectors are stable (same text → same vector) so they get 24h.
|
|
12
|
+
# Metadata and structural context refresh on re-extraction (1h).
|
|
13
|
+
# Search results and formatted context are session-scoped (15min).
|
|
14
|
+
DEFAULT_TTLS = {
|
|
15
|
+
embeddings: 86_400,
|
|
16
|
+
metadata: 3_600,
|
|
17
|
+
structural: 3_600,
|
|
18
|
+
search: 900,
|
|
19
|
+
context: 900
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Build a namespaced cache key from a domain and raw parts.
|
|
23
|
+
#
|
|
24
|
+
# @param domain [Symbol] Cache domain (:embeddings, :metadata, etc.)
|
|
25
|
+
# @param parts [Array<String>] Key components (will be SHA256-hashed if long)
|
|
26
|
+
# @return [String] Namespaced key
|
|
27
|
+
def self.cache_key(domain, *parts)
|
|
28
|
+
raw = parts.join(':')
|
|
29
|
+
suffix = raw.length > 64 ? Digest::SHA256.hexdigest(raw) : raw
|
|
30
|
+
"codebase_index:cache:#{domain}:#{suffix}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Abstract cache store interface.
|
|
34
|
+
#
|
|
35
|
+
# All cache backends must implement these methods. The interface is modeled
|
|
36
|
+
# after ActiveSupport::Cache::Store for familiarity but kept minimal.
|
|
37
|
+
#
|
|
38
|
+
# @abstract Subclass and override all public methods.
|
|
39
|
+
class CacheStore
|
|
40
|
+
# Read a value from the cache.
|
|
41
|
+
#
|
|
42
|
+
# @param key [String] Cache key
|
|
43
|
+
# @return [Object, nil] Cached value or nil if missing/expired
|
|
44
|
+
def read(key)
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Write a value to the cache.
|
|
49
|
+
#
|
|
50
|
+
# @param key [String] Cache key
|
|
51
|
+
# @param value [Object] Value to cache (must be JSON-serializable)
|
|
52
|
+
# @param ttl [Integer, nil] Time-to-live in seconds (nil = use domain default)
|
|
53
|
+
# @return [void]
|
|
54
|
+
def write(key, value, ttl: nil)
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Delete a key from the cache.
|
|
59
|
+
#
|
|
60
|
+
# @param key [String] Cache key
|
|
61
|
+
# @return [void]
|
|
62
|
+
def delete(key)
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check if a key exists and is not expired.
|
|
67
|
+
#
|
|
68
|
+
# @param key [String] Cache key
|
|
69
|
+
# @return [Boolean]
|
|
70
|
+
def exist?(key)
|
|
71
|
+
raise NotImplementedError
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Clear cached entries. If namespace is given, only clear that domain.
|
|
75
|
+
#
|
|
76
|
+
# @param namespace [Symbol, nil] Cache domain to clear, or nil for all
|
|
77
|
+
# @return [void]
|
|
78
|
+
def clear(namespace: nil)
|
|
79
|
+
raise NotImplementedError
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Read-through cache: return cached value or execute block and cache result.
|
|
83
|
+
#
|
|
84
|
+
# @note nil is treated as a cache miss. If the wrapped operation legitimately
|
|
85
|
+
# returns nil, every call will re-execute the block. Custom backend
|
|
86
|
+
# implementers should preserve this semantic — do not return nil for keys
|
|
87
|
+
# that were written with a non-nil value. This is acceptable for the
|
|
88
|
+
# built-in use cases (embeddings and formatted context are never nil).
|
|
89
|
+
#
|
|
90
|
+
# @param key [String] Cache key
|
|
91
|
+
# @param ttl [Integer, nil] TTL in seconds
|
|
92
|
+
# @yield Block that computes the value on cache miss
|
|
93
|
+
# @return [Object] Cached or freshly computed value
|
|
94
|
+
def fetch(key, ttl: nil)
|
|
95
|
+
cached = read(key)
|
|
96
|
+
return cached unless cached.nil?
|
|
97
|
+
|
|
98
|
+
value = yield
|
|
99
|
+
begin
|
|
100
|
+
write(key, value, ttl: ttl)
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
logger.warn("[CodebaseIndex] CacheStore#fetch write failed for #{key}: #{e.message}")
|
|
103
|
+
end
|
|
104
|
+
value
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
# Return a logger instance (Rails.logger in Rails apps, stderr elsewhere).
|
|
110
|
+
#
|
|
111
|
+
# @return [Logger]
|
|
112
|
+
def logger
|
|
113
|
+
@logger ||= defined?(Rails) ? Rails.logger : Logger.new($stderr)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Build a wildcard pattern for clearing cache entries.
|
|
117
|
+
#
|
|
118
|
+
# @param namespace [Symbol, nil] Cache domain, or nil for all entries
|
|
119
|
+
# @return [String]
|
|
120
|
+
def clear_pattern(namespace)
|
|
121
|
+
namespace ? "codebase_index:cache:#{namespace}:*" : 'codebase_index:cache:*'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Delete a key, silently swallowing any errors.
|
|
125
|
+
#
|
|
126
|
+
# Used for cleanup on corrupt/stale entries where failure is acceptable.
|
|
127
|
+
#
|
|
128
|
+
# @param key [String]
|
|
129
|
+
# @return [nil]
|
|
130
|
+
def delete_silently(key)
|
|
131
|
+
delete(key)
|
|
132
|
+
rescue StandardError
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# In-memory cache store with LRU eviction and TTL support.
|
|
138
|
+
#
|
|
139
|
+
# Zero external dependencies. Suitable for single-process use, development,
|
|
140
|
+
# and as a fallback when Redis/SolidCache are not available. Thread-safe.
|
|
141
|
+
#
|
|
142
|
+
# @example
|
|
143
|
+
# store = InMemory.new(max_entries: 200)
|
|
144
|
+
# store.write("ci:emb:abc", [0.1, 0.2], ttl: 3600)
|
|
145
|
+
# store.read("ci:emb:abc") # => [0.1, 0.2]
|
|
146
|
+
#
|
|
147
|
+
class InMemory < CacheStore
|
|
148
|
+
# @param max_entries [Integer] Maximum cached entries before LRU eviction
|
|
149
|
+
def initialize(max_entries: 500)
|
|
150
|
+
super()
|
|
151
|
+
@max_entries = max_entries
|
|
152
|
+
@entries = {}
|
|
153
|
+
@access_order = []
|
|
154
|
+
@mutex = Mutex.new
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Read a value, returning nil if missing or expired.
|
|
158
|
+
#
|
|
159
|
+
# @param key [String] Cache key
|
|
160
|
+
# @return [Object, nil]
|
|
161
|
+
def read(key)
|
|
162
|
+
@mutex.synchronize do
|
|
163
|
+
entry = @entries[key]
|
|
164
|
+
return nil unless entry
|
|
165
|
+
|
|
166
|
+
if entry[:expires_at] && Time.now > entry[:expires_at]
|
|
167
|
+
evict_key(key)
|
|
168
|
+
return nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
touch(key)
|
|
172
|
+
entry[:value]
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Write a value with optional TTL.
|
|
177
|
+
#
|
|
178
|
+
# @param key [String] Cache key
|
|
179
|
+
# @param value [Object] Value to cache
|
|
180
|
+
# @param ttl [Integer, nil] TTL in seconds
|
|
181
|
+
# @return [void]
|
|
182
|
+
def write(key, value, ttl: nil)
|
|
183
|
+
@mutex.synchronize do
|
|
184
|
+
evict_key(key) if @entries.key?(key)
|
|
185
|
+
|
|
186
|
+
if @entries.size >= @max_entries
|
|
187
|
+
oldest = @access_order.shift
|
|
188
|
+
@entries.delete(oldest) if oldest
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
expires_at = ttl ? Time.now + ttl : nil
|
|
192
|
+
@entries[key] = { value: value, expires_at: expires_at }
|
|
193
|
+
@access_order.push(key)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Delete a key.
|
|
198
|
+
#
|
|
199
|
+
# @param key [String] Cache key
|
|
200
|
+
# @return [void]
|
|
201
|
+
def delete(key)
|
|
202
|
+
@mutex.synchronize { evict_key(key) }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Check if a key exists and is not expired.
|
|
206
|
+
#
|
|
207
|
+
# @param key [String] Cache key
|
|
208
|
+
# @return [Boolean]
|
|
209
|
+
def exist?(key)
|
|
210
|
+
@mutex.synchronize do
|
|
211
|
+
entry = @entries[key]
|
|
212
|
+
return false unless entry
|
|
213
|
+
return false if entry[:expires_at] && Time.now > entry[:expires_at]
|
|
214
|
+
|
|
215
|
+
true
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Clear entries. If namespace is given, only clear keys matching that domain.
|
|
220
|
+
#
|
|
221
|
+
# @param namespace [Symbol, nil] Domain to clear (:embeddings, :metadata, etc.)
|
|
222
|
+
# @return [void]
|
|
223
|
+
def clear(namespace: nil)
|
|
224
|
+
@mutex.synchronize do
|
|
225
|
+
if namespace
|
|
226
|
+
prefix = "codebase_index:cache:#{namespace}:"
|
|
227
|
+
keys_to_delete = @entries.keys.select { |k| k.start_with?(prefix) }
|
|
228
|
+
keys_to_delete.each { |k| evict_key(k) }
|
|
229
|
+
else
|
|
230
|
+
@entries.clear
|
|
231
|
+
@access_order.clear
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Number of entries currently in the cache (for testing/diagnostics).
|
|
237
|
+
#
|
|
238
|
+
# @return [Integer]
|
|
239
|
+
def size
|
|
240
|
+
@mutex.synchronize { @entries.size }
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
private
|
|
244
|
+
|
|
245
|
+
# Remove a key from both the entry hash and access order.
|
|
246
|
+
#
|
|
247
|
+
# @param key [String]
|
|
248
|
+
# @return [void]
|
|
249
|
+
def evict_key(key)
|
|
250
|
+
@entries.delete(key)
|
|
251
|
+
@access_order.delete(key)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Move a key to the end of the access order (most recently used).
|
|
255
|
+
#
|
|
256
|
+
# @param key [String]
|
|
257
|
+
# @return [void]
|
|
258
|
+
def touch(key)
|
|
259
|
+
@access_order.delete(key)
|
|
260
|
+
@access_order.push(key)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'cache_store'
|
|
5
|
+
|
|
6
|
+
module CodebaseIndex
|
|
7
|
+
module Cache
|
|
8
|
+
# Redis-backed cache store using GET/SET with TTL.
|
|
9
|
+
#
|
|
10
|
+
# Uses simple key-value operations (not Lists like SessionTracer::RedisStore).
|
|
11
|
+
# Values are JSON-serialized on write and deserialized on read. TTL is
|
|
12
|
+
# enforced natively by Redis via the EX option on SET.
|
|
13
|
+
#
|
|
14
|
+
# Requires the `redis` gem at runtime.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# store = RedisCacheStore.new(redis: Redis.new, default_ttl: 3600)
|
|
18
|
+
# store.write("ci:emb:abc", [0.1, 0.2], ttl: 86_400)
|
|
19
|
+
# store.read("ci:emb:abc") # => [0.1, 0.2]
|
|
20
|
+
#
|
|
21
|
+
class RedisCacheStore < CacheStore
|
|
22
|
+
# @param redis [Redis] A Redis client instance
|
|
23
|
+
# @param default_ttl [Integer, nil] Default TTL in seconds when none specified (nil = no expiry)
|
|
24
|
+
# @raise [ConfigurationError] if the redis gem is not loaded
|
|
25
|
+
def initialize(redis:, default_ttl: nil)
|
|
26
|
+
super()
|
|
27
|
+
unless defined?(::Redis)
|
|
28
|
+
raise ConfigurationError,
|
|
29
|
+
'The redis gem is required for RedisCacheStore. Add `gem "redis"` to your Gemfile.'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@redis = redis
|
|
33
|
+
@default_ttl = default_ttl
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Read a value from Redis.
|
|
37
|
+
#
|
|
38
|
+
# @param key [String] Cache key
|
|
39
|
+
# @return [Object, nil] Deserialized value or nil
|
|
40
|
+
def read(key)
|
|
41
|
+
raw = @redis.get(key)
|
|
42
|
+
return nil unless raw
|
|
43
|
+
|
|
44
|
+
JSON.parse(raw)
|
|
45
|
+
rescue JSON::ParserError
|
|
46
|
+
delete_silently(key)
|
|
47
|
+
nil
|
|
48
|
+
rescue ::Redis::BaseError, Errno::ECONNREFUSED, Errno::ECONNRESET => e
|
|
49
|
+
logger.warn("[CodebaseIndex] RedisCacheStore#read failed for #{key}: #{e.message}")
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Write a value to Redis with optional TTL.
|
|
54
|
+
#
|
|
55
|
+
# @param key [String] Cache key
|
|
56
|
+
# @param value [Object] Value to cache (must be JSON-serializable)
|
|
57
|
+
# @param ttl [Integer, nil] TTL in seconds (falls back to default_ttl)
|
|
58
|
+
# @return [void]
|
|
59
|
+
def write(key, value, ttl: nil)
|
|
60
|
+
serialized = JSON.generate(value)
|
|
61
|
+
effective_ttl = ttl || @default_ttl
|
|
62
|
+
|
|
63
|
+
if effective_ttl
|
|
64
|
+
@redis.set(key, serialized, ex: effective_ttl)
|
|
65
|
+
else
|
|
66
|
+
@redis.set(key, serialized)
|
|
67
|
+
end
|
|
68
|
+
rescue ::Redis::BaseError, Errno::ECONNREFUSED, Errno::ECONNRESET => e
|
|
69
|
+
logger.warn("[CodebaseIndex] RedisCacheStore#write failed for #{key}: #{e.message}")
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Delete a key from Redis.
|
|
74
|
+
#
|
|
75
|
+
# @param key [String] Cache key
|
|
76
|
+
# @return [void]
|
|
77
|
+
def delete(key)
|
|
78
|
+
@redis.del(key)
|
|
79
|
+
rescue ::Redis::BaseError, Errno::ECONNREFUSED, Errno::ECONNRESET => e
|
|
80
|
+
logger.warn("[CodebaseIndex] RedisCacheStore#delete failed for #{key}: #{e.message}")
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Check if a key exists in Redis.
|
|
85
|
+
#
|
|
86
|
+
# @param key [String] Cache key
|
|
87
|
+
# @return [Boolean]
|
|
88
|
+
def exist?(key)
|
|
89
|
+
@redis.exists?(key)
|
|
90
|
+
rescue ::Redis::BaseError, Errno::ECONNREFUSED, Errno::ECONNRESET => e
|
|
91
|
+
logger.warn("[CodebaseIndex] RedisCacheStore#exist? failed for #{key}: #{e.message}")
|
|
92
|
+
false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Clear cached entries by namespace or all codebase_index cache keys.
|
|
96
|
+
#
|
|
97
|
+
# Uses SCAN (not KEYS) to avoid blocking Redis on large keyspaces.
|
|
98
|
+
#
|
|
99
|
+
# @param namespace [Symbol, nil] Domain to clear, or nil for all cache keys
|
|
100
|
+
# @return [void]
|
|
101
|
+
def clear(namespace: nil)
|
|
102
|
+
pattern = clear_pattern(namespace)
|
|
103
|
+
|
|
104
|
+
cursor = '0'
|
|
105
|
+
loop do
|
|
106
|
+
cursor, keys = @redis.scan(cursor, match: pattern, count: 100)
|
|
107
|
+
@redis.del(*keys) if keys.any?
|
|
108
|
+
break if cursor == '0'
|
|
109
|
+
end
|
|
110
|
+
rescue ::Redis::BaseError, Errno::ECONNREFUSED, Errno::ECONNRESET => e
|
|
111
|
+
logger.warn("[CodebaseIndex] RedisCacheStore#clear failed: #{e.message}")
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'cache_store'
|
|
5
|
+
|
|
6
|
+
module CodebaseIndex
|
|
7
|
+
module Cache
|
|
8
|
+
# SolidCache-backed (or any ActiveSupport::Cache::Store) cache store.
|
|
9
|
+
#
|
|
10
|
+
# Delegates to a Rails-compatible cache backend. Values are JSON-serialized
|
|
11
|
+
# to avoid Marshal dependency issues across Ruby versions. TTL is passed
|
|
12
|
+
# as `expires_in:` to the underlying cache.
|
|
13
|
+
#
|
|
14
|
+
# @example With SolidCache
|
|
15
|
+
# store = SolidCacheStore.new(cache: SolidCache::Store.new, default_ttl: 3600)
|
|
16
|
+
# store.write("ci:emb:abc", [0.1, 0.2], ttl: 86_400)
|
|
17
|
+
# store.read("ci:emb:abc") # => [0.1, 0.2]
|
|
18
|
+
#
|
|
19
|
+
# @example With Rails.cache (any backend)
|
|
20
|
+
# store = SolidCacheStore.new(cache: Rails.cache)
|
|
21
|
+
#
|
|
22
|
+
class SolidCacheStore < CacheStore
|
|
23
|
+
# @param cache [ActiveSupport::Cache::Store] A SolidCache or compatible cache instance
|
|
24
|
+
# @param default_ttl [Integer, nil] Default TTL in seconds (nil = no expiry)
|
|
25
|
+
def initialize(cache:, default_ttl: nil)
|
|
26
|
+
super()
|
|
27
|
+
@cache = cache
|
|
28
|
+
@default_ttl = default_ttl
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Read a value from the cache.
|
|
32
|
+
#
|
|
33
|
+
# @param key [String] Cache key
|
|
34
|
+
# @return [Object, nil] Deserialized value or nil
|
|
35
|
+
def read(key)
|
|
36
|
+
raw = @cache.read(key)
|
|
37
|
+
return nil unless raw
|
|
38
|
+
|
|
39
|
+
JSON.parse(raw)
|
|
40
|
+
rescue JSON::ParserError
|
|
41
|
+
delete_silently(key)
|
|
42
|
+
nil
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
logger.warn("[CodebaseIndex] SolidCacheStore#read failed for #{key}: #{e.message}")
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Write a value with optional TTL.
|
|
49
|
+
#
|
|
50
|
+
# @param key [String] Cache key
|
|
51
|
+
# @param value [Object] Value to cache (must be JSON-serializable)
|
|
52
|
+
# @param ttl [Integer, nil] TTL in seconds (falls back to default_ttl)
|
|
53
|
+
# @return [void]
|
|
54
|
+
def write(key, value, ttl: nil)
|
|
55
|
+
serialized = JSON.generate(value)
|
|
56
|
+
effective_ttl = ttl || @default_ttl
|
|
57
|
+
|
|
58
|
+
opts = effective_ttl ? { expires_in: effective_ttl } : {}
|
|
59
|
+
@cache.write(key, serialized, **opts)
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
logger.warn("[CodebaseIndex] SolidCacheStore#write failed for #{key}: #{e.message}")
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Delete a key from the cache.
|
|
66
|
+
#
|
|
67
|
+
# @param key [String] Cache key
|
|
68
|
+
# @return [void]
|
|
69
|
+
def delete(key)
|
|
70
|
+
@cache.delete(key)
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
logger.warn("[CodebaseIndex] SolidCacheStore#delete failed for #{key}: #{e.message}")
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check if a key exists in the cache.
|
|
77
|
+
#
|
|
78
|
+
# @param key [String] Cache key
|
|
79
|
+
# @return [Boolean]
|
|
80
|
+
def exist?(key)
|
|
81
|
+
@cache.exist?(key)
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
logger.warn("[CodebaseIndex] SolidCacheStore#exist? failed for #{key}: #{e.message}")
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Clear cached entries by namespace or all codebase_index cache keys.
|
|
88
|
+
#
|
|
89
|
+
# Uses `delete_matched` if the underlying cache supports it (Redis, Memcached).
|
|
90
|
+
# Falls back to a no-op if pattern deletion is not available (some backends
|
|
91
|
+
# like SolidCache don't support wildcard deletion).
|
|
92
|
+
#
|
|
93
|
+
# @param namespace [Symbol, nil] Domain to clear, or nil for all cache keys
|
|
94
|
+
# @return [void]
|
|
95
|
+
def clear(namespace: nil)
|
|
96
|
+
pattern = clear_pattern(namespace)
|
|
97
|
+
|
|
98
|
+
unless @cache.respond_to?(:delete_matched)
|
|
99
|
+
logger.warn("[CodebaseIndex] Cache#clear(namespace: #{namespace.inspect}) is a no-op: " \
|
|
100
|
+
"backend #{@cache.class} does not support delete_matched")
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
@cache.delete_matched(pattern)
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
logger.warn("[CodebaseIndex] SolidCacheStore#clear failed: #{e.message}")
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -4,6 +4,31 @@ require_relative 'chunk'
|
|
|
4
4
|
|
|
5
5
|
module CodebaseIndex
|
|
6
6
|
module Chunking
|
|
7
|
+
# Shared method-detection patterns used by ModelChunker and ControllerChunker.
|
|
8
|
+
METHOD_PATTERN = /^\s*def\s+/
|
|
9
|
+
PRIVATE_PATTERN = /^\s*(private|protected)\s*$/
|
|
10
|
+
|
|
11
|
+
# Mixin that provides the shared build_chunk helper for chunker classes.
|
|
12
|
+
#
|
|
13
|
+
# Requires the including class to have an @unit ivar (ExtractedUnit).
|
|
14
|
+
module ChunkBuilder
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# Build a Chunk for a given section.
|
|
18
|
+
#
|
|
19
|
+
# @param chunk_type [Symbol]
|
|
20
|
+
# @param content [String]
|
|
21
|
+
# @return [Chunk]
|
|
22
|
+
def build_chunk(chunk_type, content)
|
|
23
|
+
Chunk.new(
|
|
24
|
+
content: content,
|
|
25
|
+
chunk_type: chunk_type,
|
|
26
|
+
parent_identifier: @unit.identifier,
|
|
27
|
+
parent_type: @unit.type
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
7
32
|
# Splits ExtractedUnits into semantic chunks based on unit type.
|
|
8
33
|
#
|
|
9
34
|
# Models are split by: summary, associations, validations, callbacks,
|
|
@@ -62,13 +87,13 @@ module CodebaseIndex
|
|
|
62
87
|
#
|
|
63
88
|
# @api private
|
|
64
89
|
class ModelChunker
|
|
90
|
+
include ChunkBuilder
|
|
91
|
+
|
|
65
92
|
ASSOCIATION_PATTERN = /^\s*(has_many|has_one|belongs_to|has_and_belongs_to_many)\b/
|
|
66
93
|
VALIDATION_PATTERN = /^\s*validates?\b/
|
|
67
94
|
CALLBACK_ACTIONS = '(save|create|update|destroy|validation|action|commit|rollback|find|initialize|touch)'
|
|
68
95
|
CALLBACK_PATTERN = /^\s*(before_|after_|around_)#{CALLBACK_ACTIONS}\b/
|
|
69
96
|
SCOPE_PATTERN = /^\s*scope\s+:/
|
|
70
|
-
METHOD_PATTERN = /^\s*def\s+/
|
|
71
|
-
PRIVATE_PATTERN = /^\s*(private|protected)\s*$/
|
|
72
97
|
|
|
73
98
|
SECTION_PATTERNS = {
|
|
74
99
|
associations: ASSOCIATION_PATTERN,
|
|
@@ -181,16 +206,6 @@ module CodebaseIndex
|
|
|
181
206
|
state[:sections][state[:current]] << line
|
|
182
207
|
end
|
|
183
208
|
end
|
|
184
|
-
|
|
185
|
-
# @return [Chunk]
|
|
186
|
-
def build_chunk(chunk_type, content)
|
|
187
|
-
Chunk.new(
|
|
188
|
-
content: content,
|
|
189
|
-
chunk_type: chunk_type,
|
|
190
|
-
parent_identifier: @unit.identifier,
|
|
191
|
-
parent_type: @unit.type
|
|
192
|
-
)
|
|
193
|
-
end
|
|
194
209
|
end
|
|
195
210
|
|
|
196
211
|
# Chunks a controller unit by actions: summary (class + filters),
|
|
@@ -198,9 +213,9 @@ module CodebaseIndex
|
|
|
198
213
|
#
|
|
199
214
|
# @api private
|
|
200
215
|
class ControllerChunker
|
|
216
|
+
include ChunkBuilder
|
|
217
|
+
|
|
201
218
|
FILTER_PATTERN = /^\s*(before_action|after_action|around_action|skip_before_action)\b/
|
|
202
|
-
METHOD_PATTERN = /^\s*def\s+/
|
|
203
|
-
PRIVATE_PATTERN = /^\s*(private|protected)\s*$/
|
|
204
219
|
|
|
205
220
|
# @param unit [ExtractedUnit]
|
|
206
221
|
def initialize(unit)
|
|
@@ -275,16 +290,6 @@ module CodebaseIndex
|
|
|
275
290
|
|
|
276
291
|
chunks
|
|
277
292
|
end
|
|
278
|
-
|
|
279
|
-
# @return [Chunk]
|
|
280
|
-
def build_chunk(chunk_type, content)
|
|
281
|
-
Chunk.new(
|
|
282
|
-
content: content,
|
|
283
|
-
chunk_type: chunk_type,
|
|
284
|
-
parent_identifier: @unit.identifier,
|
|
285
|
-
parent_type: @unit.type
|
|
286
|
-
)
|
|
287
|
-
end
|
|
288
293
|
end
|
|
289
294
|
end
|
|
290
295
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'job_adapter'
|
|
4
|
+
|
|
3
5
|
module CodebaseIndex
|
|
4
6
|
module Console
|
|
5
7
|
module Adapters
|
|
@@ -12,53 +14,18 @@ module CodebaseIndex
|
|
|
12
14
|
# adapter = GoodJobAdapter.new
|
|
13
15
|
# adapter.queue_stats # => { tool: 'good_job_queue_stats', params: {} }
|
|
14
16
|
#
|
|
15
|
-
class GoodJobAdapter
|
|
17
|
+
class GoodJobAdapter < JobAdapter
|
|
16
18
|
# Check if GoodJob is available in the current environment.
|
|
17
19
|
#
|
|
18
20
|
# @return [Boolean]
|
|
19
21
|
def self.available?
|
|
20
|
-
defined?(::GoodJob)
|
|
22
|
+
!!defined?(::GoodJob)
|
|
21
23
|
end
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
#
|
|
25
|
-
# @return [Hash] Bridge request
|
|
26
|
-
def queue_stats
|
|
27
|
-
{ tool: 'good_job_queue_stats', params: {} }
|
|
28
|
-
end
|
|
25
|
+
private
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# @param limit [Integer] Max failures (default: 10, max: 100)
|
|
33
|
-
# @return [Hash] Bridge request
|
|
34
|
-
def recent_failures(limit: 10)
|
|
35
|
-
limit = [limit, 100].min
|
|
36
|
-
{ tool: 'good_job_recent_failures', params: { limit: limit } }
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Find a job by its ID.
|
|
40
|
-
#
|
|
41
|
-
# @param id [Object] GoodJob job ID
|
|
42
|
-
# @return [Hash] Bridge request
|
|
43
|
-
def find_job(id:)
|
|
44
|
-
{ tool: 'good_job_find_job', params: { id: id } }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# List scheduled jobs.
|
|
48
|
-
#
|
|
49
|
-
# @param limit [Integer] Max jobs (default: 20, max: 100)
|
|
50
|
-
# @return [Hash] Bridge request
|
|
51
|
-
def scheduled_jobs(limit: 20)
|
|
52
|
-
limit = [limit, 100].min
|
|
53
|
-
{ tool: 'good_job_scheduled_jobs', params: { limit: limit } }
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Retry a failed job.
|
|
57
|
-
#
|
|
58
|
-
# @param id [Object] GoodJob job ID
|
|
59
|
-
# @return [Hash] Bridge request
|
|
60
|
-
def retry_job(id:)
|
|
61
|
-
{ tool: 'good_job_retry_job', params: { id: id } }
|
|
27
|
+
def prefix
|
|
28
|
+
'good_job'
|
|
62
29
|
end
|
|
63
30
|
end
|
|
64
31
|
end
|