familia 2.11.2 → 2.13.0

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 (233) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +16 -11
  3. data/.github/workflows/claude.yml +9 -25
  4. data/.github/workflows/code-smells.yml +1 -1
  5. data/.github/workflows/release-gem.yml +1 -1
  6. data/.github/workflows/yardoc.yml +1 -1
  7. data/.gitignore +9 -2
  8. data/.talismanrc +13 -0
  9. data/AGENTS.md +28 -0
  10. data/CHANGELOG.rst +84 -92
  11. data/Gemfile +2 -2
  12. data/Gemfile.lock +16 -19
  13. data/README.md +14 -3
  14. data/changelog.d/README.md +46 -54
  15. data/changelog.d/fragments/20260901_221800_delano_find_by_id_legacy_string.rst +17 -0
  16. data/changelog.d/fragments/20260902_043148_delano_406_valid_debug_log.md +7 -0
  17. data/changelog.d/fragments/20260902_043513_delano_407_deserialization_log_redaction.md +7 -0
  18. data/changelog.d/fragments/20260902_120000_delano_405_envelope_provenance.rst +32 -0
  19. data/changelog.d/fragments/20260902_130000_delano_408_envelope_encoding_validation.rst +26 -0
  20. data/changelog.d/fragments/20260904_120000_delano_security_audit_anchors.rst +4 -0
  21. data/changelog.d/fragments/20260904_150000_delano_encryption_identifier_precondition.rst +6 -0
  22. data/changelog.d/fragments/20260904_160000_delano_rebuild_via_scan_failure.rst +4 -0
  23. data/changelog.d/fragments/20260905_000000_delano_external_identifier_format_validation.rst +4 -0
  24. data/changelog.d/fragments/20260905_000500_delano_concurrent_rebuild_temp_key.rst +78 -0
  25. data/changelog.d/fragments/20260905_111500_delano_identifier_secret_env.rst +16 -0
  26. data/changelog.d/fragments/20260905_213000_delano_related_field_lifecycle.rst +104 -0
  27. data/changelog.d/fragments/20260906_203000_delano_unique_index_rebuild_fallback.rst +10 -0
  28. data/docs/adr/0001-record-architecture-decisions.md +20 -0
  29. data/docs/adr/0002-watch-for-private-keys-lua-for-shared-keys.md +62 -0
  30. data/docs/adr/README.md +14 -0
  31. data/docs/guides/datatype-collections.md +157 -1
  32. data/docs/guides/encryption.md +42 -7
  33. data/docs/guides/feature-encrypted-fields.md +25 -0
  34. data/docs/guides/feature-external-identifiers.md +4 -0
  35. data/docs/guides/feature-housekeeping.md +50 -1
  36. data/docs/guides/feature-relationships-indexing.md +306 -3
  37. data/docs/guides/feature-relationships-participation.md +14 -0
  38. data/docs/guides/index.md +10 -7
  39. data/docs/guides/schema-validation.md +199 -0
  40. data/docs/investigation/memory-audit.md +39 -0
  41. data/docs/investigation/rebuild-memory-incident.md +136 -0
  42. data/docs/investigation/v2-passphrase-not-bound-to-ciphertext.md +163 -0
  43. data/docs/migrating/identifier-secret.md +36 -0
  44. data/docs/migrating/v2.10.md +14 -0
  45. data/docs/overview.md +120 -37
  46. data/docs/reference/api-technical.md +91 -20
  47. data/docs/reference/migrations-specification.md +703 -0
  48. data/docs/reference/transaction_safety.md +215 -0
  49. data/docs/security/2026-07-19-audit.md +123 -0
  50. data/docs/security/2026-07-26-audit.md +113 -0
  51. data/docs/security/2026-07-30-audit.md +61 -0
  52. data/docs/security/2026-08-06-audit.md +110 -0
  53. data/docs/security/2026-09-04-audit.md +106 -0
  54. data/docs/security/README.md +25 -0
  55. data/examples/encryption_upgrade_proof/README.md +14 -8
  56. data/examples/encryption_upgrade_proof/gemfiles/Gemfile.dev-no-libsodium +1 -0
  57. data/examples/encryption_upgrade_proof/gemfiles/Gemfile.dev-with-libsodium +1 -0
  58. data/examples/encryption_upgrade_proof/gemfiles/Gemfile.production-today +1 -0
  59. data/examples/encryption_upgrade_proof/phase1_gem_upgrade_no_libsodium.rb +4 -1
  60. data/examples/encryption_upgrade_proof/phase2_libsodium_enabled.rb +33 -14
  61. data/examples/encryption_upgrade_proof/phase3_rollback_hazard.rb +1 -1
  62. data/familia.gemspec +0 -2
  63. data/lib/familia/atomic_operations.rb +399 -36
  64. data/lib/familia/connection/middleware.rb +4 -2
  65. data/lib/familia/connection/operations.rb +18 -2
  66. data/lib/familia/connection/transaction_core.rb +1 -1
  67. data/lib/familia/connection.rb +49 -3
  68. data/lib/familia/data_type/class_methods.rb +33 -0
  69. data/lib/familia/data_type/collection_base.rb +55 -7
  70. data/lib/familia/data_type/types/hashkey.rb +150 -0
  71. data/lib/familia/data_type/types/listkey.rb +89 -18
  72. data/lib/familia/data_type/types/lock.rb +34 -6
  73. data/lib/familia/data_type/types/sorted_set.rb +87 -22
  74. data/lib/familia/data_type.rb +105 -8
  75. data/lib/familia/encryption/encrypted_data.rb +34 -4
  76. data/lib/familia/encryption/manager.rb +135 -73
  77. data/lib/familia/encryption/provider.rb +7 -0
  78. data/lib/familia/encryption/providers/aes_gcm_provider.rb +3 -2
  79. data/lib/familia/encryption/providers/blake2b_personalization.rb +101 -0
  80. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +24 -13
  81. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +21 -12
  82. data/lib/familia/encryption/stored_envelope.rb +28 -0
  83. data/lib/familia/encryption.rb +1 -0
  84. data/lib/familia/errors.rb +71 -1
  85. data/lib/familia/features/encrypted_fields/concealed_string.rb +14 -15
  86. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +66 -14
  87. data/lib/familia/features/encrypted_fields.rb +5 -3
  88. data/lib/familia/features/expiration.rb +5 -4
  89. data/lib/familia/features/external_identifier.rb +24 -10
  90. data/lib/familia/features/housekeeping/enforce_collection_caps.rb +94 -0
  91. data/lib/familia/features/housekeeping.rb +20 -9
  92. data/lib/familia/features/relationships/collection_operations.rb +87 -6
  93. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +49 -8
  94. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +110 -98
  95. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +243 -47
  96. data/lib/familia/features/relationships/indexing.rb +483 -15
  97. data/lib/familia/features/relationships/participation/target_methods.rb +194 -21
  98. data/lib/familia/features/relationships/participation.rb +17 -5
  99. data/lib/familia/features/relationships/participation_relationship.rb +1 -0
  100. data/lib/familia/features/relationships/score_encoding.rb +109 -28
  101. data/lib/familia/features/relationships.rb +22 -40
  102. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +9 -3
  103. data/lib/familia/features/transient_fields.rb +1 -0
  104. data/lib/familia/field_type.rb +214 -11
  105. data/lib/familia/horreum/atomic_write.rb +19 -1
  106. data/lib/familia/horreum/database_commands.rb +21 -3
  107. data/lib/familia/horreum/definition.rb +76 -111
  108. data/lib/familia/horreum/management/repair.rb +24 -62
  109. data/lib/familia/horreum/management.rb +3 -2
  110. data/lib/familia/horreum/persistence.rb +910 -119
  111. data/lib/familia/horreum/related_fields.rb +213 -13
  112. data/lib/familia/horreum/serialization.rb +39 -12
  113. data/lib/familia/horreum.rb +123 -45
  114. data/lib/familia/multi_result.rb +115 -21
  115. data/lib/familia/settings.rb +47 -14
  116. data/lib/familia/thread_safety/instrumented_mutex.rb +5 -2
  117. data/lib/familia/verifiable_identifier.rb +60 -25
  118. data/lib/familia/version.rb +1 -1
  119. data/lib/middleware/database_logger.rb +54 -7
  120. data/try/bug_fixes/overview_permission_example_try.rb +62 -0
  121. data/try/bug_fixes/partial_write_index_maintenance_try.rb +492 -0
  122. data/try/bug_fixes/permission_query_try.rb +151 -0
  123. data/try/bug_fixes/relationships_rdoc_example_try.rb +83 -0
  124. data/try/bug_fixes/stale_unique_index_try.rb +124 -0
  125. data/try/edge_cases/fast_writer_transaction_guard_try.rb +2 -4
  126. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +2 -1
  127. data/try/edge_cases/legacy_data_detection/deserialization_log_redaction_try.rb +144 -0
  128. data/try/edge_cases/legacy_data_detection/find_by_id_legacy_string_try.rb +76 -0
  129. data/try/features/atomic_write_coverage_try.rb +2 -4
  130. data/try/features/dirty_tracking_try.rb +2 -4
  131. data/try/features/dirty_write_new_object_try.rb +22 -1
  132. data/try/features/dirty_write_warnings_try.rb +2 -1
  133. data/try/features/encrypted_fields/aad_nil_fields_try.rb +4 -6
  134. data/try/features/encrypted_fields/aad_protection_try.rb +4 -6
  135. data/try/features/encrypted_fields/aad_roundtrip_try.rb +4 -6
  136. data/try/features/encrypted_fields/aad_transient_fix_try.rb +4 -6
  137. data/try/features/encrypted_fields/aad_transient_proof_try.rb +4 -6
  138. data/try/features/encrypted_fields/concealed_string_core_try.rb +8 -6
  139. data/try/features/encrypted_fields/context_isolation_try.rb +2 -4
  140. data/try/features/encrypted_fields/encrypted_data_try.rb +2 -4
  141. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +42 -1
  142. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +17 -0
  143. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +2 -4
  144. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +6 -0
  145. data/try/features/encrypted_fields/envelope_provenance_try.rb +151 -0
  146. data/try/features/encrypted_fields/envelope_version_branching_try.rb +4 -6
  147. data/try/features/encrypted_fields/envelope_version_try.rb +4 -6
  148. data/try/features/encrypted_fields/error_conditions_try.rb +2 -4
  149. data/try/features/encrypted_fields/fast_writer_try.rb +4 -6
  150. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +2 -4
  151. data/try/features/encrypted_fields/fresh_key_try.rb +4 -2
  152. data/try/features/encrypted_fields/identifier_precondition_try.rb +80 -0
  153. data/try/features/encrypted_fields/key_material_try.rb +4 -6
  154. data/try/features/encrypted_fields/key_rotation_try.rb +6 -7
  155. data/try/features/encrypted_fields/memory_security_try.rb +5 -5
  156. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +2 -4
  157. data/try/features/encrypted_fields/per_field_algorithm_try.rb +5 -2
  158. data/try/features/encrypted_fields/re_encrypt_fields_try.rb +11 -19
  159. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +5 -5
  160. data/try/features/encrypted_fields/thread_safety_try.rb +2 -4
  161. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +5 -5
  162. data/try/features/encryption/aes_gcm_salt_rotation_try.rb +24 -5
  163. data/try/features/encryption/algorithm_upgrade_try.rb +12 -10
  164. data/try/features/encryption/config_persistence_try.rb +42 -5
  165. data/try/features/encryption/core_try.rb +4 -1
  166. data/try/features/encryption/encoding_phase1_try.rb +4 -1
  167. data/try/features/encryption/encoding_phase2_try.rb +4 -1
  168. data/try/features/encryption/encrypted_data_valid_logging_try.rb +145 -0
  169. data/try/features/encryption/envelope_encoding_validation_try.rb +135 -0
  170. data/try/features/encryption/instance_variable_scope_try.rb +5 -4
  171. data/try/features/encryption/module_loading_try.rb +7 -5
  172. data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +51 -3
  173. data/try/features/encryption/request_cache_try.rb +4 -7
  174. data/try/features/encryption/roundtrip_validation_try.rb +3 -0
  175. data/try/features/encryption/secure_memory_handling_try.rb +76 -5
  176. data/try/features/encryption/xchacha20_personalization_rotation_try.rb +230 -0
  177. data/try/features/expiration/long_ttl_try.rb +196 -0
  178. data/try/features/external_identifier/external_identifier_try.rb +24 -5
  179. data/try/features/housekeeping/enforce_collection_caps_try.rb +136 -0
  180. data/try/features/housekeeping/housekeeping_try.rb +2 -2
  181. data/try/features/instance_registry_try.rb +6 -14
  182. data/try/features/real_feature_integration_try.rb +8 -0
  183. data/try/features/relationships/class_level_multi_index_try.rb +30 -0
  184. data/try/features/relationships/indexing_commands_verification_try.rb +19 -0
  185. data/try/features/relationships/indexing_rebuild_try.rb +60 -0
  186. data/try/features/relationships/rebuild_via_scan_try.rb +102 -0
  187. data/try/features/relationships/relationships_edge_cases_try.rb +3 -5
  188. data/try/features/relationships/score_encoding_permissions_try.rb +245 -0
  189. data/try/features/relationships/unique_index_cas_try.rb +546 -0
  190. data/try/features/relationships/unique_index_fallback_try.rb +132 -0
  191. data/try/features/transient_fields/refresh_reset_try.rb +4 -1
  192. data/try/features/transient_fields/single_use_redacted_string_try.rb +34 -0
  193. data/try/integration/connection/isolated_dbclient_try.rb +34 -22
  194. data/try/integration/connection/middleware_reconnect_try.rb +3 -3
  195. data/try/integration/connection/pools_try.rb +22 -13
  196. data/try/integration/familia_extended_try.rb +1 -1
  197. data/try/integration/persistence_operations_try.rb +2 -4
  198. data/try/integration/save_methods_consistency_try.rb +52 -6
  199. data/try/integration/verifiable_identifier_try.rb +255 -0
  200. data/try/investigation/memory_leak_proof.rb +20 -13
  201. data/try/investigation/pipeline_routing/CONCLUSION.md +149 -0
  202. data/try/investigation/pipeline_routing/FINDINGS.md +168 -0
  203. data/try/performance/transaction_safety_benchmark_try.rb +51 -27
  204. data/try/support/debugging/debug_aad_process.rb +2 -2
  205. data/try/{features/transient_fields → support/debugging}/simple_refresh_test.rb +2 -2
  206. data/try/support/encryption_config_helper_try.rb +114 -0
  207. data/try/support/helpers/encryption_config.rb +169 -0
  208. data/try/support/helpers/test_helpers.rb +47 -0
  209. data/try/support/prototypes/pooling/docs/README_advanced_usage.md +636 -0
  210. data/try/support/prototypes/pooling/docs/README_stress_testing.md +200 -0
  211. data/try/thread_safety/encryption_manager_cache_race_try.rb +2 -4
  212. data/try/thread_safety/instrumented_mutex_exclusion_try.rb +88 -0
  213. data/try/unit/atomic_operations_try.rb +487 -3
  214. data/try/unit/core/suite_hygiene_try.rb +32 -0
  215. data/try/unit/core/tools_try.rb +4 -2
  216. data/try/unit/data_types/enumerable_consistency/large_scale_consistency_try.rb +14 -3
  217. data/try/unit/data_types/lock_try.rb +44 -3
  218. data/try/unit/data_types/max_length_try.rb +607 -0
  219. data/try/unit/horreum/automatic_index_validation_try.rb +89 -2
  220. data/try/unit/horreum/commands_try.rb +85 -0
  221. data/try/unit/horreum/destroy_index_cleanup_try.rb +714 -22
  222. data/try/unit/horreum/multi_field_update_try.rb +154 -1
  223. data/try/unit/horreum/related_field_inheritance_snapshot_try.rb +122 -0
  224. data/try/unit/horreum/related_field_lifecycle_try.rb +1109 -0
  225. data/try/unit/horreum/serialization_try.rb +2 -2
  226. data/try/unit/horreum/unique_index_edge_cases_try.rb +46 -7
  227. data/try/unit/horreum/unique_index_guard_validation_try.rb +2 -0
  228. data/try/unit/middleware/database_logger_methods_try.rb +39 -0
  229. data/try/unit/multi_result_try.rb +298 -0
  230. data/try/unit/thread_safety_monitor_try.rb +20 -12
  231. metadata +62 -38
  232. data/docs/qodo-merge-compliance.md +0 -96
  233. /data/docs/{1106-participates_in-bidirectional-solution.md → investigation/participates_in-bidirectional-solution.md} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7b0eed1c3632c68c8c4c91617830f0b15a10907da5c7af0bbc14b9cf67a5336
