familia 2.11.2 → 2.12.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 (174) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +14 -8
  3. data/.github/workflows/claude.yml +9 -25
  4. data/.github/workflows/code-smells.yml +1 -1
  5. data/.github/workflows/yardoc.yml +1 -1
  6. data/.gitignore +8 -2
  7. data/.talismanrc +13 -0
  8. data/AGENTS.md +7 -0
  9. data/CHANGELOG.rst +91 -0
  10. data/Gemfile +2 -2
  11. data/Gemfile.lock +12 -15
  12. data/README.md +14 -3
  13. data/changelog.d/README.md +46 -54
  14. data/changelog.d/fragments/20260725_202500_delano_282_unsaved_index_guard.md +7 -0
  15. data/changelog.d/fragments/20260725_213000_delano_282_index_scope_tracker.md +18 -0
  16. data/changelog.d/fragments/20260728_120000_delano_365_stale_tracker_prune.rst +30 -0
  17. data/docs/adr/0001-record-architecture-decisions.md +20 -0
  18. data/docs/adr/0002-watch-for-private-keys-lua-for-shared-keys.md +62 -0
  19. data/docs/adr/README.md +14 -0
  20. data/docs/guides/datatype-collections.md +101 -1
  21. data/docs/guides/encryption.md +42 -7
  22. data/docs/guides/feature-encrypted-fields.md +2 -0
  23. data/docs/guides/feature-housekeeping.md +50 -1
  24. data/docs/guides/feature-relationships-indexing.md +241 -3
  25. data/docs/guides/feature-relationships-participation.md +14 -0
  26. data/docs/guides/index.md +10 -7
  27. data/docs/investigation/memory-audit.md +5 -0
  28. data/docs/migrating/v2.10.md +14 -0
  29. data/docs/overview.md +120 -37
  30. data/docs/reference/api-technical.md +74 -20
  31. data/docs/schema-validation.md +199 -0
  32. data/docs/security/2026-07-19-audit.md +96 -0
  33. data/docs/security/2026-07-30-audit.md +33 -0
  34. data/docs/transaction_safety.md +215 -0
  35. data/examples/encryption_upgrade_proof/README.md +8 -5
  36. data/examples/encryption_upgrade_proof/gemfiles/Gemfile.dev-no-libsodium +1 -0
  37. data/examples/encryption_upgrade_proof/gemfiles/Gemfile.dev-with-libsodium +1 -0
  38. data/examples/encryption_upgrade_proof/gemfiles/Gemfile.production-today +1 -0
  39. data/examples/encryption_upgrade_proof/phase2_libsodium_enabled.rb +22 -7
  40. data/examples/encryption_upgrade_proof/phase3_rollback_hazard.rb +1 -1
  41. data/familia.gemspec +0 -2
  42. data/lib/familia/connection/middleware.rb +4 -2
  43. data/lib/familia/connection/operations.rb +18 -2
  44. data/lib/familia/connection/transaction_core.rb +1 -1
  45. data/lib/familia/connection.rb +49 -3
  46. data/lib/familia/data_type/collection_base.rb +55 -7
  47. data/lib/familia/data_type/types/hashkey.rb +150 -0
  48. data/lib/familia/data_type/types/listkey.rb +89 -18
  49. data/lib/familia/data_type/types/lock.rb +34 -6
  50. data/lib/familia/data_type/types/sorted_set.rb +87 -22
  51. data/lib/familia/data_type.rb +117 -8
  52. data/lib/familia/encryption/manager.rb +128 -73
  53. data/lib/familia/encryption/provider.rb +7 -0
  54. data/lib/familia/encryption/providers/aes_gcm_provider.rb +3 -2
  55. data/lib/familia/encryption/providers/blake2b_personalization.rb +101 -0
  56. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +24 -13
  57. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +21 -12
  58. data/lib/familia/errors.rb +38 -1
  59. data/lib/familia/features/encrypted_fields/concealed_string.rb +14 -15
  60. data/lib/familia/features/encrypted_fields.rb +5 -3
  61. data/lib/familia/features/housekeeping/enforce_collection_caps.rb +94 -0
  62. data/lib/familia/features/housekeeping.rb +20 -9
  63. data/lib/familia/features/relationships/collection_operations.rb +87 -6
  64. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +49 -8
  65. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +222 -33
  66. data/lib/familia/features/relationships/indexing.rb +483 -15
  67. data/lib/familia/features/relationships/participation/target_methods.rb +194 -21
  68. data/lib/familia/features/relationships/participation.rb +17 -5
  69. data/lib/familia/features/relationships/participation_relationship.rb +1 -0
  70. data/lib/familia/features/relationships/score_encoding.rb +109 -28
  71. data/lib/familia/features/relationships.rb +22 -40
  72. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +9 -3
  73. data/lib/familia/features/transient_fields.rb +1 -0
  74. data/lib/familia/field_type.rb +206 -11
  75. data/lib/familia/horreum/atomic_write.rb +17 -0
  76. data/lib/familia/horreum/database_commands.rb +21 -3
  77. data/lib/familia/horreum/definition.rb +0 -111
  78. data/lib/familia/horreum/management/repair.rb +0 -39
  79. data/lib/familia/horreum/persistence.rb +907 -117
  80. data/lib/familia/multi_result.rb +115 -21
  81. data/lib/familia/settings.rb +47 -14
  82. data/lib/familia/version.rb +1 -1
  83. data/lib/middleware/database_logger.rb +54 -7
  84. data/try/bug_fixes/overview_permission_example_try.rb +62 -0
  85. data/try/bug_fixes/partial_write_index_maintenance_try.rb +492 -0
  86. data/try/bug_fixes/permission_query_try.rb +151 -0
  87. data/try/bug_fixes/relationships_rdoc_example_try.rb +83 -0
  88. data/try/bug_fixes/stale_unique_index_try.rb +124 -0
  89. data/try/edge_cases/fast_writer_transaction_guard_try.rb +2 -4
  90. data/try/features/atomic_write_coverage_try.rb +2 -4
  91. data/try/features/dirty_tracking_try.rb +2 -4
  92. data/try/features/dirty_write_new_object_try.rb +22 -1
  93. data/try/features/dirty_write_warnings_try.rb +2 -1
  94. data/try/features/encrypted_fields/aad_nil_fields_try.rb +4 -6
  95. data/try/features/encrypted_fields/aad_protection_try.rb +4 -6
  96. data/try/features/encrypted_fields/aad_roundtrip_try.rb +4 -6
  97. data/try/features/encrypted_fields/aad_transient_fix_try.rb +4 -6
  98. data/try/features/encrypted_fields/aad_transient_proof_try.rb +4 -6
  99. data/try/features/encrypted_fields/concealed_string_core_try.rb +8 -6
  100. data/try/features/encrypted_fields/context_isolation_try.rb +2 -4
  101. data/try/features/encrypted_fields/encrypted_data_try.rb +2 -4
  102. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +42 -1
  103. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +17 -0
  104. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +2 -4
  105. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +6 -0
  106. data/try/features/encrypted_fields/envelope_version_branching_try.rb +4 -6
  107. data/try/features/encrypted_fields/envelope_version_try.rb +4 -6
  108. data/try/features/encrypted_fields/error_conditions_try.rb +2 -4
  109. data/try/features/encrypted_fields/fast_writer_try.rb +4 -6
  110. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +2 -4
  111. data/try/features/encrypted_fields/fresh_key_try.rb +4 -2
  112. data/try/features/encrypted_fields/key_material_try.rb +4 -6
  113. data/try/features/encrypted_fields/key_rotation_try.rb +6 -7
  114. data/try/features/encrypted_fields/memory_security_try.rb +5 -5
  115. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +2 -4
  116. data/try/features/encrypted_fields/per_field_algorithm_try.rb +5 -2
  117. data/try/features/encrypted_fields/re_encrypt_fields_try.rb +11 -19
  118. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +5 -5
  119. data/try/features/encrypted_fields/thread_safety_try.rb +2 -4
  120. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +5 -5
  121. data/try/features/encryption/aes_gcm_salt_rotation_try.rb +24 -5
  122. data/try/features/encryption/algorithm_upgrade_try.rb +7 -7
  123. data/try/features/encryption/config_persistence_try.rb +42 -5
  124. data/try/features/encryption/core_try.rb +4 -1
  125. data/try/features/encryption/encoding_phase1_try.rb +4 -1
  126. data/try/features/encryption/encoding_phase2_try.rb +4 -1
  127. data/try/features/encryption/instance_variable_scope_try.rb +5 -4
  128. data/try/features/encryption/module_loading_try.rb +7 -5
  129. data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +51 -3
  130. data/try/features/encryption/request_cache_try.rb +4 -7
  131. data/try/features/encryption/roundtrip_validation_try.rb +3 -0
  132. data/try/features/encryption/secure_memory_handling_try.rb +76 -5
  133. data/try/features/encryption/xchacha20_personalization_rotation_try.rb +230 -0
  134. data/try/features/housekeeping/enforce_collection_caps_try.rb +136 -0
  135. data/try/features/housekeeping/housekeeping_try.rb +2 -2
  136. data/try/features/instance_registry_try.rb +6 -14
  137. data/try/features/real_feature_integration_try.rb +8 -0
  138. data/try/features/relationships/class_level_multi_index_try.rb +30 -0
  139. data/try/features/relationships/indexing_commands_verification_try.rb +19 -0
  140. data/try/features/relationships/relationships_edge_cases_try.rb +3 -5
  141. data/try/features/relationships/score_encoding_permissions_try.rb +245 -0
  142. data/try/features/relationships/unique_index_cas_try.rb +546 -0
  143. data/try/features/transient_fields/refresh_reset_try.rb +4 -1
  144. data/try/features/transient_fields/single_use_redacted_string_try.rb +34 -0
  145. data/try/integration/connection/isolated_dbclient_try.rb +34 -22
  146. data/try/integration/connection/middleware_reconnect_try.rb +3 -3
  147. data/try/integration/connection/pools_try.rb +22 -13
  148. data/try/integration/persistence_operations_try.rb +2 -4
  149. data/try/integration/save_methods_consistency_try.rb +52 -6
  150. data/try/investigation/pipeline_routing/CONCLUSION.md +149 -0
  151. data/try/investigation/pipeline_routing/FINDINGS.md +168 -0
  152. data/try/{features/transient_fields → support/debugging}/simple_refresh_test.rb +2 -2
  153. data/try/support/encryption_config_helper_try.rb +114 -0
  154. data/try/support/helpers/encryption_config.rb +169 -0
  155. data/try/support/helpers/test_helpers.rb +47 -0
  156. data/try/support/prototypes/pooling/docs/README_advanced_usage.md +636 -0
  157. data/try/support/prototypes/pooling/docs/README_stress_testing.md +200 -0
  158. data/try/thread_safety/encryption_manager_cache_race_try.rb +2 -4
  159. data/try/unit/core/suite_hygiene_try.rb +32 -0
  160. data/try/unit/core/tools_try.rb +4 -2
  161. data/try/unit/data_types/enumerable_consistency/large_scale_consistency_try.rb +14 -3
  162. data/try/unit/data_types/lock_try.rb +44 -3
  163. data/try/unit/data_types/max_length_try.rb +607 -0
  164. data/try/unit/horreum/automatic_index_validation_try.rb +89 -2
  165. data/try/unit/horreum/commands_try.rb +85 -0
  166. data/try/unit/horreum/destroy_index_cleanup_try.rb +714 -22
  167. data/try/unit/horreum/multi_field_update_try.rb +154 -1
  168. data/try/unit/horreum/serialization_try.rb +2 -2
  169. data/try/unit/horreum/unique_index_edge_cases_try.rb +46 -7
  170. data/try/unit/horreum/unique_index_guard_validation_try.rb +2 -0
  171. data/try/unit/middleware/database_logger_methods_try.rb +39 -0
  172. data/try/unit/multi_result_try.rb +298 -0
  173. data/try/unit/thread_safety_monitor_try.rb +20 -12
  174. metadata +30 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7b0eed1c3632c68c8c4c91617830f0b15a10907da5c7af0bbc14b9cf67a5336
