familia 2.9.1 → 2.10.1

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +1 -1
  3. data/.gitignore +1 -1
  4. data/AGENTS.md +198 -0
  5. data/CHANGELOG.rst +218 -0
  6. data/Gemfile.lock +45 -42
  7. data/README.md +2 -2
  8. data/changelog.d/20260605_220911_anthropic_sleepy-allen.rst +35 -0
  9. data/docs/guides/datatype-collections.md +31 -3
  10. data/docs/guides/feature-migrations.md +45 -0
  11. data/docs/guides/feature-relationships-indexing.md +10 -2
  12. data/docs/guides/feature-relationships-methods.md +41 -0
  13. data/docs/guides/feature-relationships-participation.md +43 -2
  14. data/docs/guides/feature-relationships.md +162 -1
  15. data/docs/guides/getting-started.md +87 -0
  16. data/docs/guides/index.md +4 -0
  17. data/docs/migrating/v2.10.md +294 -0
  18. data/examples/encrypted_fields.rb +43 -29
  19. data/examples/relationships.rb +66 -36
  20. data/examples/safe_dump.rb +7 -5
  21. data/familia.gemspec +0 -1
  22. data/lib/familia/connection/operations.rb +194 -0
  23. data/lib/familia/connection/transaction_core.rb +51 -0
  24. data/lib/familia/data_type/collection_base.rb +26 -9
  25. data/lib/familia/data_type/serialization.rb +33 -3
  26. data/lib/familia/data_type.rb +164 -8
  27. data/lib/familia/encryption/encrypted_data.rb +27 -2
  28. data/lib/familia/encryption/manager.rb +17 -2
  29. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -1
  30. data/lib/familia/encryption/request_cache.rb +4 -28
  31. data/lib/familia/encryption.rb +1 -0
  32. data/lib/familia/features/encrypted_fields/concealed_string.rb +12 -0
  33. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +50 -67
  34. data/lib/familia/features/encrypted_fields.rb +14 -0
  35. data/lib/familia/features/relationships/collection_operations.rb +23 -2
  36. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +8 -9
  37. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +23 -6
  38. data/lib/familia/features/relationships/indexing.rb +6 -2
  39. data/lib/familia/features/relationships/participation/target_methods.rb +23 -5
  40. data/lib/familia/features/relationships/participation.rb +6 -1
  41. data/lib/familia/horreum/atomic_write.rb +97 -22
  42. data/lib/familia/horreum/definition.rb +51 -0
  43. data/lib/familia/horreum/dirty_tracking.rb +28 -0
  44. data/lib/familia/horreum/management.rb +117 -3
  45. data/lib/familia/horreum/persistence.rb +71 -42
  46. data/lib/familia/horreum/related_fields.rb +2 -0
  47. data/lib/familia/horreum.rb +1 -0
  48. data/lib/familia/index_descriptor.rb +258 -0
  49. data/lib/familia/instrumentation.rb +22 -0
  50. data/lib/familia/logging.rb +24 -3
  51. data/lib/familia/migration/base.rb +1 -1
  52. data/lib/familia/migration/errors.rb +2 -0
  53. data/lib/familia/migration/model.rb +1 -1
  54. data/lib/familia/migration/pipeline.rb +1 -1
  55. data/lib/familia/migration/rake_tasks.rb +11 -17
  56. data/lib/familia/migration/registry.rb +4 -0
  57. data/lib/familia/migration/runner.rb +2 -0
  58. data/lib/familia/migration/script.rb +2 -0
  59. data/lib/familia/migration.rb +2 -0
  60. data/lib/familia/settings.rb +79 -1
  61. data/lib/familia/utils.rb +15 -0
  62. data/lib/familia/version.rb +1 -1
  63. data/lib/familia.rb +1 -0
  64. data/lib/middleware/database_logger.rb +208 -128
  65. data/try/audit/audit_cross_references_try.rb +6 -6
  66. data/try/audit/audit_unique_indexes_try.rb +3 -2
  67. data/try/audit/repair_all_integration_try.rb +2 -1
  68. data/try/audit/repair_indexes_try.rb +2 -1
  69. data/try/features/atomic_write_watch_try.rb +254 -0
  70. data/try/features/build_block_try.rb +191 -0
  71. data/try/features/create_block_try.rb +58 -0
  72. data/try/features/cross_model_atomic_write_try.rb +402 -0
  73. data/try/features/dirty_write_new_object_try.rb +181 -0
  74. data/try/features/dirty_write_warnings_try.rb +456 -0
  75. data/try/features/encrypted_fields/aad_transient_fix_try.rb +164 -0
  76. data/try/features/encrypted_fields/aad_transient_proof_try.rb +253 -0
  77. data/try/features/encrypted_fields/concealed_string_core_try.rb +6 -4
  78. data/try/features/encrypted_fields/encrypted_data_try.rb +151 -0
  79. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -0
  80. data/try/features/encrypted_fields/envelope_version_branching_try.rb +106 -0
  81. data/try/features/encrypted_fields/envelope_version_try.rb +171 -0
  82. data/try/features/encrypted_fields/key_material_try.rb +205 -0
  83. data/try/features/encryption/request_cache_try.rb +88 -0
  84. data/try/features/relationships/index_introspection_try.rb +304 -0
  85. data/try/features/relationships/multi_index_each_record_try.rb +211 -0
  86. data/try/features/relationships/participation_each_record_try.rb +247 -0
  87. data/try/features/relationships/participation_reverse_methods_try.rb +7 -4
  88. data/try/features/relationships/unique_index_each_record_try.rb +143 -0
  89. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  90. data/try/integration/examples/encrypted_fields_example_try.rb +67 -0
  91. data/try/integration/examples/relationships_example_try.rb +69 -0
  92. data/try/integration/examples/safe_dump_example_try.rb +60 -0
  93. data/try/investigation/cross_model_atomic_poc_try.rb +130 -0
  94. data/try/unit/core/trace_caching_try.rb +58 -0
  95. data/try/unit/data_types/each_record_try.rb +1 -1
  96. data/try/unit/data_types/stringkey_extended_try.rb +1 -1
  97. data/try/unit/horreum/relations_try.rb +5 -0
  98. data/try/unit/middleware/database_logger_capture_toggle_try.rb +278 -0
  99. metadata +30 -3
  100. data/CLAUDE.md +0 -322
  101. data/docs/guides/writing-migrations.md +0 -345
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d24c3b38092f1192f8e250553e8ef4d51b05c1bb00c48dfd7f6520d3a48a9a0e
4
- data.tar.gz: 1dd0a2aa47682736209f116adf378eb4a0eccafffd7c151b02a8ef4573e52551
3
+ metadata.gz: 7903e14486c85385ad0b682c009b1f7d182e0cff24b107aa0264692529bb8dfd
4
+ data.tar.gz: 445219dfcd2df1cf2054b07d90e33902fc21532279730bc274629a2082a02f7f
5
5
  SHA512:
