smplkit 3.0.96 → 3.0.97

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.
@@ -16,6 +16,13 @@ module Smplkit
16
16
  class ConfigItem
17
17
  attr_accessor :name, :value, :type, :description
18
18
 
19
+ # Create a typed config item.
20
+ #
21
+ # @param name [String] The item key within its config.
22
+ # @param value [Object] The item's value.
23
+ # @param type [String] The item value type — one of +"STRING"+,
24
+ # +"NUMBER"+, +"BOOLEAN"+, or +"JSON"+.
25
+ # @param description [String, nil] Optional human-readable description.
19
26
  def initialize(name:, value:, type:, description: nil)
20
27
  @name = name
21
28
  @value = value
@@ -23,6 +30,8 @@ module Smplkit
23
30
  @description = description
24
31
  end
25
32
 
33
+ # @return [Hash{String => Object}] The item as a plain Hash, omitting a
34
+ # +nil+ description.
26
35
  def to_h
27
36
  { "name" => @name, "value" => @value, "type" => @type, "description" => @description }.compact
28
37
  end
@@ -42,10 +51,13 @@ module Smplkit
42
51
  # setters with +environment:+ (e.g.
43
52
  # +cfg.set_string("k", "v", environment: "production")+).
44
53
  #
45
- # Per ADR-024 §2.4 the wire shape for env overrides is flat —
46
- # +{env: {key: rawValue}}+ so the in-memory representation is
47
- # simply a Hash of raw values, with no typed wrapper.
54
+ # An override stores only the raw value; the declared type and description
55
+ # come from the base item, so an item's type and description are ignored
56
+ # when an environment override is supplied.
48
57
  class ConfigEnvironment
58
+ # @param values [Hash{String => Object}, nil] Initial overrides as a flat
59
+ # +{key => raw_value}+ map. A legacy +{key => {"value" => raw}}+ wrapper
60
+ # is unwrapped to the raw value.
49
61
  def initialize(values: nil)
50
62
  @values_raw = {}
51
63
  return unless values
@@ -58,6 +70,9 @@ module Smplkit
58
70
  end
59
71
 
60
72
  # Returns overrides as a plain Hash +{ "key" => raw_value }+.
73
+ #
74
+ # @return [Hash{String => Object}] A read-only shallow copy of the
75
+ # overrides.
61
76
  def values
62
77
  @values_raw.dup
63
78
  end
@@ -65,6 +80,9 @@ module Smplkit
65
80
  # Returns overrides as a plain Hash +{ "key" => raw_value }+. Retained
66
81
  # as a separate accessor for backward compatibility; identical to
67
82
  # +values+ now that env overrides are stored flat.
83
+ #
84
+ # @return [Hash{String => Object}] A read-only shallow copy of the
85
+ # overrides.
68
86
  def values_raw
69
87
  @values_raw.dup
70
88
  end
@@ -82,6 +100,20 @@ module Smplkit
82
100
  class Config
83
101
  attr_accessor :id, :key, :name, :description, :parent_id, :created_at, :updated_at
84
102
 
103
+ # @param client [ConfigClient, nil] The owning client, or +nil+ for a
104
+ # detached model that cannot save or delete.
105
+ # @param key [String] The config identifier (slug).
106
+ # @param id [String, nil] The server-assigned id, or +nil+ for an unsaved
107
+ # config.
108
+ # @param name [String, nil] Display name.
109
+ # @param description [String, nil] Optional description.
110
+ # @param parent_id [String, nil] Parent config id (slug), or +nil+ for a
111
+ # root config.
112
+ # @param items [Array<ConfigItem>, nil] Base items.
113
+ # @param environments [Hash{String => ConfigEnvironment}, nil]
114
+ # Per-environment overrides keyed by environment id.
115
+ # @param created_at [Object, nil] Creation timestamp.
116
+ # @param updated_at [Object, nil] Last-modified timestamp.
85
117
  def initialize(client = nil, key:, id: nil, name: nil, description: nil,
86
118
  parent_id: nil, items: nil, environments: nil,
87
119
  created_at: nil, updated_at: nil)
@@ -97,14 +129,25 @@ module Smplkit
97
129
  @updated_at = updated_at
98
130
  end
99
131
 
132
+ # @return [Array<ConfigItem>] A read-only shallow copy of the base items.
100
133
  def items
101
134
  @items.dup
102
135
  end
103
136
 