4
- data.tar.gz: 4a123b805e826b8ede218675c16781f428fab6c3194968c5a3840323b949b28e
3
+ metadata.gz: d860c29966392e82cf54d842d457353d6a7268c3bf5378e25af9cb850f59d37c
4
+ data.tar.gz: 87fc36560a6cd68ffb614ef6b3ab11625300ebed8433508739f1ab4633f5b9ff
5
5
  SHA512:
6
- metadata.gz: 6c245370d354eaa0d8af0b0994cd44ea10f8dec3b260c9750d3cceb66563c51ee8f4390062429c8aced6cd578d919c7a806a0eef2e5be0f9d90bbc00dcf87b06
7
- data.tar.gz: a824edcd1af5d3535168600d90a3e189b0203c884fe17c7382cafb22ebc6c19a36c68011a468e9dd8b16fed386b902eac16709625004f73b7f4085b0ae0a601e
6
+ metadata.gz: 15b5c9028503566db28d179052982d3a38617463753ea360f22ea49eeb86d3fc896352f18c73a703fe62d3fd5a6cdcf65f254dab284a7671f3b9a0b9db413acb
7
+ data.tar.gz: 633e8ae2bab11aaf362791f23f68ea7f0287e1388c38850200809c5b43ed79ae59039492258927023ea91821d39243a18c9eaa5a63d8c068fb011edc99120468
@@ -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@be7b93b1907a4abad570368f3c74b6fe3807510b # v1.0.183
52
52
  with:
