familia 2.6.0 โ†’ 2.8.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.pre-commit-config.yaml +0 -2
  3. data/CHANGELOG.rst +209 -0
  4. data/Gemfile.lock +1 -1
  5. data/docs/guides/feature-housekeeping.md +247 -0
  6. data/docs/guides/index.md +5 -1
  7. data/lib/familia/connection/handlers.rb +131 -47
  8. data/lib/familia/connection/operations.rb +5 -4
  9. data/lib/familia/connection/pipelined_core.rb +14 -7
  10. data/lib/familia/connection/transaction_core.rb +9 -0
  11. data/lib/familia/connection.rb +2 -1
  12. data/lib/familia/data_type/connection.rb +21 -52
  13. data/lib/familia/data_type/types/hashkey.rb +247 -0
  14. data/lib/familia/data_type/types/listkey.rb +117 -4
  15. data/lib/familia/data_type/types/sorted_set.rb +385 -1
  16. data/lib/familia/data_type/types/stringkey.rb +164 -0
  17. data/lib/familia/data_type/types/unsorted_set.rb +121 -3
  18. data/lib/familia/data_type.rb +1 -0
  19. data/lib/familia/errors.rb +4 -0
  20. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +18 -0
  21. data/lib/familia/features/housekeeping.rb +195 -0
  22. data/lib/familia/field_type.rb +18 -0
  23. data/lib/familia/horreum/connection.rb +15 -10
  24. data/lib/familia/horreum/definition.rb +18 -0
  25. data/lib/familia/horreum/management/audit.rb +412 -61
  26. data/lib/familia/horreum/management/repair.rb +167 -15
  27. data/lib/familia/horreum/persistence.rb +23 -19
  28. data/lib/familia/horreum.rb +9 -6
  29. data/lib/familia/version.rb +1 -1
  30. data/try/audit/audit_instance_scoped_multi_index_try.rb +198 -0
  31. data/try/audit/m3_multi_index_stub_try.rb +18 -11
  32. data/try/audit/repair_all_robustness_try.rb +149 -0
  33. data/try/edge_cases/fast_writer_transaction_guard_try.rb +130 -0
  34. data/try/edge_cases/pipeline_handler_edge_cases_try.rb +214 -0
  35. data/try/features/atomic_write_try.rb +8 -6
  36. data/try/features/housekeeping/housekeeping_try.rb +386 -0
  37. data/try/integration/connection/handler_constraints_try.rb +8 -8
  38. data/try/integration/connection/pipeline_handler_integration_try.rb +175 -0
  39. data/try/integration/connection/pipeline_horreum_routing_try.rb +147 -0
  40. data/try/integration/data_types/datatype_pipelines_try.rb +26 -6
  41. data/try/integration/data_types/datatype_transactions_try.rb +14 -12
  42. data/try/thread_safety/fiber_pipeline_isolation_try.rb +11 -19
  43. data/try/unit/data_types/hashkey_operations_try.rb +269 -0
  44. data/try/unit/data_types/list_commands_try.rb +314 -0
  45. data/try/unit/data_types/sortedset_operations_try.rb +467 -0
  46. data/try/unit/data_types/stringkey_extended_try.rb +239 -0
  47. data/try/unit/data_types/unsortedset_operations_try.rb +174 -0
  48. data/try/unit/fiber_pipeline_handler_try.rb +147 -0
  49. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +1 -1
  50. metadata +16 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5577963e36f6c5ce2ce1fc977458c092b6883430e1c6c3e4c72742ddb4cae795
4
- data.tar.gz: bc958eef278b236f894652b4954e7235a917c858f26e8c63427b1fb7bb62713a
3
+ metadata.gz: c6cb1ccd59d4290c1d75b70e114945180fc5dbb03aaeb405d841c250cd504179
4
+ data.tar.gz: c2bd7946e7e024c8322d0bf48bea39c22cd61c7690589007f9ab32e6f12e614d
5
5
  SHA512:
