familia 2.0.0.pre19 → 2.0.0.pre21

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 (372) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +4 -9
  3. data/.github/workflows/code-smells.yml +64 -3
  4. data/.pre-commit-config.yaml +8 -6
  5. data/.reek.yml +10 -9
  6. data/.rubocop.yml +4 -0
  7. data/CHANGELOG.rst +177 -112
  8. data/CLAUDE.md +28 -1
  9. data/Gemfile +1 -1
  10. data/Gemfile.lock +20 -17
  11. data/bin/try +16 -0
  12. data/bin/tryouts +16 -0
  13. data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
  14. data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
  15. data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
  16. data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
  17. data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
  18. data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
  19. data/docs/1106-participates_in-bidirectional-solution.md +129 -0
  20. data/docs/guides/encryption.md +486 -0
  21. data/docs/guides/feature-encrypted-fields.md +123 -7
  22. data/docs/guides/feature-expiration.md +161 -117
  23. data/docs/guides/feature-external-identifiers.md +415 -443
  24. data/docs/guides/feature-object-identifiers.md +400 -269
  25. data/docs/guides/feature-quantization.md +120 -6
  26. data/docs/guides/feature-relationships-indexing.md +318 -0
  27. data/docs/guides/feature-relationships-methods.md +146 -604
  28. data/docs/guides/feature-relationships-participation.md +263 -0
  29. data/docs/guides/feature-relationships.md +118 -136
  30. data/docs/guides/feature-system-devs.md +176 -693
  31. data/docs/guides/feature-system.md +119 -6
  32. data/docs/guides/feature-transient-fields.md +81 -0
  33. data/docs/guides/field-system.md +778 -0
  34. data/docs/guides/index.md +32 -15
  35. data/docs/guides/logging.md +187 -0
  36. data/docs/guides/optimized-loading.md +674 -0
  37. data/docs/guides/thread-safety-monitoring.md +61 -0
  38. data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
  39. data/docs/migrating/v2.0.0-pre22.md +241 -0
  40. data/docs/overview.md +7 -9
  41. data/docs/reference/api-technical.md +267 -320
  42. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
  43. data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
  44. data/examples/autoloader/mega_customer.rb +2 -0
  45. data/examples/datatype_standalone.rb +4 -3
  46. data/examples/encrypted_fields.rb +2 -1
  47. data/examples/json_usage_patterns.rb +2 -0
  48. data/examples/relationships.rb +3 -0
  49. data/examples/safe_dump.rb +2 -1
  50. data/examples/sampling_demo.rb +53 -0
  51. data/examples/single_connection_transaction_confusions.rb +2 -1
  52. data/familia.gemspec +2 -1
  53. data/lib/familia/base.rb +2 -0
  54. data/lib/familia/connection/behavior.rb +2 -0
  55. data/lib/familia/connection/handlers.rb +2 -0
  56. data/lib/familia/connection/individual_command_proxy.rb +2 -0
  57. data/lib/familia/connection/middleware.rb +34 -24
  58. data/lib/familia/connection/operation_core.rb +2 -0
  59. data/lib/familia/connection/operations.rb +2 -0
  60. data/lib/familia/connection/pipelined_core.rb +2 -0
  61. data/lib/familia/connection/transaction_core.rb +68 -0
  62. data/lib/familia/connection.rb +18 -3
  63. data/lib/familia/data_type/class_methods.rb +3 -1
  64. data/lib/familia/data_type/connection.rb +2 -0
  65. data/lib/familia/data_type/database_commands.rb +2 -0
  66. data/lib/familia/data_type/serialization.rb +6 -4
  67. data/lib/familia/data_type/settings.rb +2 -0
  68. data/lib/familia/data_type/types/counter.rb +2 -0
  69. data/lib/familia/data_type/types/hashkey.rb +7 -5
  70. data/lib/familia/data_type/types/listkey.rb +2 -0
  71. data/lib/familia/data_type/types/lock.rb +2 -0
  72. data/lib/familia/data_type/types/sorted_set.rb +2 -0
  73. data/lib/familia/data_type/types/stringkey.rb +2 -0
  74. data/lib/familia/data_type/types/unsorted_set.rb +2 -0
  75. data/lib/familia/data_type.rb +2 -0
  76. data/lib/familia/encryption/encrypted_data.rb +4 -2
  77. data/lib/familia/encryption/manager.rb +2 -0
  78. data/lib/familia/encryption/provider.rb +2 -0
  79. data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
  80. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
  81. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
  82. data/lib/familia/encryption/registry.rb +2 -0
  83. data/lib/familia/encryption/request_cache.rb +2 -0
  84. data/lib/familia/encryption.rb +9 -2
  85. data/lib/familia/errors.rb +2 -0
  86. data/lib/familia/features/autoloader.rb +2 -0
  87. data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
  88. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
  89. data/lib/familia/features/encrypted_fields.rb +2 -2
  90. data/lib/familia/features/expiration/extensions.rb +3 -1
  91. data/lib/familia/features/expiration.rb +12 -4
  92. data/lib/familia/features/external_identifier.rb +33 -7
  93. data/lib/familia/features/object_identifier.rb +2 -0
  94. data/lib/familia/features/quantization.rb +3 -1
  95. data/lib/familia/features/relationships/README.md +3 -1
  96. data/lib/familia/features/relationships/collection_operations.rb +2 -0
  97. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +138 -9
  98. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
  99. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +89 -21
  100. data/lib/familia/features/relationships/indexing.rb +3 -0
  101. data/lib/familia/features/relationships/indexing_relationship.rb +3 -1
  102. data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
  103. data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
  104. data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
  105. data/lib/familia/features/relationships/participation.rb +155 -69
  106. data/lib/familia/features/relationships/participation_membership.rb +69 -0
  107. data/lib/familia/features/relationships/participation_relationship.rb +34 -6
  108. data/lib/familia/features/relationships/score_encoding.rb +2 -0
  109. data/lib/familia/features/relationships.rb +5 -3
  110. data/lib/familia/features/safe_dump.rb +2 -0
  111. data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
  112. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
  113. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
  114. data/lib/familia/features/transient_fields.rb +2 -0
  115. data/lib/familia/features.rb +2 -0
  116. data/lib/familia/field_type.rb +3 -1
  117. data/lib/familia/horreum/connection.rb +17 -1
  118. data/lib/familia/horreum/database_commands.rb +2 -0
  119. data/lib/familia/horreum/definition.rb +16 -6
  120. data/lib/familia/horreum/management.rb +212 -42
  121. data/lib/familia/horreum/persistence.rb +176 -108
  122. data/lib/familia/horreum/related_fields.rb +2 -0
  123. data/lib/familia/horreum/serialization.rb +23 -4
  124. data/lib/familia/horreum/settings.rb +2 -0
  125. data/lib/familia/horreum/utils.rb +2 -0
  126. data/lib/familia/horreum.rb +15 -1
  127. data/lib/familia/identifier_extractor.rb +2 -0
  128. data/lib/familia/instrumentation.rb +156 -0
  129. data/lib/familia/json_serializer.rb +2 -0
  130. data/lib/familia/logging.rb +92 -32
  131. data/lib/familia/refinements/dear_json.rb +2 -0
  132. data/lib/familia/refinements/stylize_words.rb +2 -14
  133. data/lib/familia/refinements/time_literals.rb +2 -0
  134. data/lib/familia/refinements.rb +2 -0
  135. data/lib/familia/secure_identifier.rb +10 -2
  136. data/lib/familia/settings.rb +2 -0
  137. data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
  138. data/lib/familia/thread_safety/monitor.rb +328 -0
  139. data/lib/familia/utils.rb +13 -0
  140. data/lib/familia/verifiable_identifier.rb +3 -1
  141. data/lib/familia/version.rb +3 -1
  142. data/lib/familia.rb +31 -4
  143. data/lib/middleware/database_command_counter.rb +152 -0
  144. data/lib/middleware/database_logger.rb +295 -170
  145. data/lib/multi_result.rb +2 -0
  146. data/try/edge_cases/empty_identifiers_try.rb +2 -0
  147. data/try/edge_cases/hash_symbolization_try.rb +2 -0
  148. data/try/edge_cases/json_serialization_try.rb +2 -0
  149. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
  150. data/try/edge_cases/race_conditions_try.rb +4 -0
  151. data/try/edge_cases/reserved_keywords_try.rb +4 -0
  152. data/try/edge_cases/string_coercion_try.rb +2 -0
  153. data/try/edge_cases/ttl_side_effects_try.rb +4 -0
  154. data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
  155. data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
  156. data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
  157. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
  158. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
  159. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
  160. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
  161. data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
  162. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
  163. data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
  164. data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
  165. data/try/features/encrypted_fields/memory_security_try.rb +4 -0
  166. data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
  167. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
  168. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
  169. data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
  170. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
  171. data/try/features/encryption/config_persistence_try.rb +4 -0
  172. data/try/features/encryption/core_try.rb +4 -0
  173. data/try/features/encryption/instance_variable_scope_try.rb +4 -0
  174. data/try/features/encryption/module_loading_try.rb +4 -0
  175. data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
  176. data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
  177. data/try/features/encryption/roundtrip_validation_try.rb +4 -0
  178. data/try/features/encryption/secure_memory_handling_try.rb +4 -0
  179. data/try/features/expiration/expiration_try.rb +4 -0
  180. data/try/features/external_identifier/external_identifier_try.rb +171 -8
  181. data/try/features/feature_dependencies_try.rb +2 -0
  182. data/try/features/feature_improvements_try.rb +2 -0
  183. data/try/features/field_groups_try.rb +2 -0
  184. data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
  185. data/try/features/object_identifier/object_identifier_try.rb +2 -0
  186. data/try/features/quantization/quantization_try.rb +4 -0
  187. data/try/features/real_feature_integration_try.rb +2 -0
  188. data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
  189. data/try/features/relationships/indexing_rebuild_try.rb +600 -0
  190. data/try/features/relationships/indexing_try.rb +2 -0
  191. data/try/features/relationships/participation_bidirectional_try.rb +242 -0
  192. data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
  193. data/try/features/relationships/participation_commands_verification_try.rb +2 -0
  194. data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
  195. data/try/features/relationships/participation_reverse_index_try.rb +15 -13
  196. data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
  197. data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
  198. data/try/features/relationships/relationships_api_changes_try.rb +2 -0
  199. data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
  200. data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
  201. data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
  202. data/try/features/relationships/relationships_performance_try.rb +4 -0
  203. data/try/features/relationships/relationships_performance_working_try.rb +4 -0
  204. data/try/features/relationships/relationships_try.rb +6 -4
  205. data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
  206. data/try/features/safe_dump/safe_dump_try.rb +4 -0
  207. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  208. data/try/features/transient_fields/refresh_reset_try.rb +3 -0
  209. data/try/features/transient_fields/simple_refresh_test.rb +3 -0
  210. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  211. data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
  212. data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
  213. data/try/integration/connection/fiber_context_preservation_try.rb +4 -0
  214. data/try/integration/connection/handler_constraints_try.rb +4 -0
  215. data/try/integration/connection/isolated_dbclient_try.rb +4 -0
  216. data/try/integration/connection/middleware_reconnect_try.rb +2 -0
  217. data/try/integration/connection/operation_mode_guards_try.rb +4 -0
  218. data/try/integration/connection/pipeline_fallback_integration_try.rb +3 -0
  219. data/try/integration/connection/pools_try.rb +4 -0
  220. data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
  221. data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
  222. data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
  223. data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
  224. data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
  225. data/try/integration/connection/transaction_modes_try.rb +4 -0
  226. data/try/integration/conventional_inheritance_try.rb +4 -0
  227. data/try/integration/create_method_try.rb +4 -0
  228. data/try/integration/cross_component_try.rb +4 -0
  229. data/try/integration/data_types/datatype_pipelines_try.rb +4 -0
  230. data/try/integration/data_types/datatype_transactions_try.rb +4 -0
  231. data/try/integration/database_consistency_try.rb +4 -0
  232. data/try/integration/familia_extended_try.rb +4 -0
  233. data/try/integration/familia_members_methods_try.rb +4 -0
  234. data/try/integration/models/customer_safe_dump_try.rb +4 -0
  235. data/try/integration/models/customer_try.rb +4 -0
  236. data/try/integration/models/datatype_base_try.rb +4 -0
  237. data/try/integration/models/familia_object_try.rb +4 -0
  238. data/try/integration/persistence_operations_try.rb +4 -0
  239. data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
  240. data/try/integration/save_methods_consistency_try.rb +241 -0
  241. data/try/integration/scenarios_try.rb +4 -0
  242. data/try/integration/secure_identifier_try.rb +4 -0
  243. data/try/integration/transaction_safety_core_try.rb +176 -0
  244. data/try/integration/transaction_safety_workflow_try.rb +291 -0
  245. data/try/integration/verifiable_identifier_try.rb +4 -0
  246. data/try/investigation/pipeline_routing/README.md +228 -0
  247. data/try/performance/benchmarks_try.rb +4 -0
  248. data/try/performance/transaction_safety_benchmark_try.rb +238 -0
  249. data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
  250. data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
  251. data/try/support/debugging/cache_behavior_tracer.rb +4 -0
  252. data/try/support/debugging/debug_aad_process.rb +3 -0
  253. data/try/support/debugging/debug_concealed_internal.rb +3 -0
  254. data/try/support/debugging/debug_concealed_reveal.rb +3 -0
  255. data/try/support/debugging/debug_context_aad.rb +3 -0
  256. data/try/support/debugging/debug_context_simple.rb +3 -0
  257. data/try/support/debugging/debug_cross_context.rb +3 -0
  258. data/try/support/debugging/debug_database_load.rb +3 -0
  259. data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
  260. data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
  261. data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
  262. data/try/support/debugging/debug_field_decrypt.rb +3 -0
  263. data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
  264. data/try/support/debugging/debug_load_path.rb +3 -0
  265. data/try/support/debugging/debug_method_definition.rb +3 -0
  266. data/try/support/debugging/debug_method_resolution.rb +3 -0
  267. data/try/support/debugging/debug_minimal.rb +3 -0
  268. data/try/support/debugging/debug_provider.rb +3 -0
  269. data/try/support/debugging/debug_secure_behavior.rb +3 -0
  270. data/try/support/debugging/debug_string_class.rb +3 -0
  271. data/try/support/debugging/debug_test.rb +3 -0
  272. data/try/support/debugging/debug_test_design.rb +3 -0
  273. data/try/support/debugging/encryption_method_tracer.rb +4 -0
  274. data/try/support/debugging/provider_diagnostics.rb +4 -0
  275. data/try/support/helpers/test_cleanup.rb +4 -0
  276. data/try/support/helpers/test_helpers.rb +5 -0
  277. data/try/support/memory/memory_basic_test.rb +4 -0
  278. data/try/support/memory/memory_detailed_test.rb +4 -0
  279. data/try/support/memory/memory_search_for_string.rb +4 -0
  280. data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
  281. data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
  282. data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
  283. data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
  284. data/try/support/prototypes/atomic_saves_v4.rb +4 -0
  285. data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
  286. data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  287. data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
  288. data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  289. data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
  290. data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
  291. data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
  292. data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
  293. data/try/support/prototypes/pooling/pool_siege.rb +4 -2
  294. data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
  295. data/try/thread_safety/README.md +496 -0
  296. data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
  297. data/try/thread_safety/connection_chain_race_try.rb +148 -0
  298. data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
  299. data/try/thread_safety/feature_registry_race_try.rb +226 -0
  300. data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
  301. data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
  302. data/try/thread_safety/field_registration_race_try.rb +222 -0
  303. data/try/thread_safety/logger_initialization_race_try.rb +170 -0
  304. data/try/thread_safety/middleware_registration_race_try.rb +154 -0
  305. data/try/thread_safety/module_config_race_try.rb +175 -0
  306. data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
  307. data/try/unit/core/autoloader_try.rb +4 -0
  308. data/try/unit/core/base_enhancements_try.rb +4 -0
  309. data/try/unit/core/connection_try.rb +4 -0
  310. data/try/unit/core/errors_try.rb +4 -0
  311. data/try/unit/core/extensions_try.rb +4 -0
  312. data/try/unit/core/familia_logger_try.rb +2 -0
  313. data/try/unit/core/familia_try.rb +4 -0
  314. data/try/unit/core/middleware_sampling_try.rb +335 -0
  315. data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
  316. data/try/unit/core/middleware_thread_safety_try.rb +245 -0
  317. data/try/unit/core/middleware_try.rb +4 -0
  318. data/try/unit/core/settings_try.rb +4 -0
  319. data/try/unit/core/time_utils_try.rb +4 -0
  320. data/try/unit/core/tools_try.rb +4 -0
  321. data/try/unit/core/utils_try.rb +37 -0
  322. data/try/unit/data_types/boolean_try.rb +4 -0
  323. data/try/unit/data_types/counter_try.rb +4 -0
  324. data/try/unit/data_types/datatype_base_try.rb +4 -0
  325. data/try/unit/data_types/hash_try.rb +4 -0
  326. data/try/unit/data_types/list_try.rb +4 -0
  327. data/try/unit/data_types/lock_try.rb +4 -0
  328. data/try/unit/data_types/sorted_set_try.rb +4 -0
  329. data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
  330. data/try/unit/data_types/string_try.rb +4 -0
  331. data/try/unit/data_types/unsortedset_try.rb +4 -0
  332. data/try/unit/familia_resolve_class_try.rb +116 -0
  333. data/try/unit/horreum/auto_indexing_on_save_try.rb +5 -1
  334. data/try/unit/horreum/automatic_index_validation_try.rb +2 -0
  335. data/try/unit/horreum/base_try.rb +4 -0
  336. data/try/unit/horreum/class_methods_try.rb +4 -0
  337. data/try/unit/horreum/commands_try.rb +4 -0
  338. data/try/unit/horreum/defensive_initialization_try.rb +4 -0
  339. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
  340. data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
  341. data/try/unit/horreum/field_categories_try.rb +4 -0
  342. data/try/unit/horreum/field_definition_try.rb +4 -0
  343. data/try/unit/horreum/initialization_try.rb +4 -0
  344. data/try/unit/horreum/json_type_preservation_try.rb +2 -0
  345. data/try/unit/horreum/optimized_loading_try.rb +156 -0
  346. data/try/unit/horreum/relations_try.rb +4 -0
  347. data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
  348. data/try/unit/horreum/serialization_try.rb +4 -0
  349. data/try/unit/horreum/settings_try.rb +4 -0
  350. data/try/unit/horreum/unique_index_edge_cases_try.rb +4 -0
  351. data/try/unit/horreum/unique_index_guard_validation_try.rb +2 -0
  352. data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
  353. data/try/unit/middleware/database_logger_methods_try.rb +251 -0
  354. data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
  355. data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
  356. data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
  357. data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
  358. data/try/unit/thread_safety_monitor_try.rb +149 -0
  359. metadata +72 -17
  360. data/.github/workflows/code-quality.yml +0 -138
  361. data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +0 -91
  362. data/changelog.d/20251011_203905_delano_next.rst +0 -30
  363. data/changelog.d/20251011_212633_delano_next.rst +0 -13
  364. data/changelog.d/20251011_221253_delano_next.rst +0 -26
  365. data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
  366. data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
  367. data/docs/archive/FAMILIA_UPDATE.md +0 -226
  368. data/docs/archive/README.md +0 -64
  369. data/docs/archive/api-reference.md +0 -333
  370. data/docs/guides/core-field-system.md +0 -806
  371. data/docs/guides/implementation.md +0 -276
  372. data/docs/guides/security-model.md +0 -183
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/relationships/participation/participant_methods.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative '../collection_operations'
4
6
 
