brainzlab 0.1.1 → 0.1.2

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -0
  3. data/lib/brainzlab/beacon/client.rb +209 -0
  4. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  5. data/lib/brainzlab/beacon.rb +215 -0
  6. data/lib/brainzlab/configuration.rb +341 -3
  7. data/lib/brainzlab/cortex/cache.rb +59 -0
  8. data/lib/brainzlab/cortex/client.rb +141 -0
  9. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  10. data/lib/brainzlab/cortex.rb +227 -0
  11. data/lib/brainzlab/dendrite/client.rb +232 -0
  12. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  13. data/lib/brainzlab/dendrite.rb +195 -0
  14. data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
  15. data/lib/brainzlab/devtools/assets/devtools.js +322 -0
  16. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  17. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -0
  18. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  19. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  20. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  21. data/lib/brainzlab/devtools/middleware/database_handler.rb +180 -0
  22. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  23. data/lib/brainzlab/devtools/middleware/error_page.rb +376 -0
  24. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +155 -0
  25. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +94 -0
  26. data/lib/brainzlab/devtools.rb +75 -0
  27. data/lib/brainzlab/flux/buffer.rb +96 -0
  28. data/lib/brainzlab/flux/client.rb +70 -0
  29. data/lib/brainzlab/flux/provisioner.rb +57 -0
  30. data/lib/brainzlab/flux.rb +174 -0
  31. data/lib/brainzlab/instrumentation/active_record.rb +18 -1
  32. data/lib/brainzlab/instrumentation/aws.rb +179 -0
  33. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  34. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  35. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  36. data/lib/brainzlab/instrumentation/resque.rb +115 -0
  37. data/lib/brainzlab/instrumentation/solid_queue.rb +198 -0
  38. data/lib/brainzlab/instrumentation/stripe.rb +164 -0
  39. data/lib/brainzlab/instrumentation/typhoeus.rb +104 -0
  40. data/lib/brainzlab/instrumentation.rb +72 -0
  41. data/lib/brainzlab/nerve/client.rb +217 -0
  42. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  43. data/lib/brainzlab/nerve.rb +219 -0
  44. data/lib/brainzlab/pulse/instrumentation.rb +35 -2
  45. data/lib/brainzlab/pulse/propagation.rb +1 -1
  46. data/lib/brainzlab/pulse/tracer.rb +1 -1
  47. data/lib/brainzlab/pulse.rb +1 -1
  48. data/lib/brainzlab/rails/log_subscriber.rb +1 -2
  49. data/lib/brainzlab/rails/railtie.rb +36 -3
  50. data/lib/brainzlab/recall/provisioner.rb +17 -0
  51. data/lib/brainzlab/recall.rb +6 -1
  52. data/lib/brainzlab/reflex.rb +2 -2
  53. data/lib/brainzlab/sentinel/client.rb +218 -0
  54. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  55. data/lib/brainzlab/sentinel.rb +165 -0
  56. data/lib/brainzlab/signal/client.rb +62 -0
  57. data/lib/brainzlab/signal/provisioner.rb +55 -0
  58. data/lib/brainzlab/signal.rb +136 -0
  59. data/lib/brainzlab/synapse/client.rb +290 -0
  60. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  61. data/lib/brainzlab/synapse.rb +270 -0
  62. data/lib/brainzlab/utilities/circuit_breaker.rb +265 -0
  63. data/lib/brainzlab/utilities/health_check.rb +296 -0
  64. data/lib/brainzlab/utilities/log_formatter.rb +256 -0
  65. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  66. data/lib/brainzlab/utilities.rb +17 -0
  67. data/lib/brainzlab/vault/cache.rb +80 -0
  68. data/lib/brainzlab/vault/client.rb +198 -0
  69. data/lib/brainzlab/vault/provisioner.rb +49 -0
  70. data/lib/brainzlab/vault.rb +268 -0
  71. data/lib/brainzlab/version.rb +1 -1
  72. data/lib/brainzlab/vision/client.rb +128 -0
  73. data/lib/brainzlab/vision/provisioner.rb +136 -0
  74. data/lib/brainzlab/vision.rb +157 -0
  75. data/lib/brainzlab.rb +101 -0
  76. metadata +60 -1
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cortex/client"
4
+ require_relative "cortex/cache"
5
+ require_relative "cortex/provisioner"
6
+
7
+ module BrainzLab
8
+ module Cortex
9
+ class << self
10
+ # Check if a feature flag is enabled
11
+ # @param flag_name [String, Symbol] The flag name
12
+ # @param context [Hash] Evaluation context (user, attributes, etc.)
13
+ # @return [Boolean] True if the flag is enabled
14
+ #
15
+ # @example
16
+ # if BrainzLab::Cortex.enabled?(:new_checkout, user: current_user)
17
+ # render_new_checkout
18
+ # end
19
+ #
20
+ def enabled?(flag_name, **context)
21
+ result = get(flag_name, **context)
22
+ result == true || result == "true"
23
+ end
24
+
25
+ # Check if a feature flag is disabled
26
+ # @param flag_name [String, Symbol] The flag name
27
+ # @param context [Hash] Evaluation context
28
+ # @return [Boolean] True if the flag is disabled
29
+ def disabled?(flag_name, **context)
30
+ !enabled?(flag_name, **context)
31
+ end
32
+
33
+ # Get the value of a feature flag
34
+ # @param flag_name [String, Symbol] The flag name
35
+ # @param context [Hash] Evaluation context
36
+ # @param default [Object] Default value if flag not found
37
+ # @return [Object] The flag value
38
+ #
39
+ # @example
40
+ # limit = BrainzLab::Cortex.get(:rate_limit, user: current_user, default: 100)
41
+ #
42
+ def get(flag_name, default: nil, **context)
43
+ return default unless module_enabled?
44
+
45
+ ensure_provisioned!
46
+ return default unless BrainzLab.configuration.cortex_valid?
47
+
48
+ flag_key = flag_name.to_s
49
+ merged_context = merge_context(context)
50
+ cache_key = build_cache_key(flag_key, merged_context)
51
+
52
+ # Check cache first
53
+ if BrainzLab.configuration.cortex_cache_enabled && cache.has?(cache_key)
54
+ return cache.get(cache_key)
55
+ end
56
+
57
+ result = client.evaluate(flag_key, context: merged_context)
58
+
59
+ if result.nil?
60
+ default
61
+ else
62
+ cache.set(cache_key, result) if BrainzLab.configuration.cortex_cache_enabled
63
+ result
64
+ end
65
+ end
66
+
67
+ # Get the variant for an A/B test flag
68
+ # @param flag_name [String, Symbol] The flag name
69
+ # @param context [Hash] Evaluation context
70
+ # @param default [String] Default variant if flag not found
71
+ # @return [String, nil] The variant name
72
+ #
73
+ # @example
74
+ # variant = BrainzLab::Cortex.variant(:checkout_experiment, user: current_user)
75
+ # case variant
76
+ # when "control" then render_control
77
+ # when "treatment_a" then render_treatment_a
78
+ # when "treatment_b" then render_treatment_b
79
+ # end
80
+ #
81
+ def variant(flag_name, default: nil, **context)
82
+ result = get(flag_name, **context)
83
+ result.is_a?(String) ? result : default
84
+ end
85
+
86
+ # Get all flags for a context
87
+ # @param context [Hash] Evaluation context
88
+ # @return [Hash] All flag values
89
+ #
90
+ # @example
91
+ # flags = BrainzLab::Cortex.all(user: current_user)
92
+ # flags[:new_checkout] # => true
93
+ # flags[:rate_limit] # => 200
94
+ #
95
+ def all(**context)
96
+ return {} unless module_enabled?
97
+
98
+ ensure_provisioned!
99
+ return {} unless BrainzLab.configuration.cortex_valid?
100
+
101
+ merged_context = merge_context(context)
102
+ client.evaluate_all(context: merged_context)
103
+ end
104
+
105
+ # List all flag definitions
106
+ # @return [Array<Hash>] List of flag metadata
107
+ def list_flags
108
+ return [] unless module_enabled?
109
+
110
+ ensure_provisioned!
111
+ return [] unless BrainzLab.configuration.cortex_valid?
112
+
113
+ client.list
114
+ end
115
+
116
+ # Get a flag's configuration
117
+ # @param flag_name [String, Symbol] The flag name
118
+ # @return [Hash, nil] Flag configuration
119
+ def flag_config(flag_name)
120
+ return nil unless module_enabled?
121
+
122
+ ensure_provisioned!
123
+ return nil unless BrainzLab.configuration.cortex_valid?
124
+
125
+ client.get_flag(flag_name.to_s)
126
+ end
127
+
128
+ # Clear the flag cache
129
+ def clear_cache!
130
+ cache.clear!
131
+ end
132
+
133
+ # === Context Helpers ===
134
+
135
+ # Set default context for all evaluations in current request
136
+ # @param context [Hash] Context to merge
137
+ def set_context(**context)
138
+ Thread.current[:cortex_context] = (Thread.current[:cortex_context] || {}).merge(context)
139
+ end
140
+
141
+ # Clear the current context
142
+ def clear_context!
143
+ Thread.current[:cortex_context] = nil
144
+ end
145
+
146
+ # Evaluate flags with a temporary context
147
+ # @param context [Hash] Temporary context
148
+ def with_context(**context)
149
+ previous = Thread.current[:cortex_context]
150
+ Thread.current[:cortex_context] = (previous || {}).merge(context)
151
+ yield
152
+ ensure
153
+ Thread.current[:cortex_context] = previous
154
+ end
155
+
156
+ # === INTERNAL ===
157
+
158
+ def ensure_provisioned!
159
+ return if @provisioned
160
+
161
+ @provisioned = true
162
+ provisioner.ensure_project!
163
+ end
164
+
165
+ def provisioner
166
+ @provisioner ||= Provisioner.new(BrainzLab.configuration)
167
+ end
168
+
169
+ def client
170
+ @client ||= Client.new(BrainzLab.configuration)
171
+ end
172
+
173
+ def cache
174
+ @cache ||= Cache.new(BrainzLab.configuration.cortex_cache_ttl)
175
+ end
176
+
177
+ def reset!
178
+ @client = nil
179
+ @provisioner = nil
180
+ @cache = nil
181
+ @provisioned = false
182
+ Thread.current[:cortex_context] = nil
183
+ end
184
+
185
+ private
186
+
187
+ def module_enabled?
188
+ BrainzLab.configuration.cortex_enabled
189
+ end
190
+
191
+ def merge_context(context)
192
+ default_context = BrainzLab.configuration.cortex_default_context || {}
193
+ thread_context = Thread.current[:cortex_context] || {}
194
+
195
+ # Also include user from BrainzLab context if available
196
+ brainzlab_context = {}
197
+ if BrainzLab::Context.current.user
198
+ brainzlab_context[:user] = BrainzLab::Context.current.user
199
+ end
200
+
201
+ # Normalize user context
202
+ merged = default_context.merge(brainzlab_context).merge(thread_context).merge(context)
203
+
204
+ # Convert user object to hash if needed
205
+ if merged[:user].respond_to?(:id)
206
+ merged[:user] = {
207
+ id: merged[:user].id.to_s,
208
+ email: merged[:user].try(:email),
209
+ name: merged[:user].try(:name)
210
+ }.compact
211
+ end
212
+
213
+ merged
214
+ end
215
+
216
+ def build_cache_key(flag_name, context)
217
+ # Include relevant context in cache key
218
+ user_id = context.dig(:user, :id) || context[:user_id]
219
+ env = BrainzLab.configuration.environment
220
+
221
+ parts = [env, flag_name]
222
+ parts << "u:#{user_id}" if user_id
223
+ parts.join(":")
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+ require "cgi"
7
+
8
+ module BrainzLab
9
+ module Dendrite
10
+ class Client
11
+ def initialize(config)
12
+ @config = config
13
+ @base_url = config.dendrite_url || "https://dendrite.brainzlab.ai"
14
+ end
15
+
16
+ # Connect a repository
17
+ def connect_repository(url:, name: nil, branch: "main", **options)
18
+ response = request(
19
+ :post,
20
+ "/api/v1/repositories",
21
+ body: {
22
+ url: url,
23
+ name: name,
24
+ branch: branch,
25
+ **options
26
+ }
27
+ )
28
+
29
+ return nil unless response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
30
+
31
+ JSON.parse(response.body, symbolize_names: true)
32
+ rescue StandardError => e
33
+ log_error("connect_repository", e)
34
+ nil
35
+ end
36
+
37
+ # Trigger sync for a repository
38
+ def sync_repository(repo_id)
39
+ response = request(:post, "/api/v1/repositories/#{repo_id}/sync")
40
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPAccepted)
41
+ rescue StandardError => e
42
+ log_error("sync_repository", e)
43
+ false
44
+ end
45
+
46
+ # Get repository status
47
+ def get_repository(repo_id)
48
+ response = request(:get, "/api/v1/repositories/#{repo_id}")
49
+
50
+ return nil unless response.is_a?(Net::HTTPSuccess)
51
+
52
+ JSON.parse(response.body, symbolize_names: true)
53
+ rescue StandardError => e
54
+ log_error("get_repository", e)
55
+ nil
56
+ end
57
+
58
+ # List repositories
59
+ def list_repositories
60
+ response = request(:get, "/api/v1/repositories")
61
+
62
+ return [] unless response.is_a?(Net::HTTPSuccess)
63
+
64
+ data = JSON.parse(response.body, symbolize_names: true)
65
+ data[:repositories] || []
66
+ rescue StandardError => e
67
+ log_error("list_repositories", e)
68
+ []
69
+ end
70
+
71
+ # Get wiki pages for a repository
72
+ def get_wiki(repo_id)
73
+ response = request(:get, "/api/v1/wiki/#{repo_id}")
74
+
75
+ return nil unless response.is_a?(Net::HTTPSuccess)
76
+
77
+ JSON.parse(response.body, symbolize_names: true)
78
+ rescue StandardError => e
79
+ log_error("get_wiki", e)
80
+ nil
81
+ end
82
+
83
+ # Get a specific wiki page
84
+ def get_wiki_page(repo_id, page_slug)
85
+ response = request(:get, "/api/v1/wiki/#{repo_id}/#{CGI.escape(page_slug)}")
86
+
87
+ return nil unless response.is_a?(Net::HTTPSuccess)
88
+
89
+ JSON.parse(response.body, symbolize_names: true)
90
+ rescue StandardError => e
91
+ log_error("get_wiki_page", e)
92
+ nil
93
+ end
94
+
95
+ # Semantic search across codebase
96
+ def search(repo_id, query, limit: 10)
97
+ response = request(
98
+ :get,
99
+ "/api/v1/search",
100
+ params: { repo_id: repo_id, q: query, limit: limit }
101
+ )
102
+
103
+ return [] unless response.is_a?(Net::HTTPSuccess)
104
+
105
+ data = JSON.parse(response.body, symbolize_names: true)
106
+ data[:results] || []
107
+ rescue StandardError => e
108
+ log_error("search", e)
109
+ []
110
+ end
111
+
112
+ # Ask a question about the codebase
113
+ def ask(repo_id, question, session_id: nil)
114
+ response = request(
115
+ :post,
116
+ "/api/v1/chat",
117
+ body: {
118
+ repo_id: repo_id,
119
+ question: question,
120
+ session_id: session_id
121
+ }
122
+ )
123
+
124
+ return nil unless response.is_a?(Net::HTTPSuccess)
125
+
126
+ JSON.parse(response.body, symbolize_names: true)
127
+ rescue StandardError => e
128
+ log_error("ask", e)
129
+ nil
130
+ end
131
+
132
+ # Explain a specific file or function
133
+ def explain(repo_id, path, symbol: nil)
134
+ response = request(
135
+ :post,
136
+ "/api/v1/explain",
137
+ body: {
138
+ repo_id: repo_id,
139
+ path: path,
140
+ symbol: symbol
141
+ }
142
+ )
143
+
144
+ return nil unless response.is_a?(Net::HTTPSuccess)
145
+
146
+ JSON.parse(response.body, symbolize_names: true)
147
+ rescue StandardError => e
148
+ log_error("explain", e)
149
+ nil
150
+ end
151
+
152
+ # Generate a diagram
153
+ def generate_diagram(repo_id, type:, scope: nil)
154
+ response = request(
155
+ :post,
156
+ "/api/v1/diagrams",
157
+ body: {
158
+ repo_id: repo_id,
159
+ type: type, # class, er, sequence, architecture
160
+ scope: scope
161
+ }
162
+ )
163
+
164
+ return nil unless response.is_a?(Net::HTTPSuccess)
165
+
166
+ JSON.parse(response.body, symbolize_names: true)
167
+ rescue StandardError => e
168
+ log_error("generate_diagram", e)
169
+ nil
170
+ end
171
+
172
+ def provision(project_id:, app_name:)
173
+ response = request(
174
+ :post,
175
+ "/api/v1/projects/provision",
176
+ body: { project_id: project_id, app_name: app_name },
177
+ use_service_key: true
178
+ )
179
+
180
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
181
+ rescue StandardError => e
182
+ log_error("provision", e)
183
+ false
184
+ end
185
+
186
+ private
187
+
188
+ def request(method, path, headers: {}, body: nil, params: nil, use_service_key: false)
189
+ uri = URI.parse("#{@base_url}#{path}")
190
+
191
+ if params
192
+ uri.query = URI.encode_www_form(params)
193
+ end
194
+
195
+ http = Net::HTTP.new(uri.host, uri.port)
196
+ http.use_ssl = uri.scheme == "https"
197
+ http.open_timeout = 10
198
+ http.read_timeout = 60 # Longer timeout for AI operations
199
+
200
+ request = case method
201
+ when :get
202
+ Net::HTTP::Get.new(uri)
203
+ when :post
204
+ Net::HTTP::Post.new(uri)
205
+ when :put
206
+ Net::HTTP::Put.new(uri)
207
+ when :delete
208
+ Net::HTTP::Delete.new(uri)
209
+ end
210
+
211
+ request["Content-Type"] = "application/json"
212
+ request["Accept"] = "application/json"
213
+
214
+ if use_service_key
215
+ request["X-Service-Key"] = @config.dendrite_master_key || @config.secret_key
216
+ else
217
+ auth_key = @config.dendrite_api_key || @config.secret_key
218
+ request["Authorization"] = "Bearer #{auth_key}" if auth_key
219
+ end
220
+
221
+ headers.each { |k, v| request[k] = v }
222
+ request.body = body.to_json if body
223
+
224
+ http.request(request)
225
+ end
226
+
227
+ def log_error(operation, error)
228
+ BrainzLab.debug_log("[Dendrite::Client] #{operation} failed: #{error.message}")
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Dendrite
5
+ class Provisioner
6
+ def initialize(config)
7
+ @config = config
8
+ @provisioned = false
9
+ end
10
+
11
+ def ensure_project!
12
+ return if @provisioned
13
+ return unless @config.dendrite_auto_provision
14
+ return unless valid_auth?
15
+
16
+ @provisioned = true
17
+
18
+ project_id = detect_project_id
19
+ return unless project_id
20
+
21
+ client = Client.new(@config)
22
+ client.provision(
23
+ project_id: project_id,
24
+ app_name: @config.app_name || @config.service
25
+ )
26
+
27
+ BrainzLab.debug_log("[Dendrite::Provisioner] Project provisioned: #{project_id}")
28
+ rescue StandardError => e
29
+ BrainzLab.debug_log("[Dendrite::Provisioner] Provisioning failed: #{e.message}")
30
+ end
31
+
32
+ private
33
+
34
+ def valid_auth?
35
+ key = @config.dendrite_api_key || @config.dendrite_master_key || @config.secret_key
36
+ !key.nil? && !key.empty?
37
+ end
38
+
39
+ def detect_project_id
40
+ ENV["BRAINZLAB_PROJECT_ID"]
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dendrite/client"
4
+ require_relative "dendrite/provisioner"
5
+
6
+ module BrainzLab
7
+ module Dendrite
8
+ class << self
9
+ # Connect a Git repository for documentation
10
+ # @param url [String] Git repository URL
11
+ # @param name [String] Optional display name
12
+ # @param branch [String] Branch to track (default: main)
13
+ # @return [Hash, nil] Repository info
14
+ #
15
+ # @example
16
+ # BrainzLab::Dendrite.connect(
17
+ # "https://github.com/org/repo",
18
+ # name: "My API",
19
+ # branch: "main"
20
+ # )
21
+ #
22
+ def connect(url, name: nil, branch: "main", **options)
23
+ return nil unless enabled?
24
+
25
+ ensure_provisioned!
26
+ return nil unless BrainzLab.configuration.dendrite_valid?
27
+
28
+ client.connect_repository(url: url, name: name, branch: branch, **options)
29
+ end
30
+
31
+ # Trigger documentation sync for a repository
32
+ # @param repo_id [String] Repository ID
33
+ # @return [Boolean] True if sync started
34
+ def sync(repo_id)
35
+ return false unless enabled?
36
+
37
+ ensure_provisioned!
38
+ return false unless BrainzLab.configuration.dendrite_valid?
39
+
40
+ client.sync_repository(repo_id)
41
+ end
42
+
43
+ # Get repository info
44
+ # @param repo_id [String] Repository ID
45
+ # @return [Hash, nil] Repository details
46
+ def repository(repo_id)
47
+ return nil unless enabled?
48
+
49
+ ensure_provisioned!
50
+ return nil unless BrainzLab.configuration.dendrite_valid?
51
+
52
+ client.get_repository(repo_id)
53
+ end
54
+
55
+ # List all connected repositories
56
+ # @return [Array<Hash>] List of repositories
57
+ def repositories
58
+ return [] unless enabled?
59
+
60
+ ensure_provisioned!
61
+ return [] unless BrainzLab.configuration.dendrite_valid?
62
+
63
+ client.list_repositories
64
+ end
65
+
66
+ # Get wiki for a repository
67
+ # @param repo_id [String] Repository ID
68
+ # @return [Hash, nil] Wiki structure
69
+ def wiki(repo_id)
70
+ return nil unless enabled?
71
+
72
+ ensure_provisioned!
73
+ return nil unless BrainzLab.configuration.dendrite_valid?
74
+
75
+ client.get_wiki(repo_id)
76
+ end
77
+
78
+ # Get a specific wiki page
79
+ # @param repo_id [String] Repository ID
80
+ # @param page [String] Page slug (e.g., "models/user")
81
+ # @return [Hash, nil] Page content
82
+ def page(repo_id, page_slug)
83
+ return nil unless enabled?
84
+
85
+ ensure_provisioned!
86
+ return nil unless BrainzLab.configuration.dendrite_valid?
87
+
88
+ client.get_wiki_page(repo_id, page_slug)
89
+ end
90
+
91
+ # Semantic search across the codebase
92
+ # @param repo_id [String] Repository ID
93
+ # @param query [String] Search query
94
+ # @param limit [Integer] Max results (default: 10)
95
+ # @return [Array<Hash>] Search results
96
+ #
97
+ # @example
98
+ # results = BrainzLab::Dendrite.search(repo_id, "authentication flow")
99
+ #
100
+ def search(repo_id, query, limit: 10)
101
+ return [] unless enabled?
102
+
103
+ ensure_provisioned!
104
+ return [] unless BrainzLab.configuration.dendrite_valid?
105
+
106
+ client.search(repo_id, query, limit: limit)
107
+ end
108
+
109
+ # Ask a question about the codebase
110
+ # @param repo_id [String] Repository ID
111
+ # @param question [String] Question to ask
112
+ # @param session_id [String] Optional session for follow-up questions
113
+ # @return [Hash, nil] AI response with answer
114
+ #
115
+ # @example
116
+ # response = BrainzLab::Dendrite.ask(repo_id, "How does the payment flow work?")
117
+ # puts response[:answer]
118
+ #
119
+ def ask(repo_id, question, session_id: nil)
120
+ return nil unless enabled?
121
+
122
+ ensure_provisioned!
123
+ return nil unless BrainzLab.configuration.dendrite_valid?
124
+
125
+ client.ask(repo_id, question, session_id: session_id)
126
+ end
127
+
128
+ # Explain a file or code symbol
129
+ # @param repo_id [String] Repository ID
130
+ # @param path [String] File path
131
+ # @param symbol [String] Optional specific symbol (class, method)
132
+ # @return [Hash, nil] Explanation
133
+ #
134
+ # @example
135
+ # explanation = BrainzLab::Dendrite.explain(repo_id, "app/models/user.rb", symbol: "authenticate")
136
+ #
137
+ def explain(repo_id, path, symbol: nil)
138
+ return nil unless enabled?
139
+
140
+ ensure_provisioned!
141
+ return nil unless BrainzLab.configuration.dendrite_valid?
142
+
143
+ client.explain(repo_id, path, symbol: symbol)
144
+ end
145
+
146
+ # Generate a diagram
147
+ # @param repo_id [String] Repository ID
148
+ # @param type [Symbol] Diagram type (:class, :er, :sequence, :architecture)
149
+ # @param scope [String] Optional scope (module, class name)
150
+ # @return [Hash, nil] Mermaid diagram
151
+ #
152
+ # @example
153
+ # diagram = BrainzLab::Dendrite.diagram(repo_id, :er)
154
+ # puts diagram[:mermaid]
155
+ #
156
+ def diagram(repo_id, type:, scope: nil)
157
+ return nil unless enabled?
158
+
159
+ ensure_provisioned!
160
+ return nil unless BrainzLab.configuration.dendrite_valid?
161
+
162
+ client.generate_diagram(repo_id, type: type, scope: scope)
163
+ end
164
+
165
+ # === INTERNAL ===
166
+
167
+ def ensure_provisioned!
168
+ return if @provisioned
169
+
170
+ @provisioned = true
171
+ provisioner.ensure_project!
172
+ end
173
+
174
+ def provisioner
175
+ @provisioner ||= Provisioner.new(BrainzLab.configuration)
176
+ end
177
+
178
+ def client
179
+ @client ||= Client.new(BrainzLab.configuration)
180
+ end
181
+
182
+ def reset!
183
+ @client = nil
184
+ @provisioner = nil
185
+ @provisioned = false
186
+ end
187
+
188
+ private
189
+
190
+ def enabled?
191
+ BrainzLab.configuration.dendrite_enabled
192
+ end
193
+ end
194
+ end
195
+ end