4
- data.tar.gz: 4a123b805e826b8ede218675c16781f428fab6c3194968c5a3840323b949b28e
3
+ metadata.gz: 889cca334ccb762d4a5cb4fa64a400c3df8727a8219bf19129b6699bf4056069
4
+ data.tar.gz: c2e315e109a866d8b1943be21fb7cf215c3db767e801138fa4097229e98c5dcb
5
5
  SHA512:
6
- metadata.gz: 6c245370d354eaa0d8af0b0994cd44ea10f8dec3b260c9750d3cceb66563c51ee8f4390062429c8aced6cd578d919c7a806a0eef2e5be0f9d90bbc00dcf87b06
7
- data.tar.gz: a824edcd1af5d3535168600d90a3e189b0203c884fe17c7382cafb22ebc6c19a36c68011a468e9dd8b16fed386b902eac16709625004f73b7f4085b0ae0a601e
6
+ metadata.gz: d7e65ba3aa75c03d7c0328a34f7c6ad13f9c840efc2aefbe0c06bba678a41ac27704928962b5d9a3be769bd7df186bf1bc56647ee80aa173a1000ff6404d599e
7
+ data.tar.gz: e34f5200cac7681ff999b31cf842d326e49882ffd823602d8d677bec95b6376edca29f967f9da494eb431f9a857bc81758519dd4fd5969714ed478bd085cdf54
@@ -48,7 +48,7 @@ jobs:
48
48
 