@@ -8,37 +10,48 @@ module Familia
8
10
  # Methods added to PARTICIPANT classes (the ones calling participates_in)
9
11
  # These methods allow participant instances to manage their membership in target collections
10
12
  #
11
- # Example: When Domain calls `participates_in Customer, :domains`
12
- # Domain instances get methods to check/manage their presence in Customer collections
13
+ # Example: When Domain calls `participates_in Employee, :domains`
14
+ # Domain instances get methods to check/manage their presence in Employee collections
13
15
  module ParticipantMethods
14
16
  using Familia::Refinements::StylizeWords
15
17
  extend CollectionOperations
16
18
 
17
19
  # Visual Guide for methods added to PARTICIPANT instances:
18
20
  # =========================================================
19
- # When Domain calls: participates_in Customer, :domains
21
+ # When Domain calls: participates_in Employee, :domains
20
22
  #
21
23
  # Domain instances (PARTICIPANT) get these methods:
22
- # ├── in_customer_domains?(customer) # Check if I'm in this customer's domains
23
- # ├── add_to_customer_domains(customer, score) # Add myself to customer's domains
24
- # ├── remove_from_customer_domains(customer) # Remove myself from customer's domains
25
- # ├── score_in_customer_domains(customer) # Get my score (sorted_set only)
26
- # └── position_in_customer_domains(customer) # Get my position (list only)
24
+ # ├── in_employee_domains?(employee) # Check if I'm in this employee's domains
25
+ # ├── add_to_employee_domains(employee, score) # Add myself to employee's domains
26
+ # ├── remove_from_employee_domains(employee) # Remove myself from employee's domains
27
+ # ├── score_in_employee_domains(employee) # Get my score (sorted_set only)
28
+ # └── position_in_employee_domains(employee) # Get my position (list only)
27
29
  #