137
+ # @return [Hash{String => ConfigEnvironment}] A read-only shallow copy of
138
+ # the per-environment overrides keyed by environment id.
104
139
  def environments
105
140
  @environments.dup
106
141
  end
107
142
 
143
+ # Persist this config to the server. Creates a new config if unsaved, or
144
+ # updates the existing one.
145
+ #
146
+ # @return [self]
147
+ # @raise [Smplkit::NotFoundError] If the config no longer exists
148
+ # (update).
149
+ # @raise [Smplkit::ValidationError] If the server rejects the request.
150
+ # @raise [RuntimeError] If the model was constructed without a client.
108
151
  def save
109
152
  raise "Config was constructed without a client; cannot save" if @client.nil?
110
153
 
@@ -119,6 +162,10 @@ module Smplkit
119
162
  end
120
163
  alias save! save
121
164
 
165
+ # Delete this config from the server.
166
+ #
167
+ # @return [void]
168
+ # @raise [RuntimeError] If the model was constructed without a client.
122
169
  def delete
123
170
  raise "Config was constructed without a client; cannot delete" if @client.nil?
124
171
 
@@ -126,26 +173,86 @@ module Smplkit
126
173
  end
127
174
  alias delete! delete
128
175
 
176
+ # Set (or replace) an item. When +environment:+ is given, sets an
177
+ # override on that environment.
178
+ #
179
+ # An environment override stores only the raw value; the declared type and
180
+ # description come from the base item, so the +ConfigItem+'s type and
181
+ # description are ignored when +environment:+ is supplied.
182
+ #
183
+ # @param item [ConfigItem] The item to set; its +name+ is the item key.
184
+ # @param environment [String, nil] When given, set the value as an
185
+ # override on this environment rather than on the base config.
186
+ # @return [self]
187
+ def set(item, environment: nil)
188
+ set_typed(item.name, item.value, item.type, environment: environment, description: item.description)
189
+ end
190
+
191
+ # Convenience: set a STRING item (or environment override).
192
+ #
193
+ # @param name [String] The item key to set.
194
+ # @param value [String] The string value.
195
+ # @param environment [String, nil] When given, set the value as an
196
+ # override on this environment rather than on the base config.
197
+ # @param description [String, nil] Optional human-readable description.
198
+ # Ignored when setting an environment override.
199
+ # @return [self]
129
200
  def set_string(name, value, environment: nil, description: nil)
130
201
  set_typed(name, value, ItemType::STRING, environment: environment, description: description)
131
202
  end
132
203
 
204
+ # Convenience: set a NUMBER item (or environment override).
205
+ #
206
+ # @param name [String] The item key to set.
207
+ # @param value [Integer, Float] The numeric value.
208
+ # @param environment [String, nil] When given, set the value as an
209
+ # override on this environment rather than on the base config.
210
+ # @param description [String, nil] Optional human-readable description.
211
+ # Ignored when setting an environment override.
212
+ # @return [self]
133
213
  def set_number(name, value, environment: nil, description: nil)
134
214
  set_typed(name, value, ItemType::NUMBER, environment: environment, description: description)
135
215
  end
136
216
 
217
+ # Convenience: set a BOOLEAN item (or environment override).
218
+ #
219
+ # @param name [String] The item key to set.
220
+ # @param value [Boolean] The boolean value.
221
+ # @param environment [String, nil] When given, set the value as an
222
+ # override on this environment rather than on the base config.
223
+ # @param description [String, nil] Optional human-readable description.
224
+ # Ignored when setting an environment override.
225
+ # @return [self]
137
226
  def set_boolean(name, value, environment: nil, description: nil)
138
227
  set_typed(name, value, ItemType::BOOLEAN, environment: environment, description: description)
139
228
  end
140
229
 
230
+ # Convenience: set a JSON item (or environment override).
231
+ #
232
+ # @param name [String] The item key to set.
233
+ # @param value [Object] Any JSON-serializable value (Hash, Array, or
234
+ # primitive).
235
+ # @param environment [String, nil] When given, set the value as an
236
+ # override on this environment rather than on the base config.
237
+ # @param description [String, nil] Optional human-readable description.
238
+ # Ignored when setting an environment override.
239
+ # @return [self]
141
240
  def set_json(name, value, environment: nil, description: nil)
142
241
  set_typed(name, value, ItemType::JSON, environment: environment, description: description)