49
49
  - name: Run Claude Code Review
50
50
  id: claude-review
51
- uses: anthropics/claude-code-action@beta
51
+ uses: anthropics/claude-code-action@833fb0f8c9f6686b33d963a8bae0a94f4936ab2a # v1.0.211
52
52
  with:
53
53
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
54
54
 
@@ -56,13 +56,24 @@ jobs:
56
56
  # runs) -> the CLAUDE_MODEL repo variable -> this built-in default.
57
57
  # Pinning a current id avoids the action's frozen default, which 404s
58
58
  # ("model: claude-sonnet-4-20250514"). Fall back to Sonnet on overload.
59
- model: "${{ inputs.model || vars.CLAUDE_MODEL || 'claude-opus-4-6' }}"
60
- fallback_model: "${{ vars.CLAUDE_FALLBACK_MODEL || 'claude-sonnet-4-6' }}"
59
+ # v1 of the action dropped the model/fallback_model/allowed_tools
60
+ # inputs; they are CLI flags passed via claude_args now (see
61
+ # docs/migration-guide.md and docs/usage.md in the action repo).
62
+ claude_args: |
63
+ --model ${{ inputs.model || vars.CLAUDE_MODEL || 'claude-opus-4-6' }}
64
+ --fallback-model ${{ vars.CLAUDE_FALLBACK_MODEL || 'claude-sonnet-4-6' }}
65
+ --allowedTools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"
61
66
 
