familia 2.0.0.pre17 → 2.0.0.pre18

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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +60 -0
  3. data/CLAUDE.md +9 -2
  4. data/Gemfile.lock +1 -1
  5. data/README.md +13 -0
  6. data/bin/irb +1 -1
  7. data/docs/guides/core-field-system.md +48 -26
  8. data/docs/migrating/v2.0.0-pre18.md +58 -0
  9. data/docs/qodo-merge-compliance.md +96 -0
  10. data/lib/familia/base.rb +0 -2
  11. data/lib/familia/connection/middleware.rb +58 -4
  12. data/lib/familia/connection.rb +1 -1
  13. data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
  14. data/lib/familia/data_type/serialization.rb +5 -5
  15. data/lib/familia/data_type.rb +2 -2
  16. data/lib/familia/encryption/encrypted_data.rb +12 -2
  17. data/lib/familia/encryption/manager.rb +11 -4
  18. data/lib/familia/features/autoloader.rb +3 -1
  19. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
  20. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
  21. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +41 -27
  22. data/lib/familia/features/safe_dump.rb +2 -3
  23. data/lib/familia/horreum/database_commands.rb +1 -1
  24. data/lib/familia/horreum/definition.rb +6 -37
  25. data/lib/familia/horreum/management.rb +17 -12
  26. data/lib/familia/horreum/persistence.rb +1 -1
  27. data/lib/familia/horreum/serialization.rb +91 -73
  28. data/lib/familia/horreum.rb +10 -6
  29. data/lib/familia/identifier_extractor.rb +60 -0
  30. data/lib/familia/logging.rb +271 -112
  31. data/lib/familia/refinements.rb +0 -1
  32. data/lib/familia/version.rb +1 -1
  33. data/lib/familia.rb +2 -2
  34. data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -14
  35. data/pr_agent.toml +31 -0
  36. data/pr_compliance_checklist.yaml +45 -0
  37. data/try/edge_cases/empty_identifiers_try.rb +1 -1
  38. data/try/edge_cases/hash_symbolization_try.rb +31 -31
  39. data/try/edge_cases/json_serialization_try.rb +2 -2
  40. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
  41. data/try/edge_cases/race_conditions_try.rb +1 -1
  42. data/try/edge_cases/reserved_keywords_try.rb +1 -1
  43. data/try/edge_cases/string_coercion_try.rb +1 -1
  44. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  45. data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
  46. data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
  47. data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
  48. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  49. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
  50. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
  51. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
  52. data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
  53. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
  54. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  55. data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
  56. data/try/features/encrypted_fields/memory_security_try.rb +1 -1
  57. data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
  58. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  59. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
  60. data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
  61. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
  62. data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
  63. data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
  64. data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
  65. data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
  66. data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
  67. data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
  68. data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
  69. data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
  70. data/try/features/expiration/expiration_try.rb +1 -1
  71. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  72. data/try/features/feature_dependencies_try.rb +1 -1
  73. data/try/features/feature_improvements_try.rb +1 -1
  74. data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
  75. data/try/features/object_identifier/object_identifier_try.rb +1 -1
  76. data/try/features/quantization/quantization_try.rb +1 -1
  77. data/try/features/real_feature_integration_try.rb +17 -14
  78. data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
  79. data/try/features/relationships/indexing_try.rb +6 -1
  80. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  81. data/try/features/relationships/participation_commands_verification_try.rb +4 -4
  82. data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
  83. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  84. data/try/features/relationships/relationships_api_changes_try.rb +1 -1
  85. data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
  86. data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
  87. data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
  88. data/try/features/relationships/relationships_performance_try.rb +1 -1
  89. data/try/features/relationships/relationships_performance_working_try.rb +1 -1
  90. data/try/features/relationships/relationships_try.rb +1 -1
  91. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  92. data/try/features/safe_dump/safe_dump_try.rb +1 -1
  93. data/try/features/transient_fields/redacted_string_try.rb +1 -1
  94. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  95. data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
  96. data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
  97. data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
  98. data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +1 -1
  99. data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
  100. data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
  101. data/try/integration/connection/middleware_reconnect_try.rb +87 -0
  102. data/try/{connection → integration/connection}/operation_mode_guards_try.rb +1 -1
  103. data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
  104. data/try/{core → integration/connection}/pools_try.rb +1 -1
  105. data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
  106. data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
  107. data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
  108. data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
  109. data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
  110. data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
  111. data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
  112. data/try/{core → integration}/create_method_try.rb +1 -1
  113. data/try/integration/cross_component_try.rb +1 -1
  114. data/try/{core → integration}/database_consistency_try.rb +11 -8
  115. data/try/{core → integration}/familia_extended_try.rb +1 -1
  116. data/try/{core → integration}/familia_members_methods_try.rb +1 -1
  117. data/try/{models → integration/models}/customer_safe_dump_try.rb +1 -1
  118. data/try/{models → integration/models}/customer_try.rb +1 -1
  119. data/try/{models → integration/models}/datatype_base_try.rb +1 -1
  120. data/try/{models → integration/models}/familia_object_try.rb +1 -1
  121. data/try/{core → integration}/persistence_operations_try.rb +1 -1
  122. data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
  123. data/try/{configuration → integration}/scenarios_try.rb +1 -1
  124. data/try/{core → integration}/secure_identifier_try.rb +1 -1
  125. data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
  126. data/try/performance/benchmarks_try.rb +2 -2
  127. data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
  128. data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
  129. data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
  130. data/try/{core → unit/core}/autoloader_try.rb +1 -1
  131. data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
  132. data/try/{core → unit/core}/connection_try.rb +1 -1
  133. data/try/{core → unit/core}/errors_try.rb +1 -1
  134. data/try/{core → unit/core}/extensions_try.rb +1 -1
  135. data/try/unit/core/familia_logger_try.rb +110 -0
  136. data/try/{core → unit/core}/familia_try.rb +1 -1
  137. data/try/{core → unit/core}/middleware_try.rb +41 -1
  138. data/try/{core → unit/core}/settings_try.rb +1 -1
  139. data/try/{core → unit/core}/time_utils_try.rb +1 -1
  140. data/try/{core → unit/core}/tools_try.rb +1 -1
  141. data/try/{core → unit/core}/utils_try.rb +17 -14
  142. data/try/{data_types → unit/data_types}/boolean_try.rb +1 -1
  143. data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
  144. data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
  145. data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
  146. data/try/{data_types → unit/data_types}/list_try.rb +1 -1
  147. data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
  148. data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
  149. data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
  150. data/try/{data_types → unit/data_types}/string_try.rb +1 -1
  151. data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
  152. data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +1 -1
  153. data/try/{horreum → unit/horreum}/base_try.rb +3 -3
  154. data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
  155. data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
  156. data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
  157. data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
  158. data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
  159. data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
  160. data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
  161. data/try/{horreum → unit/horreum}/initialization_try.rb +2 -2
  162. data/try/unit/horreum/json_type_preservation_try.rb +248 -0
  163. data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
  164. data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
  165. data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
  166. data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
  167. data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
  168. data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
  169. data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
  170. data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
  171. metadata +134 -125
  172. data/lib/familia/distinguisher.rb +0 -85
  173. data/lib/familia/refinements/logger_trace.rb +0 -60
  174. data/try/refinements/logger_trace_methods_try.rb +0 -44
  175. /data/try/{debugging → support/debugging}/README.md +0 -0
  176. /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
  177. /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
  178. /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
  179. /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
  180. /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
  181. /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
  182. /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
  183. /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
  184. /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
  185. /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
  186. /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
  187. /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
  188. /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
  189. /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
  190. /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
  191. /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
  192. /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
  193. /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
  194. /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
  195. /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
  196. /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
  197. /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
  198. /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
  199. /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
  200. /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
  201. /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
  202. /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
  203. /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
  204. /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
  205. /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
  206. /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
  207. /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
  208. /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
  209. /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
  210. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
  211. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  212. /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
  213. /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
  214. /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  215. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
  216. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
  217. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
  218. /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
  219. /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
  220. /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e760a3ff094446126c56a0c2b76fd5bed0f6ff64d8eed89580e19d98a5a9ba66