143
242
  end
144
243
 
145
- def remove_item(name, environment: nil)
244
+ # Remove an item by name. When +environment:+ is given, removes the
245
+ # per-environment override only. Removing an item that isn't present is a
246
+ # no-op.
247
+ #
248
+ # @param name [String] The item key to remove.
249
+ # @param environment [String, nil] When given, remove only this
250
+ # environment's override for +name+, leaving the base item intact.
251
+ # @return [self]
252
+ def remove(name, environment: nil)
146
253
  if environment
147
254
  env = @environments[environment]
148
- return unless env
255
+ return self unless env
149
256
 
150
257
  raw = env.values_raw
151
258
  raw.delete(name)
@@ -181,8 +288,8 @@ module Smplkit
181
288
  @items << ConfigItem.new(name: name, value: value, type: type, description: description)
182
289
  end
183
290
  else
184
- # Per ADR-024 §2.4 env overrides carry the raw value only — type
185
- # and description live on the base item, not on the override.
291
+ # Env overrides carry the raw value only — type and description live
292
+ # on the base item, not on the override.
186
293
  env = (@environments[environment] ||= ConfigEnvironment.new)
187
294
  raw = env.values_raw
188
295
  raw[name] = value
@@ -4,6 +4,8 @@ require_relative "errors"
4
4
 
5
5
  module Smplkit
6
6
  # SDK configuration resolution: defaults -> file -> env vars -> constructor args.
7
+ #
8
+ # @api private
7
9
  module ConfigResolution
8
10
  CONFIG_KEYS = {
9
11
  "api_key" => "SMPLKIT_API_KEY",
@@ -33,7 +35,7 @@ module Smplkit
33
35
  keyword_init: true
34
36
  )
35
37
 
36
- ResolvedManagementConfig = Struct.new(
38
+ ResolvedClientConfig = Struct.new(
37
39
  :api_key, :base_domain, :scheme, :debug, :extra_headers,
38
40
  keyword_init: true
39
41
  )
@@ -157,8 +159,8 @@ module Smplkit
157
159
  )
158
160
  end
159
161
 