62
- # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
67
+ # A bare `prompt` puts the action in agent mode, which creates no tracking
68
+ # comment, so use_sticky_comment alone did nothing and every push got a
69
+ # fresh `gh pr comment`. track_progress forces tag mode: the action owns
70
+ # one tracking comment, use_sticky_comment finds and reuses it on later
71
+ # pushes, and Claude writes the review into it via the action's built-in
72
+ # comment tool. The prompt becomes <custom_instructions> in tag mode.
73
+ track_progress: true
63
74
  use_sticky_comment: true
64
75
 
65
- direct_prompt: |
76
+ prompt: |
66
77
  Please review this pull request and provide feedback on:
67
78
  - Code quality and best practices
68
79
  - Potential bugs or issues
@@ -71,9 +82,3 @@ jobs:
71
82
  - Test coverage
72
83
 
73
84
  Use the repository's AGENTS.md for guidance on style and conventions. Be constructive and helpful in your feedback.
74
-
75
- Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
76
-
77
- # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
78
- # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
79
- allowed_tools: "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"
@@ -32,47 +32,31 @@ jobs:
32
32
 
33
33
  - name: Run Claude Code
34
34
  id: claude
35
- uses: anthropics/claude-code-action@beta
35
+ uses: anthropics/claude-code-action@833fb0f8c9f6686b33d963a8bae0a94f4936ab2a # v1.0.211
36
36
  with:
37
37
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
38
 
39
39
  # Primary model: set the CLAUDE_MODEL repo variable to override without
40
40
  # editing this file. Pinning a current id avoids the action's frozen
41
41
  # default, which 404s ("model: claude-sonnet-4-20250514"). Fall back to
42
- # Sonnet if the primary is unavailable or overloaded.
43
- model: "${{ vars.CLAUDE_MODEL || 'claude-opus-4-6' }}"
44
- fallback_model: "${{ vars.CLAUDE_FALLBACK_MODEL || 'claude-sonnet-4-6' }}"
42
+ # Sonnet if the primary is unavailable or overloaded. v1 of the action
43
+ # dropped the model/fallback_model inputs; models are CLI flags passed
44
+ # via claude_args (see docs/migration-guide.md in the action repo).
45
+ # Other supported flags (--allowedTools, --max-turns, ...):
46
+ # https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
47
+ claude_args: |
48
+ --model ${{ vars.CLAUDE_MODEL || 'claude-opus-4-6' }}
49
+ --fallback-model ${{ vars.CLAUDE_FALLBACK_MODEL || 'claude-sonnet-4-6' }}
45
50
 
46
51
  # This is an optional setting that allows Claude to read CI results on PRs
47
52
  additional_permissions: |
48
53
  actions: read
49
54
 
50
- # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
51
- # model: "claude-opus-4-20250514"
52
-
53
55
  # Optional: Customize the trigger phrase (default: @claude)
54
56
  # trigger_phrase: "/claude"
55
57
 
56
58
  # Optional: Trigger when specific user is assigned to an issue
57
59
  # assignee_trigger: "claude-bot"
58
60
 
59
- # Optional: Allow Claude to run specific commands
60
- # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
61
-
62
- # Optional: Add custom instructions for Claude to customize its behavior for your project
63
- # custom_instructions: |
64
- # Follow our coding standards
65
- # Ensure all new code has tests
66
- # Use TypeScript for new files
67
-
68
- # Optional: Custom environment variables for Claude
69
- # claude_env: |
70
- # NODE_ENV: test
71
-
72
61
  # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
73
62
  # prompt: 'Update the pull request description to include a summary of changes.'
74
-
75
- # Optional: Add claude_args to customize behavior and configuration
76
- # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
77
- # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
78
- # claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)'
@@ -74,7 +74,7 @@ jobs:
74
74
  continue-on-error: true
75
75
 
76
76
  - name: Upload Reek report as artifact
77
- uses: actions/upload-artifact@v4
77
+ uses: actions/upload-artifact@v7
78
78
  if: always()
79
79
  with:
80
80
  name: reek-report
@@ -158,4 +158,4 @@ jobs:
158
158
  echo "Releasing familia ${gem_version} from tag ${RELEASE_TAG}"
159
159
 
160
160
  - name: Build and push gem to RubyGems
161
- uses: rubygems/release-gem@052cc82692552de3ef2b81fd670e41d13cba8092 # v1.4.0
161
+ uses: rubygems/release-gem@7f9650160c1a4e7989fdc9855807bdbd421d8b6b # v1.4.1
@@ -75,7 +75,7 @@ jobs:
75
75
  echo "::endgroup::"
76
76
 
77
77
  - name: Setup GitHub Pages configuration
78
- uses: actions/configure-pages@v4
78
+ uses: actions/configure-pages@v6
79
79
 
80
80
  - name: Upload documentation artifact
81
81
  uses: actions/upload-pages-artifact@v5
data/.gitignore CHANGED
@@ -18,14 +18,21 @@
18
18
  dump.rdb
19
19
  appendonlydir
20
20
  data
21
+ doc
21
22
  log
22
23
  tmp
23
24
  vendor
24
25
  *.gem
25
26
  public/
26
27
 
27
- # Ignore WIP or temp dev files with uppercase names
28
- [A-Z]*.md
28
+ # Ignore WIP or temp dev files with uppercase names, at the repo root only.
29
+ # Anchored because core.ignoreCase is true on macOS/Windows checkouts: git
30
+ # casefolds the [A-Z] class, so an unanchored pattern silently swallowed every
31
+ # new lowercase .md anywhere in the tree (docs/*.md included).
32
+ /[A-Z]*.md
33
+
34
+ # Unredacted security notes stay local; publish redacted summaries only
35
+ *.private.md
29
36
 
30
37
  # Exclusions
31
38
  !README.md
data/.talismanrc CHANGED
@@ -12,4 +12,17 @@ fileignoreconfig:
12
12
  checksum: 10c94f802b2fa3a39fa33bf7dc34dac2ecaa2dd453ff97a19313f96a32fadeff
13
13
  - filename: examples/encrypted_fields.rb
14
14
  checksum: d1bd77f85d951d367e1c2bfd066a1d68d5f486346ee479121a3d5dcc2560bf52