4
- data.tar.gz: 74545b0bbb8c89b06ffd385c1448a95a7808b168686a955155a004b91160dafa
3
+ metadata.gz: e5bbf0ac2fc6a1243d74f679c7027040be00783af625dd90d376637e32417394
4
+ data.tar.gz: 4b354347a0c2403490dc20fdc12f93b3e71b826cf0b2b08638f6fd884a883630
5
5
  SHA512:
6
- metadata.gz: cd9cd6c374f24859279125ef05199b0dfdac51f7cccdf2bcdf0489c7cca5126ac03d7cab1d54aa8021ce38b286203212514b2b412322ea151b8957fa16c9b445
7
- data.tar.gz: 3e44e06249e6daf715ce09b02643b8faf153c85fc2d8451d4cf56d77c5115e5e5fdb1b752acef3f1fdd46f7b7eb7f019213f22850ffe728ec51a05d98ecc5528
6
+ metadata.gz: 221116e1aa14bc7114cb51164727587dcb0e8435dd8b85bb7d14618393ef446446376fd7febf6dbc9993098e2c819b3b0aa6f3c8bedce1c965b99ec4c8832a7b
7
+ data.tar.gz: 457b0e5bfef1f203c8f2edf934d1ee37915df04c49b0cb8b097ab9732738186ba9e8e92496851699f0952426971d2d3d244e61e594719439672996a0ccde2050
data/CHANGELOG.rst CHANGED
@@ -12,6 +12,66 @@ Versioning <https://semver.org/spec/v2.0.0.html>`__.
12
12
 
13
13
  <!--scriv-insert-here-->
14
14
 
