familia 2.0.0.pre6 → 2.0.0.pre7

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -13
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +2 -2
  9. data/docs/wiki/Feature-System-Guide.md +36 -5
  10. data/docs/wiki/Home.md +30 -20
  11. data/docs/wiki/Relationships-Guide.md +684 -0
  12. data/examples/bit_encoding_integration.rb +237 -0
  13. data/examples/redis_command_validation_example.rb +231 -0
  14. data/examples/relationships_basic.rb +273 -0
  15. data/lib/familia/connection.rb +3 -3
  16. data/lib/familia/data_type.rb +7 -4
  17. data/lib/familia/features/encrypted_fields/concealed_string.rb +21 -23
  18. data/lib/familia/features/encrypted_fields.rb +413 -4
  19. data/lib/familia/features/expiration.rb +319 -33
  20. data/lib/familia/features/quantization.rb +385 -44
  21. data/lib/familia/features/relationships/cascading.rb +438 -0
  22. data/lib/familia/features/relationships/indexing.rb +370 -0
  23. data/lib/familia/features/relationships/membership.rb +503 -0
  24. data/lib/familia/features/relationships/permission_management.rb +264 -0
  25. data/lib/familia/features/relationships/querying.rb +620 -0
  26. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  27. data/lib/familia/features/relationships/score_encoding.rb +442 -0
  28. data/lib/familia/features/relationships/tracking.rb +379 -0
  29. data/lib/familia/features/relationships.rb +466 -0
  30. data/lib/familia/features/transient_fields.rb +192 -10
  31. data/lib/familia/features.rb +2 -1
  32. data/lib/familia/horreum/subclass/definition.rb +1 -1
  33. data/lib/familia/validation/command_recorder.rb +336 -0
  34. data/lib/familia/validation/expectations.rb +519 -0
  35. data/lib/familia/validation/test_helpers.rb +443 -0
  36. data/lib/familia/validation/validator.rb +412 -0
  37. data/lib/familia/validation.rb +140 -0
  38. data/lib/familia/version.rb +1 -1
  39. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  40. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  41. data/try/edge_cases/string_coercion_try.rb +2 -0
  42. data/try/encryption/encryption_core_try.rb +3 -1
  43. data/try/features/categorical_permissions_try.rb +515 -0
  44. data/try/features/encryption_fields/concealed_string_core_try.rb +3 -0
  45. data/try/features/encryption_fields/context_isolation_try.rb +1 -0
  46. data/try/features/relationships_edge_cases_try.rb +145 -0
  47. data/try/features/relationships_performance_minimal_try.rb +132 -0
  48. data/try/features/relationships_performance_simple_try.rb +155 -0
  49. data/try/features/relationships_performance_try.rb +420 -0
  50. data/try/features/relationships_performance_working_try.rb +144 -0
  51. data/try/features/relationships_try.rb +237 -0
  52. data/try/features/safe_dump_try.rb +3 -0
  53. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  54. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  55. data/try/helpers/test_helpers.rb +1 -1
  56. data/try/horreum/base_try.rb +14 -8
  57. data/try/horreum/enhanced_conflict_handling_try.rb +2 -0
  58. data/try/horreum/relations_try.rb +1 -1
  59. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  60. data/try/validation/command_validation_try.rb.disabled +207 -0
  61. data/try/validation/performance_validation_try.rb.disabled +324 -0
  62. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  63. metadata +32 -4
  64. data/docs/wiki/RelatableObjects-Guide.md +0 -563
  65. data/lib/familia/features/relatable_objects.rb +0 -125
  66. data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,442 @@