15
+ # SHA-pinned actions read as "hex encoded text" to talisman; the pin is the point.
16
+ - filename: .github/workflows/claude.yml
17
+ checksum: 3a7acf45fb032644c40bad629ba6c0271689a1233ef6893aba3caf03f6327b50
18
+ - filename: .github/workflows/claude-code-review.yml
19
+ checksum: 698ebf39c1c49a72212cd8d9fd0117aaf15079dcb9f1e22c94dc7a871dd7952e
20
+ - filename: docs/adr/0002-watch-for-private-keys-lua-for-shared-keys.md
21
+ checksum: 0ace51636017e6d0a5b489dc14fc6b684cabd64c6bb706a710f869d31ea5f4f0
22
+ - filename: .github/workflows/ruby-lint.yml
23
+ checksum: b8009bc29189d214229c88f1cf7960c651404e659be74436a79efb130177ff58
24
+ - filename: .github/workflows/ci.yml
25
+ checksum: 0b356104ddf0b192d29b25669c61b17c863eca5de0e36822ea4d7b353d065016
26
+ - filename: .github/workflows/release-gem.yml
27
+ checksum: 1ad4786c939da966b79275105f60005c7a3214f424d5701d8118a7175b087112
15
28
  version: ""
data/AGENTS.md CHANGED
@@ -11,6 +11,8 @@ Guidance for AI coding agents working in this repository.
11
11
 
12
12
  ### Testing (Tryouts v3)
13
13
 
14
+ Use `valkey-server try/valkey.conf` to start the test redis.
15
+
14
16
  Each file has optional setup, testcases, and optional teardown. A testcase is a
15
17
  `##` description line, Ruby code, then one or more expectation comments
16
18
  (`#=>`, `#==>`, `#=:>`, `#=!>`, ...). The last expression is the result.