160
- def resolve_management_config(profile: nil, api_key: nil, base_domain: nil,
161
- scheme: nil, debug: nil, home_dir: nil)
162
+ def resolve_client_config(profile: nil, api_key: nil, base_domain: nil,
163
+ scheme: nil, debug: nil, home_dir: nil)
162
164
  resolved = {
163
165
  "api_key" => nil,
164
166
  "base_domain" => "smplkit.com",
@@ -191,7 +193,7 @@ module Smplkit
191
193
 
192
194
  missing_required(resolved, "api_key", active_profile)
193
195
 
194
- ResolvedManagementConfig.new(
196
+ ResolvedClientConfig.new(
195
197
  api_key: resolved["api_key"].to_s,
196
198
  base_domain: resolved["base_domain"].to_s,
197
199
  scheme: resolved["scheme"].to_s,
@@ -6,9 +6,9 @@ module Smplkit
6
6
  # A single error object from the server's JSON:API +errors+ array.
7
7
  #
8
8
  # +code+ is the application-specific machine-readable error code (e.g.
9
- # +environment_unmanaged+); per JSON:API §7 and ADR-014, smplkit sets
10
- # this on every error so callers can branch without string-matching
11
- # the human +detail+. +meta+ carries additional structured context
9
+ # +environment_unmanaged+); smplkit sets this on every error so callers
10
+ # can branch without string-matching the human +detail+. +meta+ carries
11
+ # additional structured context
12
12
  # (e.g. <tt>{"environment" => "staging"}</tt>).
13
13
  class ApiErrorDetail
14
14
  attr_reader :status, :code, :title, :detail, :source, :meta
@@ -62,6 +62,7 @@ module Smplkit
62
62
  end
63
63
  end
64
64
 
65
+ # @api private — Build a human-readable message from a list of error details.
65
66
  def self.derive_message(errors)
66
67
  return "An API error occurred" if errors.nil? || errors.empty?
67
68
 
@@ -92,6 +93,8 @@ module Smplkit
92
93
  # this.
93
94
  class NotInstalledError < Error; end
94
95
 
96
+ # @api private — Internal helpers that parse JSON:API error bodies and map
97
+ # HTTP status codes onto the error classes above.
95
98
  module Errors
96
99
  module_function
97
100
 
@@ -8,7 +8,7 @@ require "digest"
8
8
  # Smpl Flags has two surfaces on a single client, mirroring how the config,
9
9
  # audit, and jobs clients expose their full surface from one class:
10
10
  #
11
- # * *Management surface* — pure CRUD, no live connection: +new_boolean_flag+ /
11
+ # * *CRUD surface* — pure CRUD, no live connection: +new_boolean_flag+ /
12
12
  # +new_string_flag+ / +new_number_flag+ / +new_json_flag+ constructors, +get+
13
13
  # / +list+ / +delete+ CRUD, and the flag-declaration discovery buffer
14
14
  # (+register+ / +flush+ / +flush_sync+ / +pending_count+). The client owns the
@@ -33,6 +33,14 @@ require "digest"
33
33
  module Smplkit
34
34
  module Flags
35
35
  # Describes a flag definition change. Frozen — fields are set at construction.
36
+ #
37
+ # @!attribute [r] id
38
+ # @return [String] id of the flag whose definition changed.
39
+ # @!attribute [r] source
40
+ # @return [String] origin of the change (e.g. +"websocket"+ for a live
41
+ # update or +"manual"+ for a refresh).
42
+ # @!attribute [r] deleted
43
+ # @return [Boolean] whether the change was a deletion of the flag.
36
44
  class FlagChangeEvent
37
45
  attr_reader :id, :source, :deleted
38
46
 
@@ -54,6 +62,8 @@ module Smplkit
54
62
  end
55
63
 
56
64
  # Thread-safe LRU resolution cache with hit/miss stats.
65
+ #
66
+ # @api private
57
67
  class ResolutionCache
58
68
  DEFAULT_MAX_SIZE = 10_000
59
69
 
@@ -96,18 +106,29 @@ module Smplkit
96
106
  end
97
107
 
98
108
  # Evaluation statistics for the flags runtime.
109
+ #
110
+ # @!attribute [r] cache_hits
111
+ # @return [Integer] number of flag evaluations served from the local cache.
112
+ # @!attribute [r] cache_misses
113
+ # @return [Integer] number of flag evaluations that missed the cache and
114
+ # were evaluated from the flag definitions.
99
115
  FlagStats = Struct.new(:cache_hits, :cache_misses, keyword_init: true)
100
116
 
101
117
  # Convert a list of Context objects to the nested evaluation dict.
118
+ #
119
+ # @api private
102
120
  def self.contexts_to_eval_dict(contexts)
103
121
  contexts.to_h { |ctx| [ctx.type, ctx.to_eval_hash] }
104
122
  end
105
123
 
106
124
  # Compute a stable hash for a context evaluation dict.
125
+ #
126
+ # @api private
107
127
  def self.hash_context(eval_dict)
108
128
  Digest::MD5.hexdigest(JSON.generate(deep_sort(eval_dict)))
109
129
  end
110
130
 
131
+ # @api private
111
132
  def self.deep_sort(value)
112
133
  case value
113
134
  when Hash
@@ -127,15 +148,17 @@ module Smplkit
127
148
  # defaults). The app transport backs the standalone contexts client
128
149
  # (evaluation-context registration); the app base URL is returned so a
129
150
  # standalone client can open its own WebSocket against the event gateway.
151
+ #
152
+ # @api private
130
153
  def self.flags_transport(api_key:, base_url:, profile:, base_domain:, scheme:, debug:, extra_headers:)
131
- cfg = ConfigResolution.resolve_management_config(
154
+ cfg = ConfigResolution.resolve_client_config(
132
155
  profile: profile, api_key: api_key, base_domain: base_domain, scheme: scheme, debug: debug
133
156
  )
134
157
  resolved_key = api_key.nil? ? cfg.api_key : api_key
135
158
  merged = {}
136
159
  merged.merge!(cfg.extra_headers || {})
137
160
  merged.merge!(extra_headers || {})
138
- tcfg = ConfigResolution::ResolvedManagementConfig.new(
161
+ tcfg = ConfigResolution::ResolvedClientConfig.new(
139
162
  api_key: resolved_key, base_domain: cfg.base_domain, scheme: cfg.scheme,
140
163
  debug: cfg.debug, extra_headers: merged
141
164
  )
@@ -156,7 +179,7 @@ module Smplkit
156
179
  # beta = flags.boolean_flag("beta", default: false)
157
180
  # beta.get # => ...
158
181
  #
159
- # The management surface (+new_*+ / +get+ / +list+ / +delete+ and discovery)
182
+ # The CRUD surface (+new_*+ / +get+ / +list+ / +delete+ and discovery)
160
183
  # is pure CRUD. The live surface (+boolean_flag+ / +string_flag+ /
161
184
  # +number_flag+ / +json_flag+ / +refresh+ / +stats+ / +on_change+) connects
162
185
  # lazily on first use — the first call flushes discovery, fetches all flag
@@ -208,6 +231,14 @@ module Smplkit
208
231
  # ----------------------------------------------------------------
209
232
 
210
233
  # Return a new unsaved boolean +BooleanFlag+. Call +save+ to persist.
234
+ #
235
+ # @param id [String] stable flag identifier, unique per account.
236
+ # @param default [Boolean] value served when no environment override or
237
+ # rule applies.
238
+ # @param name [String, nil] human-readable display name; defaults to a
239
+ # title-cased form of +id+.
240
+ # @param description [String, nil] optional free-text description of the flag.
241
+ # @return [BooleanFlag] an unsaved flag; call +save+ to persist it.
211
242
  def new_boolean_flag(id, default:, name: nil, description: nil)
212
243
  BooleanFlag.new(
213
244
  self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
@@ -218,6 +249,17 @@ module Smplkit
218
249
  end
219
250
 
220
251
  # Return a new unsaved string +StringFlag+. Call +save+ to persist.
252
+ #
253
+ # @param id [String] stable flag identifier, unique per account.
254
+ # @param default [String] value served when no environment override or
255
+ # rule applies.
256
+ # @param name [String, nil] human-readable display name; defaults to a
257
+ # title-cased form of +id+.
258
+ # @param description [String, nil] optional free-text description of the flag.
259
+ # @param values [Array<FlagValue>, nil] optional list of allowed values
260
+ # constraining what the flag may serve; when omitted the flag is
261
+ # unconstrained.
262
+ # @return [StringFlag] an unsaved flag; call +save+ to persist it.
221
263
  def new_string_flag(id, default:, name: nil, description: nil, values: nil)
222
264
  StringFlag.new(
223
265
  self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
@@ -226,6 +268,17 @@ module Smplkit
226
268
  end
227
269
 
228
270
  # Return a new unsaved numeric +NumberFlag+. Call +save+ to persist.
271
+ #
272
+ # @param id [String] stable flag identifier, unique per account.
273
+ # @param default [Numeric] value served when no environment override or
274
+ # rule applies.
275
+ # @param name [String, nil] human-readable display name; defaults to a
276
+ # title-cased form of +id+.
277
+ # @param description [String, nil] optional free-text description of the flag.
278
+ # @param values [Array<FlagValue>, nil] optional list of allowed values
279
+ # constraining what the flag may serve; when omitted the flag is
280
+ # unconstrained.
281
+ # @return [NumberFlag] an unsaved flag; call +save+ to persist it.
229
282
  def new_number_flag(id, default:, name: nil, description: nil, values: nil)
230
283
  NumberFlag.new(
231
284
  self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
@@ -234,6 +287,17 @@ module Smplkit
234
287
  end
235
288
 
236
289
  # Return a new unsaved JSON +JsonFlag+. Call +save+ to persist.
290
+ #
291
+ # @param id [String] stable flag identifier, unique per account.
292
+ # @param default [Hash] value served when no environment override or
293
+ # rule applies.
294
+ # @param name [String, nil] human-readable display name; defaults to a
295
+ # title-cased form of +id+.
296
+ # @param description [String, nil] optional free-text description of the flag.
297
+ # @param values [Array<FlagValue>, nil] optional list of allowed values
298
+ # constraining what the flag may serve; when omitted the flag is
299
+ # unconstrained.
300
+ # @return [JsonFlag] an unsaved flag; call +save+ to persist it.
237
301
  def new_json_flag(id, default:, name: nil, description: nil, values: nil)
238
302
  JsonFlag.new(
239
303
  self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
@@ -242,12 +306,22 @@ module Smplkit
242
306
  end
243
307
 
244
308
  # Fetch the editable +Flag+ resource by id.
309
+ #
310
+ # @param id [String] identifier of the flag to fetch.
311
+ # @return [Flag] the flag, ready to mutate and +save+.
312
+ # @raise [Smplkit::NotFoundError] no flag with that id exists for the account.
245
313
  def get(id)
246
314
  response = ApiSupport::ErrorMapping.call { @api.get_flag(id) }
247
315
  model_from_resource(ApiSupport::ResourceShim.from_model(response.data))
248
316
  end
249
317
 
250
318
  # List flags for the authenticated account.
319
+ #
320
+ # @param page_number [Integer, nil] 1-based page index to fetch; when
321
+ # omitted the server default applies.
322
+ # @param page_size [Integer, nil] number of flags per page; when omitted
323
+ # the server default applies.
324
+ # @return [Array<Flag>] the flags on the requested page.
251
325
  def list(page_number: nil, page_size: nil)
252
326
  opts = {}
253
327
  opts[:page_number] = page_number unless page_number.nil?
@@ -257,6 +331,10 @@ module Smplkit
257
331
  end
258
332
 
259
333
  # Delete a flag by id.
334
+ #
335
+ # @param id [String] identifier of the flag to delete.
336
+ # @return [void]
337
+ # @raise [Smplkit::NotFoundError] no flag with that id exists for the account.
260
338
  def delete(id)
261
339
  ApiSupport::ErrorMapping.call { @api.delete_flag(id) }
262
340
  nil
@@ -277,6 +355,14 @@ module Smplkit
277
355
  # ----------------------------------------------------------------
278
356
 
279
357
  # Buffer flag declarations for bulk-discovery upload; optionally flush now.
358
+ #
359
+ # @param items [FlagDeclaration, Array<FlagDeclaration>] a single
360
+ # declaration or an array of them to queue.
361
+ # @param flush [Boolean] when true, send the buffered declarations
362
+ # immediately via +flush+ before returning. When false (the default),
363
+ # they stay buffered and are sent on the next flush — automatic once the
364
+ # buffer reaches its batch size, or on the first live call.
365
+ # @return [void]
280
366
  def register(items, flush: false)
281
367
  batch = items.is_a?(Array) ? items : [items]
282
368
  batch.each { |d| @buffer.add(d) }
@@ -295,6 +381,8 @@ module Smplkit
295
381
  # against an unhealthy +flags+ service is automatically retried by the
296
382
  # next +flush+ call (periodic background flush, install retry, or final
297
383
  # flush on close).
384
+ #
385
+ # @return [void]
298
386
  def flush
299
387
  batch = @buffer.peek
300
388
  return if batch.empty?
@@ -305,11 +393,15 @@ module Smplkit
305
393
  end
306
394
 
307
395
  # Synchronous flush — alias of +flush+ for the periodic-flush path.
396
+ #
397
+ # @return [void]
308
398
  def flush_sync
309
399
  flush
310
400
  end
311
401
 
312
402
  # Number of pending flag declarations awaiting flush.
403
+ #
404
+ # @return [Integer] number of pending flag declarations awaiting flush.
313
405
  def pending_count
314
406
  @buffer.pending_count
315
407
  end
@@ -319,6 +411,11 @@ module Smplkit
319
411
  # ----------------------------------------------------------------
320
412
 
321
413
  # Declare a boolean flag handle for live evaluation. Connects lazily on first use.
414
+ #
415
+ # @param id [String] identifier of the flag to evaluate.
416
+ # @param default [Boolean] value returned by +handle.get+ when the flag is
417
+ # unknown or no environment override or rule applies.
418
+ # @return [BooleanFlag] a handle whose +get+ evaluates against the live cache.
322
419
  def boolean_flag(id, default:)
323
420
  ensure_connected
324
421
  handle = BooleanFlag.new(self, id: id, name: id, type: "BOOLEAN", default: default)
@@ -328,6 +425,11 @@ module Smplkit
328
425
  end
329
426
 
330
427
  # Declare a string flag handle for live evaluation. Connects lazily on first use.
428
+ #
429
+ # @param id [String] identifier of the flag to evaluate.
430
+ # @param default [String] value returned by +handle.get+ when the flag is
431
+ # unknown or no environment override or rule applies.
432
+ # @return [StringFlag] a handle whose +get+ evaluates against the live cache.
331
433
  def string_flag(id, default:)
332
434
  ensure_connected
333
435
  handle = StringFlag.new(self, id: id, name: id, type: "STRING", default: default)
@@ -337,6 +439,11 @@ module Smplkit
337
439
  end
338
440
 
339
441
  # Declare a numeric flag handle for live evaluation. Connects lazily on first use.
442
+ #
443
+ # @param id [String] identifier of the flag to evaluate.
444
+ # @param default [Numeric] value returned by +handle.get+ when the flag is
445
+ # unknown or no environment override or rule applies.
446
+ # @return [NumberFlag] a handle whose +get+ evaluates against the live cache.
340
447
  def number_flag(id, default:)
341
448
  ensure_connected
342
449
  handle = NumberFlag.new(self, id: id, name: id, type: "NUMERIC", default: default)
@@ -346,6 +453,11 @@ module Smplkit
346
453
  end
347
454
 
348
455
  # Declare a JSON flag handle for live evaluation. Connects lazily on first use.
456
+ #
457
+ # @param id [String] identifier of the flag to evaluate.
458
+ # @param default [Hash] value returned by +handle.get+ when the flag is
459
+ # unknown or no environment override or rule applies.
460
+ # @return [JsonFlag] a handle whose +get+ evaluates against the live cache.
349
461
  def json_flag(id, default:)
350
462
  ensure_connected
351
463
  handle = JsonFlag.new(self, id: id, name: id, type: "JSON", default: default)
@@ -361,12 +473,16 @@ module Smplkit
361
473
  # Re-fetch all flag definitions and clear cache.
362
474
  #
363
475
  # Connects lazily on first use — no explicit install step.
476
+ #
477
+ # @return [void]
364
478
  def refresh
365
479
  ensure_connected
366
480
  do_refresh("manual")
367
481
  end
368
482
 
369
483
  # Return evaluation statistics. Connects lazily on first use.
484
+ #
485
+ # @return [FlagStats] the evaluation statistics.
370
486
  def stats
371
487
  ensure_connected
372
488
  FlagStats.new(cache_hits: @cache.cache_hits, cache_misses: @cache.cache_misses)
@@ -378,6 +494,11 @@ module Smplkit
378
494
  # client.flags.on_change("checkout-v2") { |e| ... } # flag-scoped
379
495
  #
380
496
  # Connects lazily on first use — no explicit install step.
497
+ #
498
+ # @param flag_id [String, nil] optional flag id scoping the listener to
499
+ # that flag; when +nil+ a global listener is registered.
500
+ # @yield [FlagChangeEvent] the listener block, invoked on each change.
501
+ # @return [Proc] the registered listener block.
381
502
  def on_change(flag_id = nil, &block)
382
503
  ensure_connected
383
504
  raise ArgumentError, "on_change requires a block" unless block
@@ -395,6 +516,8 @@ module Smplkit
395
516
  # Tears down the owned WebSocket (standalone install). A wired client
396
517
  # borrows the parent's transport, WebSocket, and contexts client and
397
518
  # closes none of them.
519
+ #
520
+ # @return [void]
398
521
  def close
399
522
  if @owns_ws && @ws_manager
400
523
  @ws_manager.stop
@@ -408,6 +531,9 @@ module Smplkit
408
531
  alias _close close
409
532
 
410
533
  # Construct, yield to the block, and close on exit.
534
+ #
535
+ # @yield [FlagsClient] the constructed client.
536
+ # @return [Object] the block's return value; closes the client on exit.
411
537
  def self.open(**kwargs)
412
538
  client = new(**kwargs)
413
539
  begin
@@ -652,9 +778,9 @@ module Smplkit
652
778
 
653
779
  # Evaluate a flag definition against the given context.
654
780
  #
655
- # Follows ADR-022 §2.6 semantics:
656
- # 1. Look up the environment. If missing, return flag-level default.
657
- # 2. If disabled, return env default or flag default.
781
+ # Evaluation steps:
782
+ # 1. Look up the environment. If missing, return the flag-level default.
783
+ # 2. If disabled, return the env default or flag default.
658
784
  # 3. Iterate rules; first match wins.
659
785
  # 4. No match -> env default or flag default.
660
786
  def evaluate_flag(flag_def, environment, eval_dict)
@@ -743,6 +869,8 @@ module Smplkit
743
869
  # correct — the Java SDK followed the same pattern. Operators supported:
744
870
  # +==+, +!=+, +<+, +<=+, +>+, +>=+, +in+, +var+, +and+, +or+, +!+, +if+,
745
871
  # +missing+, +none+.
872
+ #
873
+ # @api private
746
874
  module JsonLogicEvaluator
747
875
  module_function
748
876