15
+ .. _changelog-2.0.0.pre18:
16
+
17
+ 2.0.0.pre18 — 2025-10-05
18
+ ========================
19
+
20
+ Added
21
+ -----
22
+
23
+ - Added ``Familia.reconnect!`` method to refresh connection pools with current middleware configuration. This solves issues in test suites where middleware (like DatabaseLogger) is enabled after connection pools are created. The method clears the connection chain, increments the middleware version, and clears fiber-local connections, ensuring new connections include the latest middleware. See ``lib/familia/connection/middleware.rb:81-117``.
24
+
25
+ Changed
26
+ -------
27
+
28
+ - **BREAKING**: Implemented type-preserving JSON serialization for Horreum field values. Non-string values (Integer, Boolean, Float, nil, Hash, Array) are JSON-encoded for storage and JSON-decoded on retrieval. **Strings are stored as-is without JSON encoding** to avoid double-quoting and maintain Redis baseline simplicity. Type preservation is achieved through smart deserialization: values that parse as JSON restore to their original types, otherwise remain as strings.
29
+
30
+ - **BREAKING**: Changed default Hash key format from symbols to strings throughout the codebase (``symbolize: false`` default). This eliminates ambiguity with HTTP request parameters and IndifferentHash-style implementations, providing strict adherence to JSON parsing rules and avoiding key duplication issues.
31
+
32
+ - **BREAKING**: Fixed ``initialize_with_keyword_args`` to properly handle ``false`` and ``0`` values during object initialization. Previously, falsy values were incorrectly skipped due to truthiness checks. Now uses explicit nil checking with ``fetch`` to preserve all non-nil values including ``false`` and ``0``.
33
+
34
+ - **String serialization now uses JSON encoding**: All string values are JSON-encoded during storage (wrapped in quotes) for consistent type preservation. The lenient deserializer handles both new JSON-encoded strings and legacy plain strings automatically. PR #152
35
+
36
+ Removed
37
+ -------
38
+
39
+ - **BREAKING**: Removed ``dump_method`` and ``load_method`` configuration options from ``Familia::Base`` and ``Familia::Horreum::Definition``. JSON serialization is now hard-coded for consistency and type safety. Custom serialization methods are no longer supported.
40
+
41
+ Fixed
42
+ -----
43
+
44
+ - Fixed type coercion bugs where Integer fields (e.g., ``age: 35``) became Strings (``"35"``) and Boolean fields (e.g., ``active: true``) became Strings (``"true"``) after database round-trips. All primitive types now maintain their original types through ``find_by_dbkey``, ``refresh!``, and ``batch_update`` operations.
45
+
46
+ - Fixed ``deserialize_value`` to return all JSON-parsed types instead of filtering to Hash/Array only. This enables proper deserialization of primitive types (Integer, Boolean, Float, String) from Redis storage.
47
+
48
+ - Added JSON deserialization in ``find_by_dbkey`` using existing ``initialize_with_keyword_args_deserialize_value`` helper method to maintain DRY principles and ensure loaded objects receive properly typed field values rather than raw Redis strings.
49
+
50
+ - Optimized serialization to avoid double-encoding strings - strings stored directly in Redis as-is, only non-string types use JSON encoding. This reduces storage overhead and maintains Redis's string baseline semantics.
51
+
52
+ - Fixed encrypted fields with ``category: :encrypted`` appearing in ``to_h()`` output. These fields now correctly set ``loggable: false`` to prevent accidental exposure in logs, APIs, or external interfaces. PR #152
53
+
54
+ - Fixed middleware registration to only set ``@middleware_registered`` flag when middleware is actually enabled and registered. Previously, calling ``create_dbclient`` before enabling middleware would set the flag to ``true`` without registering anything, preventing later middleware enablement from working. The fix ensures ``register_middleware_once`` only sets the flag after successful registration. See ``lib/familia/connection/middleware.rb:124-146``.
55
+
56
+ Security
57
+ --------
58
+
59
+ - Encrypted fields defined via ``field :name, category: :encrypted`` now properly excluded from ``to_h()`` serialization, matching the security behavior of ``encrypted_field``. PR #152
60
+
61
+ Documentation
62
+ -------------
63
+
64
+ - Added comprehensive type preservation test suite (``try/unit/horreum/json_type_preservation_try.rb``) with 30 test cases covering Integer, Boolean, String, Float, Hash, Array, nested structures, nil handling, empty strings, zero values, round-trip consistency, ``batch_update``, and ``refresh!`` operations.
65
+
66
+ AI Assistance
67
+ -------------
68
+
69
+ - Claude Code (claude-sonnet-4-5) provided implementation guidance, identified the ``initialize_with_keyword_args`` falsy value bug, wrote comprehensive test suite, and coordinated multi-file changes across serialization, management, and base modules.
70
+
71
+ - Issue analysis, implementation guidance, test verification, and documentation for JSON serialization changes and encrypted field security fix.
72
+
73
+ - Claude Code (Sonnet 4.5) provided architecture analysis, implementation design, and identified critical issues through the second-opinion agent. Key contributions included recommending the simplified approach without pool shutdown lifecycle management, identifying the race condition risk in clearing ``@middleware_registered``, and suggesting the use of natural pool aging instead of explicit shutdown.
74
+
15
75
  .. _changelog-2.0.0.pre17:
16
76
 