@@ -21,6 +23,13 @@ Run with `--agent` for token-efficient output (`--agent-focus summary|first-fail
21
23
  See `bundle exec try --help` for the full CLI, framework integration (`--rspec`,
22
24
  `--minitest`), and debugging flags.
23
25
 
26
+ The whole suite runs in one process, so anything a file sets globally outlives
27
+ it. For encryption keys use the scoped helpers rather than assigning
28
+ `Familia.config.encryption_keys` directly: `set_test_encryption_keys(keys,
29
+ current_version:)` in setup with `clear_test_encryption_keys` in teardown, or
30
+ `with_test_encryption_keys(keys, current_version:) { ... }` for a single
31
+ testcase. See @try/support/helpers/encryption_config.rb.
32
+
24
33
  ### Changelog
25
34
 
26
35
  Add a changelog fragment (RST) with each user-facing change. See @changelog.d/README.md
@@ -196,3 +205,22 @@ last-write timestamps (ZADD score), not a registry.
196
205
  DataType instances are frozen (immutable). Configure module-level settings once
197
206
  at startup, before threads spawn. `Familia.start_monitoring!` tracks contention.
198
207
  Tests and contention patterns live in `try/thread_safety/`.
208
+
209
+ ## Project claims and source authority
210
+
211
+ ### Attribution
212
+
213
+ Do not infer project terminology or guarantees from repetition. Before attributing a claim to the project, locate an authoritative primary source and provide its exact wording. Treat delivery notes, commit messages, agent output, and documents created or modified during the current task as leads, not evidence. If the wording is absent, call it an interpretation or proposal. Never place paraphrases in quotation marks.
214
+
215
+ ### Authoritative sources
216
+
217
+ Authoritative sources must be identified explicitly; repository presence alone does not confer authority. Accepted specifications and ADRs may establish project claims only within their stated scope. Delivery notes, commit messages, issue discussions, summaries, and agent-authored text are non-authoritative unless an authoritative source incorporates them explicitly.
218
+
219
+ ### Normative claims
220
+
221
+ For normative claims concerning security, privacy, compatibility, persistence, or data loss:
222
+
223
+ 1. Cite the authoritative source and its exact wording.
224
+ 2. Distinguish quotations, paraphrases, interpretations, and proposals.
225
+ 3. Do not use material created or modified during the current task to validate that task’s claims.
226
+ 4. If no authoritative wording exists, report the claim as unsupported.
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,77 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`
7
7
 
8
8
  <!--scriv-insert-here-->
9
9
 
10
+ .. _changelog-2.12.0:
11
+
12
+ 2.12.0 — 2026-08-03
13
+ ===================
14
+
15
+ Added
16
+ -----
17
+
18
+ - Added ``Familia::HashKey#claim_field`` and ``#release_field`` for single-field compare-and-set and compare-and-delete. They raise ``Familia::OperationModeError`` in pipelines and transactions.
19
+ - Added generated ``claim_unique_<index>!`` and ``release_unique_<index>!`` methods for class-level unique indexes. #353
20
+ - Added ``Familia::MultiResult#aborted?`` and ``#inspect`` for transaction outcome handling and logging.
21
+ - Added ``max_length:`` to ``ListKey``, ``SortedSet``, ``participates_in``, and ``class_participates_in``. ``ListKey`` retains the N elements nearest the end written to, while ``SortedSet`` retains its N highest-scoring members. Existing oversized collections are trimmed on the next capped write; call ``enforce_max_length!`` or run ``Familia::Features::Housekeeping::EnforceCollectionCaps`` to enforce a new cap immediately. ``DataType#max_length`` exposes the configured cap. #351
22
+ - Added ``encryption_personalization_history`` to support XChaCha20-Poly1305 personalization rotation. See ``docs/guides/encryption.md``. #333
23
+ - Added ``limit:``, ``offset:``, and ``each_<collection>_with_permission`` for memory-bounded permission-filtered collection queries. #309
24
+ - Added the ``dirty_write_warnings:`` collection option to override a parent class's diagnostic mode. #282
25
+
26
+ Changed
27
+ -------
28
+
29
+ - Class-level unique-index updates inside transactions now require a prior claim and raise ``Familia::OperationModeError`` otherwise. #353
30
+ - Class-level index removal and destruction no longer delete entries owned by other records. #353
31
+ - ``Familia::MultiResult#results`` now returns an empty ``Array`` on transaction aborts, ``#to_h`` includes ``:aborted``, and result objects are read-only.
32
+ - ``SortedSet#increment`` now applies the dirty-write guard. #351
33
+ - XChaCha20 decryption and request caching now support personalization rotation candidates. #333
34
+ - ``multi_field_fast_write`` rejects class-indexed fields, and ``field!`` rejects class-indexed fields in transactions or pipelines and unsaved records. #308
35
+ - Registered instance-scoped index memberships refresh on ``save``. ``atomic_write`` callers must update those indexes explicitly. #282
36
+ - Saving a record with an instance-scoped unique-index collision now raises ``Familia::RecordExistsError`` without replacing the existing entry. #282
37
+ - Direct instance- and class-scoped index mutations now require a persisted record. #282
38
+ - Instance-scoped index mutations now reject scopes without an identifier or a Symbol/String ``identifier_field``. #282
39
+
40
+ Removed
41
+ -------
42
+
43
+ - Removed unused ``csv`` and ``stringio`` runtime dependencies from the gemspec. #354
44
+
45
+ Fixed
46
+ -----
47
+
48
+ - Fixed ``decrby`` and ``decr`` (and aliases) to decrement hash fields correctly and validate integer amounts.
49
+ - Fixed ``encryption_info`` to report correct provider details and key sizes.
50
+ - Fixed unique-index save races and ownership-checked removals, preventing collisions from replacing or deleting another record's entry. #353
51
+ - Fixed claim leaks when unpersisted records call ``update_in_<scope>_<index_name>``. #370
52
+ - Fixed ``Familia::MultiResult`` handling of WATCH-aborted transactions. #355
53
+ - Fixed XChaCha20 key derivation to preserve caller-provided context values. #356
54
+ - Fixed ``SortedSet#increment`` in transactions and pipelines to return the queued result. #351
55
+ - ``:maxlength`` now warns at definition time; rename it to ``max_length:`` to opt in to collection capping. #351
56
+ - Fixed ``Lock#acquire`` with a TTL to acquire and set the expiration atomically. #347
57
+ - Fixed ``DatabaseLogger.sample_rate`` validation and ``SingleUseRedactedString`` loading through ``require 'familia'``. #347
58
+ - Fixed memory use in ``<collection>_with_permission`` queries by processing results in bounded batches. #309
59
+ - Fixed invalid permission-symbol lookups to fail rather than silently misconfigure permission queries.
60
+ - Fixed partial and fast writes to maintain class-level unique indexes atomically. #308
61
+ - Fixed instance-scoped index cleanup after indexed values change, scope classes share an index name, or identifiers are reused. #282, #365
62
+
63
+ Security
64
+ --------
65
+
66
+ - ``multi_field_update`` and ``multi_field_fast_write`` now reject plaintext writes to encrypted fields and writes to transient fields.
67
+ - Pinned GitHub Workflows holding ``CLAUDE_CODE_OAUTH_TOKEN`` to immutable release commit SHAs.
68
+ - Encryption now fails closed when ``encryption_hkdf_salt`` is blank, including with a warm request cache. Configure a non-blank salt as described in ``docs/guides/encryption.md``. #380
69
+
70
+ Documentation
71
+ -------------
72
+
73
+ - Corrected permission-management, relationship, and connection-provider examples.
74
+ - Documented ``<collection>_with_permission`` flags, permission categories, exclusive tiers, and encryption personalization rotation.
75
+
76
+ AI Assistance
77
+ -------------
78
+
79
+ - Core fixes, tryout coverage (120+ test cases), and documentation updates implemented with AI assistance.
80
+
10
81
  .. _changelog-2.11.2:
11
82
 
12
83
  2.11.2 — 2026-07-05
@@ -16,25 +87,14 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`
16
87
  Changed
17
88
  -------
18
89
 
19
- - ``to_h_for_storage`` omits nil fields; every write path removes a field that has
20
- become nil. ``save``/``commit_fields`` do a full overwrite (the stored hash
21
- matches the in-memory object, so a field nil in memory is deleted);
22
- ``save_fields``/``multi_field_update``/``multi_field_fast_write`` delete a named
23
- field passed as nil. ``to_h`` still returns every declared field, nils included.
90
+ - Nil-valued fields are now omitted from storage. Setting a field to ``nil`` removes it on every write path, while ``to_h`` continues to include declared fields with nil values.
24
91
 
25
- - Claim caveat: a full ``save``/``commit_fields`` of a stale copy clears a field
26
- another writer claimed via ``HSETNX``. To claim and update without disturbing
27
- it, use the targeted writers or ``refresh!`` first.
92
+ - Refresh stale instances before a full ``save`` or ``commit_fields`` when another writer may have claimed a field; use targeted writers to update only named fields.
28
93
 
29
94
  Fixed
30
95
  -----
31
96
 
32
- - Nil-valued fields are no longer stored as the JSON string ``"null"``. Because a
33
- hash has no native NULL, this left declared fields perpetually present, breaking
34
- ``HSETNX``/``HEXISTS`` atomic-claim patterns and wasting memory. Nil fields are
35
- now omitted, and clearing a field to nil removes it from storage (``HDEL``), so
36
- absence again means "no value". No migration required: stale ``"null"`` values
37
- are cleaned up on the next save and already decode back to ``nil`` on read.
97
+ - Fixed nil-valued fields being stored as the JSON string ``"null"``. No migration is required; existing values are cleaned up on the next save.
38
98
 
39
99
  AI Assistance
40
100
  -------------
@@ -49,96 +109,28 @@ AI Assistance
49
109
  Added
50
110
  -----
51
111
 
52
- - ``encrypted_field`` now honors a per-field ``algorithm:`` option, pinning that
53
- field's write algorithm to a specific registered provider (``'aes-256-gcm'`` or
54
- ``'xchacha20poly1305'``) independent of the registry's default-provider
55
- priority. The option was previously documented but silently ignored, so writes
56
- always used the default provider. Decryption stays envelope-driven, so a pin can
57
- be added, changed, or removed without breaking ciphertext already at rest, and
58
- ``re_encrypt_fields!`` re-encrypts under the pin rather than the default. This is
59
- the supported lever for a reader-before-writer format migration: deploy
60
- ``rbnacl`` fleet-wide so every node can *read* XChaCha20-Poly1305 while keeping
61
- *writes* pinned to AES-256-GCM until all readers are confirmed capable, then drop
62
- the pin. Issue #334
112
+ - ``encrypted_field`` now honors its per-field ``algorithm:`` option for new writes, including ``re_encrypt_fields!``. Existing ciphertext remains decryptable when the pin changes or is removed. See ``docs/guides/feature-encrypted-fields.md`` for rollout guidance. #334
63
113
 
64
114
  Changed
65
115
  -------
66
116
 
67
- - ``Familia::Encryption::Registry.get`` now distinguishes an unknown algorithm
68
- from a *known* algorithm whose provider is not available on the current node.
69
- Because ``Registry.register`` only stores providers whose runtime dependency is
70
- present, pinning ``encrypted_field ..., algorithm: 'xchacha20poly1305'`` on a
71
- node without ``rbnacl``/libsodium previously raised the misleading
72
- ``"Unsupported algorithm: xchacha20poly1305"`` -- pointing an operator at a typo
73
- when the real fix is a missing dependency. It now names the provider, explains
74
- the dependency is missing, and (because ``get`` runs on both the encrypt and
75
- decrypt paths) states that installing the dependency is what enables reading
76
- *and* writing the algorithm, framing an algorithm pin as a write-time
77
- workaround that cannot decrypt existing ciphertext. Each provider declares its
78
- own dependency via a new ``Provider.dependency_hint`` class method (nil for
79
- always-available providers like OpenSSL AES-256-GCM), so the generic error path
80
- names the correct library as providers are added rather than hardcoding any one
81
- of them. The set of registerable providers is centralized in
82
- ``Registry.known_providers``, the single source of truth shared by ``setup!``
83
- and ``get``. Error-message and internal-refactor only; the resolution of every
84
- available algorithm is unchanged. Issue #334
117
+ - Encryption errors now distinguish unsupported algorithms from known algorithms whose provider dependency is unavailable. #334
85
118
 
86
119
  Fixed
87
120
  -----
88
121
 
89
- - ``Familia::DataType#exists?`` no longer returns ``true`` for a deleted or
90
- never-created scalar key (``StringKey``, ``Counter``, ``Lock``,
91
- ``JsonStringKey``). The check was ``dbclient.exists(dbkey) && !size.zero?``,
92
- but ``EXISTS`` returns an Integer count and ``0`` is truthy in Ruby, so the
93
- guard never short-circuited on a missing key -- existence was decided
94
- entirely by the size check. ``exists?`` now uses a boolean-coerced ``EXISTS``
95
- count directly. Issue #331
96
-
97
- - Relatedly, ``StringKey#size``/``#length``/``#empty?`` (and ``Lock``'s) no
98
- longer reflect the never-nil ``#to_s`` fallback. ``#char_count`` derived from
99
- ``#to_s.size``, and ``#to_s`` intentionally returns ``Familia::Base``'s
100
- documented "never nil" inspect-string when the value is absent -- so
101
- ``#size`` was non-zero (and ``#empty?`` false) for a missing key.
102
- ``#char_count`` now reads ``#value`` directly; ``#to_s`` is left unchanged.
103
- Issue #331
104
-
105
- - ``encrypted_fields_status`` now reports each field's real algorithm for a live
106
- encrypted value (e.g. ``{ encrypted: true, algorithm: "aes-256-gcm", cleared:
107
- false }``), honoring any per-field pin. Previously it returned ``{ encrypted:
108
- false, value: "[CONCEALED]" }`` for every encrypted field, because
109
- ``ConcealedString`` had no ``concealed?`` predicate for the status check to
110
- match -- so the algorithm shown in the method's docstring and the guides was
111
- never actually produced. ``ConcealedString`` gains ``#concealed?`` and
112
- ``#algorithm`` readers (the latter reads the stored envelope). Issue #334
122
+ - Fixed ``Familia::DataType#exists?`` and missing ``StringKey`` and ``Lock`` size and emptiness checks. #331
123
+
124
+ - Fixed ``encrypted_fields_status`` to report the stored algorithm for encrypted fields, including per-field algorithm pins. #334
113
125
 
114
126
  Documentation
115
127
  -------------
116
128
 
117
- - Added an executable, multi-phase proof (``examples/encryption_upgrade_proof/``)
118
- demonstrating that installing ``rbnacl`` safely flips new writes to
119
- XChaCha20-Poly1305 while every existing AES-256-GCM envelope — including
120
- ciphertext written by the released 2.10.1 gem, under the pre-#310 static HKDF
121
- salt, and under a retired master key version — keeps decrypting. Also pins,
122
- as deliberately-passing checks, two operational hazards: the XChaCha
123
- ``encryption_personalization`` cannot be rotated (no history/fallback like
124
- ``encryption_hkdf_salt_history``), and once any XChaCha envelope exists,
125
- every node that may read it needs libsodium installed. PR #330
126
-
127
- - The encrypted-fields guide previously showed a ``provider: :aes_gcm`` field
128
- option that was never implemented; those examples now use the real
129
- ``algorithm: 'aes-256-gcm'`` form, and the ``Familia::Encryption`` facade
130
- docstring documents the shipped behavior instead of a hypothetical
131
- implementation sketch. The ``encrypted_fields_status`` output examples across
132
- the guides and the overview were corrected to match what the method now
133
- returns. Issue #334
134
-
135
- - Added a memory-audit investigation (``docs/investigation/memory-audit.md``)
136
- diagnosing #309's ``<collection>_with_permission`` O(N) query as a transient,
137
- GC-reclaimable spike rather than a per-process leak, and auditing the rest of
138
- ``lib/`` for per-process growth (concluding Familia has no unconditional leak).
139
- Ships two executable proofs in ``try/investigation/`` — a pure-Ruby
140
- ``process_memory_leak_proof.rb`` and a live-Redis ``memory_leak_proof.rb``.
141
- Diagnosis only; no runtime behaviour is changed by the investigation. Issue #309
129
+ - Added ``examples/encryption_upgrade_proof/`` to demonstrate encrypted-field algorithm upgrades. PR #330
130
+
131
+ - Corrected encrypted-field option and status examples to match supported behavior. #334
132
+
133
+ - Documented memory characteristics of ``<collection>_with_permission`` queries in ``docs/investigation/memory-audit.md``. #309
142
134
 
143
135
  AI Assistance
144
136
  -------------
data/Gemfile CHANGED
@@ -5,7 +5,7 @@ source 'https://rubygems.org'
5
5
  gemspec
6
6
 
7
7
  group :test do
8
- gem 'concurrent-ruby', '~> 1.3.7', require: false
8
+ gem 'concurrent-ruby', '~> 1.3.8', require: false
9
9
  gem 'ruby-prof'
10
10
  gem 'stackprof'
11
11
  gem 'timecop', require: false
@@ -26,7 +26,7 @@ group :development, :test do
26
26
  gem 'rake', '~> 13.0', require: false
27
27
  gem 'redcarpet', require: false
28
28
  gem 'reek', require: false
29
- gem 'rubocop', '~> 1.88.0', require: false
29
+ gem 'rubocop', '~> 1.90.0', require: false
30
30
  gem 'rubocop-performance', require: false
31
31
  gem 'rubocop-thread_safety', require: false
32
32
  gem 'ruby-lsp', require: false
data/Gemfile.lock CHANGED
@@ -1,15 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (2.11.2)
4
+ familia (2.13.0)
5
5
  concurrent-ruby (~> 1.3)
6
6
  connection_pool (>= 2.4, < 4.0)
7
- csv (~> 3.3)
8
7
  json_schemer (~> 2.0)
9
8
  logger (~> 1.7)
10
9
  oj (~> 3.16)
11
10
  redis (>= 5.0, < 6.0)
12
- stringio (>= 3.1.1, < 3.3.0)
13
11
  uri-valkey (~> 1.4)
14
12
 
15
13
  GEM
@@ -19,9 +17,8 @@ GEM
19
17
  base64 (0.3.0)
20
18
  benchmark (0.5.0)
21
19
  bigdecimal (4.1.2)
22
- concurrent-ruby (1.3.7)
20
+ concurrent-ruby (1.3.8)
23
21
  connection_pool (3.0.2)
24
- csv (3.3.5)
25
22
  date (3.5.1)
26
23
  debug (1.11.1)
27
24
  irb (~> 1.10)
@@ -66,22 +63,22 @@ GEM
66
63
  prism (>= 1.3.0)
67
64
  rdoc (>= 4.0.0)
68
65
  reline (>= 0.4.2)
69
- json (2.19.9)
66
+ json (2.21.2)
70
67
  json_schemer (2.5.0)
71
68
  bigdecimal
72
69
  hana (~> 1.3)
73
70
  regexp_parser (~> 2.0)
74
71
  simpleidn (~> 0.2)
75
- language_server-protocol (3.17.0.5)
72
+ language_server-protocol (3.17.0.6)
76
73
  lint_roller (1.1.0)
77
74
  logger (1.7.0)
78
75
  minitest (5.27.0)
79
- oj (3.17.3)
76
+ oj (3.17.6)
80
77
  bigdecimal (>= 3.0)
81
78
  ostruct (>= 0.2)
82
79
  ostruct (0.6.3)
83
80
  parallel (1.28.0)
84
- parser (3.3.11.1)
81
+ parser (3.3.12.0)
85
82
  ast (~> 2.4.1)
86
83
  racc
87
84
  pastel (0.8.0)
@@ -98,7 +95,7 @@ GEM
98
95
  rake (13.4.2)
99
96
  rbnacl (7.1.2)
100
97
  ffi (~> 1)
101
- rbs (4.0.2)
98
+ rbs (4.1.3)
102
99
  logger
103
100
  prism (>= 1.6.0)
104
101
  tsort
@@ -134,8 +131,8 @@ GEM
134
131
  diff-lcs (>= 1.2.0, < 2.0)
135
132
  rspec-support (~> 3.13.0)
136
133
  rspec-support (3.13.7)
137
- rubocop (1.88.0)
138
- json (~> 2.3)
134
+ rubocop (1.90.0)
135
+ json (>= 2.3)
139
136
  language_server-protocol (~> 3.17.0.2)
140
137
  lint_roller (~> 1.1.0)
141
138
  parallel (>= 1.10)
@@ -145,18 +142,18 @@ GEM
145
142
  rubocop-ast (>= 1.49.0, < 2.0)
146
143
  ruby-progressbar (~> 1.7)
147
144
  unicode-display_width (>= 2.4.0, < 4.0)
148
- rubocop-ast (1.49.1)
145
+ rubocop-ast (1.50.0)
149
146
  parser (>= 3.3.7.2)
150
147
  prism (~> 1.7)
151
- rubocop-performance (1.26.1)
148
+ rubocop-performance (1.27.0)
152
149
  lint_roller (~> 1.1)
153
- rubocop (>= 1.75.0, < 2.0)
150
+ rubocop (>= 1.89.0, < 2.0)
154
151
  rubocop-ast (>= 1.47.1, < 2.0)
155
152
  rubocop-thread_safety (0.7.3)
156
153
  lint_roller (~> 1.1)
157
154
  rubocop (~> 1.72, >= 1.72.1)
158
155
  rubocop-ast (>= 1.44.0, < 2.0)
159
- ruby-lsp (0.26.9)
156
+ ruby-lsp (0.26.11)
160
157
  language_server-protocol (~> 3.17.0)
161
158
  prism (>= 1.2, < 2.0)
162
159
  rbs (>= 3, < 5)
@@ -185,7 +182,7 @@ GEM
185
182
  unicode-emoji (~> 4.1)
186
183
  unicode-emoji (4.2.0)
187
184
  uri-valkey (1.4.0)
188
- yard (0.9.44)
185
+ yard (0.9.45)
189
186
  zeitwerk (2.8.2)
190
187
 
191
188
  PLATFORMS
@@ -194,7 +191,7 @@ PLATFORMS
194
191
 
195
192
  DEPENDENCIES
196
193
  benchmark (~> 0.4)
197
- concurrent-ruby (~> 1.3.7)
194
+ concurrent-ruby (~> 1.3.8)
198
195
  debug
199
196
  dry-configurable (>= 1.3, < 1.5)
200
197
  familia!
@@ -204,7 +201,7 @@ DEPENDENCIES
204
201
  rbnacl (~> 7.1, >= 7.1.1)
205
202
  redcarpet
206
203
  reek
207
- rubocop (~> 1.88.0)
204
+ rubocop (~> 1.90.0)
208
205
  rubocop-performance
209
206
  rubocop-thread_safety
210
207
  ruby-lsp
data/README.md CHANGED
@@ -377,13 +377,24 @@ end
377
377
  ```ruby
378
378
  require 'connection_pool'
379
379
 
380
+ POOLS = {}
381
+ POOLS_MUTEX = Mutex.new
382
+
380
383
  Familia.connection_provider = lambda do |uri|
381
- ConnectionPool.new(size: 10, timeout: 5) do
382
- Redis.new(url: uri)
383
- end.with { |conn| yield conn if block_given?; conn }
384
+ POOLS_MUTEX.synchronize do
385
+ POOLS[uri] ||= ConnectionPool::Wrapper.new(size: 10, timeout: 5) do
386
+ Redis.new(url: uri)
387
+ end
388
+ end
384
389
  end
385
390
  ```
386
391
 
392
+ Build each pool once, outside the lambda, and return a `ConnectionPool::Wrapper`
393
+ — it checks a connection out for the duration of each command and checks it back
394
+ in afterwards. Returning `pool.with { |conn| conn }` instead hands back a
395
+ connection the pool already considers free, so concurrent callers share it. See
396
+ [the provider contract](docs/reference/api-technical.md#provider-contract).
397
+
387
398
  ### Encryption Setup
388
399
 
389
400
  ```ruby