6
- metadata.gz: 2820cc6134dd2a707a8e7c91855c42b622807748cc428984bb725f71faada7489ed5211ac3dedbe887132e13523cd4e8c565abeaf57c7d9df411566fb60766ed
7
- data.tar.gz: 1bc50c330c90ac7edcb460c7f8688d8e47d8329e2770a36b8963e24ea00cb258ee017f2c7ec905a2f19b38949ea609f381a9d65d0460711cc725c26b9f664053
6
+ metadata.gz: 54d3c3020c53fe01de9baf6af78fbcc1ffbea72a671b6e6269c08c9c5eaab786bb267514aa64212ff8ec854e0363cfb1c1de9b9c66674e57cb067ed419a8944b
7
+ data.tar.gz: d8b8ecd85ed1273c64ae75cab7559dee0ddb92fe5d552b60690c3a66ee9df93b3829aa32deef5438677ad9fb8d9acebb0bdbf071f3bccdb63161c4a772b1c021
@@ -62,8 +62,6 @@ repos:
62
62
  rev: v1.32.2
63
63
  hooks:
64
64
  - id: talisman-commit
65
- env:
66
- TALISMAN_SCAN_MODE: CI
67
65
 
68
66
  # Commit message issue tracking integration
69
67
  - repo: https://github.com/delano/add-msg-issue-prefix-hook
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,215 @@ 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.8.0:
11
+
12
+ 2.8.0 โ€” 2026-05-15
13
+ ==================
14
+
15
+ Added
16
+ -----
17
+
18
+ - Expanded Redis 7 command coverage across all DataType classes:
19
+
20
+ - **StringKey**: ``incrbyfloat``, ``getex``, ``getdel``, ``setex``, ``psetex``,
21
+ ``bitcount``, ``bitpos``, ``bitfield``, plus class methods ``mget``, ``mset``,
22
+ ``msetnx``, ``bitop`` for multi-key and bitwise operations.
23
+
24
+ - **List**: ``trim``/``ltrim``, ``set``/``lset``, ``insert``/``linsert``,
25
+ ``move``/``lmove``, ``pushx``/``rpushx``, ``unshiftx``/``lpushx``.
26
+ Updated ``pop`` and ``shift`` to support optional count parameter for batch operations.
27
+
28
+ - **UnsortedSet**: ``intersection``/``inter``, ``union``, ``difference``/``diff``,
29
+ ``member_any?``/``members?``, ``scan``, ``intercard``/``intersection_cardinality``,
30
+ ``interstore``/``intersection_store``, ``unionstore``/``union_store``,
31
+ ``diffstore``/``difference_store``.
32
+
33
+ - **SortedSet**: ``popmin``, ``popmax``, ``score_count``/``zcount``, ``mscore``,
34
+ ``union``, ``inter``, ``rangebylex``, ``revrangebylex``, ``remrangebylex``,
35
+ ``lexcount``, ``randmember``, ``scan``, ``unionstore``, ``interstore``,
36
+ ``diff``, ``diffstore``.
37
+
38
+ - **HashKey**: ``scan``/``hscan``, ``incrbyfloat``/``incrfloat``,
39
+ ``strlen``/``hstrlen``, ``randfield``/``hrandfield``, plus field-level TTL
40
+ commands (Redis 7.4+): ``expire_fields``, ``pexpire_fields``, ``expireat_fields``,
41
+ ``pexpireat_fields``, ``ttl_fields``, ``pttl_fields``, ``persist_fields``,
42
+ ``expiretime_fields``, ``pexpiretime_fields``.
43
+
44
+ - Added 158 new tests across 5 test files covering all new methods.
45
+
46
+ - Instance-scoped ``audit_multi_indexes`` is now fully implemented.
47
+ Discovers per-scope bucket keys via SCAN, partitions them by scope
48
+ instance, and reports stale members, orphaned buckets, and missing
49
+ entries in the same shape as the class-level audit. Orphan entries
50
+ carry a ``:reason`` (``:scope_missing`` or ``:field_value_unheld``)
51
+ and a ``:scope_id``. Missing entries are detected via the indexed
52
+ class's ``participates_in`` relationship to the scope class; when
53
+ absent, the result carries ``missing_status: :not_audited``.
54
+ Resolves the ``:not_implemented`` follow-up from #217.
55
+
56
+ - ``repair_multi_indexes!`` class method that invokes the existing
57
+ ``rebuild_<index_name>`` methods for both class-level (one call on
58
+ the indexed class) and instance-scoped (one call per scope
59
+ instance) multi-indexes. Indexes whose audit status is ``:ok`` are
60
+ skipped; rebuild methods that don't exist or scope classes
61
+ without an ``instances`` collection are recorded in ``:skipped``
62
+ with a reason.
63
+
64
+ - ``housekeeping`` feature gains a class-level bulk runner,
65
+ ``Klass.run_chores!(chore_name:, limit:, batch_size:)``. It iterates
66
+ the class's ``instances`` collection in pipelined batches via
67
+ ``load_multi``, runs all registered chores (or one named chore)
68
+ against each record, and returns a stats hash:
69
+ ``{ model:, scanned:, chores: { name => { modified:, errors: } } }``.
70
+ Truthy chore returns increment ``modified``; raised exceptions are
71
+ isolated per-record, logged via ``Familia.warn``, and counted as
72
+ ``errors`` so a single failure doesn't halt the run. Lifted from the
73
+ shape proven out in OneTime Secret's ``HousekeepingJob``.
74
+
75
+ - Trace events for connection-mode conflicts. ``Familia.trace`` now
76
+ emits ``CONFLICTING_CONTEXT`` when pipeline and transaction
77
+ contexts collide (in both ``FiberPipelineHandler``/
78
+ ``FiberTransactionHandler`` and the
79
+ ``execute_transaction``/``execute_pipeline`` entry points), and
80
+ ``FAST_WRITER_BLOCKED`` when a fast writer (``field!``) is called
81
+ inside a transaction or pipeline. These fire just before the
82
+ corresponding ``ConflictingContextError`` /
83
+ ``OperationModeError`` is raised, so operators can pinpoint where
84
+ blocked operations originate when ``FAMILIA_TRACE=1``.
85
+
86
+ Changed
87
+ -------
88
+
89
+ - ``repair_all!`` now runs each repair stage inside its own rescue
90
+ boundary; a failure in one dimension no longer prevents the others
91
+ from running. The return hash gains ``:status`` (``:ok`` or
92
+ ``:partial_failure``), ``:errors`` (per-stage exception details
93
+ when raised), and ``:multi_indexes`` (results from the new
94
+ ``repair_multi_indexes!``). An opt-in ``verify: true`` kwarg
95
+ re-runs ``health_check`` after repair and exposes the result as
96
+ ``:post_audit`` / ``:verified`` so callers can confirm the run
97
+ actually drove the model back to a healthy state.
98
+
99
+ - ``AuditReport#complete?`` is no longer false-positive due to
100
+ ``:not_implemented`` stubs in ``multi_indexes`` -- instance-scoped
101
+ indexes return ``:ok`` or ``:issues_found`` like class-level ones.
102
+
103
+ - ``housekeeping`` feature: split the dual-purpose ``tidy!`` into two
104
+ explicit instance methods. ``do_chore!(name)`` runs a single named
105
+ chore and returns the block's raw return value (no longer wrapped
106
+ in a ``{name => result}`` hash). ``do_chores!`` runs every
107
+ registered chore and returns the ``{name => result}`` hash.
108
+ ``tidy!`` is preserved as an alias of ``do_chores!`` for backwards
109
+ compatibility with the 2.7.0 no-arg call site; the single-arg form
110
+ ``tidy!(:name)`` now raises ``ArgumentError``.
111
+
112
+ - The connection handler hierarchy has been refactored from class
113
+ inheritance (``BaseConnectionHandler``) to module composition.
114
+ Handlers now ``include Familia::Connection::Handler`` and declare
115
+ their operation-mode capabilities with a small DSL:
116
+ ``supports transaction: true, pipelined: false``. The
117
+ ``BaseConnectionHandler`` constant is gone. This is only relevant if
118
+ you have custom handlers in application code โ€” the public
119
+ ``allows_transaction`` / ``allows_pipelined`` class methods continue
120
+ to work, and the singleton ``.instance`` accessors on
121
+ ``FiberPipelineHandler`` / ``FiberTransactionHandler`` are
122
+ unchanged. The previous default of "allow all operations" when
123
+ capability flags were not set has been removed; every handler is now
124
+ expected to declare its capabilities explicitly via ``supports``.
125
+ - ``Familia.dbclient`` and ``Familia::DataType#dbclient`` now route through ``FiberPipelineHandler`` before ``FiberTransactionHandler``, matching ``Horreum#dbclient``. With both handlers in the chain, attempting to mix pipeline and transaction contexts raises ``Familia::ConflictingContextError`` uniformly from every call site.
126
+
127
+ Removed
128
+ -------
129
+
130
+ - ``Familia::DataType#direct_access`` has been removed. The method
131
+ was a legacy escape hatch for issuing raw Redis commands from
132
+ inside a DataType wrapper; it predates the chain-based routing of
133
+ ``Fiber[:familia_transaction]`` and ``Fiber[:familia_pipeline]``.
134
+ All in-tree call sites now go through the wrapper's own mutating
135
+ methods (which auto-route through the active transaction or
136
+ pipeline) or through the wrapper's ``transaction`` / ``pipelined``
137
+ blocks. If you were calling ``direct_access do |conn, key| ... end``,
138
+ replace it with either the DataType's own mutator or the
139
+ corresponding block API.
140
+
141
+ Fixed
142
+ -----
143
+
144
+ - ``SortedSet#popmin`` and ``SortedSet#popmax`` now normalize an explicitly
145
+ passed ``nil`` count to the default of ``1``. Previously, calling
146
+ ``zset.popmin(nil)`` or ``zset.popmax(nil)`` would bypass the ``count == 1``
147
+ branch of the structural dispatch added in the prior commit, causing
148
+ redis-rb's flat ``[member, score]`` return shape to be iterated as if it
149
+ were a nested result โ€” yielding a malformed pair. Omitting the argument
150
+ was and remains unaffected.
151
+
152
+ - Restored ``require 'set'`` in ``lib/familia/horreum/management/audit.rb``. ``Set`` is autoloaded as a core class only on Ruby 3.4+; on Ruby 3.2/3.3 (the gem's supported floor) the require is mandatory for the five ``Set.new`` usages in that file.
153
+
154
+ AI Assistance
155
+ -------------
156
+
157
+ - Claude Opus 4.5 analyzed Redis 7 command documentation and compared coverage
158
+ against existing Familia DataType implementations using parallel Explore agents.
159
+ - Implementation performed by 5 parallel backend-dev agents, one per DataType.
160
+ - Test coverage written by 5 parallel qa-automation-engineer agents focusing on
161
+ Familia-specific behavior (serialization, deserialization, aliases) rather than
162
+ re-testing redis-rb gem functionality.
163
+
164
+ - Edge case identified by the Claude Code Review GitHub Action
165
+ (``.github/workflows/claude-code-review.yml``) when reviewing the
166
+ structural-dispatch change in commit ``010d5be``. Fix drafted and verified
167
+ by Claude Opus 4.7 under supervision.
168
+
169
+ - Instance-scoped multi-index audit algorithm (bucket discovery,
170
+ scope existence batching, participation-driven missing detection),
171
+ ``repair_multi_indexes!``, the ``repair_all!`` robustness
172
+ refactor, and the accompanying tryouts coverage were authored
173
+ with Claude Code assistance against the #217 review branch.
174
+
175
+ - Method split, alias wiring, bulk runner port from OTS, doc updates,
176
+ and expanded tryouts coverage (25 โ†’ 48 testcases) authored with
177
+ Claude Code.
178
+
179
+ - Added the trace instrumentation in response to PR #263 review
180
+ feedback (Claude Code review bot) recommending tracing for
181
+ conflict detection events.
182
+
183
+ - The handler refactor, ``direct_access`` removal, and changelog drafting were performed with Claude Code assistance while resolving review feedback on PR #263.
184
+
185
+ .. _changelog-2.7.0:
186
+
187
+ 2.7.0 โ€” 2026-05-13
188
+ ==================
189
+
190
+ Added
191
+ -----
192
+
193
+ - New ``housekeeping`` feature for ``Familia::Horreum``: a declarative DSL
194
+ (``chore :name do |obj| ... end``) for registering named cleanup blocks on
195
+ a model class, plus an instance method ``tidy!`` that runs all (or one)
196
+ registered chore against a single object. The feature owns registration
197
+ and per-instance execution only -- iteration, batching, scheduling and
198
+ error aggregation are the consumer application's responsibility, keeping
199
+ it distinct from ``Familia::Migration`` (which is for versioned, one-shot
200
+ transformations). Resolves #258.
201
+
202
+ Documentation
203
+ -------------
204
+
205
+ - Added ``docs/guides/feature-housekeeping.md`` covering the API, the
206
+ ``housekeeping`` vs ``migration`` vs defensive-setter trade-off,
207
+ generated method reference, design constraints, and common patterns
208
+ (multiple chores, sequential steps in one chore, tracking modified
209
+ records, error aggregation).
210
+
211
+ AI Assistance
212
+ -------------
213
+
214
+ - Drafted the housekeeping feature module, the tryouts test suite, and the
215
+ guide using Claude Code, working from the API proposal in issue #258 and
216
+ the existing ``feature-relationships.md`` and ``safe_dump.rb`` as style
217
+ templates.
218
+
10
219
  .. _changelog-2.6.0:
11
220
 
12
221
  2.6.0 โ€” 2026-04-17
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (2.6.0)
4
+ familia (2.8.0)
5
5
  concurrent-ruby (~> 1.3)
6
6
  connection_pool (>= 2.4, < 4.0)
7
7
  csv (~> 3.3)
@@ -0,0 +1,247 @@
1
+ # Housekeeping Feature Guide
2
+
3
+ The Housekeeping feature provides a declarative DSL for registering named cleanup chores on Horreum models. It is designed for short-lived, repeated tidying against fields whose values have drifted over time -- not for versioned, one-shot migrations.
4
+
5
+ > [!TIP]
6
+ > Enable with `feature :housekeeping` and register cleanup blocks with `chore :name do |obj| ... end`. Run all of them with `obj.do_chores!` (aliased `tidy!`), or one with `obj.do_chore!(:name)`. Iteration and persistence are the caller's responsibility.
7
+
8
+ ## Quick Start
9
+
10
+ ```ruby
11
+ class Organization < Familia::Horreum
12
+ feature :housekeeping
13
+
14
+ field :planid
15
+
16
+ chore :standardize_planid do |org|
17
+ canonical = case org.planid
18
+ when "pro", "Pro", "professional_v1" then "professional"
19
+ when "free", "Free", "basic" then "free"
20
+ end
21
+ if canonical && canonical != org.planid
22
+ org.planid = canonical
23
+ org.save
24
+ true
25
+ end
26
+ end
27
+ end
28
+
29
+ org = Organization.from_identifier("acme-corp")
30
+ org.do_chores!
31
+ # => { standardize_planid: true }
32
+
33
+ # Or run a single chore by name (returns the block's raw value):
34
+ org.do_chore!(:standardize_planid)
35
+ # => true
36
+ ```
37
+
38
+ ## When to Use
39
+
40
+ | Tool | Use When |
41
+ |------|----------|
42
+ | `Familia::Migration::Base` | Versioned, one-shot transformation tracked across releases |
43
+ | `feature :housekeeping` | Short-lived chore run nightly until data is clean, then removed |
44
+ | Defensive code in setters | Permanent invariant enforced on every write |
45
+
46
+ Housekeeping fills the gap between migrations (heavy, tracked) and inline coercion (permanent). Register a chore, run it on a schedule for a few days, verify clean data, then delete the chore and the defensive code that handled the messy values.
47
+
48
+ ## Core Capabilities
49
+
50
+ ### Registration -- Class-Level DSL
51
+
52
+ Each chore is a named block bound to the model class:
53
+
54
+ ```ruby
55
+ class User < Familia::Horreum
56
+ feature :housekeeping
57
+
58
+ field :email, :timezone
59
+
60
+ chore :downcase_email do |user|
61
+ next unless user.email && user.email != user.email.downcase
62
+ user.email = user.email.downcase
63
+ user.save
64
+ true
65
+ end
66
+
67
+ chore :default_timezone do |user|
68
+ next if user.timezone
69
+ user.timezone = "UTC"
70
+ user.save
71
+ true
72
+ end
73
+ end
74
+
75
+ User.chores.keys
76
+ # => [:downcase_email, :default_timezone]
77
+ ```
78
+
79
+ ### Execution -- Single Instance
80
+
81
+ Run all registered chores, or one by name:
82
+
83
+ ```ruby
84
+ user = User.from_identifier("alice@example.com")
85
+
86
+ user.do_chores!
87
+ # => { downcase_email: true, default_timezone: nil }
88
+
89
+ user.tidy! # alias for do_chores!
90
+ # => { downcase_email: nil, default_timezone: nil }
91
+
92
+ user.do_chore!(:downcase_email)
93
+ # => true
94
+ ```
95
+
96
+ `do_chores!` returns a hash mapping chore name to the block's return value. `do_chore!` returns the block's raw return value (not wrapped in a hash). A truthy result signals "modified"; `nil` or `false` signals "no-op". The feature does not interpret these values -- they are passed through for the caller's stats collection.
97
+
98
+ ### Iteration -- Bulk Runner
99
+
100
+ For running chores across every record, the feature ships a class-level `run_chores!` that iterates the `instances` collection in pipelined batches (via `load_multi`), executes each chore per record with error isolation, and returns a stats hash:
101
+
102
+ ```ruby
103
+ Organization.run_chores!
104
+ # => {
105
+ # model: "Organization",
106
+ # scanned: 4200,
107
+ # chores: {
108
+ # standardize_planid: { modified: 37, errors: 0 },
109
+ # uppercase_country: { modified: 102, errors: 1 },
110
+ # },
111
+ # }
112
+
113
+ Organization.run_chores!(chore_name: :standardize_planid, limit: 500)
114
+ # Filter to one chore and cap records scanned.
115
+
116
+ Organization.run_chores!(batch_size: 50)
117
+ # Tune the load_multi pipeline batch size (default: 100).
118
+ ```
119
+
120
+ A truthy chore return increments `modified`; a raised exception increments `errors` (logged via `Familia.warn`) and iteration continues. The runner requires the class to expose `instances` (Horreum's default class-level sorted set) and `load_multi`.
121
+
122
+ For scheduling, cross-model orchestration, custom logging, or non-default iteration (e.g. a configured allowlist of model classes), wrap `run_chores!` in your own job. The feature deliberately stays out of cron, multi-model discovery, and project-specific logging layers.
123
+
124
+ ## Generated Method Reference
125
+
126
+ ### When a class declares `feature :housekeeping`
127
+
128
+ | Class | Method | Purpose |
129
+ |-------|--------|---------|
130
+ | **Class** | `chore(name, &block)` | Register a chore |
131
+ | | `chores` | Hash of registered chores |
132
+ | | `run_chores!(chore_name:, limit:, batch_size:)` | Bulk runner across `instances`; returns stats hash |
133
+ | **Instance** | `do_chore!(name)` | Run a single chore by name; returns the block's raw value |
134
+ | | `do_chores!` | Run every registered chore; returns Hash |
135
+ | | `tidy!` | Alias for `do_chores!` |
136
+
137
+ ## Design Constraints
138
+
139
+ 1. **No implicit saves.** The block must call `save` (or `commit_fields`) itself. The feature does not auto-persist.
140
+ 2. **Bulk via `run_chores!` only.** The feature operates on a single instance (`do_chore!`/`do_chores!`) plus one bulk runner (`run_chores!`) that iterates `instances`. Scheduling, multi-model orchestration, and custom logging stay in the consumer app.
141
+ 3. **No ordering.** Chores run in registration order, but should not depend on each other. If order matters, write one chore with sequential steps.
142
+ 4. **Idempotent by convention.** Use the conditional pattern (`if canonical && canonical != org.planid`) so a second run is a no-op.
143
+ 5. **Errors isolate in `run_chores!`, propagate in `do_chore!`/`do_chores!`.** Single-instance methods let exceptions propagate; the bulk runner rescues per-record and increments the chore's `errors` counter so one failure doesn't halt the run.
144
+
145
+ ## Common Patterns
146
+
147
+ ### Multiple Independent Chores
148
+
149
+ ```ruby
150
+ class Customer < Familia::Horreum
151
+ feature :housekeeping
152
+
153
+ chore :trim_whitespace do |c|
154
+ next unless c.name && c.name != c.name.strip
155
+ c.name = c.name.strip
156
+ c.save
157
+ true
158
+ end
159
+
160
+ chore :uppercase_country do |c|
161
+ next unless c.country && c.country != c.country.upcase
162
+ c.country = c.country.upcase
163
+ c.save
164
+ true
165
+ end
166
+ end
167
+
168
+ customer.do_chores!
169
+ # => { trim_whitespace: true, uppercase_country: nil }
170
+ ```
171
+
172
+ ### Sequential Steps in One Chore
173
+
174
+ When step B depends on step A's result, keep them in one block:
175
+
176
+ ```ruby
177
+ chore :reconcile_billing do |account|
178
+ changed = false
179
+ if account.plan_id == "legacy"
180
+ account.plan_id = "standard"
181
+ changed = true
182
+ end
183
+ if account.plan_id == "standard" && account.billing_cycle.nil?
184
+ account.billing_cycle = "monthly"
185
+ changed = true
186
+ end
187
+ if changed
188
+ account.save
189
+ true
190
+ end
191
+ end
192
+ ```
193
+
194
+ ### Tracking Modified Records (Bulk)
195
+
196
+ `run_chores!` already aggregates `modified` and `errors` counts per chore. Use it directly:
197
+
198
+ ```ruby
199
+ report = Organization.run_chores!
200
+ report[:chores].each do |name, counts|
201
+ puts "#{name}: #{counts[:modified]} modified, #{counts[:errors]} errors"
202
+ end
203
+ ```
204
+
205
+ ### Custom Iteration (e.g. SCAN-Based)
206
+
207
+ If `instances`-driven iteration isn't suitable (sharded data, custom scoping), drop down to `do_chore!`/`do_chores!`:
208
+
209
+ ```ruby
210
+ modified = []
211
+ Organization.instances.each do |id|
212
+ org = Organization.find_by_identifier(id) or next
213
+ results = org.do_chores!
214
+ modified << id if results.values.any?
215
+ end
216
+ puts "Modified #{modified.size} records: #{modified.inspect}"
217
+ ```
218
+
219
+ ### Wrapping `run_chores!` for a Job Framework
220
+
221
+ ```ruby
222
+ class HousekeepingJob
223
+ def self.perform_for(klass)
224
+ report = klass.run_chores!(batch_size: 50)
225
+ StatsD.gauge("housekeeping.#{klass.name}.scanned", report[:scanned])
226
+ report[:chores].each do |chore, counts|
227
+ StatsD.increment("housekeeping.#{klass.name}.#{chore}.modified", counts[:modified])
228
+ StatsD.increment("housekeeping.#{klass.name}.#{chore}.errors", counts[:errors])
229
+ end
230
+ report
231
+ end
232
+ end
233
+ ```
234
+
235
+ ## Best Practices
236
+
237
+ 1. **Keep chores short-lived.** Delete the registration once data is clean.
238
+ 2. **Use `||=` and conditional checks** so a second run is a no-op.
239
+ 3. **Save inside the block** -- the feature does not persist for you.
240
+ 4. **Return truthy on modification, nil on no-op** so callers can collect stats.
241
+ 5. **Prefer migrations for one-shot, versioned transformations.** Use housekeeping for ongoing tidying that can be run repeatedly.
242
+
243
+ ## See Also
244
+
245
+ - [**Writing Migrations**](writing-migrations.md) - Versioned, one-shot data transformations
246
+ - [**Field System**](field-system.md) - How field values are stored and serialized
247
+ - [**Feature System**](feature-system.md) - How features are mixed into Horreum classes
data/docs/guides/index.md CHANGED
@@ -37,9 +37,13 @@ Welcome to the comprehensive documentation for Familia v2.0. This guide collecti
37
37
  13. **[Quantization](feature-quantization.md)** - Time-based data bucketing for analytics
38
38
  14. **[Time Literals](time-literals.md)** - Time manipulation and formatting utilities
39
39
 
40
+ ### ๐Ÿงน Data Maintenance
41
+
42
+ 15. **[Housekeeping](feature-housekeeping.md)** - Declarative cleanup chores for drifted field values
43
+
40
44
  ### ๐Ÿ› ๏ธ Implementation & Usage
41
45
 
42
- 15. **[Optimized Loading](optimized-loading.md)** - Reduce Redis commands by 50-96% for bulk object loading _(new!)_
46
+ 16. **[Optimized Loading](optimized-loading.md)** - Reduce Redis commands by 50-96% for bulk object loading _(new!)_
43
47
 
44
48
 
45
49
  ## ๐Ÿš€ Quick Start Examples