6
- metadata.gz: 10d69edb30370c7dba83b387f53429878fded6bf736e04fbefec4c228baf375806eeefa6c1d44c45c296e5d79b82345d1bc9cfbb0992ee18fc4c204dac250e10
7
- data.tar.gz: 9b52e601ae93d44a6db8b0f995828ae45d55872377a471658a8d63210607c56f70958e372654ac20a13b07344e9b9da0afcdaaa50203f0796326d5d08e93bf6b
6
+ metadata.gz: 635d35b86d7c6a85332517e3b238b8b932d4823b0cfad2f58b7c349a530de44ae02b91930c9ddac7ed538c619cea3a6f8355eeaba20ebf52358ebd45f4547067
7
+ data.tar.gz: d96538d6fbb486fcd92bc329acd52cba07008e40db5df059f338e060cb6dbb4789c8fe0e8e52ee69891662b1b2353eec3832032aa3ccda50848a986d06ea6a4d
@@ -43,7 +43,7 @@ jobs:
43
43
  - Security concerns
44
44
  - Test coverage
45
45
 
46
- Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
46
+ Use the repository's AGENTS.md for guidance on style and conventions. Be constructive and helpful in your feedback.
47
47
 
48
48
  Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
49
49
 
data/.gitignore CHANGED
@@ -30,5 +30,5 @@ public/
30
30
  # Exclusions
31
31
  !README.md
32
32
  !CHANGELOG.md
33
- !CLAUDE.md
33
+ !AGENTS.md
34
34
  !LICENSE.txt
