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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +95 -300
  4. data/exe/codebase-index-mcp +3 -31
  5. data/exe/codebase-index-mcp-http +3 -31
  6. data/lib/codebase_index/ast/method_extractor.rb +3 -8
  7. data/lib/codebase_index/ast/node.rb +28 -0
  8. data/lib/codebase_index/ast/parser.rb +53 -92
  9. data/lib/codebase_index/builder.rb +67 -4
  10. data/lib/codebase_index/cache/cache_middleware.rb +199 -0
  11. data/lib/codebase_index/cache/cache_store.rb +264 -0
  12. data/lib/codebase_index/cache/redis_cache_store.rb +116 -0
  13. data/lib/codebase_index/cache/solid_cache_store.rb +111 -0
  14. data/lib/codebase_index/chunking/semantic_chunker.rb +29 -24
  15. data/lib/codebase_index/console/adapters/good_job_adapter.rb +7 -40
  16. data/lib/codebase_index/console/adapters/job_adapter.rb +68 -0
  17. data/lib/codebase_index/console/adapters/sidekiq_adapter.rb +7 -40
  18. data/lib/codebase_index/console/adapters/solid_queue_adapter.rb +7 -40
  19. data/lib/codebase_index/console/bridge.rb +7 -0
  20. data/lib/codebase_index/console/console_response_renderer.rb +3 -7
  21. data/lib/codebase_index/console/embedded_executor.rb +2 -1
  22. data/lib/codebase_index/console/server.rb +1 -4
  23. data/lib/codebase_index/dependency_graph.rb +28 -19
  24. data/lib/codebase_index/embedding/indexer.rb +18 -8
  25. data/lib/codebase_index/embedding/openai.rb +27 -6
  26. data/lib/codebase_index/embedding/provider.rb +29 -2
  27. data/lib/codebase_index/evaluation/evaluator.rb +5 -12
  28. data/lib/codebase_index/extractor.rb +40 -44
  29. data/lib/codebase_index/extractors/action_cable_extractor.rb +9 -36
  30. data/lib/codebase_index/extractors/callback_analyzer.rb +22 -8
  31. data/lib/codebase_index/extractors/controller_extractor.rb +3 -93
  32. data/lib/codebase_index/extractors/decorator_extractor.rb +7 -14
  33. data/lib/codebase_index/extractors/engine_extractor.rb +20 -1
  34. data/lib/codebase_index/extractors/graphql_extractor.rb +4 -29
  35. data/lib/codebase_index/extractors/job_extractor.rb +11 -6
  36. data/lib/codebase_index/extractors/lib_extractor.rb +0 -31
  37. data/lib/codebase_index/extractors/mailer_extractor.rb +15 -85
  38. data/lib/codebase_index/extractors/manager_extractor.rb +1 -15
  39. data/lib/codebase_index/extractors/model_extractor.rb +20 -53
  40. data/lib/codebase_index/extractors/phlex_extractor.rb +8 -8
  41. data/lib/codebase_index/extractors/policy_extractor.rb +1 -24
  42. data/lib/codebase_index/extractors/poro_extractor.rb +0 -17
  43. data/lib/codebase_index/extractors/serializer_extractor.rb +12 -7
  44. data/lib/codebase_index/extractors/service_extractor.rb +1 -38
  45. data/lib/codebase_index/extractors/shared_utility_methods.rb +183 -1
  46. data/lib/codebase_index/extractors/validator_extractor.rb +3 -17
  47. data/lib/codebase_index/extractors/view_component_extractor.rb +10 -9
  48. data/lib/codebase_index/filename_utils.rb +32 -0
  49. data/lib/codebase_index/flow_analysis/operation_extractor.rb +1 -4
  50. data/lib/codebase_index/formatting/base.rb +0 -10
  51. data/lib/codebase_index/graph_analyzer.rb +1 -1
  52. data/lib/codebase_index/mcp/bootstrapper.rb +58 -0
  53. data/lib/codebase_index/mcp/renderers/markdown_renderer.rb +35 -34
  54. data/lib/codebase_index/mcp/renderers/plain_renderer.rb +29 -29
  55. data/lib/codebase_index/mcp/server.rb +59 -68
  56. data/lib/codebase_index/mcp/tool_response_renderer.rb +23 -0
  57. data/lib/codebase_index/notion/client.rb +2 -2
  58. data/lib/codebase_index/notion/mapper.rb +1 -0
  59. data/lib/codebase_index/notion/mappers/column_mapper.rb +3 -11
  60. data/lib/codebase_index/notion/mappers/model_mapper.rb +20 -23
  61. data/lib/codebase_index/notion/mappers/shared.rb +22 -0
  62. data/lib/codebase_index/observability/health_check.rb +0 -2
  63. data/lib/codebase_index/observability/structured_logger.rb +12 -30
  64. data/lib/codebase_index/operator/pipeline_guard.rb +0 -7
  65. data/lib/codebase_index/resilience/index_validator.rb +3 -21
  66. data/lib/codebase_index/retrieval/context_assembler.rb +19 -7
  67. data/lib/codebase_index/retrieval/query_classifier.rb +14 -12
  68. data/lib/codebase_index/retrieval/ranker.rb +6 -2
  69. data/lib/codebase_index/retrieval/search_executor.rb +8 -19
  70. data/lib/codebase_index/retriever.rb +1 -9
  71. data/lib/codebase_index/ruby_analyzer/class_analyzer.rb +5 -25
  72. data/lib/codebase_index/ruby_analyzer/dataflow_analyzer.rb +6 -7
  73. data/lib/codebase_index/ruby_analyzer/mermaid_renderer.rb +58 -53
  74. data/lib/codebase_index/ruby_analyzer/trace_enricher.rb +11 -7
  75. data/lib/codebase_index/session_tracer/file_store.rb +1 -8
  76. data/lib/codebase_index/session_tracer/redis_store.rb +1 -7
  77. data/lib/codebase_index/session_tracer/session_flow_assembler.rb +4 -13
  78. data/lib/codebase_index/session_tracer/solid_cache_store.rb +1 -7
  79. data/lib/codebase_index/session_tracer/store.rb +14 -0
  80. data/lib/codebase_index/storage/metadata_store.rb +37 -10
  81. data/lib/codebase_index/storage/pgvector.rb +37 -5
  82. data/lib/codebase_index/storage/qdrant.rb +39 -6
  83. data/lib/codebase_index/storage/vector_store.rb +11 -0
  84. data/lib/codebase_index/temporal/snapshot_store.rb +14 -10
  85. data/lib/codebase_index/token_utils.rb +19 -0
  86. data/lib/codebase_index/version.rb +1 -1
  87. data/lib/codebase_index.rb +25 -6
  88. data/lib/tasks/codebase_index.rake +2 -2
  89. 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) ? true : false
22
+ !!defined?(::GoodJob)
21
23
  end
22
24
 
23
- # Get queue statistics (sizes, latencies).
24
- #
25
- # @return [Hash] Bridge request
26
- def queue_stats
27
- { tool: 'good_job_queue_stats', params: {} }
28
- end
25
+ private
29
26
 
30
- # List recent job failures.
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