1
+ # lib/familia/features/relationships/score_encoding.rb
2
+
3
+ module Familia
4
+ module Features
5
+ module Relationships
6
+ # Score encoding using bit flags for permissions
7
+ #
8
+ # Encodes permissions as bit flags in the decimal portion of Redis sorted set scores:
9
+ # - Integer part: Unix timestamp for time-based ordering
10
+ # - Decimal part: 8-bit permission flags (0-255)
11
+ #
12
+ # Format: [timestamp].[permission_bits]
13
+ # Example: 1704067200.037 = Jan 1, 2024 with read(1) + write(4) + delete(32) = 37
14
+ #
15
+ # Bit positions:
16
+ # 0: read - View/list items
17
+ # 1: append - Add new items
18
+ # 2: write - Modify existing items
19
+ # 3: edit - Edit metadata
20
+ # 4: configure - Change settings
21
+ # 5: delete - Remove items
22
+ # 6: transfer - Change ownership
23
+ # 7: admin - Full control
24
+ #
25
+ # This allows combining permissions (read + delete without write) and efficient
26
+ # permission checking using bitwise operations while maintaining time-based ordering.
27
+ module ScoreEncoding
28
+ # Maximum value for metadata to preserve precision (3 decimal places)
29
+ # For 8-bit permission system, max value is 255
30
+ MAX_METADATA = 255
31
+ METADATA_PRECISION = 1000.0
32
+
33
+ # Permission bit flags (8-bit system)
34
+ PERMISSION_FLAGS = {
35
+ none: 0b00000000, # 0 - No permissions
36
+ read: 0b00000001, # 1 - View/list
37
+ append: 0b00000010, # 2 - Add new items
38
+ write: 0b00000100, # 4 - Modify existing
39
+ edit: 0b00001000, # 8 - Edit metadata
40
+ configure: 0b00010000, # 16 - Change settings
41
+ delete: 0b00100000, # 32 - Remove items
42
+ transfer: 0b01000000, # 64 - Change ownership
43
+ admin: 0b10000000 # 128 - Full control
44
+ }.freeze
45
+
46
+ # Predefined permission combinations
47
+ PERMISSION_ROLES = {
48
+ viewer: PERMISSION_FLAGS[:read],
49
+ editor: PERMISSION_FLAGS[:read] | PERMISSION_FLAGS[:write] | PERMISSION_FLAGS[:edit],
50
+ moderator: PERMISSION_FLAGS[:read] | PERMISSION_FLAGS[:write] | PERMISSION_FLAGS[:edit] | PERMISSION_FLAGS[:delete],
51
+ admin: 0b11111111 # All permissions
52
+ }.freeze
53
+
54
+ # Categorical masks for efficient broad queries
55
+ PERMISSION_CATEGORIES = {
56
+ readable: 0b00000001, # Has basic access
57
+ content_editor: 0b00001110, # Can modify content (append|write|edit)
58
+ administrator: 0b11110000, # Has any admin powers
59
+ privileged: 0b11111110, # Has beyond read-only
60
+ owner: 0b11111111 # All permissions
61
+ }.freeze
62
+
63
+
64
+ class << self
65
+ # Get permission bit flag value for a permission symbol
66
+ #
67
+ # @param permission [Symbol] Permission symbol to get value for
68
+ # @return [Integer] Bit flag value for the permission
69
+ # @raise [ArgumentError] If permission is unknown
70
+ def permission_level_value(permission)
71
+ PERMISSION_FLAGS[permission] || raise(ArgumentError, "Unknown permission: #{permission.inspect}")
72
+ end
73
+
74
+ # Encode timestamp and permission (alias for encode_score)
75
+ #
76
+ # @param timestamp [Time, Integer] The timestamp to encode
77
+ # @param permission [Symbol, Integer, Array] Permission(s) to encode
78
+ # @return [Float] Encoded score suitable for Redis sorted sets
79
+ def permission_encode(timestamp, permission)
80
+ encode_score(timestamp, permission)
81
+ end
82
+
83
+ # Decode score into permission information
84
+ #
85
+ # @param score [Float] The encoded score
86
+ # @return [Hash] Hash with timestamp, permissions bits, and permission list
87
+ def permission_decode(score)
88
+ decoded = decode_score(score)
89
+ {
90
+ timestamp: decoded[:timestamp],
91
+ permissions: decoded[:permissions],
92
+ permission_list: decoded[:permission_list]
93
+ }
94
+ end
95
+
96
+
97
+ # Encode a timestamp and permissions into a Redis score
98
+ #
99
+ # @param timestamp [Time, Integer] The timestamp to encode
100
+ # @param permissions [Integer, Symbol, Array] Permissions to encode
101
+ # @return [Float] Encoded score suitable for Redis sorted sets
102
+ #
103
+ # @example Basic encoding with bit flag
104
+ # encode_score(Time.now, 5) # read(1) + write(4) = 5
105
+ # #=> 1704067200.005
106
+ #
107
+ # @example Permission symbol encoding
108
+ # encode_score(Time.now, :read)
109
+ # #=> 1704067200.001
110
+ #
111
+ # @example Multiple permissions
112
+ # encode_score(Time.now, [:read, :write, :delete])
113
+ # #=> 1704067200.037
114
+ def encode_score(timestamp, permissions = 0)
115
+ time_part = timestamp.respond_to?(:to_i) ? timestamp.to_i : timestamp
116
+
117
+ permission_bits = case permissions
118
+ when Symbol
119
+ PERMISSION_ROLES[permissions] || PERMISSION_FLAGS[permissions] || 0
120
+ when Array
121
+ # Support array of permission symbols
122
+ permissions.reduce(0) { |acc, p| acc | (PERMISSION_FLAGS[p] || 0) }
123
+ when Integer
124
+ validate_permission_bits(permissions)
125
+ else
126
+ 0
127
+ end
128
+
129
+ time_part + (permission_bits / METADATA_PRECISION)
130
+ end
131
+
132
+ # Decode a Redis score back into timestamp and permissions
133
+ #
134
+ # @param score [Float] The encoded score
135
+ # @return [Hash] Hash with :timestamp, :permissions, and :permission_list keys
136
+ #
137
+ # @example Basic decoding
138
+ # decode_score(1704067200.037)
139
+ # #=> { timestamp: 1704067200, permissions: 37, permission_list: [:read, :write, :delete] }
140
+ def decode_score(score)
141
+ return { timestamp: 0, permissions: 0, permission_list: [] } unless score.is_a?(Numeric)
142
+
143
+ time_part = score.to_i
144
+ permission_bits = ((score - time_part) * METADATA_PRECISION).round
145
+
146
+ {
147
+ timestamp: time_part,
148
+ permissions: permission_bits,
149
+ permission_list: decode_permission_flags(permission_bits)
150
+ }
151
+ end
152
+
153
+ # Check if score has specific permissions
154
+ #
155
+ # @param score [Float] The encoded score
156
+ # @param permissions [Array<Symbol>] Permissions to check
157
+ # @return [Boolean] True if all permissions are present
158
+ #
159
+ # @example
160
+ # permission?(1704067200.005, :read) # score has read(1) + write(4)
161
+ # #=> true
162
+ def permission?(score, *permissions)
163
+ decoded = decode_score(score)
164
+ permission_bits = decoded[:permissions]
165
+
166
+ permissions.all? do |perm|
167
+ flag = PERMISSION_FLAGS[perm]
168
+ flag && permission_bits.anybits?(flag)
169
+ end
170
+ end
171
+
172
+ # Add permissions to existing score
173
+ #
174
+ # @param score [Float] The existing encoded score
175
+ # @param permissions [Array<Symbol>] Permissions to add
176
+ # @return [Float] New score with added permissions
177
+ #
178
+ # @example
179
+ # add_permissions(1704067200.001, :write, :delete) # add write(4) + delete(32) to read(1)
180
+ # #=> 1704067200.037
181
+ def add_permissions(score, *permissions)
182
+ decoded = decode_score(score)
183
+ current_bits = decoded[:permissions]
184
+
185
+ new_bits = permissions.reduce(current_bits) do |acc, perm|
186
+ acc | (PERMISSION_FLAGS[perm] || 0)
187
+ end
188
+
189
+ encode_score(decoded[:timestamp], new_bits)
190
+ end
191
+
192
+ # Remove permissions from existing score
193
+ #
194
+ # @param score [Float] The existing encoded score
195
+ # @param permissions [Array<Symbol>] Permissions to remove
196
+ # @return [Float] New score with removed permissions
197
+ #
198
+ # @example
199
+ # remove_permissions(1704067200.037, :write) # remove write(4) from read(1)+write(4)+delete(32)
200
+ # #=> 1704067200.033
201
+ def remove_permissions(score, *permissions)
202
+ decoded = decode_score(score)
203
+ current_bits = decoded[:permissions]
204
+
205
+ new_bits = permissions.reduce(current_bits) do |acc, perm|
206
+ acc & ~(PERMISSION_FLAGS[perm] || 0)
207
+ end
208
+
209
+ encode_score(decoded[:timestamp], new_bits)
210
+ end
211
+
212
+ # Create score range for permissions
213
+ #
214
+ # @param min_permissions [Array<Symbol>, nil] Minimum required permissions
215
+ # @param max_permissions [Array<Symbol>, nil] Maximum allowed permissions
216
+ # @return [Array<Float>] Min and max scores for Redis range queries
217
+ #
218
+ # @example
219
+ # permission_range([:read], [:read, :write])
220
+ # #=> [0.001, 0.005]
221
+ def permission_range(min_permissions = [], max_permissions = nil)
222
+ min_bits = Array(min_permissions).reduce(0) { |acc, p| acc | (PERMISSION_FLAGS[p] || 0) }
223
+ max_bits = if max_permissions
224
+ Array(max_permissions).reduce(0) do |acc, p|
225
+ acc | (PERMISSION_FLAGS[p] || 0)
226
+ end
227
+ else
228
+ 255
229
+ end
230
+
231
+ [min_bits / METADATA_PRECISION, max_bits / METADATA_PRECISION]
232
+ end
233
+
234
+ # Get current timestamp as score (no permissions)
235
+ #
236
+ # @return [Float] Current time as Redis score
237
+ def current_score
238
+ encode_score(Time.now, 0)
239
+ end
240
+
241
+ # Create score range for Redis operations based on time bounds
242
+ #
243
+ # @param start_time [Time, nil] Start time (nil for -inf)
244
+ # @param end_time [Time, nil] End time (nil for +inf)
245
+ # @param min_permissions [Array<Symbol>, nil] Minimum required permissions
246
+ # @return [Array] Array suitable for Redis ZRANGEBYSCORE operations
247
+ #
248
+ # @example Time range
249
+ # score_range(1.hour.ago, Time.now)
250
+ # #=> [1704063600.0, 1704067200.255]
251
+ #
252
+ # @example Permission filter
253
+ # score_range(nil, nil, min_permissions: [:read])
254
+ # #=> [0.001, "+inf"]
255
+ def score_range(start_time = nil, end_time = nil, min_permissions: nil)
256
+ min_bits = if min_permissions
257
+ Array(min_permissions).reduce(0) do |acc, p|
258
+ acc | (PERMISSION_FLAGS[p] || 0)
259
+ end
260
+ else
261
+ 0
262
+ end
263
+
264
+ min_score = if start_time
265
+ encode_score(start_time, min_bits)
266
+ elsif min_permissions
267
+ encode_score(0, min_bits)
268
+ else
269
+ '-inf'
270
+ end
271
+
272
+ max_score = if end_time
273
+ encode_score(end_time, 255) # Use max valid permission bits
274
+ else
275
+ '+inf'
276
+ end
277
+
278
+ [min_score, max_score]
279
+ end
280
+
281
+ # Decode permission bits into array of permission symbols
282
+ #
283
+ # @param bits [Integer] Permission bits to decode
284
+ # @return [Array<Symbol>] Array of permission symbols
285
+ def decode_permission_flags(bits)
286
+ PERMISSION_FLAGS.select { |_name, flag| bits.anybits?(flag) }.keys
287
+ end
288
+
289
+ # Check broad permission categories
290
+ #
291
+ # @param score [Float] The encoded score
292
+ # @param category [Symbol] Category to check (:readable, :content_editor, :administrator, etc.)
293
+ # @return [Boolean] True if score meets the category requirements
294
+ def category?(score, category)
295
+ decoded = decode_score(score)
296
+ permission_bits = decoded[:permissions]
297
+
298
+ mask = PERMISSION_CATEGORIES[category]
299
+ return false unless mask
300
+
301
+ permission_bits.anybits?(mask)
302
+ end
303
+
304
+ # Filter collection by permission category
305
+ #
306
+ # @param scores [Array<Float>] Array of scores to filter
307
+ # @param category [Symbol] Category to filter by
308
+ # @return [Array<Float>] Scores matching the category
309
+ def filter_by_category(scores, category)
310
+ mask = PERMISSION_CATEGORIES[category]
311
+ return [] unless mask
312
+
313
+ scores.select do |score|
314
+ permission_bits = ((score % 1) * METADATA_PRECISION).round
315
+ permission_bits.anybits?(mask)
316
+ end
317
+ end
318
+
319
+ # Get permission tier for score
320
+ #
321
+ # @param score [Float] The encoded score
322
+ # @return [Symbol] Permission tier (:administrator, :content_editor, :viewer, :none)
323
+ def permission_tier(score)
324
+ decoded = decode_score(score)
325
+ bits = decoded[:permissions]
326
+
327
+ if bits.anybits?(PERMISSION_CATEGORIES[:administrator])
328
+ :administrator
329
+ elsif bits.anybits?(PERMISSION_CATEGORIES[:content_editor])
330
+ :content_editor
331
+ elsif bits.anybits?(PERMISSION_CATEGORIES[:readable])
332
+ :viewer
333
+ else
334
+ :none
335
+ end
336
+ end
337
+
338
+ # Efficient bulk categorization
339
+ #
340
+ # @param scores [Array<Float>] Array of scores to categorize
341
+ # @return [Hash] Hash mapping tiers to arrays of scores
342
+ def categorize_scores(scores)
343
+ scores.group_by { |score| permission_tier(score) }
344
+ end
345
+
346
+ # Check if permissions meet minimum category
347
+ #
348
+ # @param permission_bits [Integer] Permission bits to check
349
+ # @param category [Symbol] Category to check against
350
+ # @return [Boolean] True if permissions meet the category requirements
351
+ def meets_category?(permission_bits, category)
352
+ mask = PERMISSION_CATEGORIES[category]
353
+ return false unless mask
354
+
355
+ case category
356
+ when :readable
357
+ permission_bits.positive? # Any permission implies read
358
+ when :privileged
359
+ permission_bits > 1 # More than just read
360
+ when :administrator
361
+ permission_bits.anybits?(PERMISSION_CATEGORIES[:administrator])
362
+ else
363
+ permission_bits.anybits?(mask)
364
+ end
365
+ end
366
+
367
+ # Range queries for categorical filtering
368
+ #
369
+ # @param category [Symbol] Category to create range for
370
+ # @param start_time [Time, nil] Optional start time filter
371
+ # @param end_time [Time, nil] Optional end time filter
372
+ # @return [Array<String>] Min and max range strings for Redis queries
373
+ def category_score_range(category, start_time = nil, end_time = nil)
374
+ PERMISSION_CATEGORIES[category] || 0
375
+
376
+ # Any permission matching the category mask
377
+ min_score = start_time ? start_time.to_i : 0
378
+ max_score = end_time ? end_time.to_i : Time.now.to_i
379
+
380
+ # Return range that includes any matching permissions
381
+ ["#{min_score}.000", "#{max_score}.999"]
382
+ end
383
+
384
+ private
385
+
386
+ # Validate permission bits are within acceptable range
387
+ #
388
+ # @param bits [Integer] Permission bits to validate
389
+ # @return [Integer] Validated permission bits
390
+ # @raise [ArgumentError] If bits are outside 0-255 range
391
+ def validate_permission_bits(bits)
392
+ raise ArgumentError, 'Permission bits must be 0-255' unless (0..255).cover?(bits)
393
+
394
+ bits
395
+ end
396
+ end
397
+
398
+ # Instance methods for classes that include this module
399
+ def encode_score(timestamp, permissions = 0)
400
+ ScoreEncoding.encode_score(timestamp, permissions)
401
+ end
402
+
403
+ def decode_score(score)
404
+ ScoreEncoding.decode_score(score)
405
+ end
406
+
407
+ def permission?(score, *permissions)
408
+ ScoreEncoding.permission?(score, *permissions)
409
+ end
410
+
411
+ def add_permissions(score, *permissions)
412
+ ScoreEncoding.add_permissions(score, *permissions)
413
+ end
414
+
415
+ def remove_permissions(score, *permissions)
416
+ ScoreEncoding.remove_permissions(score, *permissions)
417
+ end
418
+
419
+ def permission_range(min_permissions = [], max_permissions = nil)
420
+ ScoreEncoding.permission_range(min_permissions, max_permissions)
421
+ end
422
+
423
+ def current_score
424
+ ScoreEncoding.current_score
425
+ end
426
+
427
+ def score_range(start_time = nil, end_time = nil, min_permissions: nil)
428
+ ScoreEncoding.score_range(start_time, end_time, min_permissions: min_permissions)
429
+ end
430
+
431
+ # Legacy method aliases for backward compatibility
432
+ def permission_encode(timestamp, permission)
433
+ ScoreEncoding.permission_encode(timestamp, permission)
434
+ end
435
+
436
+ def permission_decode(score)
437
+ ScoreEncoding.permission_decode(score)
438
+ end
439
+ end
440
+ end
441
+ end
442
+ end