data/AGENTS.md ADDED
@@ -0,0 +1,198 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for AI coding agents working in this repository.
4
+
5
+ ## Development Commands
6
+
7
+ - **Install**: `bundle install`
8
+ - **Docs**: `bundle exec yard`
9
+ - **Lint**: `bundle exec rubocop`
10
+ - **Test**: `bundle exec try` (auto-discovers `*_try.rb` / `*.try.rb`)
11
+
12
+ ### Testing (Tryouts v3)
13
+
14
+ Each file has optional setup, testcases, and optional teardown. A testcase is a
15
+ `##` description line, Ruby code, then one or more expectation comments
16
+ (`#=>`, `#==>`, `#=:>`, `#=!>`, ...). The last expression is the result.
17
+ Instance variables (`@var`) persist across sections; locals do not. Write plain
18
+ realistic code; avoid mocks and test DSL.
19
+
20
+ Run with `--agent` for token-efficient output (`--agent-focus summary|first-failure|critical`).
21
+ See `bundle exec try --help` for the full CLI, framework integration (`--rspec`,
22
+ `--minitest`), and debugging flags.
23
+
24
+ ### Changelog
25
+
26
+ Add a changelog fragment (RST) with each user-facing change. See @changelog.d/README.md
27
+
28
+ ### Known Issues & Quirks
29
+
30
+ - **Reserved field names**: `ttl`, `db`, `valkey`, `redis` cannot be field names — use prefixed alternatives.
31
+ - **Empty identifiers**: Cause a stack overflow in key generation — validate before operations.
32
+ - **Lazy initialization**: Connection chains and field collections initialize lazily without synchronization (generally safe under the GIL, not guaranteed).
33
+
34
+ ### Debugging
35
+
36
+ Ask the user for real-time database command monitoring (commands with timestamps
37
+ and database numbers, live) when debugging multi/exec, pipelining, or
38
+ `logical_database` issues.
39
+
40
+ ## Architecture
41
+
42
+ **Familia** is a Valkey-compatible ORM providing Ruby object storage with
43
+ expiration, safe dumping, and quantization.
44
+
45
+ ### Core Classes
46
+
47
+ - **`Familia::Horreum`** (`lib/familia/horreum.rb`) — base class for Valkey-backed objects (ActiveRecord-like). Field definitions, data type relationships, lifecycle.
48
+ - **`Familia::DataType`** (`lib/familia/data_type.rb`) — base for type wrappers (String, JsonStringKey, List, UnsortedSet, SortedSet, HashKey). Each type in `lib/familia/data_type/types/`.
49
+ - **`Familia::Base`** (`lib/familia/base.rb`) — shared module for both, hosts the feature system.
50
+
51
+ Features (Expiration, SafeDump, Relationships, ...) are modules mixed into
52
+ classes via `Familia::Base`. See `lib/familia/features/`.
53
+
54
+ ### Defining a Model
55
+
56
+ ```ruby
57
+ class User < Familia::Horreum
58
+ field :email # scalar field
59
+ list :sessions # Valkey/Redis list
60
+ set :tags # set
61
+ zset :metrics # sorted set
62
+ hashkey :settings # hash
63
+ end
64
+ ```
65
+
66
+ Identifier strategies:
67
+
68
+ ```ruby
69
+ identifier_field :email # symbol
70
+ identifier ->(user) { "user:#{user.email}" } # proc
71
+ identifier [:type, :email] # array
72
+ ```
73
+
74
+ Connection handling lives in `lib/familia/connection.rb` and `lib/familia/settings.rb`;
75
+ select databases with the `logical_database` class method (URI configuration supported).
76
+
77
+ ### Initialization: do not override `initialize` without `super`
78
+
79
+ Familia's `initialize` sets fields from kwargs, then sets up DataType objects,
80
+ then calls your `init` hook. Overriding `initialize` without `super` breaks
81
+ related-field setup.
82
+
83
+ Apply defaults in the `init` hook with `||=` (never `=`, which would overwrite
84
+ values Horreum already set from kwargs):
85
+
86
+ ```ruby
87
+ class User < Familia::Horreum
88
+ field :objid
89
+ field :email
90
+
91
+ def init
92
+ @objid ||= SecureRandom.uuid
93
+ end
94
+ end
95
+
96
+ User.new(email: 'test@example.com').objid # => generated UUID
97
+ ```
98
+
99
+ Only override `initialize` (with `super`) when you must transform arguments
100
+ before Horreum processes them.
101
+
102
+ ## Serialization
103
+
104
+ Horreum fields are JSON-encoded for storage and JSON-decoded on load, preserving
105
+ Ruby types (Integer, Boolean, String, Float, Hash, Array, nil). `false` and `0`
106
+ are preserved; only `nil` values are omitted from storage.
107
+
108
+ | Context | Serialize | Ruby `"UK"` stored as | Ruby `123` stored as |
109
+ |---|---|---|---|
110
+ | Horreum `field` | `serialize_value` (JSON) | `"\"UK\""` | `"123"` |
111
+ | `StringKey` | `.to_s` (raw) | `"UK"` | `"123"` |
112
+ | `JsonStringKey` | JSON dump | `"\"UK\""` | `"123"` |
113
+ | List/Set/SortedSet/HashKey values | `serialize_value` (JSON) | `"\"UK\""` | `"123"` |
114
+
115
+ `StringKey` uses raw `.to_s` (not JSON) to support `INCR`/`DECR`/`APPEND`; a
116
+ Horreum string field stores `"UK"` as `"\"UK\""` while a `StringKey` stores it as
117
+ `"UK"`. Use `instance.debug_fields` to compare Ruby values vs stored JSON.
118
+
119
+ Database keys are generated as `classname:identifier:fieldname` (aka dbkey).
120
+ DataType instances are frozen after instantiation.
121
+
122
+ ## Write Model: Deferred vs Immediate
123
+
124
+ **Scalar fields** (`field`) use deferred writes: normal setters
125
+ (`user.name = "Alice"`) only touch memory until `save`/`commit_fields`/`batch_update`.
126
+ Fast writers (`user.name! "Alice"`) do an immediate `HSET`.
127
+
128
+ **Collection fields** (`list`, `set`, `zset`, `hashkey`) use immediate writes:
129
+ every mutator (`add`, `push`, `remove`, `clear`, `[]=`) hits Redis right away.
130
+ Collections live on separate keys from the object hash.
131
+
132
+ **Safe pattern — scalars first, then collections:**
133
+
134
+ ```ruby
135
+ # Option A: explicit save, then mutate collections directly
136
+ plan.name = "Premium"
137
+ plan.save # HMSET for scalar fields
138
+ plan.features.clear
139
+ plan.features.add("sso")
140
+
141
+ # Option B: convenience wrapper (calls save internally, then yields the block)
142
+ plan.name = "Premium"
143
+ plan.save_with_collections do
144
+ plan.features.clear
145
+ plan.features.add("sso")
146
+ end
147
+ ```
148
+
149
+ Mutating collections before `save` is unsafe: if `save` raises, the collections
150
+ are already mutated.
151
+
152
+ **Atomic pattern — scalars and collections in one MULTI/EXEC:**
153
+
154
+ ```ruby
155
+ plan.atomic_write do
156
+ plan.name = "Premium" # deferred: queued as HMSET
157
+ plan.features.clear # immediate: queued as DEL in the open MULTI
158
+ plan.features.add("sso")
159
+ end
160
+ ```
161
+
162
+ `atomic_write` composes the `transaction` infrastructure so every command lands
163
+ in one MULTI/EXEC; collection mutations auto-route into the open transaction via
164
+ `Fiber[:familia_transaction]`. Constraints:
165
+
166
+ - All related DataTypes must share the parent's `logical_database`, else `Familia::CrossDatabaseError` (fall back to `save_with_collections`). MULTI/EXEC is single-database only.
167
+ - Cannot nest inside another `transaction`/`atomic_write` (`Familia::OperationModeError`).
168
+ - Collection return values inside the block are `Redis::Future` — do not inspect before EXEC.
169
+
170
+ **Factory — `build` for create-and-populate:**
171
+
172
+ ```ruby
173
+ user = User.build(email: "alice@example.com") do |u|
174
+ u.name = "Alice" # deferred scalar
175
+ u.tags.add("admin") # folded into the same MULTI
176
+ end
177
+ ```
178
+
179
+ `build` is class-level sugar over `new` + `atomic_write` with create-only
180
+ semantics: raises `RecordExistsError` if the identifier exists, same
181
+ single-database constraint. Without a block it degenerates to `new(...).save`.
182
+ For upsert, use `save`/`save_with_collections`.
183
+
184
+ ## Instances Timeline
185
+
186
+ Every Horreum subclass has a class-level `instances` sorted set — a timeline of
187
+ last-write timestamps (ZADD score), not a registry.
188
+
189
+ - **Touch** (`touch_instances!`): `save`/`save_if_not_exists!` (via `persist_to_storage`), `commit_fields`, `batch_update`, `save_fields`, fast writers.
190
+ - **Remove**: instance `destroy!` (`remove_from_instances!`), class `destroy!(id)`, lazy `cleanup_stale_instance_entry` in `find_by_dbkey`.
191
+ - **Ghosts**: a hash key expiring via TTL leaves a stale identifier in `instances`. `find_by_dbkey` prunes on access; raw enumeration (`instances.members`) still sees ghosts.
192
+ - **`in_instances?(id)`** — fast O(log N), may report ghosts or miss non-Familia objects. **`exists?(id)`** — authoritative hash-key check (round-trip). `load`/`find_by_id` read the hash key directly and bypass `instances`.
193
+
194
+ ## Thread Safety
195
+
196
+ DataType instances are frozen (immutable). Configure module-level settings once
197
+ at startup, before threads spawn. `Familia.start_monitoring!` tracks contention.
198
+ Tests and contention patterns live in `try/thread_safety/`.
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,224 @@ 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.10.1:
11
+
12
+ 2.10.1 — 2026-06-06
13
+ ===================
14
+
15
+ Added
16
+ -----
17
+
18
+ - ``record_class:`` option for collection DataTypes (``list``/``set``/
19
+ ``sorted_set``/``hashkey``). This loading-only hint tells ``each_record`` which
20
+ class to hydrate via ``load_multi`` without changing how the collection
21
+ serializes or deserializes reads. Use this when you want ``each_record`` lookup
22
+ behavior but no changes to read behavior. Issue #297
23
+
24
+ - ``Familia.atomic_write(*instances)`` persists multiple Horreum instances in a
25
+ single ``MULTI/EXEC``. Includes an optional ``watch_keys:``/``pre_check:``
26
+ variant for race-safe, write-once semantics. All participating instances must
27
+ resolve to the same logical database (raising ``Familia::CrossDatabaseError``
28
+ otherwise) and must share a hash slot on Redis Cluster. #296
29
+
30
+ Changed
31
+ -------
32
+
33
+ - ``participates_in`` / ``class_participates_in`` collections now default to
34
+ using ``record_class:``. This change requires **no data migration and causes no
35
+ behavior changes**: existing collections already stored raw identifiers, and
36
+ read operations (``members``, ``to_a``, ``member?``, ``score``) behave exactly
37
+ as before. The only difference is that ``each_record`` is now supported. Pre-
38
+ declared collections are left untouched. Issue #297
39
+
40
+ Fixed
41
+ -----
42
+
43
+ - Enabled ``each_record`` on ``participates_in`` and ``class_participates_in``
44
+ collections by automatically declaring them with ``record_class: <participant
45
+ class>``. This resolves ``Familia::Problem`` exceptions and loads participant
46
+ records via ``load_multi`` across all collection types. Issue #297
47
+
48
+ - Suppressed per-member ``[deserialize] Raw fallback`` warning storm when
49
+ iterating ``record_class`` collections with non-JSON identifiers (such as UUIDs
50
+ or prefixed IDs). These expected raw values are now logged at the debug level
51
+ instead of warnings. Issue #297
52
+
53
+ - Resolved a connection-pooling bug where the ``WATCH``-based optimistic lock
54
+ in ``atomic_write(watch_keys:)``, ``save_if_not_exists!``, and ``create!`` was
55
+ silent/inert. The ``WATCH`` and ``MULTI/EXEC`` commands are now driven through
56
+ the same connection, ensuring concurrent modifications correctly abort and raise
57
+ as
58
+ documented. #296
59
+
60
+ AI Assistance
61
+ -------------
62
+
63
+ - AI diagnosed the participation iteration bug and identified that ``reference: true``
64
+ introduced unintended read-behavior changes. Designed and implemented the
65
+ ``record_class:`` option to decouple ``each_record`` lookup from read deserialization,
66
+ suppressed a resulting per-member deserialize warning storm, kept intentional
67
+ raw-string semantics on ``instances`` and ``unique_index``, updated stale
68
+ flowcharts in ``datatype-collections.md``, and added regression coverage. Issue #297
69
+
70
+ - Root-caused and fixed a split-connection defect with Claude Code: implemented a
71
+ single-connection ``execute_watched_transaction`` primitive (avoiding fiber-pinning
72
+ that degrades atomic commands) and added real concurrent-modification tests to
73
+ replace simulated aborts. #296
74
+
75
+ - Designed and built multi-model atomic writes on top of the new ``WATCH`` primitive:
76
+ implemented the same-database guard, orchestration logic, and a test suite covering
77
+ two-model commits, rollback on error, cross-database rejection, and race conditions. #296
78
+
79
+ .. _changelog-2.10.0:
80
+
81
+ 2.10.0 — 2026-06-04
82
+ ====================
83
+
84
+ Added
85
+ -----
86
+
87
+ - ``Horreum.build``: A factory block that yields a new instance, then commits
88
+ all scalar and collection changes in a single ``MULTI/EXEC`` upon exit.
89
+ This avoids sequencing ``save`` before collection writes. Raises
90
+ ``Familia::RecordExistsError`` if the identifier exists (create-only).
91
+ Without a block, it behaves as ``new(...).save``. #279
92
+
93
+ - ``atomic_write`` now supports ``watch_keys:`` (keys to watch) and
94
+ ``pre_check:`` (a callable run between ``WATCH`` and ``MULTI``) to enable
95
+ optimistic locking. Retries with exponential backoff on abort. #288
96
+
97
+ - ``encrypted_field`` now accepts a ``key_material:`` proc. This mixes
98
+ additional entropy into key derivation (separate from AAD), requiring
99
+ the correct material at decryption to avoid producing garbage output. PR #280
100
+
101
+ - Encrypted-field envelopes now store their own ``envelope_version`` and
102
+ ``aad_fields`` list. Decryption rebuilds AAD from these stored fields
103
+ rather than the active class declaration, preventing breakage when model
104
+ definitions change. PR #280
105
+
106
+ - ``DatabaseLogger.capture_enabled`` (Boolean, default ``true``) controls
107
+ in-memory buffer capturing. Disabling it bypasses clock checks, message
108
+ allocations, and buffer appends, offering a zero-overhead production path. Issue #233
109
+
110
+ - ``Familia::Instrumentation.hooks?(type)`` reports whether hooks are
111
+ registered for a given event type (e.g. ``:command``, ``:pipeline``). Issue #233
112
+
113
+ - ``Familia.reset_trace!`` clears the cached trace environment lookup. Issue #233
114
+
115
+ - ``dirty_write_warnings`` class method configures write-order warnings per
116
+ class (inheritable). Accepts ``:strict``, ``:warn``, ``:once``, or ``:off``. Issue #277
117
+
118
+ - ``Familia.dirty_write_warnings`` global setting providing the default mode for
119
+ classes that do not set their own. Issue #277
120
+
121
+ - ``Familia.raise_on_unsaved_parent_write`` (default ``true``) controls whether a
122
+ collection write on a new, unsaved, dirty parent raises or warns. Issue #278
123
+
124
+ Changed
125
+ -------
126
+
127
+ - Mutating a collection on a *new, unsaved* parent Horreum now **raises**
128
+ ``Familia::Problem`` by default. The guard fires *before* the command runs,
129
+ preventing orphaned data. Save the parent first, or set
130
+ ``Familia.raise_on_unsaved_parent_write = false`` to restore warnings. Issue #278
131
+
132
+ - Dirty-write warnings are now **deduplicated per dirty window** (mode ``:once``).
133
+ Writing to a collection on a parent with unsaved scalar fields warns once per
134
+ distinct set of unsaved fields instead of on every write. Set
135
+ ``dirty_write_warnings :warn`` to restore the old behavior. Issue #277
136
+
137
+ - Dirty-write warnings and strict raises now append the hint:
138
+ ``(call #save first or wrap in atomic_write)``. Issue #277
139
+
140
+ - ``trace_enabled?`` now caches the ``FAMILIA_TRACE`` lookup. Use
141
+ ``Familia.reset_trace!`` to force a re-read of the environment. Issue #233
142
+
143
+ - ``unique_index`` hashkeys now store identifiers as raw strings rather than
144
+ JSON-encoded strings. Rebuild existing unique indexes to convert legacy entries,
145
+ e.g., via ``User.rebuild_email_lookup`` or ``company.rebuild_badge_index``. Issue #276
146
+
147
+ Fixed
148
+ -----
149
+
150
+ - ``Horreum.build`` with a block no longer has a TOCTOU race between the
151
+ ``exists?`` check and the ``atomic_write`` commit. The block path now uses
152
+ ``atomic_write(watch_keys:, pre_check:)`` so the existence check runs between
153
+ ``WATCH`` and ``MULTI``. #288
154
+
155
+ - ``aad_fields`` containing a ``transient_field`` now bind to the field's real
156
+ value. Previously ``build_aad`` called ``RedactedString#to_s``, which returns
157
+ ``"[REDACTED]"`` for every value -- so all passphrases produced identical AAD
158
+ and the binding was defeated. PR #280
159
+
160
+ - ``each_record`` now works on ``unique_index`` hashkeys. Previously it raised
161
+ ``Familia::Problem`` because ``unique_index`` created its backing hashkey
162
+ without the ``class:`` option. Issue #276
163
+
164
+ - ``each_record`` extracts the stored identifier (the hash *value*) from a
165
+ HashKey instead of the indexed field (the hash *key*). Issue #276
166
+
167
+ - The unguarded ``Familia.trace`` sites in ``Horreum#destroy!`` and
168
+ ``find_by_dbkey`` now carry an inline ``if Familia.debug?`` guard. Issue #233
169
+
170
+ - Two latent encryption bugs surfaced while repairing the examples (issue #250):
171
+
172
+ - ``Familia::Encryption.with_request_cache`` and ``clear_request_cache!``
173
+ were unreachable. The implementation lived in
174
+ ``lib/familia/encryption/request_cache.rb``, which was never ``require``\ d.
175
+ The file is now loaded with the rest of the encryption stack.
176
+
177
+ - The XChaCha20-Poly1305 provider derived keys with
178
+ ``context.force_encoding('BINARY')``, mutating the caller's string. A
179
+ frozen context raised ``FrozenError``. It now uses ``context.b``.
180
+
181
+ Security
182
+ --------
183
+
184
+ - The ``aad_fields`` transient-field fix changes AAD output for any field that
185
+ lists a ``transient_field``. Values encrypted by an earlier release using a
186
+ transient field in ``aad_fields`` were bound to ``"[REDACTED]"`` and will no
187
+ longer decrypt after upgrading. Re-encrypt affected values if any exist.
188
+ PR #280
189
+
190
+ Documentation
191
+ -------------
192
+
193
+ - Repaired every script in ``examples/`` so each runs top-to-bottom and is
194
+ re-runnable (issue #250). Added ``try/integration/examples/`` with one
195
+ subprocess-driven tryouts file per example script for automated regression
196
+ coverage.
197
+
198
+ - ``Horreum.create!``: added ``@yield``, ``@yieldparam``, and
199
+ ``@yieldreturn`` YARD tags documenting the post-success block semantics. #286
200
+
201
+ - ``Horreum#save``: added ``@example`` tags showing idiomatic Ruby patterns
202
+ for post-save callbacks (``if save`` and ``&&`` short-circuit). #286
203
+
204
+ - Renamed ``CLAUDE.md`` to ``AGENTS.md`` and pruned it to remove volatile
205
+ content better served by its source of truth. Kept the non-obvious behavioral
206
+ contracts like deferred-vs-immediate write model and the serialization table.
207
+
208
+ AI Assistance
209
+ -------------
210
+
211
+ - AI implemented ``build`` factory block (#279) and WATCH composition in
212
+ ``atomic_write`` (#288), including tryouts for both.
213
+
214
+ - AI refactored encryption envelope handling (#280): unified AAD construction
215
+ through ``EncryptedData``, added envelope versioning, and fixed the
216
+ transient-field AAD bypass.
217
+
218
+ - AI implemented ``DatabaseLogger.capture_enabled`` toggle and middleware
219
+ consolidation (#233), per-class ``dirty_write_warnings`` (#277), and
220
+ unsaved-parent guard (#278) with tryouts for each.
221
+
222
+ - AI diagnosed and fixed ``each_record`` on ``unique_index`` hashkeys (#276)
223
+ and repaired all example scripts with regression tryouts (#250).
224
+
225
+ - AI evaluated and rejected ``save_and_then`` (#286) after cross-ORM analysis;
226
+ added YARD docs and ``create_block_try.rb`` instead.
227
+
10
228
  .. _changelog-2.9.1:
11
229
 
12
230
  2.9.1 — 2026-05-18
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (2.9.1)
4
+ familia (2.10.1)
5
5
  concurrent-ruby (~> 1.3)
6
6
  connection_pool (>= 2.4, < 4.0)
7
7
  csv (~> 3.3)
@@ -15,59 +15,59 @@ PATH
15
15
  GEM
16
16
  remote: https://rubygems.org/
17
17
  specs:
18
- addressable (2.8.9)
18
+ addressable (2.9.0)
19
19
  public_suffix (>= 2.0.2, < 8.0)
20
20
  ast (2.4.3)
21
21
  base64 (0.3.0)
22
22
  benchmark (0.5.0)
23
- bigdecimal (3.3.1)
23
+ bigdecimal (4.1.2)
24
24
  concurrent-ruby (1.3.6)
25
25
  connection_pool (3.0.2)
26
26
  csv (3.3.5)
27
- date (3.5.0)
28
- debug (1.11.0)
27
+ date (3.5.1)
28
+ debug (1.11.1)
29
29
  irb (~> 1.10)
30
30
  reline (>= 0.3.8)
31
31
  diff-lcs (1.6.2)
32
- dry-configurable (1.3.0)
33
- dry-core (~> 1.1)
32
+ dry-configurable (1.4.0)
33
+ dry-core (~> 1.0)
34
34
  zeitwerk (~> 2.6)
35
- dry-core (1.1.0)
35
+ dry-core (1.2.0)
36
36
  concurrent-ruby (~> 1.0)
37
37
  logger
38
38
  zeitwerk (~> 2.6)
39
- dry-inflector (1.2.0)
39
+ dry-inflector (1.3.1)
40
40
  dry-initializer (3.2.0)
41
41
  dry-logic (1.6.0)
42
42
  bigdecimal
43
43
  concurrent-ruby (~> 1.0)
44
44
  dry-core (~> 1.1)
45
45
  zeitwerk (~> 2.6)
46
- dry-schema (1.14.1)
46
+ dry-schema (1.16.0)
47
47
  concurrent-ruby (~> 1.0)
48
48
  dry-configurable (~> 1.0, >= 1.0.1)
49
49
  dry-core (~> 1.1)
50
50
  dry-initializer (~> 3.2)
51
- dry-logic (~> 1.5)
52
- dry-types (~> 1.8)
51
+ dry-logic (~> 1.6)
52
+ dry-types (~> 1.9, >= 1.9.1)
53
53
  zeitwerk (~> 2.6)
54
- dry-types (1.8.3)
55
- bigdecimal (~> 3.0)
54
+ dry-types (1.9.1)
55
+ bigdecimal (>= 3.0)
56
56
  concurrent-ruby (~> 1.0)
57
57
  dry-core (~> 1.0)
58
58
  dry-inflector (~> 1.0)
59
59
  dry-logic (~> 1.4)
60
60
  zeitwerk (~> 2.6)
61
- erb (5.1.3)
62
- ffi (1.17.2)
63
- ffi (1.17.2-arm64-darwin)
61
+ erb (6.0.4)
62
+ ffi (1.17.4)
63
+ ffi (1.17.4-arm64-darwin)
64
64
  hana (1.3.7)
65
- io-console (0.8.1)
65
+ io-console (0.8.2)
66
66
  irb (1.15.3)
67
67
  pp (>= 0.6.0)
68
68
  rdoc (>= 4.0.0)
69
69
  reline (>= 0.4.2)
70
- json (2.15.1)
70
+ json (2.19.8)
71
71
  json-schema (6.2.0)
72
72
  addressable (~> 2.8)
73
73
  bigdecimal (>= 3.1, < 5)
@@ -79,15 +79,15 @@ GEM
79
79
  language_server-protocol (3.17.0.5)
80
80
  lint_roller (1.1.0)
81
81
  logger (1.7.0)
82
- mcp (0.8.0)
82
+ mcp (0.18.0)
83
83
  json-schema (>= 4.1)
84
84
  minitest (5.27.0)
85
- oj (3.16.13)
85
+ oj (3.17.3)
86
86
  bigdecimal (>= 3.0)
87
87
  ostruct (>= 0.2)
88
88
  ostruct (0.6.3)
89
- parallel (1.27.0)
90
- parser (3.3.9.0)
89
+ parallel (1.28.0)
90
+ parser (3.3.11.1)
91
91
  ast (~> 2.4.1)
92
92
  racc
93
93
  pastel (0.8.0)
@@ -96,25 +96,27 @@ GEM
96
96
  prettyprint
97
97
  prettyprint (0.2.0)
98
98
  prism (1.9.0)
99
- psych (5.2.6)
99
+ psych (5.4.0)
100
100
  date
101
101
  stringio
102
102
  public_suffix (7.0.5)
103
103
  racc (1.8.1)
104
104
  rainbow (3.1.1)
105
- rake (13.3.1)
105
+ rake (13.4.2)
106
106
  rbnacl (7.1.2)
107
107
  ffi (~> 1)
108
- rbs (3.9.5)
108
+ rbs (4.0.2)
109
109
  logger
110
- rdoc (6.15.1)
110
+ prism (>= 1.6.0)
111
+ tsort
112
+ rdoc (7.2.0)
111
113
  erb
112
114
  psych (>= 4.0.0)
113
115
  tsort
114
116
  redcarpet (3.6.1)
115
117
  redis (5.4.1)
116
118
  redis-client (>= 0.22.0)
117
- redis-client (0.26.2)
119
+ redis-client (0.29.0)
118
120
  connection_pool
119
121
  reek (6.5.0)
120
122
  dry-schema (~> 1.13)
@@ -122,10 +124,10 @@ GEM
122
124
  parser (~> 3.3.0)
123
125
  rainbow (>= 2.0, < 4.0)
124
126
  rexml (~> 3.1)
125
- regexp_parser (2.11.3)
126
- reline (0.6.2)
127
+ regexp_parser (2.12.0)
128
+ reline (0.6.3)
127
129
  io-console (~> 0.5)
128
- rexml (3.4.1)
130
+ rexml (3.4.4)
129
131
  rspec (3.13.2)
130
132
  rspec-core (~> 3.13.0)
131
133
  rspec-expectations (~> 3.13.0)
@@ -135,10 +137,10 @@ GEM
135
137
  rspec-expectations (3.13.5)
136
138
  diff-lcs (>= 1.2.0, < 2.0)
137
139
  rspec-support (~> 3.13.0)
138
- rspec-mocks (3.13.7)
140
+ rspec-mocks (3.13.8)
139
141
  diff-lcs (>= 1.2.0, < 2.0)
140
142
  rspec-support (~> 3.13.0)
141
- rspec-support (3.13.6)
143
+ rspec-support (3.13.7)
142
144
  rubocop (1.85.1)
143
145
  json (~> 2.3)
144
146
  language_server-protocol (~> 3.17.0.2)
@@ -151,28 +153,29 @@ GEM
151
153
  rubocop-ast (>= 1.49.0, < 2.0)
152
154
  ruby-progressbar (~> 1.7)
153
155
  unicode-display_width (>= 2.4.0, < 4.0)
154
- rubocop-ast (1.49.0)
156
+ rubocop-ast (1.49.1)
155
157
  parser (>= 3.3.7.2)
156
158
  prism (~> 1.7)
157
- rubocop-performance (1.25.0)
159
+ rubocop-performance (1.26.1)
158
160
  lint_roller (~> 1.1)
159
161
  rubocop (>= 1.75.0, < 2.0)
160
- rubocop-ast (>= 1.38.0, < 2.0)
162
+ rubocop-ast (>= 1.47.1, < 2.0)
161
163
  rubocop-thread_safety (0.7.3)
162
164
  lint_roller (~> 1.1)
163
165
  rubocop (~> 1.72, >= 1.72.1)
164
166
  rubocop-ast (>= 1.44.0, < 2.0)
165
- ruby-lsp (0.26.1)
167
+ ruby-lsp (0.26.9)
166
168
  language_server-protocol (~> 3.17.0)
167
169
  prism (>= 1.2, < 2.0)
168
170
  rbs (>= 3, < 5)
169
- ruby-prof (1.7.2)
171
+ ruby-prof (2.0.4)
170
172
  base64
173
+ ostruct
171
174
  ruby-progressbar (1.13.0)
172
175
  simpleidn (0.2.3)
173
- stackprof (0.2.27)
176
+ stackprof (0.2.28)
174
177
  stringio (3.1.9)
175
- timecop (0.9.10)
178
+ timecop (0.9.11)
176
179
  tryouts (3.7.1)
177
180
  concurrent-ruby (~> 1.0, < 2)
178
181
  irb
@@ -190,8 +193,8 @@ GEM
190
193
  unicode-emoji (~> 4.1)
191
194
  unicode-emoji (4.2.0)
192
195
  uri-valkey (1.4.0)
193
- yard (0.9.37)
194
- zeitwerk (2.7.3)
196
+ yard (0.9.44)
197
+ zeitwerk (2.8.2)
195
198
 
196
199
  PLATFORMS
197
200
  arm64-darwin-24
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Familia - 2.5
1
+ # Familia - v2
2
2
 
3
3
  **Organize and store Ruby objects in Valkey/Redis using native database types (an ORM of sorts).**
4
4
 
@@ -56,7 +56,7 @@ The performance characteristics you rely on in Valkey/Redis remain unchanged. Se
56
56
 
57
57
  ```bash
58
58
  # Add to Gemfile
59
- gem 'familia', '~> 2.5'
59
+ gem 'familia', '~> 2.10'
60
60
 
61
61
  # Or install directly
62
62
  gem install familia