53
53
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
54
54
 
@@ -56,13 +56,23 @@ 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 comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"
61
66
 
62
67
  # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
63
68
  use_sticky_comment: true
64
69
 
65
- direct_prompt: |
70
+ # v1 renamed direct_prompt -> prompt. The REPO/PR NUMBER header is the
71
+ # format the migration guide calls for so Claude reviews the right PR.
72
+ prompt: |
73
+ REPO: ${{ github.repository }}
74
+ PR NUMBER: ${{ github.event.pull_request.number }}
75
+
66
76
  Please review this pull request and provide feedback on:
67
77
  - Code quality and best practices
68
78
  - Potential bugs or issues
@@ -73,7 +83,3 @@ jobs:
73
83
  Use the repository's AGENTS.md for guidance on style and conventions. Be constructive and helpful in your feedback.
74
84
 
75
85
  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@be7b93b1907a4abad570368f3c74b6fe3807510b # v1.0.183
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
@@ -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
@@ -24,8 +24,14 @@ vendor
24
24
  *.gem
25
25
  public/
26
26
 
27
- # Ignore WIP or temp dev files with uppercase names
28
- [A-Z]*.md
27
+ # Ignore WIP or temp dev files with uppercase names, at the repo root only.
28
+ # Anchored because core.ignoreCase is true on macOS/Windows checkouts: git
29
+ # casefolds the [A-Z] class, so an unanchored pattern silently swallowed every
30
+ # new lowercase .md anywhere in the tree (docs/*.md included).
31
+ /[A-Z]*.md
32
+
33
+ # Unredacted security notes stay local; publish redacted summaries only
34
+ *.private.md
29
35
 
