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,684 @@
1
+ # Relationships Guide
2
+
3
+ ## Overview
4
+
5
+ The Relationships feature provides a sophisticated system for managing object relationships in Familia applications. It enables objects to track membership, create bidirectional associations, and maintain indexed lookups while supporting advanced features like permission bit encoding and time-based analytics.
6
+
7
+ ## Core Concepts
8
+
9
+ ### Relationship Types
10
+
11
+ The Familia v2.0 relationships system provides three distinct relationship patterns:
12
+
13
+ 1. **`tracked_in`** - Multi-presence tracking with score encoding (sorted sets)
14
+ 2. **`indexed_by`** - O(1) hash-based lookups by field values
15
+ 3. **`member_of`** - Bidirectional membership with collision-free naming
16
+
17
+ Each type is optimized for different use cases and provides specific performance characteristics.
18
+
19
+ ## Basic Usage
20
+
21
+ ### Enabling Relationships
22
+
23
+ ```ruby
24
+ class Customer < Familia::Horreum
25
+ feature :relationships # Enable relationship functionality
26
+
27
+ identifier_field :custid
28
+ field :custid, :name, :email
29
+
30
+ # Define relationship collections
31
+ tracked_in :active_users, type: :sorted_set
32
+ indexed_by :email_lookup, type: :hash
33
+ end
34
+
35
+ class Domain < Familia::Horreum
36
+ feature :relationships
37
+
38
+ identifier_field :domain_id
39
+ field :domain_id, :name, :dns_zone
40
+
41
+ # Define bidirectional membership
42
+ member_of Customer, :domains, type: :set
43
+ end
44
+ ```
45
+
46
+ ## Tracked In Relationships
47
+
48
+ ### Basic Tracking
49
+
50
+ The `tracked_in` relationship creates collections that track object membership with sophisticated scoring:
51
+
52
+ ```ruby
53
+ class User < Familia::Horreum
54
+ feature :relationships
55
+
56
+ identifier_field :user_id
57
+ field :user_id, :name, :score_value
58
+
59
+ # Simple sorted set tracking
60
+ tracked_in :leaderboard, type: :sorted_set, score: :score_value
61
+
62
+ # Time-based tracking with automatic timestamps
63
+ tracked_in :activity_log, type: :sorted_set
64
+
65
+ # Proc-based scoring for complex calculations
66
+ tracked_in :performance_metrics, type: :sorted_set,
67
+ score: ->(user) { (user.score_value || 0) * 2 }
68
+ end
69
+
70
+ # Usage
71
+ user = User.new(user_id: 'user123', score_value: 85)
72
+
73
+ # Add to collections
74
+ User.add_to_leaderboard(user) # Uses score_value (85)
75
+ User.add_to_activity_log(user) # Uses current timestamp
76
+ User.add_to_performance_metrics(user) # Uses proc result (170)
77
+
78
+ # Query collections
79
+ User.leaderboard.score('user123') # => 85.0
80
+ User.activity_log.range_by_score('-inf', '+inf') # All users by time
81
+ User.performance_metrics.rank('user123') # User's rank by performance
82
+ ```
83
+
84
+ ### Score Encoding System
85
+
86
+ The relationships feature includes a sophisticated bit encoding system for permissions and metadata:
87
+
88
+ ```ruby
89
+ class Document < Familia::Horreum
90
+ feature :relationships
91
+
92
+ identifier_field :doc_id
93
+ field :doc_id, :title, :content
94
+
95
+ # Permission-based tracking with 8-bit encoding
96
+ tracked_in :authorized_users, type: :sorted_set,
97
+ score: :encode_permissions
98
+
99
+ private
100
+
101
+ def encode_permissions
102
+ # Combine timestamp with permission bits
103
+ timestamp = Time.now.to_f.floor
104
+ permissions = calculate_user_permissions # Returns 0-255
105
+ "#{timestamp}.#{permissions}".to_f
106
+ end
107
+ end
108
+ ```
109
+
110
+ #### Permission Bit Flags
111
+
112
+ The system supports 8 permission flags (0-255 range):
113
+
114
+ | Flag | Value | Permission | Description |
115
+ |------|-------|------------|-------------|
116
+ | read | 1 | Read access | View document content |
117
+ | append | 2 | Append access | Add new content |
118
+ | write | 4 | Write access | Modify existing content |
119
+ | edit | 8 | Edit access | Full content editing |
120
+ | configure | 16 | Configure access | Change document settings |
121
+ | delete | 32 | Delete access | Remove document |
122
+ | transfer | 64 | Transfer access | Change ownership |
123
+ | admin | 128 | Admin access | Full administrative control |
124
+
125
+ #### Predefined Permission Roles
126
+
127
+ ```ruby
128
+ # Permission combinations for common roles
129
+ ROLES = {
130
+ viewer: 1, # read only
131
+ editor: 1 | 2 | 4, # read + append + write
132
+ moderator: 15, # read + append + write + edit
133
+ admin: 255 # all permissions
134
+ }
135
+
136
+ # Usage with score encoding
137
+ class DocumentAccess
138
+ include Familia::Features::Relationships::ScoreEncoding
139
+
140
+ def grant_access(user_id, role = :viewer)
141
+ permissions = ROLES[role]
142
+ encoded_score = encode_score_with_permissions(permissions)
143
+ Document.authorized_users.add(user_id, encoded_score)
144
+ end
145
+
146
+ def check_permission(user_id, permission_flag)
147
+ score = Document.authorized_users.score(user_id)
148
+ return false unless score
149
+
150
+ _, permissions = decode_score_with_permissions(score)
151
+ (permissions & permission_flag) != 0
152
+ end
153
+ end
154
+
155
+ # Example usage
156
+ access = DocumentAccess.new
157
+ access.grant_access('user123', :editor)
158
+ access.check_permission('user123', 4) # => true (write permission)
159
+ access.check_permission('user123', 32) # => false (no delete permission)
160
+ ```
161
+
162
+ #### Time-Based Queries with Permissions
163
+
164
+ ```ruby
165
+ # Range queries combining time and permissions
166
+ class DocumentAnalytics
167
+ def users_with_access_since(timestamp, min_permissions = 1)
168
+ min_score = "#{timestamp}.#{min_permissions}".to_f
169
+ Document.authorized_users.range_by_score(min_score, '+inf')
170
+ end
171
+
172
+ def admin_users_last_week
173
+ week_ago = (Time.now - 7.days).to_f.floor
174
+ admin_permissions = 128
175
+ min_score = "#{week_ago}.#{admin_permissions}".to_f
176
+
177
+ Document.authorized_users.range_by_score(min_score, '+inf')
178
+ end
179
+ end
180
+ ```
181
+
182
+ ## Indexed By Relationships
183
+
184
+ ### Hash-Based Lookups
185
+
186
+ The `indexed_by` relationship creates O(1) hash-based indexes for field values:
187
+
188
+ ```ruby
189
+ class User < Familia::Horreum
190
+ feature :relationships
191
+
192
+ field :email, :username, :department
193
+
194
+ # Create indexes for fast lookups
195
+ indexed_by :email_index, field: :email
196
+ indexed_by :username_index, field: :username
197
+ indexed_by :department_index, field: :department
198
+ end
199
+
200
+ # Usage
201
+ user = User.new(email: 'john@example.com', username: 'johndoe')
202
+ User.add_to_email_index(user)
203
+ User.add_to_username_index(user)
204
+
205
+ # Fast O(1) lookups
206
+ user_id = User.email_index.get('john@example.com') # => user.identifier
207
+ user_id = User.username_index.get('johndoe') # => user.identifier
208
+
209
+ # Batch operations
210
+ users = [user1, user2, user3]
211
+ users.each { |u| User.add_to_email_index(u) }
212
+
213
+ # Check if indexed
214
+ User.email_index.exists?('john@example.com') # => true
215
+ ```
216
+
217
+ ### Multi-Field Indexing
218
+
219
+ ```ruby
220
+ class Product < Familia::Horreum
221
+ feature :relationships
222
+
223
+ field :category, :brand, :status
224
+
225
+ # Composite indexing for complex queries
226
+ indexed_by :category_brand_index, field: ->(product) {
227
+ "#{product.category}:#{product.brand}"
228
+ }
229
+
230
+ indexed_by :active_products, field: ->(product) {
231
+ product.status == 'active' ? product.identifier : nil
232
+ }
233
+ end
234
+
235
+ # Usage
236
+ product = Product.new(category: 'electronics', brand: 'apple', status: 'active')
237
+ Product.add_to_category_brand_index(product)
238
+ Product.add_to_active_products(product)
239
+
240
+ # Query by composite key
241
+ Product.category_brand_index.get('electronics:apple') # => product.identifier
242
+ Product.active_products.exists?(product.identifier) # => true
243
+ ```
244
+
245
+ ## Member Of Relationships
246
+
247
+ ### Bidirectional Membership
248
+
249
+ The `member_of` relationship creates bidirectional associations with collision-free method naming:
250
+
251
+ ```ruby
252
+ class Customer < Familia::Horreum
253
+ feature :relationships
254
+
255
+ identifier_field :custid
256
+ field :custid, :name
257
+
258
+ # Collections for owned objects
259
+ set :domains
260
+ list :projects
261
+ set :users
262
+ end
263
+
264
+ class Domain < Familia::Horreum
265
+ feature :relationships
266
+
267
+ identifier_field :domain_id
268
+ field :domain_id, :name
269
+
270
+ # Declare membership in customer collections
271
+ member_of Customer, :domains, type: :set
272
+ end
273
+
274
+ class Project < Familia::Horreum
275
+ feature :relationships
276
+
277
+ identifier_field :project_id
278
+ field :project_id, :name
279
+
280
+ member_of Customer, :projects, type: :list
281
+ end
282
+
283
+ class User < Familia::Horreum
284
+ feature :relationships
285
+
286
+ identifier_field :user_id
287
+ field :user_id, :email
288
+
289
+ member_of Customer, :users, type: :set
290
+ end
291
+ ```
292
+
293
+ ### Collision-Free Method Generation
294
+
295
+ The system automatically generates collision-free methods when multiple classes have the same collection name:
296
+
297
+ ```ruby
298
+ class Team < Familia::Horreum
299
+ feature :relationships
300
+ set :users # Same collection name as Customer
301
+ end
302
+
303
+ class User < Familia::Horreum
304
+ feature :relationships
305
+
306
+ # Both memberships create unique methods
307
+ member_of Customer, :users, type: :set
308
+ member_of Team, :users, type: :set
309
+ end
310
+
311
+ # Generated methods are collision-free:
312
+ user = User.new(user_id: 'user123')
313
+
314
+ # Add to different collections
315
+ user.add_to_customer_users(customer.custid) # Specific to Customer.users
316
+ user.add_to_team_users(team.team_id) # Specific to Team.users
317
+
318
+ # Check membership
319
+ user.in_customer_users?(customer.custid) # => true
320
+ user.in_team_users?(team.team_id) # => true
321
+
322
+ # Remove from specific collections
323
+ user.remove_from_customer_users(customer.custid)
324
+ user.remove_from_team_users(team.team_id)
325
+ ```
326
+
327
+ ### Multi-Context Membership Patterns
328
+
329
+ ```ruby
330
+ class Document < Familia::Horreum
331
+ feature :relationships
332
+
333
+ identifier_field :doc_id
334
+ field :doc_id, :title
335
+
336
+ # Multiple membership contexts
337
+ member_of Customer, :documents, type: :set
338
+ member_of Project, :documents, type: :list
339
+ member_of Team, :shared_docs, type: :sorted_set
340
+ end
341
+
342
+ # Usage - same document can belong to multiple contexts
343
+ doc = Document.new(doc_id: 'doc123', title: 'Requirements')
344
+
345
+ # Add to different organizational contexts
346
+ doc.add_to_customer_documents(customer.custid)
347
+ doc.add_to_project_documents(project.project_id)
348
+ doc.add_to_team_shared_docs(team.team_id, score: Time.now.to_i)
349
+
350
+ # Query membership across contexts
351
+ doc.in_customer_documents?(customer.custid) # => true
352
+ doc.in_project_documents?(project.project_id) # => true
353
+ doc.in_team_shared_docs?(team.team_id) # => true
354
+ ```
355
+
356
+ ## Advanced Features
357
+
358
+ ### Atomic Multi-Collection Operations
359
+
360
+ ```ruby
361
+ class BusinessLogic
362
+ def transfer_domain(domain, from_customer, to_customer)
363
+ # Atomic transfer across multiple collections
364
+ Familia.transaction do |conn|
365
+ # Remove from old customer
366
+ domain.remove_from_customer_domains(from_customer.custid)
367
+ from_customer.domains.remove(domain.identifier)
368
+
369
+ # Add to new customer
370
+ domain.add_to_customer_domains(to_customer.custid)
371
+ to_customer.domains.add(domain.identifier)
372
+ end
373
+ end
374
+
375
+ def bulk_permission_update(user_ids, new_permissions)
376
+ Document.authorized_users.pipeline do |pipe|
377
+ user_ids.each do |user_id|
378
+ current_score = Document.authorized_users.score(user_id)
379
+ if current_score
380
+ timestamp = current_score.floor
381
+ new_score = "#{timestamp}.#{new_permissions}".to_f
382
+ pipe.zadd(Document.authorized_users.key, new_score, user_id)
383
+ end
384
+ end
385
+ end
386
+ end
387
+ end
388
+ ```
389
+
390
+ ### Performance Optimizations
391
+
392
+ ```ruby
393
+ class OptimizedQueries
394
+ # Batch membership checks
395
+ def check_multiple_memberships(user_ids, customer)
396
+ # Single Redis call instead of multiple
397
+ Customer.users.pipeline do |pipe|
398
+ user_ids.each { |uid| pipe.sismember(customer.users.key, uid) }
399
+ end
400
+ end
401
+
402
+ # Efficient range queries with permissions
403
+ def recent_editors_with_write_access(hours = 24)
404
+ since = (Time.now - hours.hours).to_f.floor
405
+ write_permission = 4
406
+ min_score = "#{since}.#{write_permission}".to_f
407
+
408
+ Document.authorized_users.range_by_score(min_score, '+inf')
409
+ end
410
+
411
+ # Batch index updates
412
+ def reindex_users(users)
413
+ User.email_index.pipeline do |pipe|
414
+ users.each do |user|
415
+ pipe.hset(User.email_index.key, user.email, user.identifier)
416
+ end
417
+ end
418
+ end
419
+ end
420
+ ```
421
+
422
+ ## Integration Patterns
423
+
424
+ ### Multi-Tenant Applications
425
+
426
+ ```ruby
427
+ class Organization < Familia::Horreum
428
+ feature :relationships
429
+
430
+ identifier_field :org_id
431
+ field :org_id, :name, :plan
432
+
433
+ # Organization collections
434
+ set :members
435
+ set :projects
436
+ sorted_set :activity_feed
437
+ end
438
+
439
+ class User < Familia::Horreum
440
+ feature :relationships
441
+
442
+ identifier_field :user_id
443
+ field :user_id, :email, :role
444
+
445
+ # Multi-tenant membership
446
+ member_of Organization, :members, type: :set
447
+ tracked_in :global_activity, type: :sorted_set
448
+ indexed_by :email_lookup, field: :email
449
+ end
450
+
451
+ class Project < Familia::Horreum
452
+ feature :relationships
453
+
454
+ identifier_field :project_id
455
+ field :project_id, :name, :status
456
+
457
+ member_of Organization, :projects, type: :set
458
+ tracked_in :status_timeline, type: :sorted_set,
459
+ score: ->(proj) { "#{Time.now.to_i}.#{proj.status.hash}" }
460
+ end
461
+
462
+ # Usage
463
+ org = Organization.new(org_id: 'org123', name: 'Acme Corp')
464
+ user = User.new(user_id: 'user456', email: 'john@acme.com')
465
+ project = Project.new(project_id: 'proj789', name: 'Website')
466
+
467
+ # Establish relationships
468
+ user.add_to_organization_members(org.org_id)
469
+ project.add_to_organization_projects(org.org_id)
470
+
471
+ # Query organization structure
472
+ org.members.size # Number of organization members
473
+ org.projects.members # All project IDs in organization
474
+
475
+ # Global indexes
476
+ User.add_to_email_lookup(user)
477
+ user_id = User.email_lookup.get('john@acme.com') # Fast email lookup
478
+ ```
479
+
480
+ ### Analytics and Reporting
481
+
482
+ ```ruby
483
+ class AnalyticsService
484
+ def user_engagement_report(days = 30)
485
+ since = (Time.now - days.days).to_f.floor
486
+
487
+ # Get all users active in time period
488
+ active_users = User.global_activity.range_by_score(since, '+inf')
489
+
490
+ # Analyze permission levels
491
+ permission_breakdown = Document.authorized_users
492
+ .range_by_score(since, '+inf', with_scores: true)
493
+ .group_by { |user_id, score| decode_permissions(score) }
494
+
495
+ {
496
+ total_active_users: active_users.size,
497
+ permission_breakdown: permission_breakdown.transform_values(&:size),
498
+ top_contributors: User.global_activity.range(0, 9, with_scores: true)
499
+ }
500
+ end
501
+
502
+ def project_status_timeline(project_id)
503
+ project = Project.find(project_id)
504
+ Project.status_timeline
505
+ .range_by_score('-inf', '+inf', with_scores: true)
506
+ .select { |id, _| id == project_id }
507
+ .map { |_, score| decode_status_change(score) }
508
+ end
509
+
510
+ private
511
+
512
+ def decode_permissions(score)
513
+ _, permissions = score.to_s.split('.').map(&:to_i)
514
+ case permissions
515
+ when 1 then :viewer
516
+ when 15 then :moderator
517
+ when 255 then :admin
518
+ else :custom
519
+ end
520
+ end
521
+
522
+ def decode_status_change(score)
523
+ timestamp, status_hash = score.to_s.split('.').map(&:to_i)
524
+ {
525
+ timestamp: Time.at(timestamp),
526
+ status: reverse_status_hash(status_hash)
527
+ }
528
+ end
529
+ end
530
+ ```
531
+
532
+ ## Testing Relationships
533
+
534
+ ### RSpec Testing Patterns
535
+
536
+ ```ruby
537
+ RSpec.describe "Relationships Feature" do
538
+ let(:customer) { Customer.new(custid: 'cust123', name: 'Acme Corp') }
539
+ let(:domain) { Domain.new(domain_id: 'dom456', name: 'acme.com') }
540
+ let(:user) { User.new(user_id: 'user789', email: 'john@acme.com') }
541
+
542
+ describe "tracked_in relationships" do
543
+ it "tracks objects with score encoding" do
544
+ User.add_to_leaderboard(user)
545
+ score = User.leaderboard.score(user.identifier)
546
+
547
+ expect(score).to be_a(Float)
548
+ expect(User.leaderboard.rank(user.identifier)).to be >= 0
549
+ end
550
+
551
+ it "supports permission bit encoding" do
552
+ # Test permission encoding
553
+ encoded = encode_score_with_permissions(15) # moderator permissions
554
+ timestamp, permissions = decode_score_with_permissions(encoded)
555
+
556
+ expect(permissions).to eq(15)
557
+ expect(timestamp).to be_within(1).of(Time.now.to_i)
558
+ end
559
+ end
560
+
561
+ describe "indexed_by relationships" do
562
+ it "creates O(1) hash lookups" do
563
+ User.add_to_email_lookup(user)
564
+ found_id = User.email_lookup.get(user.email)
565
+
566
+ expect(found_id).to eq(user.identifier)
567
+ end
568
+
569
+ it "handles batch operations" do
570
+ users = [user, user2, user3]
571
+ users.each { |u| User.add_to_email_lookup(u) }
572
+
573
+ users.each do |u|
574
+ expect(User.email_lookup.get(u.email)).to eq(u.identifier)
575
+ end
576
+ end
577
+ end
578
+
579
+ describe "member_of relationships" do
580
+ it "creates bidirectional associations" do
581
+ domain.add_to_customer_domains(customer.custid)
582
+ customer.domains.add(domain.identifier)
583
+
584
+ expect(domain.in_customer_domains?(customer.custid)).to be true
585
+ expect(customer.domains.member?(domain.identifier)).to be true
586
+ end
587
+
588
+ it "generates collision-free methods" do
589
+ expect(domain).to respond_to(:add_to_customer_domains)
590
+ expect(domain).to respond_to(:in_customer_domains?)
591
+ expect(domain).to respond_to(:remove_from_customer_domains)
592
+ end
593
+ end
594
+ end
595
+ ```
596
+
597
+ ### Integration Testing
598
+
599
+ ```ruby
600
+ RSpec.describe "Relationships Integration" do
601
+ scenario "multi-tenant organization with full relationship graph" do
602
+ # Create organization structure
603
+ org = Organization.create(org_id: 'org123', name: 'Tech Corp')
604
+
605
+ # Create users with different roles
606
+ admin = User.create(user_id: 'admin1', email: 'admin@tech.com', role: 'admin')
607
+ dev = User.create(user_id: 'dev1', email: 'dev@tech.com', role: 'developer')
608
+
609
+ # Create projects
610
+ project = Project.create(project_id: 'proj1', name: 'Web App')
611
+
612
+ # Establish all relationships
613
+ admin.add_to_organization_members(org.org_id)
614
+ dev.add_to_organization_members(org.org_id)
615
+ project.add_to_organization_projects(org.org_id)
616
+
617
+ # Add to indexes
618
+ User.add_to_email_lookup(admin)
619
+ User.add_to_email_lookup(dev)
620
+
621
+ # Test relationship integrity
622
+ expect(org.members.size).to eq(2)
623
+ expect(org.projects.size).to eq(1)
624
+
625
+ # Test lookups
626
+ expect(User.email_lookup.get('admin@tech.com')).to eq(admin.identifier)
627
+ expect(User.email_lookup.get('dev@tech.com')).to eq(dev.identifier)
628
+
629
+ # Test membership queries
630
+ expect(admin.in_organization_members?(org.org_id)).to be true
631
+ expect(project.in_organization_projects?(org.org_id)).to be true
632
+ end
633
+ end
634
+ ```
635
+
636
+ ## Best Practices
637
+
638
+ ### Relationship Design
639
+
640
+ 1. **Choose the Right Type**:
641
+ - Use `tracked_in` for activity feeds, leaderboards, time-series data
642
+ - Use `indexed_by` for fast lookups by field values
643
+ - Use `member_of` for bidirectional ownership/membership
644
+
645
+ 2. **Score Encoding Strategy**:
646
+ - Combine timestamps with metadata for rich queries
647
+ - Use bit flags for permissions (supports 8 flags efficiently)
648
+ - Consider sort order requirements when designing scores
649
+
650
+ 3. **Performance Optimization**:
651
+ - Batch operations when possible using pipelines
652
+ - Use appropriate Redis data types for your access patterns
653
+ - Index only frequently-queried fields
654
+
655
+ ### Memory and Storage
656
+
657
+ 1. **Efficient Bit Encoding**:
658
+ - 8 bits can encode 256 permission combinations
659
+ - Single Redis sorted set score contains time + permissions
660
+ - Reduces memory vs. separate permission records
661
+
662
+ 2. **Key Design**:
663
+ - Relationship keys follow pattern: `class:field:collection`
664
+ - Collision-free method names prevent namespace conflicts
665
+ - Predictable key structure aids debugging
666
+
667
+ 3. **Cleanup Strategies**:
668
+ - Remove objects from all relationship collections on deletion
669
+ - Use TTL on temporary relationship data
670
+ - Regular cleanup of stale indexes
671
+
672
+ ### Security Considerations
673
+
674
+ 1. **Permission Validation**:
675
+ - Always validate permissions before operations
676
+ - Use bit flags for efficient permission checking
677
+ - Audit permission changes with timestamps
678
+
679
+ 2. **Access Control**:
680
+ - Verify relationship membership before granting access
681
+ - Use consistent permission models across features
682
+ - Log relationship changes for audit trails
683
+
684
+ The Relationships feature provides a comprehensive foundation for building sophisticated multi-tenant applications with efficient object relationships, permission management, and analytics capabilities.