28
30
  # Note: To update scores, use the DataType API directly:
29
- # customer.domains.add(domain.identifier, new_score, xx: true)
30
-
31
+ # employee.domains.add(domain.identifier, new_score, xx: true)
32
+ #
31
33
  module Builder
32
34
  extend CollectionOperations
33
35
 
34
36
  # Build all participant methods for a participation relationship
37
+ #
35
38
  # @param participant_class [Class] The class receiving these methods (e.g., Domain)
36
- # @param target_class_name [String] Name of the target class (e.g., "Customer")
39
+ # @param target_class [Class, String] Target class object or 'class' for class-level participation (e.g., Employee or 'class')
37
40
  # @param collection_name [Symbol] Name of the collection (e.g., :domains)
38
41
  # @param type [Symbol] Collection type (:sorted_set, :set, :list)
39
- def self.build(participant_class, target_class_name, collection_name, type)
40
- # Convert to snake_case once for consistency (target_class_name is PascalCase)
41
- target_name = target_class_name.to_s.snake_case
42
+ # @param as [Symbol, nil] Optional custom name for relationship methods (e.g., :employees)
43
+ #
44
+ def self.build(participant_class, target_class, collection_name, type, as)
45
+ # Determine target name based on participation context:
46
+ # - Instance-level: target_class is a Class object (e.g., Team) → use config_name ("project_team")
47
+ # - Class-level: target_class is the string 'class' (from class_participates_in) → use as-is
48
+ # The string 'class' is passed from TargetMethods.build_class_add_method when calling
49
+ # calculate_participation_score('class', collection_name) for class-level scoring
50
+ target_name = if target_class.is_a?(String)
51
+ target_class # 'class' for class-level participation
52
+ else
53
+ target_class.config_name # snake_case class name for instance-level
54
+ end
42
55
 