17
77
  2.0.0.pre17 — 2025-10-03
data/CLAUDE.md CHANGED
@@ -154,8 +154,15 @@ end
154
154
 
155
155
  ### Important Implementation Notes
156
156
 
157
- **Field Initialization**: Objects can be initialized with positional args (brittle) or keyword args (robust). Keyword args are recommended.
158
- **Serialization**: Uses JSON by default but supports custom `serialize_value`/`deserialize_value` methods.
157
+ **Field Initialization**: Objects can be initialized with positional args (brittle) or keyword args (robust). Keyword args are recommended. All non-nil values including `false` and `0` are preserved during initialization.
158
+
159
+ **Serialization**: All field values are JSON-encoded for storage and JSON-decoded on retrieval to preserve Ruby types (Integer, Boolean, String, Float, Hash, Array, nil). This ensures type preservation across the Redis storage boundary. For example:
160
+ - `age: 35` (Integer) stores as `"35"` in Redis and loads back as Integer `35`
161
+ - `active: true` (Boolean) stores as `"true"` in Redis and loads back as Boolean `true`
162
+ - `metadata: {key: "value"}` (Hash) stores as JSON and loads back as Hash with proper types
163
+
159
164
  **Database Key Generation**: Automatic key generation using class name, identifier, and field/type names (aka dbkey). Pattern: `classname:identifier:fieldname`
165
+
160
166
  **Memory Efficiency**: Only non-nil values are stored in keystore database to optimize memory usage.
167
+
161
168
  **Thread Safety**: Data types are frozen after instantiation to ensure immutability.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (2.0.0.pre17)
4
+ familia (2.0.0.pre18)
5
5
  benchmark (~> 0.4)
6
6
  connection_pool (~> 2.5)
7
7
  csv (~> 3.3)
data/README.md CHANGED
@@ -438,6 +438,19 @@ Contributions are welcome! Please feel free to submit a Pull Request.
438
438
  4. Push to the branch (`git push origin feature/amazing-feature`)
439
439
  5. Open a Pull Request
440
440
 
