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.
- checksums.yaml +4 -4
- data/README.md +8 -0
- data/lib/brainzlab/beacon/client.rb +209 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +341 -3
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +141 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +227 -0
- data/lib/brainzlab/dendrite/client.rb +232 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
- data/lib/brainzlab/devtools/assets/devtools.js +322 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -0
- data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
- data/lib/brainzlab/devtools/data/collector.rb +248 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
- data/lib/brainzlab/devtools/middleware/database_handler.rb +180 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +376 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +155 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +94 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +70 -0
- data/lib/brainzlab/flux/provisioner.rb +57 -0
- data/lib/brainzlab/flux.rb +174 -0
- data/lib/brainzlab/instrumentation/active_record.rb +18 -1
- data/lib/brainzlab/instrumentation/aws.rb +179 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/resque.rb +115 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +198 -0
- data/lib/brainzlab/instrumentation/stripe.rb +164 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +104 -0
- data/lib/brainzlab/instrumentation.rb +72 -0
- data/lib/brainzlab/nerve/client.rb +217 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/instrumentation.rb +35 -2
- data/lib/brainzlab/pulse/propagation.rb +1 -1
- data/lib/brainzlab/pulse/tracer.rb +1 -1
- data/lib/brainzlab/pulse.rb +1 -1
- data/lib/brainzlab/rails/log_subscriber.rb +1 -2
- data/lib/brainzlab/rails/railtie.rb +36 -3
- data/lib/brainzlab/recall/provisioner.rb +17 -0
- data/lib/brainzlab/recall.rb +6 -1
- data/lib/brainzlab/reflex.rb +2 -2
- data/lib/brainzlab/sentinel/client.rb +218 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +62 -0
- data/lib/brainzlab/signal/provisioner.rb +55 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +290 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +265 -0
- data/lib/brainzlab/utilities/health_check.rb +296 -0
- data/lib/brainzlab/utilities/log_formatter.rb +256 -0
- data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
- data/lib/brainzlab/utilities.rb +17 -0
- data/lib/brainzlab/vault/cache.rb +80 -0
- data/lib/brainzlab/vault/client.rb +198 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +268 -0
- data/lib/brainzlab/version.rb +1 -1
- data/lib/brainzlab/vision/client.rb +128 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +157 -0
- data/lib/brainzlab.rb +101 -0
- 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
|