43
56
  # Core participant methods
44
57
  build_membership_check(participant_class, target_name, collection_name, type)
@@ -52,6 +65,110 @@ module Familia
52
65
  when :list
53
66
  build_position_method(participant_class, target_name, collection_name)
54
67
  end
68
+
69
+ # Build reverse collection methods on PARTICIPANT class for instance-level participation
70
+ # Skip for class-level participation because:
71
+ # - Class-level uses class_participates_in (e.g., User.all_users)
72
+ # - Bidirectional methods don't make sense: an individual User can't have "all_users"
73
+ # - Class-level collections are accessed directly on the class (User.all_users)
74
+ return if target_class.is_a?(String) # 'class' indicates class-level participation
75
+
76
+ # If `as` is specified, create a custom method for just this collection
77
+ # Otherwise, add to the default pluralized method that unions all collections
78
+ if as
79
+ # Custom method for just this specific collection
80
+ build_reverse_collection_methods(participant_class, target_class, as, [collection_name])
81
+ else
82
+ # Default pluralized method - will include ALL collections for this target
83
+ build_reverse_collection_methods(participant_class, target_class, nil, nil)
84
+ end
85
+ end
86
+
87
+ # Generate reverse collection methods on participant class for bidirectional access
88
+ #
89
+ # Creates methods like:
90
+ # - user.team_instances (returns Array of Team instances)
91
+ # - user.team_ids (returns Array of IDs)
92
+ # - user.team? (returns Boolean)
93
+ # - user.team_count (returns Integer)
94
+ #
95
+ # @param participant_class [Class] The participant class (e.g., User)
96
+ # @param target_class [Class] The target class (e.g., Team)
97
+ # @param custom_name [Symbol, nil] Custom method name override (base name without suffix)
98
+ # @param collection_names [Array<Symbol>, nil] Specific collections to include (nil = all)
99
+ #
100
+ def self.build_reverse_collection_methods(participant_class, target_class, custom_name = nil, collection_names = nil)
101
+ # Determine base method name - either custom or target class config_name
102
+ # e.g., "project_team" or "contracting_org"
103
+ base_name = if custom_name
104
+ custom_name.to_s
105
+ else
106
+ # Use config_name as-is (e.g., "project_team")
107
+ target_class.config_name
108
+ end
109
+
110
+ # Store collection names as string array for matching
111
+ collections_filter = collection_names&.map(&:to_s)
112
+
113
+ # Generate the main collection method (e.g., user.project_team_instances)
114
+ #
115
+ # Loads actual objects - verifies Redis key existence via load_multi.
116
+ # No caching - load_multi is efficient enough and avoids stale data.
117
+ #
118
+ # @note Error Handling: This method lets database errors bubble up to the
119
+ # application layer, consistent with Familia's error handling pattern.
120
+ # Potential failures include:
121
+ # - Familia::NotConnected - Redis connection unavailable
122
+ # - Redis::TimeoutError - Operation timed out
123
+ # - Redis::ConnectionError - Network/connection issues
124
+ #
125
+ # For production environments, consider wrapping calls in application-level
126
+ # error handling:
127
+ #
128
+ # @example Application-level error handling
129
+ # begin
130
+ # teams = user.project_team_instances
131
+ # rescue Familia::PersistenceError => e
132
+ # # Handle database failure (log, fallback, retry, etc.)
133
+ # Rails.logger.error("Failed to load teams: #{e.message}")
134
+ # [] # Return empty array or other fallback
135
+ # end
136
+ #
137
+ participant_class.define_method("#{base_name}_instances") do
138
+ ids = participating_ids_for_target(target_class, collections_filter)
139
+ # Use load_multi for Horreum objects (stored as Redis hashes)
140
+ target_class.load_multi(ids).compact
141
+ end
142
+
143
+ # Generate the IDs-only method (e.g., user.project_team_ids)
144
+ #
145
+ # Shallow - returns IDs from participation index without verifying key existence.
146
+ #
147
+ # @note Database errors (connection, timeout) will bubble up to caller.
148
+ #
149
+ participant_class.define_method("#{base_name}_ids") do
150
+ participating_ids_for_target(target_class, collections_filter)
151
+ end
152
+
153
+ # Generate the boolean check method (e.g., user.project_team?)
154
+ #
155
+ # Shallow check - verifies participation index membership, not Redis key existence.
156
+ #
157
+ # @note Database errors (connection, timeout) will bubble up to caller.
158
+ #
159
+ participant_class.define_method("#{base_name}?") do
160
+ participating_in_target?(target_class, collections_filter)
161
+ end
162
+
163
+ # Generate the count method (e.g., user.project_team_count)
164
+ #
165
+ # Shallow - counts IDs from participation index without verifying key existence.
166
+ #
167
+ # @note Database errors (connection, timeout) will bubble up to caller.
168
+ #
169
+ participant_class.define_method("#{base_name}_count") do
170
+ participating_ids_for_target(target_class, collections_filter).size
171
+ end
55
172
  end