441
+ ### PR Compliance Checks
442
+
443
+ Pull requests are automatically reviewed by [Qodo Merge](https://qodo.ai) with compliance checks for:
444
+ - **Error Handling** - External API calls and database operations must have proper error handling
445
+ - **Test Coverage** - New features must include tests using the Tryouts framework
446
+ - **Changelog Fragments** - User-facing changes should include a changelog entry
447
+ - **Documentation** - API changes must update documentation
448
+ - **Backward Compatibility** - Breaking changes must be documented
449
+ - **Thread Safety** - Shared state must be properly synchronized
450
+ - **Database Key Naming** - Keys must follow Familia conventions
451
+
452
+ See [docs/qodo-merge-compliance.md](docs/qodo-merge-compliance.md) for details.
453
+
441
454
  ## License
442
455
 
443
456
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
data/bin/irb CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/bin/bash
2
2
 
3
- irb -Ilib -r familia -Itry -r helpers/test_helpers
3
+ irb -Ilib -r familia -Itry -r support/helpers/test_helpers
@@ -50,17 +50,20 @@ customer.email = "admin@acme.com" # Custom method name
50
50
  customer.name!("Updated Corp") # Fast writer (immediate DB persistence)
51
51
  ```
52
52
 
53
- ### Field Categories
53
+ ### Special Field Types
54
54
 
55
- Fields can be categorized for special processing by features:
55
+ Use dedicated field methods provided by features for special field behaviors:
56
56
 
57
57
  ```ruby
58
58
  class Document < Familia::Horreum
59
- field :title # Regular field
60
- field :content, category: :encrypted # Will be processed by encrypted_fields feature
61
- field :api_key, category: :transient # Non-persistent field
62
- field :tags, category: :indexed # Custom category for indexing
63
- field :metadata, category: :json # Custom JSON serialization
59
+ feature :encrypted_fields
60
+ feature :transient_fields
61
+
62
+ field :title # Regular persistent field
63
+ encrypted_field :content # Encrypted storage
64
+ transient_field :api_key # Non-persistent (memory only)
65
+ field :tags # Regular field
66
+ field :metadata # Regular field
64
67
  end
65
68
  ```
66
69
 
@@ -285,27 +288,28 @@ order.priority_urgent? # => false
285
288
 
286
289
  ```ruby
287
290
  class Product < Familia::Horreum
288
- field :name, category: :searchable
289
- field :price, category: :numeric
290
- field :description, category: :text
291
- field :secret_key, category: :encrypted
291
+ feature :transient_fields
292
+
293
+ field :name
294
+ field :price
295
+ field :description
292
296
  transient_field :temp_data
293
297
  end
294
298
 
295
299
  # Get all field names
296
300
  Product.fields
297
- # => [:name, :price, :description, :secret_key, :temp_data]
301
+ # => [:name, :price, :description, :temp_data]
298
302
 
299
303
  # Get field types registry
300
304
  Product.field_types
301
305
  # => { name: #<FieldType...>, price: #<FieldType...>, ... }
302
306
 
303
- # Get fields by category
304
- Product.fields.select { |f| Product.field_types[f].category == :searchable }
305
- # => [:name]
307
+ # Get fields by category (read-only introspection)
308
+ Product.fields.select { |f| Product.field_types[f].category == :transient }
309
+ # => [:temp_data]
306
310
 
307
311
  # Get persistent vs transient fields
308
- Product.persistent_fields # => [:name, :price, :description, :secret_key]
312
+ Product.persistent_fields # => [:name, :price, :description]
309
313
  Product.transient_fields # => [:temp_data]
310
314
 
311
315
  # Field method mapping (for backward compatibility)
@@ -313,15 +317,24 @@ Product.field_method_map
313
317
  # => { name: :name, price: :price, secret_key: :secret_key, temp_data: :temp_data }
314
318
  ```
315
319
 
316
- ### Field Categories for Feature Processing
320
+ ### Using Field Type Category for Introspection
321
+
322
+ The `category` method on FieldType provides read-only metadata for introspection:
317
323
 
318
324
  ```ruby
319
- # Features can process fields by category
325
+ # Custom field type with category metadata
326
+ class SearchableFieldType < Familia::FieldType
327
+ def category
328
+ :searchable
329
+ end
330
+ end
331
+
332
+ # Features can process fields by inspecting their category
320
333
  module SearchableFieldsFeature
321
334
  def self.included(base)
322
335
  base.extend ClassMethods
323
336
 
324
- # Process all searchable fields
337
+ # Find all searchable fields by inspecting field type category
325
338
  searchable_fields = base.fields.select do |field|
326
339
  base.field_types[field].category == :searchable
327
340
  end
@@ -348,11 +361,17 @@ module SearchableFieldsFeature
348
361
  end
349
362
 
350
363
  class Product < Familia::Horreum
351
- feature :searchable_fields # Processes all :searchable category fields
364
+ feature :searchable_fields
352
365
 
353
- field :name, category: :searchable
354
- field :description, category: :searchable
355
- field :internal_id, category: :system
366
+ # Use custom field type with searchable category
367
+ def self.searchable_field(name, **options)
368
+ field_type = SearchableFieldType.new(name, **options)
369
+ register_field_type(field_type)
370
+ end
371
+
372
+ searchable_field :name
373
+ searchable_field :description
374
+ field :internal_id
356
375
  end
357
376
 
358
377
  # Auto-generated search methods available
@@ -733,10 +752,13 @@ end
733
752
  ### 1. Choose Appropriate Field Types
734
753
 
735
754
  ```ruby
736
- # Use built-in field types when possible
755
+ # Use dedicated field methods provided by features
737
756
  class User < Familia::Horreum
738
- field :name # Simple string field
739
- field :metadata, category: :json # For complex data
757
+ feature :transient_fields
758
+ feature :encrypted_fields
759
+
760
+ field :name # Simple persistent field
761
+ field :metadata # For complex data
740
762
  transient_field :temp_token # For runtime-only data
741
763
  encrypted_field :api_key # For sensitive data
742
764
  end
@@ -0,0 +1,58 @@
1
+ # Migrating Guide: v2.0.0-pre18
2
+
3
+ This version completes the JSON serialization implementation by removing the string-as-is optimization and fixes encrypted field visibility in serialization.
4
+
5
+ ## JSON Serialization for All Types
6
+
7
+ **What Changed:**
8
+
9
+ All field values (including strings) are now JSON-encoded during storage for consistent type preservation.
10
+
11
+ **Storage Format:**
12
+
13
+ ```ruby
14
+ # Before (v2.0.0-pre14):
15
+ HGET user:123 name
16
+ "John Doe" # Plain string
17
+
18
+ # After (v2.0.0-pre18):
19
+ HGET user:123 name
20
+ "\"John Doe\"" # JSON-encoded string
21
+ ```
22
+
23
+ **Migration:**
24
+
25
+ No migration needed. The deserializer automatically handles:
26
+ - **New format**: `"\"value\""` → `"value"`
27
+ - **Legacy format**: `"value"` → `"value"`
28
+
29
+ **Why This Matters:**
30
+
31
+ Prevents data corruption for edge cases:
32
+ - Badge `"007"` stays `"007"` (not converted to integer `7`)
33
+ - String `"true"` stays `"true"` (not converted to boolean)
34
+ - String `"null"` stays `"null"` (not converted to `nil`)
35
+
36
+ ## Encrypted Field Security Fix
37
+
38
+ **What Changed:**
39
+
40
+ Fields defined with `category: :encrypted` now correctly exclude encrypted data from `to_h()` output.
41
+
42
+ **Before:**
43
+ ```ruby
44
+ field :secret, category: :encrypted
45
+ user.to_h # => {"id" => "123", "secret" => {...}} # ❌ Exposed!
46
+ ```
47
+
48
+ **After:**
49
+ ```ruby
50
+ field :secret, category: :encrypted
51
+ user.to_h # => {"id" => "123"} # ✅ Secure
52
+ ```
53
+
54
+ Both `encrypted_field` and `field :name, category: :encrypted` now behave identically for security.
55
+
56
+ **Migration:**
57
+
58
+ No code changes needed. Review any code relying on encrypted fields appearing in `to_h()` output.
@@ -0,0 +1,96 @@
1
+ # Qodo Merge Compliance Configuration
2
+
3
+ This document describes the Qodo Merge (formerly PR-Agent) configuration for the Familia project.
4
+
5
+ ## Overview
6
+
7
+ Qodo Merge provides automated PR analysis, code reviews, and compliance checks. Our configuration enables two key compliance features:
8
+
9
+ 1. **Codebase Duplication Compliance** - Uses RAG (Retrieval-Augmented Generation) to check for duplicate code across related repositories
10
+ 2. **Custom Compliance** - Project-specific rules tailored to Familia's development practices
11
+
12
+ ## Configuration Files
13
+
14
+ ### pr_agent.toml
15
+
16
+ The main Qodo Merge configuration file located in the repository root. It includes:
17
+
18
+ - **Response Language**: Set to English for consistency
19
+ - **RAG Context Enrichment**: Enabled with related repositories (`delano/familia`, `delano/tryouts`, `delano/otto`)
20
+ - **Custom Compliance Path**: References our custom compliance checklist
21
+ - **Ignore Rules**: Excludes generated files and build artifacts from analysis
22
+
23
+ ### pr_compliance_checklist.yaml
24
+
25
+ Custom compliance rules specific to Familia development:
26
+
27
+ #### ErrorHandling
28
+ All external API calls and database operations must have proper error handling with try-catch blocks or appropriate error handling mechanisms.
29
+
30
+ #### TestCoverage
31
+ New features must include tests using the Tryouts framework. Test files should be in the `try/` directory following the `*_try.rb` or `*.try.rb` naming convention.
32
+
33
+ #### ChangelogFragment
34
+ User-facing changes must include a changelog fragment in the `changelog.d/` directory following RST format, or provide explicit justification for omission.
35
+
36
+ #### DocumentationUpdates
37
+ API changes must be reflected in documentation, including YARD comments for new public methods or updates to the `docs/` directory.
38
+
39
+ #### BackwardCompatibility
40
+ Changes must maintain backward compatibility or document breaking changes in migration guides with deprecation warnings.
41
+
42
+ #### ThreadSafety
43
+ Code handling shared state must be thread-safe with proper synchronization or clear documentation of thread-safety assumptions.
44
+
45
+ #### DatabaseKeyNaming
46
+ Database key generation must follow Familia conventions:
47
+ - Use the configured `delim` separator (default `:`)
48
+ - Avoid reserved keywords: `ttl`, `db`, `valkey`, `redis`
49
+ - Handle empty identifiers to prevent stack overflow
50
+
51
+ ## Interactive Commands
52
+
53
+ Team members can trigger on-demand Qodo Merge analysis in PR comments:
54
+
55
+ - `/analyze --review` - Run code review
56
+ - `/analyze --test` - Generate test suggestions
57
+ - `/improve` - Get improvement suggestions
58
+ - `/ask` - Ask questions about the PR
59
+
60
+ ## Compliance Status
61
+
62
+ In PR comments from `@qodo-merge-pro`, you'll see:
63
+
64
+ - 🟢 Green circle - Compliance check passed
65
+ - 🔴 Red circle - Compliance check failed with details
66
+ - ⚪ White circle - Compliance check not configured (should not appear with proper configuration)
67
+
68
+ ## References
69
+
70
+ - [Qodo Merge Configuration Options](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/)
71
+ - [RAG Context Enrichment Guide](https://qodo-merge-docs.qodo.ai/core-abilities/rag_context_enrichment/)
72
+ - [Compliance Guide](https://qodo-merge-docs.qodo.ai/tools/compliance/)
73
+ - [Best Practices](https://docs.qodo.ai/qodo-documentation/qodo-merge/features/best-practices)
74
+
75
+ ## Maintenance
76
+
77
+ ### Updating Compliance Rules
78
+
79
+ To add or modify compliance rules:
80
+
81
+ 1. Edit `pr_compliance_checklist.yaml`
82
+ 2. Ensure YAML syntax is valid: `ruby -r yaml -e "YAML.load_file('pr_compliance_checklist.yaml')"`
83
+ 3. Commit changes - they take effect immediately on new PRs
84
+
85
+ ### Updating Ignore Rules
86
+
87
+ To exclude additional files from analysis:
88
+
89
+ 1. Edit the `[ignore]` section in `pr_agent.toml`
90
+ 2. Use glob patterns to match file paths
91
+ 3. Test with new PRs to verify exclusions work as expected
92
+
93
+ ## Future Improvements (Optional)
94
+
95
+ - **Centralized Configuration**: Create a `pr-agent-settings` repository with `metadata.yaml` to share configuration across all repos
96
+ - **Wiki Configuration**: Enable repo wiki and create `.pr_agent.toml` page (wiki config takes precedence over local files)
data/lib/familia/base.rb CHANGED
@@ -17,8 +17,6 @@ module Familia
17
17
 
18
18
  @features_available = nil
19
19
  @feature_definitions = nil
20
- @dump_method = :to_json
21
- @load_method = :from_json
22
20
 
23
21
  def self.included(base)
24
22
  # Ensure the including class gets its own feature registry
@@ -1,6 +1,6 @@
1
1
  # lib/familia/connection/middleware.rb
2
2
 
3
- require_relative '../../middleware/database_middleware'
3
+ require_relative '../../middleware/database_logger'
4
4
 
5
5
  module Familia
6
6
  module Connection
@@ -19,13 +19,13 @@ module Familia
19
19
  # Increments the middleware version, invalidating all cached connections
20
20
  def increment_middleware_version!
21
21
  @middleware_version += 1
22
- Familia.trace :MIDDLEWARE_VERSION, nil, "Incremented to #{@middleware_version}" if Familia.debug?
22
+ Familia.trace :MIDDLEWARE_VERSION, nil, "Incremented to #{@middleware_version}"
23
23
  end
24
24
 
25
25
  # Sets a versioned fiber-local connection
26
- def set_fiber_connection(connection)
26
+ def fiber_connection=(connection)
27
27
  Fiber[:familia_connection] = [connection, middleware_version]
28
- Familia.trace :FIBER_CONNECTION, nil, "Set with version #{middleware_version}" if Familia.debug?
28
+ Familia.trace :FIBER_CONNECTION, nil, "Set with version #{middleware_version}"
29
29
  end
30
30
 
31
31
  # Clears the fiber-local connection
@@ -50,24 +50,78 @@ module Familia
50
50
  increment_middleware_version! if value
51
51
  end
52
52
 
53
+ # Reconnects with fresh middleware registration
54
+ #
55
+ # This method is useful when middleware needs to be applied to connection pools
56
+ # that were created before middleware was enabled. It:
57
+ #
58
+ # 1. Clears the middleware registration flag to allow re-registration
59
+ # 2. Re-runs the middleware registration logic
60
+ # 3. Clears connection chain to force rebuild
61
+ # 4. Increments middleware version to invalidate cached connections
62
+ # 5. Clears fiber-local connections
63
+ #
64
+ # The next connection request will use the updated middleware configuration.
65
+ # Existing connection pools will naturally create new connections with middleware
66
+ # as old connections are cycled out.
67
+ #
68
+ # @note If no middleware is enabled, this method safely clears connection state
69
+ # but won't register any middleware until it's enabled.
70
+ #
71
+ # @example Enable middleware and reconnect
72
+ # Familia.enable_database_logging = true
73
+ # Familia.reconnect!
74
+ #
75
+ # @example In test suites
76
+ # # Test file A creates pools
77
+ # Familia.connection_provider = ->(uri) { pool.with { |c| c } }
78
+ #
79
+ # # Test file B enables middleware
80
+ # Familia.enable_database_logging = true
81
+ # Familia.reconnect! # Force new connections with middleware
82
+ #
83
+ def reconnect!
84
+ # Allow middleware to be re-registered
85
+ @middleware_registered = false
86
+ register_middleware_once
87
+
88
+ # Clear connection chain to force rebuild
89
+ @connection_chain = nil
90
+
91
+ # Increment version to invalidate all cached connections
92
+ increment_middleware_version!
93
+
94
+ # Clear fiber-local connections
95
+ clear_fiber_connection!
96
+
97
+ Familia.trace :RECONNECT, nil, 'Connection chain cleared, will rebuild with current middleware on next use'
98
+ end
99
+
53
100
  private
54
101
 
55
102
  # Registers middleware once globally, regardless of when clients are created.
56
103
  # This prevents duplicate middleware registration and ensures all clients get middleware.
57
104
  def register_middleware_once
105
+ # Skip if already registered
58
106
  return if @middleware_registered
59
107
 
108
+ # Check if any middleware is enabled
109
+ return unless Familia.enable_database_logging || Familia.enable_database_counter
110
+
60
111
  if Familia.enable_database_logging
61
112
  DatabaseLogger.logger = Familia.logger
62
113
  RedisClient.register(DatabaseLogger)
114
+ Familia.trace :MIDDLEWARE_REGISTERED, nil, 'Registered DatabaseLogger'
63
115
  end
64
116
 
65
117
  if Familia.enable_database_counter
66
118
  # NOTE: This middleware uses AtomicFixnum from concurrent-ruby which is
67
119
  # less contentious than Mutex-based counters. Safe for production.
68
120
  RedisClient.register(DatabaseCommandCounter)
121
+ Familia.trace :MIDDLEWARE_REGISTERED, nil, 'Registered DatabaseCommandCounter'
69
122
  end
70
123
 
124
+ # Set flag after successful registration
71
125
  @middleware_registered = true
72
126
  end
73
127
  end
@@ -42,7 +42,7 @@ module Familia
42
42
  @connection_chain = nil # Force rebuild of chain
43
43
  end
44
44
 
45
- # Sets the default URI for Database connections.
45
+ # Sets the default URI for Database connections.
46
46
  #
47
47
  # NOTE: uri is not a property of the Settings module b/c it's not
48
48
  # configured in class defintions like default_expiration or logical DB index.
@@ -1,10 +1,10 @@
1
- # lib/familia/data_type/commands.rb
1
+ # lib/familia/data_type/database_commands.rb
2
2
 
3
3
  module Familia
4
4
  class DataType
5
5
  # Must be included in all DataType classes to provide Valkey/Redis
6
6
  # commands. The class must have a dbkey method.
7
- module Commands
7
+ module DatabaseCommands
8
8
  def move(logical_database)
9
9
  dbclient.move dbkey, logical_database
10
10
  end
@@ -11,9 +11,9 @@ module Familia
11
11
  # @return [String, nil] The serialized representation of the value, or nil
12
12
  # if serialization fails.
13
13
  #
14
- # @note When a class option is specified, it uses that class's
15
- # serialization method. Otherwise, it relies on Familia.distinguisher for
16
- # serialization.
14
+ # @note When a class option is specified, it uses Familia.identifier_extractor
15
+ # to extract the identifier from objects. Otherwise, it extracts identifiers
16
+ # from Familia::Base instances or class names.
17
17
  #
18
18
  # @example With a class option
19
19
  # serialize_value(User.new(name: "Cloe"), strict_values: false) #=> '{"name":"Cloe"}'
@@ -31,13 +31,13 @@ module Familia
31
31
  Familia.trace :TOREDIS, nil, "#{val}<#{val.class}|#{opts[:class]}>" if Familia.debug?
32
32
 
33
33
  if opts[:class]
34
- prepared = Familia.distinguisher(opts[:class], strict_values: strict_values)
34
+ prepared = Familia.identifier_extractor(opts[:class])
35
35
  Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared || '<nil>'}"
36
36
  end
37
37
 
38
38
  if prepared.nil?
39
39
  # Enforce strict values when no class option is specified
40
- prepared = Familia.distinguisher(val, strict_values: true)
40
+ prepared = Familia.identifier_extractor(val)
41
41
  Familia.ld " from <#{val.class}> => <#{prepared.class}>"
42
42
  end
43
43
 
@@ -3,7 +3,7 @@
3
3
  require_relative 'data_type/class_methods'
4
4
  require_relative 'data_type/settings'
5
5
  require_relative 'data_type/connection'
6
- require_relative 'data_type/commands'
6
+ require_relative 'data_type/database_commands'
7
7
  require_relative 'data_type/serialization'
8
8
 
9
9
  # Familia
@@ -77,7 +77,7 @@ module Familia
77
77
 
78
78
  include Settings
79
79
  include Connection
80
- include Commands
80
+ include DatabaseCommands
81
81
  include Serialization
82
82
  end
83
83
 
@@ -44,8 +44,18 @@ module Familia
44
44
  new(**parsed)
45
45
  end
46
46
 
47
- def self.from_json(json_string)
48
- validate!(json_string)
47
+ def self.from_json(json_string_or_hash)
48
+ # Support both JSON strings (legacy) and already-parsed Hashes (v2.0 deserialization)
49
+ if json_string_or_hash.is_a?(Hash)
50
+ # Already parsed - use directly
51
+ parsed = json_string_or_hash
52
+ # Symbolize keys if they're strings
53
+ parsed = parsed.transform_keys(&:to_sym) if parsed.keys.first.is_a?(String)
54
+ new(**parsed)
55
+ else
56
+ # JSON string - validate and parse
57
+ validate!(json_string_or_hash)
58
+ end
49
59
  end
50
60
 
51
61
  # Instance methods for decryptability validation
@@ -32,15 +32,22 @@ module Familia
32
32
  Familia::Encryption.secure_wipe(key) if key
33
33
  end
34
34
 
35
- def decrypt(encrypted_json, context:, additional_data: nil)
36
- return nil if encrypted_json.nil? || encrypted_json.empty?
35
+ def decrypt(encrypted_json_or_hash, context:, additional_data: nil)
36
+ return nil if encrypted_json_or_hash.nil? || (encrypted_json_or_hash.respond_to?(:empty?) && encrypted_json_or_hash.empty?)
37
37
 
38
38
  # Increment counter immediately to track all decryption attempts, even failed ones
39
39
  Familia::Encryption.derivation_count.increment
40
40
 
41
41
  begin
42
- data = Familia::Encryption::EncryptedData.new(**Familia::JsonSerializer.parse(encrypted_json,
43
- symbolize_names: true))
42
+ # Delegate parsing and instantiation to EncryptedData.from_json
43
+ # Wrap validation errors for security (don't expose internal structure details)
44
+ begin
45
+ data = Familia::Encryption::EncryptedData.from_json(encrypted_json_or_hash)
46
+ raise EncryptionError, 'Failed to parse encrypted data' unless data
47
+ rescue EncryptionError => e
48
+ # Re-wrap validation errors with generic message for security
49
+ raise EncryptionError, "Decryption failed: #{e.message}"
50
+ end
44
51
 
45
52
  # Validate algorithm support
46
53
  provider = Registry.get(data.algorithm)