30
36
  # Exclusions
31
37
  !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
@@ -21,6 +21,13 @@ Run with `--agent` for token-efficient output (`--agent-focus summary|first-fail
21
21
  See `bundle exec try --help` for the full CLI, framework integration (`--rspec`,
22
22
  `--minitest`), and debugging flags.
23
23
 
24
+ The whole suite runs in one process, so anything a file sets globally outlives
25
+ it. For encryption keys use the scoped helpers rather than assigning
26
+ `Familia.config.encryption_keys` directly: `set_test_encryption_keys(keys,
27
+ current_version:)` in setup with `clear_test_encryption_keys` in teardown, or
28
+ `with_test_encryption_keys(keys, current_version:) { ... }` for a single
29
+ testcase. See @try/support/helpers/encryption_config.rb.
30
+
24
31
  ### Changelog
25
32
 
26
33
  Add a changelog fragment (RST) with each user-facing change. See @changelog.d/README.md
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,97 @@ 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 server-side compare-and-set/compare-and-delete on single hash fields. Raises ``Familia::OperationModeError`` in pipelines/transactions.
19
+ - Added generated ``claim_unique_<index>!`` and ``release_unique_<index>!`` instance methods with automatic partial claim rollback on unique index save collisions. #353
20
+ - Added tryout test helpers (``set_test_encryption_keys``, ``clear_test_encryption_keys``, ``with_test_encryption_keys``) to safely manage encryption key configurations during test runs. #363
21
+ - Added ``Familia::MultiResult#aborted?`` to distinguish WATCH aborts from individual command errors.
22
+ - Added ``Familia::MultiResult#inspect`` for log-safe, compact transaction outcome summaries.
23
+ - Added ``max_length:`` option to ``SortedSet`` and ``ListKey`` to automatically cap collection sizes at write time (retaining newest N elements). #351
24
+ - Added ``DataType#max_length`` to query configured collection caps, with definition-time validation of the parameter. #351
25
+ - Added ``SortedSet#enforce_max_length!`` and ``ListKey#enforce_max_length!`` to trim existing collections. #351
26
+ - Added ``max_length:`` option to ``participates_in`` and ``class_participates_in``. #351
27
+ - Added ``Familia::Features::Housekeeping::EnforceCollectionCaps`` chore class to support bulk cap enforcement. #351
28
+ - Added ``encryption_personalization_history`` setting to support key personalization rotation for XChaCha20-Poly1305 providers. #333
29
+ - Added ``limit:``, ``offset:``, and ``each_<collection>_with_permission`` to stream permission-filtered collection members via ``ZSCAN`` with O(1) memory. #309
30
+
31
+ Changed
32
+ -------
33
+
34
+ - Unique index updates (``add_to_class_<index>``, ``update_in_class_<index>``) now raise ``Familia::OperationModeError`` inside transactions unless the written value was previously claimed. #353
35
+ - Retained ``guard_unique_indexes!`` for fast-fail read checks before acquiring index claims. #353
36
+ - Calls to ``remove_from_class_*`` and class-level ``destroy!`` no longer evict index entries that point to different identifiers. #353
37
+ - ``Familia::MultiResult#results`` now always returns an ``Array`` (empty instead of ``nil`` on transaction abort) to prevent upstream crashes.
38
+ - ``Familia::MultiResult#to_h`` now includes an ``:aborted`` boolean key.
39
+ - ``Familia::MultiResult`` instances are now read-only, freezing both ``#results`` and ``#errors``.
40
+ - ``SortedSet#increment`` now runs the ``warn_if_dirty!`` guard, warning or raising when called on an unsaved parent. #351
41
+ - ``Manager#decrypt`` now handles XChaCha20 provider personalization candidates correctly during decryption walks, matching AES-GCM salt history behavior. #333
42
+ - The request-scoped key cache key now includes a personalization segment to prevent collisions across different rotation candidates. #333
43
+ - ``multi_field_fast_write`` now raises ``Familia::IndexedFieldFastWriteError`` if any written field backs a class-level index. #308
44
+ - Fast writer ``field!`` on a class-indexed field now raises ``Familia::IndexedFieldFastWriteError`` inside transactions/pipelines, and raises ``Familia::PersistenceError`` on unsaved records. #308
45
+ - With a blank ``encryption_hkdf_salt``, ``Manager#encrypt`` now raises ``EncryptionError`` when the request-cache key is built rather than at key derivation. The raise happens earlier and now also fires on what would previously have been a warm-cache hit; correctly configured deployments see no change. #380
46
+
47
+ Removed
48
+ -------
49
+
50
+ - Removed the unused ``ScoreEncoding.category_score_range`` method.
51
+ - Removed unused ``csv`` and ``stringio`` runtime dependencies from the gemspec. #354
52
+ - Removed dead private helper methods ``Horreum::define_attr_accessor_methods``, ``remove_stale_collection_member``, and ``define_fast_writer_method``. #347, #308
53
+
54
+ Fixed
55
+ -----
56
+
57
+ - Fixed ``decrby`` and ``decr`` (and aliases) to correctly use ``HINCRBY`` with a negated amount and added client-side integer validation.
58
+ - Fixed ``encryption_info`` to reference the correct provider APIs and accurately expose ``key_size``.
59
+ - Fixed a TOCTOU race in ``unique_index`` by enforcing server-side CAS claims using Lua before opening transactions, raising ``Familia::RecordExistsError`` on collision. #353
60
+ - Fixed ``remove_from_class_*`` and ``update_in_class_*`` to use ownership-checked deletes, preventing accidental deletion of other records' valid entries. #353
61
+ - Fixed process-global encryption configuration leaks across tryouts by using scoped helpers and restoring baselines. #363
62
+ - Fixed test isolation in ``real_feature_integration_try.rb`` and ``module_loading_try.rb`` by explicitly managing encryption key rings. #363
63
+ - Fixed fiber-local key cache teardowns to clear the correct request cache key. #363
64
+ - Fixed claim leaks on unpersisted records in ``update_in_<scope>_<index_name>`` by running the persisted-record guard before checking claims. #370
65
+ - Fixed ``ConcealedString`` by removing a redundant and non-functional GC finalizer. #359
66
+ - Fixed ``Familia::MultiResult`` to prevent ``NoMethodError`` crashes on WATCH-aborted transactions. #355
67
+ - Fixed ``SecureXChaCha20Poly1305Provider#derive_key`` to avoid mutating the receiver context string and handle non-String contexts correctly. #356
68
+ - Fixed ``SortedSet#increment`` inside transactions/pipelines to safely return the future instead of calling ``to_f`` prematurely. #351
69
+ - Fixed long-ignored ``:maxlength`` spelling to explicitly warn at definition time, prompting rename to ``max_length:``. #351
70
+ - Fixed ``Lock#acquire`` with a positive TTL to run as a single atomic ``SET NX EX`` command. #347
71
+ - Fixed ``DatabaseLogger.sample_rate`` assignment to validate and clamp input values, preventing crashes on the hot path. #347
72
+ - Fixed ``SingleUseRedactedString`` to be loaded automatically by ``require 'familia'``. #347
73
+ - Fixed memory overhead in ``<collection>_with_permission`` queries by batching and paging internally via ``ZRANGEBYSCORE ... LIMIT``. #309
74
+ - Fixed performance by removing redundant post-save ``update_all_indexes`` calls inside relationships save. #307
75
+ - Fixed partial write paths (``commit_fields``, ``save_fields``, ``multi_field_update``) to safely guard and claim unique indexes before executing writes. #308
76
+ - Fixed fast writers (``field!``) on indexed fields to claim and update the index atomically before execution. #308
77
+
78
+ Security
79
+ --------
80
+
81
+ - Enforced field-type semantics in ``multi_field_update`` and ``multi_field_fast_write`` to prevent writing plaintext to encrypted fields or persisting transient fields.
82
+ - Enforced loud failures for invalid permission symbol lookups in ``Relationships::ScoreEncoding`` to prevent silent misconfigurations.
83
+ - Pinned GitHub Workflows holding ``CLAUDE_CODE_OAUTH_TOKEN`` to immutable release commit SHAs.
84
+ - The encrypt-path request-cache key now resolves the HKDF salt through the fail-closed ``current_hkdf_salt`` accessor instead of the permissive candidate list's head (``hkdf_salts.first``). Previously, with a blank ``encryption_hkdf_salt``, a decrypt inside ``with_request_cache`` could warm an entry keyed on a historical salt that a subsequent encrypt would silently reuse -- because the cache lookup precedes derivation, the blank-salt refusal in the provider never fired. Encrypts now refuse a blank salt even against a warm cache. #380
85
+
86
+ Documentation
87
+ -------------
88
+
89
+ - Replaced fabricated/incorrect examples in ``docs/overview.md`` and RDoc comments with accurate, tested examples for permission management and relationships.
90
+ - Documented that ``<collection>_with_permission`` requires atomic flags and rejects role symbols.
91
+ - Corrected the error message for ``guard_allowed_fields!``.
92
+ - Documented ``PERMISSION_CATEGORIES`` and exclusive permission tiers.
93
+ - Fixed incorrect connection provider examples in docs.
94
+ - Updated encryption guides to cover personalization history and rotation.
95
+
96
+ AI Assistance
97
+ -------------
98
+
99
+ - Core fixes, tryout coverage (120+ test cases), and documentation updates implemented with AI assistance.
100
+
10
101
  .. _changelog-2.11.2:
11
102
 
12
103
  2.11.2 — 2026-07-05
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.88.2', 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.12.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,17 +63,17 @@ 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.20.0)
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.4)
80
77
  bigdecimal (>= 3.0)
81
78
  ostruct (>= 0.2)
82
79
  ostruct (0.6.3)
@@ -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.0.3)
102
99
  logger
103
100
  prism (>= 1.6.0)
104
101
  tsort
@@ -134,7 +131,7 @@ 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)
134
+ rubocop (1.88.2)
138
135
  json (~> 2.3)
139
136
  language_server-protocol (~> 3.17.0.2)
140
137
  lint_roller (~> 1.1.0)
@@ -145,7 +142,7 @@ 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
148
  rubocop-performance (1.26.1)
@@ -156,7 +153,7 @@ GEM
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.10)
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.88.2)
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
@@ -4,64 +4,56 @@ This directory contains changelog fragments managed by [Scriv](https://scriv.rea
4
4
 
5
5
  ## Our Approach
6
6
 
7
- Changelogs are for humans and agents, not just machines. We follow the core principles of [Keep a Changelog](https://keepachangelog.com) and semvar to ensure our release notes are clear, consistent, and useful.
8
-
9
- To achieve this, we use a fragment-based workflow with `scriv`. Instead of a single, large `CHANGELOG.md` file that can cause merge conflicts, each developer includes a small changelog fragment with their pull request. At release time, these fragments are collected and aggregated into the main changelog.
10
-
11
- This approach provides several benefits:
12
- - **Reduces Merge Conflicts:** Developers can work in parallel without conflicting over a central changelog file.
13
- - **Improves Developer Experience:** Creating a small, focused fragment is a simple and repeatable task during development.
14
- - **Ensures Consistency:** Automation helps maintain a consistent structure for all changelog entries.
15
- - **AI Transparency:** An opportunity to be specific and detailed about the assistance provided.
16
- - **Builds Trust:** A clear and well-maintained changelog communicates respect for our users and collaborators.
17
-
18
- ## Relevant paths
19
-
20
- * `changelog.d/` - (e.g. changelog.d/YYYYMMDD_HHmmss_username_branch.rst)
21
- * `docs/migrating/` - (e.g. docs/migrating/v2.0.0-pre.md)
22
- * `CHANGELOG.rst` - The full changelog for all releases, in reverse chronological order. Careful: LARGE DOCUMENT. Limit reading to the first 50 lines.
23
-
24
- * `changelog.d/scriv.ini` - Scriv tool settings
25
-
26
- ## How to Add a Changelog Entry
27
-
28
- 1. **Create a New Fragment:**
29
-
30
- ```bash
31
- # This will create a new file in the `changelog.d/` directory.
32
- scriv create
33
- ```
34
-
35
- 2. **Edit the Fragment File:**
36
- Open the newly created file and add your entry under the relevant category. See the guidelines below for writing good CHANGELOG entries.
37
-
38
- 3. **Add or Update Migrating Guide:** (optional)
39
- Include technical details to help developers update to the new version. Start with a specific introduction, e.g. "This version introduces significant improvements to Familia's feature system, making it easier to organize and use features across complex projects.". Including code snippets and multi-line content that is too detailed for the CHANGELOG.
40
-
41
- Use the content of an existing `docs/migrating/vMajor.Minor.Patch*.md file as a reference.
42
-
43
- Compare the headers of your draft content with the headers of the previous migration guide to make sure it does not repeat or overlap.
44
-
45
- 4. **Commit with Your Code:**
46
- ```bash
47
- git add changelog.d/YYYYMMDD_HHmmss_username_branch.rst [docs/migrating/v2.0.0-pre.md]
48
- git commit
49
- ```
7
+ Changelogs are for humans and agents, not just machines. We follow [Keep a Changelog](https://keepachangelog.com) and semver to ensure clear, consistent, and useful release notes.
8
+
9
+ We use a fragment-based workflow with `scriv`. Each developer includes a small, focused changelog fragment with their pull request. At release time, these are compiled into the main changelog.
10
+
11
+ Benefits:
12
+ - **No Merge Conflicts:** Developers work in parallel without conflicting over a single file.
13
+ - **Improved DX:** Creating a small fragment is simple and repeatable.
14
+ - **AI Transparency:** Briefly notes AI involvement without cluttering technical sections.
15
+ - **Consistency:** Automation maintains a unified structure.
16
+
17
+ ### Relevant Paths
18
+
19
+ * `changelog.d/` - Fragment directory (e.g. `changelog.d/YYYYMMDD_HHmmss_username_branch.rst`)
20
+ * `docs/migrating/` - Migration guides (e.g. `docs/migrating/v2.0.0-pre.md`)
21
+ * `CHANGELOG.rst` - The full changelog (reverse chronological, large file; read only the top 100 lines default)
22
+ * `changelog.d/scriv.ini` - Scriv configuration
23
+
24
+ ## Add a Changelog Entry
25
+
26
+ 1. **Create a New Fragment:**
27
+ ```bash
28
+ scriv create
29
+ ```
30
+ 2. **Edit the Fragment:**
31
+ Open the new `.rst` file and write your entry under the relevant category using the guidelines below.
32
+ 3. **Add or Update Migrating Guide (Optional):**
33
+ If the change requires developer action to upgrade, add or update a guide in `docs/migrating/`. Use existing guides as a reference and ensure headers do not repeat.
34
+ 4. **Commit with Your Code:**
35
+ ```bash
36
+ git add changelog.d/YYYYMMDD_HHmmss_username_branch.rst [docs/migrating/v2.0.0-pre.md]
37
+ git commit
38
+ ```
50
39
 
51
40
  ## Fragment Guidelines
52
41
 
53
42
  - **One Fragment Per Change:** Keep each fragment focused on a single feature, fix, or improvement.
54
- - **Documenting AI Assistance:** If a change involved significant AI assistance, place it in its own fragment. This ensures the `### AI Assistance` section clearly corresponds to the single change described in that fragment.
55
- - **Write for a Human Audience:** Describe the *impact* of the change, not just the implementation details.
56
- - **Good:** "Improved the performance and stability of Database connections under high load."
57
- - **Bad:** "Refactored the `DatabaseManager`."
58
- - **Be Specific:** Avoid generic messages like "fixed a bug." Clearly state what was fixed.
59
- - **Include Context:** Reference issue or pull request numbers to provide a link to the discussion and implementation details. `scriv` will automatically create links for them.
60
- - **Example:** `- Fixed a bug where users could not reset their passwords. PR #123`
43
+ - **Reference Context:** Include issue or PR numbers. Scriv will automatically link them. (e.g., `PR #123`).
44
+
45
+ ## Content Guidelines
46
+
47
+ - **Target the Consumer:** Focus exclusively on external, breaking, or actionable behavior. Omit internal implementation steps, development metadata, and agent logs.
48
+ - **Impact-Driven Filtering:** Focus strictly on technical facts (method/class signatures, parameters, exceptions, issues resolved) and eliminate explanatory "how" or "why" narratives.
49
+ - **Good:** "Added ``Familia::HashKey#claim_field`` and ``#release_field`` for single-hash server-side CAS/CAD operations. Raises ``Familia::OperationModeError`` in pipelines/transactions."
50
+ - **Bad:** "We found a race condition during an audit, so we added a nice compare-and-set wrapper called `#claim_field` to allow callers to safely claim a field in single hash fields."
51
+ - **Process Log Exclusion:** Remove all development metadata (such as tool-specific implementation notes, agent logs, and audit timelines) that do not change library APIs or behavior.
52
+ - **Maintain Consistency:** Match the terse style, semantic classification, and spacing of previous changelog versions.
61
53
 
62
- ### Categories
54
+ ## Categories
63
55
 
64
- Use these categories:
56
+ Use these standard headers in your fragment:
65
57
 
66
58
  - **Added**: New features or capabilities.
67
59
  - **Changed**: Changes to existing functionality.
@@ -70,8 +62,8 @@ Use these categories:
70
62
  - **Fixed**: Bug fixes.
71
63
  - **Security**: Security-related improvements.
72
64
  - **Documentation**: Documentation improvements.
73
- - **AI Assistance**: Significant AI assistance in the change, including discussion, rubber ducking, formatting, writing documentation, writing tests.
65
+ - **AI Assistance**: Terse, single-sentence acknowledgment of AI assistance for the change. Do not duplicate technical details already listed in other categories.
74
66
 
75
67
  ## Release Process
76
68
 
77
- At release time, scriv will collect all fragments into the main `CHANGELOG.rst` file with th command `scriv collect`. The version is taken automatically from `lib/familia/version.rb`.
69
+ At release, run `scriv collect` to aggregate all fragments into `CHANGELOG.rst`. The version is parsed automatically from `lib/familia/version.rb`.
@@ -0,0 +1,7 @@
1
+ ### Changed
2
+
3
+ - **Index mutations now require a persisted record**: the `add_to_*`/`update_in_*` methods generated by `unique_index`/`multi_index` — both instance-scoped (`within:`) and class-level (`add_to_class_*`/`update_in_class_*`) — raise `Familia::PersistenceError` when called directly on an object that has never been saved. Previously the entry landed in the index pointing at a record that did not exist yet — and if the process never saved, nothing (including `destroy!` cleanup) could find it. The check runs before any write, so a rejected call leaves no partial state, and is skipped inside transactions/pipelines — which is why the save path (auto-indexing runs inside the save MULTI) and index rebuilds are unaffected. Call `save` before indexing. (#282; surfaced by the #278 unsaved-parent write guard)
4
+
5
+ ### AI Assistance
6
+
7
+ - Root-cause analysis of the #278/#282 guard interaction, the fail-fast persisted-record guard, and test updates were developed with AI assistance.
@@ -0,0 +1,18 @@
1
+ ### Added
2
+
3
+ - **Instance-scoped indexes now refresh on save**: once a membership has been registered with `add_to_*`, saving the record keeps its index entry current. Unique indexes route through `update_in_*` with the previous value from dirty tracking, so a changed indexed field retracts its stale entry and the freed value becomes reusable; multi indexes are add-only, matching the existing class-level decision that a value change does not retract prior buckets (`destroy!` still cleans every bucket — see the tracker cardinality note below — so add-only costs no cleanup coverage; call `update_in_*` to retract at change time instead). The refresh runs inside save's MULTI (alongside class-level index maintenance) using a tracker snapshot read just before the transaction opens, so index mutations commit atomically with the object hash. The *initial* `add_to_*` remains manual — save has no scope instance to infer. Note that `atomic_write` does **not** refresh instance-scoped indexes: it calls the shared persistence path from inside a MULTI it already opened, leaving no pre-transaction point at which the tracker snapshot could be read, so index entries silently keep their previous values on that path. Use `save`, or call `update_in_*` explicitly afterwards. (#282)
4
+ - **Instance-scoped unique indexes are validated on save**: changing an indexed field to a value another record already holds in that scope now raises `Familia::RecordExistsError` and leaves the index untouched, matching how class-level `unique_index` has always behaved on save. Previously the uniqueness guard existed only in `add_to_*`, never `update_in_*`, so the save-refresh path could silently evict the other record's entry — and because the evicted record's tracker still claimed the slot, its later `destroy!` would unindex the live record that had taken it. The check runs before the transaction opens, alongside the existing class-level `guard_unique_indexes!`, and only inspects fields that actually changed. (#282)
5
+ - **`dirty_write_warnings:` option on DataType**: overrides the parent class's dirty-write diagnostic mode for a single collection. Added for ORM-internal structures that the persistence path writes deliberately while the parent is dirty — the index scope tracker sets `:off`, since the warning is false there (the scalar write is queued in the same transaction) and `Familia.strict_write_order` would otherwise abort every save that refreshes an index.
6
+
7
+ ### Fixed
8
+
9
+ - **Instance-scoped index cleanup now survives a changed field value**: the reverse index tracker (`_idx_scopes`) is a hash mapping each membership to the field value that was written into the index, instead of a set of memberships alone. Tracker cardinality mirrors index cardinality: unique entries are keyed by `scope_config`/`index_name`/`scope_id` (1:1 within a scope, so HSET-overwrite is exact), while multi entries append the value, giving one entry per bucket the object occupies — a single triple-keyed entry could only name one of several simultaneous buckets, and `destroy!` would orphan the rest. `destroy!` removes the recorded buckets rather than re-reading the current field value, so cleanup still lands correctly when the indexed field changed after `add_to_*` — and when the object being destroyed was constructed identifier-only (`Model.new(id: x).destroy!`), where the current value is nil and cleanup previously short-circuited and orphaned the entry. `update_in_*` now updates the tracker too, so a value change needs no old-value bookkeeping. The generated `remove_from_{scope}_{index}` methods accept an optional field value (defaulting to the current one, so the single-argument call is unchanged) which is how cleanup passes the recorded value. (#282)
10
+ - **Instance-scoped index cleanup with a shared index name**: a tracker entry is now matched on scope class *and* index name. When two scope classes declared the same `index_name`, the first-declared relationship always won, so cleanup built a stub of the wrong class and mutated the wrong key. (#282)
11
+
12
+ ### Changed
13
+
14
+ - **Instance-scoped index writes reject untrackable scopes**: `add_to_*`/`update_in_*` raise `Familia::NoIdentifier` when the scope instance has no identifier, and `ArgumentError` when the scope class's `identifier_field` is not a Symbol or String. Both are checked before the index write, so a rejected call leaves no partial state. The identifier-field restriction exists because `destroy!` must rebuild the scope from its identifier alone inside a MULTI, where reads return futures — a scope that cannot be rebuilt that way would leave an index entry nothing could clean up. (#282)
15
+
16
+ ### AI Assistance
17
+
18
+ - The stored-value tracker design, the scope-config-aware entry lookup, the save-path refresh wiring, and the accompanying tests were developed with AI assistance.
@@ -0,0 +1,30 @@
1
+ Fixed
2
+ -----
3
+
4
+ - A record saved under a reused identifier no longer inherits the previous
5
+ record's index tracker. ``delete!`` removes only the main object hash, so the
6
+ instance-scoped index tracker (``_idx_scopes``) survived it — and the next
7
+ ``save`` under the same identifier replayed the stale entries, silently
8
+ joining the new record to every scope the *previous* record had been added to
9
+ (or raising ``Familia::RecordExistsError`` when the new value collided with
10
+ another record's entry). Save now detects the staleness — tracker entries can
11
+ only outlive the object hash when a previous incarnation was ``delete!``'d or
12
+ expired, since ``add_to_*`` refuses never-saved records — and prunes them
13
+ inside the save transaction: ``remove_from_*`` is replayed with the recorded
14
+ values, clearing the dead incarnation's index entries (previously orphaned
15
+ forever) along with the tracker itself. The new record starts with no
16
+ instance-scoped memberships; ``add_to_*`` remains the explicit opt-in.
17
+ ``save_if_not_exists!`` applies the same reconciliation (replacing its
18
+ previous behavior of re-syncing stale entries onto the resurrected record),
19
+ and the detection costs an EXISTS probe only when the tracker actually has
20
+ entries. Note that ``atomic_write`` performs no staleness detection (it has
21
+ no pre-transaction point to probe from), and once it has recreated the hash a
22
+ later ``save`` can no longer tell the inherited entries are stale — the first
23
+ write to a reused identifier should be a ``save``. (#365)
24
+
25
+ AI Assistance
26
+ -------------
27
+
28
+ - The staleness-detection design (prune-on-stale rather than a ``delete!``
29
+ override or an epoch marker), the save-path wiring, and the accompanying
30
+ tests were developed with AI assistance.