56
173
 
57
174
  # Build method to check membership in target's collection
@@ -0,0 +1,41 @@
1
+ ## Indexes vs. Participations: Critical Distinction
2
+
3
+ **Indexes are derived data** - they can be rebuilt from source:
4
+ ```ruby
5
+ # Indexes derive from object fields and can be reconstructed
6
+ User.rebuild_email_lookup # Rebuilds from all User instances
7
+ ```
8
+
9
+ **Participations are primary data** - they represent business decisions:
10
+ ```ruby
11
+ # Participations are intentional relationships that must be explicitly created
12
+ @team.add_members_instance(@user) # Human/business decision about team membership
13
+ ```
14
+
15
+ **Why this matters for rebuilding:**
16
+
17
+ | Aspect | Indexes | Participations |
18
+ |--------|---------|----------------|
19
+ | **Source of truth** | Object field values | Business logic/user actions |
20
+ | **Can rebuild?** | ✅ Yes - iterate instances | ❌ No - requires domain knowledge |
21
+ | **Fix when wrong** | Run rebuild method | Re-apply business logic |
22
+ | **Nature** | Computed/derived | Intentional/chosen |
23
+
24
+ **Examples:**
25
+
26
+ ```ruby
27
+ # ✅ INDEXES - Can rebuild because source exists
28
+ User.rebuild_email_lookup # Rebuilds from User.email field values
29
+ company.rebuild_badge_index # Rebuilds from Employee.badge_number values
30
+
31
+ # ❌ PARTICIPATIONS - Cannot rebuild without knowing intent
32
+ @team.members # Which users should be members? (business decision)
33
+ @org.employees # Who works here? (HR/business logic)
34
+ @project.contributors # Who contributed? (tracked externally)
35
+
36
+ # To fix participation data, reapply the business logic:
37
+ correct_members.each { |user| @team.add_members_instance(user) }
38
+ ```
39
+
40
+ **When indexes fail**, run the rebuild method.
41
+ **When participations are wrong**, understand why they're wrong and reapply your application's business rules.
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/relationships/participation/target_methods.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative '../collection_operations'
4
6
 
@@ -72,10 +74,9 @@ module Familia
72
74
  end
73
75
 
74
76
  # Build method to add an item to the collection
75
- # Creates: customer.add_domain(domain, score)
77
+ # Creates: customer.add_domains_instance(domain, score)
76
78
  def self.build_add_item(target_class, collection_name, type)
77
- singular_name = collection_name.to_s.singularize
78
- method_name = "add_#{singular_name}"
79
+ method_name = "add_#{collection_name}_instance"
79
80
 
80
81
  target_class.define_method(method_name) do |item, score = nil|
81
82
  collection = send(collection_name)
@@ -105,10 +106,9 @@ module Familia
105
106
  end
106
107
 
107
108
  # Build method to remove an item from the collection
108
- # Creates: customer.remove_domain(domain)
109
+ # Creates: customer.remove_domains_instance(domain)
109
110
  def self.build_remove_item(target_class, collection_name, type)
110
- singular_name = collection_name.to_s.singularize
111
- method_name = "remove_#{singular_name}"
111
+ method_name = "remove_#{collection_name}_instance"
112
112
 
113
113
  target_class.define_method(method_name) do |item|
114
114
  collection = send(collection_name)
@@ -1,6 +1,9 @@
1
1
  # lib/familia/features/relationships/participation.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative 'participation_relationship'
6
+ require_relative 'participation_membership'
4
7
  require_relative 'collection_operations'
5
8
  require_relative 'participation/participant_methods'
6
9
  require_relative 'participation/target_methods'
@@ -160,10 +163,10 @@ module Familia
160
163
  type: :sorted_set, bidirectional: true)
161
164
  # Store metadata for this participation relationship
162
165
  participation_relationships << ParticipationRelationship.new(
163
- target_class: self,
166
+ _original_target: self, # For class-level, original and resolved are the same
167
+ target_class: self, # The class itself
164
168
  collection_name: collection_name,
165
169
  score: score,
166
-
167
170
  type: type,
168
171
  bidirectional: bidirectional,
169
172
  )
@@ -176,7 +179,10 @@ module Familia
176
179
  # e.g., user.in_class_all_users?, user.add_to_class_all_users
177
180
  return unless bidirectional
178
181
 
179
- ParticipantMethods::Builder.build(self, 'class', collection_name, type)
182
+ # Pass the string 'class' as target to distinguish class-level from instance-level
183
+ # This prevents generating reverse collection methods (user can't have "all_users")
184
+ # See ParticipantMethods::Builder.build for handling of this special case
185
+ ParticipantMethods::Builder.build(self, 'class', collection_name, type, nil)
180
186
  end
181
187
 
182
188
  # Define an instance-level participation relationship between two classes.
@@ -208,12 +214,14 @@ module Familia
208
214
  # all collections the instance belongs to. This enables efficient membership queries
209
215
  # and cleanup operations without scanning all possible collections.
210
216
  #
211
- # @param target_class [Class, Symbol, String] The class that owns the collection. Can be:
212
- # - +Class+ object (e.g., +Customer+)
213
- # - +Symbol+ referencing class name (e.g., +:customer+, +:Customer+)
214
- # - +String+ class name (e.g., +"Customer"+)
215
- # @param collection_name [Symbol] Name of the collection on the target class (e.g., +:domains+, +:members+)
216
- # @param score [Symbol, Proc, Numeric, nil] Scoring strategy for sorted collections:
217
+ # @param target [Class, Symbol, String] The class that owns the collection. Can be:
218
+ # - +Class+ object (e.g., +Employee+)
219
+ # - +Symbol+ referencing class name (e.g., +:employee+, +:Employee+)
220
+ # - +String+ class name (e.g., +"Employee"+)
221
+ # @param collection_name [Symbol] Name of the collection on the
222
+ # target class (e.g., +:domains+, +:members+)
223
+ # @param score [Symbol, Proc, Numeric, nil] Scoring strategy for
224
+ # sorted collections:
217
225
  # - +Symbol+: Field name or method name (e.g., +:priority+, +:created_at+)
218
226
  # - +Proc+: Dynamic calculation executed in participant instance context
219
227
  # - +Numeric+: Static score applied to all participants
@@ -221,17 +229,24 @@ module Familia
221
229
  # - +:remove+: Remove from all collections on destruction (default)
222
230
  # - +:ignore+: Leave in collections when destroyed
223
231
  # @param type [Symbol] Valkey/Redis collection type:
224
- # - +:sorted_set+: Ordered by score, allows duplicates with different scores (default)
232
+ # - +:sorted_set+: Ordered by score, allows duplicates with
233
+ # different scores (default)
225
234
  # - +:set+: Unordered unique membership
226
235
  # - +:list+: Ordered sequence, allows duplicates
227
- # @param bidirectional [Boolean] Whether to generate convenience methods on participant class (default: +true+)
236
+ # @param bidirectional [Boolean] Whether to generate reverse collection
237
+ # methods on participant class. If true, methods are generated using the
238
+ # name of the target class. (default: +true+)
239
+ # @param as [Symbol, nil] Custom name for reverse collection methods
240
+ # (e.g., +as: :contracting_orgs+). When provided, overrides the default
241
+ # method name derived from the target class.
242
+ #
243
+ # @example Basic domain-employee relationship
228
244
  #
229
- # @example Basic domain-customer relationship
230
245
  # class Domain < Familia::Horreum
231
246
  # field :name
232
247
  # field :created_at
233
248
  #
234
- # participates_in Customer, :domains, score: :created_at
249
+ # participates_in Employee, :domains, score: :created_at
235
250
  # end
236
251
  #
237
252
  # # Usage:
@@ -241,6 +256,7 @@ module Familia
241
256
  # domain.current_participations # All collections domain belongs to
242
257
  #
243
258
  # @example Multi-collection participation with different types
259
+ #
244
260
  # class Employee < Familia::Horreum
245
261
  # field :hire_date
246
262
  # field :skill_level
@@ -267,42 +283,54 @@ module Familia
267
283
  # @see #class_participates_in for class-level participation
268
284
  # @see ModelInstanceMethods#current_participations for membership queries
269
285
  # @see ModelInstanceMethods#calculate_participation_score for scoring details
270
- # @since 1.0.0
271
- def participates_in(target_class, collection_name, score: nil,
272
- type: :sorted_set, bidirectional: true)
273
- # Handle class target using Familia.resolve_class
274
- resolved_class = Familia.resolve_class(target_class)
286
+ #
287
+ def participates_in(target, collection_name, score: nil, type: :sorted_set, bidirectional: true, as: nil)
288
+
289
+ # Normalize the target class parameter
290
+ target_class = Familia.resolve_class(target)
291
+
292
+ # Raise helpful error if target class can't be resolved
293
+ if target_class.nil?
294
+ raise ArgumentError, <<~ERROR
295
+ Cannot resolve target class: #{target.inspect}
296
+
297
+ The target class '#{target}' could not be found in Familia.members.
298
+ This usually means:
299
+ 1. The target class hasn't been loaded/required yet (load order issue)
300
+ 2. The target class name is misspelled
301
+ 3. The target class doesn't inherit from Familia::Horreum
302
+
303
+ Current registered classes: #{Familia.members.filter_map(&:name).sort.join(', ')}
304
+
305
+ Solution: Ensure #{target} is defined and loaded before #{self.name}
306
+ ERROR
307
+ end
275
308
 
276
309
  # Store metadata for this participation relationship
277
310
  participation_relationships << ParticipationRelationship.new(
278
- target_class: target_class, # as passed to `participates_in`
311
+ _original_target: target, # Original value as passed (Symbol/String/Class)
312
+ target_class: target_class, # Resolved Class object
279
313
  collection_name: collection_name,
280
314
  score: score,
281
-
282
315
  type: type,
283
316
  bidirectional: bidirectional,
284
317
  )
285
318
 
286
- # Resolve target class if it's a symbol/string
287
- actual_target_class = if target_class.is_a?(Class)
288
- target_class
289
- else
290
- Familia.member_by_config_name(target_class)
291
- end
292
-
293
319
  # STEP 0: Add participations tracking field to PARTICIPANT class (Domain)
294
- # This creates the proper key: "domain:123:participations" (not "domain:123:object:participations")
320
+ # This creates the proper key: "domain:123:participations"
295
321
  set :participations unless method_defined?(:participations)
296
322
 
297
- # STEP 1: Add collection management methods to TARGET class (Customer)
298
- # Customer gets: domains, add_domain, remove_domain, etc.
299
- TargetMethods::Builder.build(actual_target_class, collection_name, type)
323
+ # STEP 1: Add collection management methods to TARGET class (Employee)
324
+ # Employee gets: domains, add_domain, remove_domain, etc.
325
+ TargetMethods::Builder.build(target_class, collection_name, type)
300
326
 
301
- # STEP 2: Add participation methods to PARTICIPANT class (Domain) - only if bidirectional
302
- # Domain gets: in_customer_domains?, add_to_customer_domains, etc.
303
- return unless bidirectional
304
-
305
- ParticipantMethods::Builder.build(self, resolved_class.familia_name, collection_name, type)
327
+ # STEP 2: Add participation methods to PARTICIPANT class (Domain) - only if
328
+ # bidirectional. e.g. in_employee_domains?, add_to_employee_domains, etc.
329
+ if bidirectional
330
+ # `as` parameter allows custom naming for reverse collections
331
+ # If not provided, we'll let the builder use the pluralized target class name
332
+ ParticipantMethods::Builder.build(self, target_class, collection_name, type, as)
333
+ end
306
334
  end
307
335
 
308
336
  # Get all participation relationships defined for this class.
@@ -365,6 +393,8 @@ module Familia
365
393
  # within scoring Procs.
366
394
  #
367
395
  # @param target_class [Class, Symbol, String] The target class containing the collection
396
+ # - For instance-level participation: Class object (e.g., +Project+, +Team+)
397
+ # - For class-level participation: The string +'class'+ (from +class_participates_in+)
368
398
  # @param collection_name [Symbol] The collection name within the target class
369
399
  # @return [Float] Calculated score for sorted set positioning, falls back to current_score
370
400
  #
@@ -402,18 +432,9 @@ module Familia
402
432
  # @see #track_participation_in for reverse index management
403
433
  # @since 1.0.0
404
434
  def calculate_participation_score(target_class, collection_name)
405
- # Find the participation configuration with robust type comparison
435
+ # Find the participation configuration using the new matches? method
406
436
  participation_config = self.class.participation_relationships.find do |details|
407
- # Normalize both sides for comparison to handle Class, Symbol, and String types
408
- config_target = details.target_class
409
- config_target = config_target.name if config_target.is_a?(Class)
410
- config_target = config_target.to_s
411
-
412
- comparison_target = target_class
413
- comparison_target = comparison_target.name if comparison_target.is_a?(Class)
414
- comparison_target = comparison_target.to_s
415
-
416
- config_target == comparison_target && details.collection_name == collection_name
437
+ details.matches?(target_class, collection_name)
417
438
  end
418
439
 
419
440
  return current_score unless participation_config
@@ -542,6 +563,69 @@ module Familia
542
563
  # @see #track_participation_in for reverse index management
543
564
  # @see #calculate_participation_score for scoring details
544
565
  # @since 1.0.0
566
+ # Get all IDs where this instance participates for a specific target class
567
+ #
568
+ # This is a shallow check - it extracts IDs from the participation index without
569
+ # verifying that the target Redis keys actually exist. Use this for fast ID
570
+ # enumeration; use *_instances methods if you need existence verification.
571
+ #
572
+ # Optimized to iterate through keys once and use Set for efficient uniqueness,
573
+ # reducing string operations and object allocations.
574
+ #
575
+ # @param target_class [Class] The target class to filter by
576
+ # @param collection_names [Array<String>, nil] Optional collection name filter
577
+ # @return [Array<String>] Array of unique target instance IDs
578
+ def participating_ids_for_target(target_class, collection_names = nil)
579
+
580
+ # Use config_name to get the proper snake_case format (e.g., "project_team")
581
+ target_prefix = "#{target_class.config_name}#{Familia.delim}"
582
+ ids = Set.new
583
+
584
+ participations.members.each do |key|
585
+ next unless key.start_with?(target_prefix)
586
+
587
+ parts = key.split(Familia.delim, 3) # Split into ["targetclass", "id", "collection"]
588
+ id = parts[1]
589
+
590
+ # If filtering by collection names, check before adding
591
+ if collection_names && !collection_names.empty?
592
+ collection = parts[2]
593
+ ids << id if collection_names.include?(collection)
594
+ else
595
+ ids << id
596
+ end
597
+ end
598
+
599
+ ids.to_a
600
+ end
601
+
602
+ # Check if this instance participates in any target of a specific class
603
+ #
604
+ # This is a shallow check - it only verifies that participation entries exist
605
+ # in the participation index. It does NOT verify that the target Redis keys
606
+ # actually exist. Use this for fast membership checks.
607
+ #
608
+ # Optimized to stop scanning as soon as a match is found.
609
+ #
610
+ # @param target_class [Class] The target class to check
611
+ # @param collection_names [Array<String>, nil] Optional collection name filter
612
+ # @return [Boolean] true if any matching participation exists
613
+ def participating_in_target?(target_class, collection_names = nil)
614
+ target_prefix = "#{target_class.config_name}#{Familia.delim}"
615
+
616
+ participations.members.any? do |key|
617
+ next false unless key.start_with?(target_prefix)
618
+
619
+ # If filtering by specific collections, check the collection name
620
+ if collection_names && !collection_names.empty?
621
+ collection = key.split(Familia.delim, 3)[2]
622
+ collection_names.include?(collection)
623
+ else
624
+ true
625
+ end
626
+ end
627
+ end
628
+
545
629
  def current_participations
546
630
  return [] unless self.class.respond_to?(:participation_relationships)
547
631
 
@@ -555,59 +639,61 @@ module Familia
555
639
  collection_keys.each do |collection_key|
556
640
  # Parse the collection key to extract target info
557
641
  # Expected format: "targetclass:targetid:collectionname"
558
- key_parts = collection_key.split(':')
559
- next unless key_parts.length >= 3
560
-
561
- target_class_config = key_parts[0]
562
- target_id = key_parts[1]
563
- collection_name_from_key = key_parts[2]
642
+ target_class_config, target_id, collection_name_from_key = collection_key.split(Familia.delim, 3)
643
+ next unless target_class_config && target_id && collection_name_from_key
564
644
 
565
645
  # Find the matching participation configuration
566
646
  # Note: target_class_config from key is snake_case
567
647
  config = self.class.participation_relationships.find do |cfg|
568
- cfg.target_class_config_name == target_class_config &&
648
+ cfg.target_class.config_name == target_class_config &&
569
649
  cfg.collection_name.to_s == collection_name_from_key
570
650
  end
571
651
 
572
652
  next unless config
573
653
 
574
654
  # Find the target instance and check membership using Horreum DataTypes
655
+ # config.target_class is already a resolved Class object
575
656
  begin
576
- target_class = Familia.resolve_class(config.target_class)
577
- target_instance = target_class.find_by_id(target_id)
657
+ target_instance = config.target_class.find_by_id(target_id)
578
658
  next unless target_instance
579
659
 
580
660
  # Use Horreum's DataType accessor to get the collection
581
661
  collection = target_instance.send(config.collection_name)
582
662
 
583
- # Check membership using DataType methods
584
- membership_data = {
585
- target_class: config.target_class.familia_name,
586
- target_id: target_id,
587
- collection_name: config.collection_name,
588
- type: config.type,
589
- }
663
+ # Check membership using DataType methods and build ParticipationMembership
664
+ score = nil
665
+ decoded_score = nil
666
+ position = nil
590
667
 
591
668
  case config.type
592
669
  when :sorted_set
593
670
  score = collection.score(identifier)
594
671
  next unless score
595
672
 
596
- membership_data[:score] = score
597
- membership_data[:decoded_score] = decode_score(score) if respond_to?(:decode_score)
673
+ decoded_score = decode_score(score) if respond_to?(:decode_score)
598
674
  when :set
599
675
  is_member = collection.member?(identifier)
600
676
  next unless is_member
601
677
  when :list
602
678
  position = collection.to_a.index(identifier)
603
679
  next unless position
604
-
605
- membership_data[:position] = position
606
680
  end
607
681
 
608
- memberships << membership_data
682
+ # Create ParticipationMembership instance
683
+ # Use target_class_base to get clean class name without namespace
684
+ membership = ParticipationMembership.new(
685
+ target_class: config.target_class_base,
686
+ target_id: target_id,
687
+ collection_name: config.collection_name,
688
+ type: config.type,
689
+ score: score,
690
+ decoded_score: decoded_score,
691
+ position: position
692
+ )
693
+
694
+ memberships << membership
609
695
  rescue StandardError => e
610
- Familia.ld "[#{collection_key}] Error checking membership: #{e.message}"
696
+ Familia.debug "[#{collection_key}] Error checking membership: #{e.message}"
611
697
  next
612
698
  end
613
699
  end