familia 2.10.0 → 2.11.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -4
  3. data/.github/workflows/release-gem.yml +161 -0
  4. data/.rubocop.yml +3 -1
  5. data/CHANGELOG.rst +160 -0
  6. data/Gemfile +6 -0
  7. data/Gemfile.lock +44 -40
  8. data/README.md +26 -0
  9. data/Rakefile +19 -0
  10. data/docs/guides/datatype-collections.md +30 -2
  11. data/docs/guides/feature-encrypted-fields.md +4 -1
  12. data/docs/guides/feature-migrations.md +45 -0
  13. data/docs/guides/feature-relationships-indexing.md +10 -2
  14. data/docs/guides/feature-relationships-methods.md +41 -0
  15. data/docs/guides/feature-relationships-participation.md +43 -2
  16. data/docs/guides/feature-relationships.md +162 -1
  17. data/docs/migrating/{v2.10.0.md → v2.10.md} +128 -1
  18. data/docs/reference/api-technical.md +2 -1
  19. data/lib/familia/connection/operations.rb +194 -0
  20. data/lib/familia/connection/transaction_core.rb +51 -0
  21. data/lib/familia/connection.rb +1 -1
  22. data/lib/familia/core_ext/securerandom.rb +57 -0
  23. data/lib/familia/data_type/collection_base.rb +22 -7
  24. data/lib/familia/data_type/serialization.rb +21 -4
  25. data/lib/familia/data_type/types/sorted_set.rb +4 -2
  26. data/lib/familia/data_type.rb +1 -1
  27. data/lib/familia/encryption/manager.rb +47 -8
  28. data/lib/familia/encryption/providers/aes_gcm_provider.rb +62 -2
  29. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +5 -0
  30. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +6 -0
  31. data/lib/familia/encryption/request_cache.rb +18 -1
  32. data/lib/familia/features/encrypted_fields.rb +4 -1
  33. data/lib/familia/features/external_identifier.rb +50 -19
  34. data/lib/familia/features/relationships/collection_operations.rb +23 -2
  35. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +8 -9
  36. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +5 -3
  37. data/lib/familia/features/relationships/participation/target_methods.rb +33 -12
  38. data/lib/familia/features/relationships/participation.rb +6 -1
  39. data/lib/familia/features/relationships/participation_membership.rb +12 -5
  40. data/lib/familia/horreum/atomic_write.rb +24 -34
  41. data/lib/familia/horreum/management.rb +22 -7
  42. data/lib/familia/horreum/persistence.rb +110 -52
  43. data/lib/familia/index_descriptor.rb +258 -0
  44. data/lib/familia/migration/base.rb +1 -1
  45. data/lib/familia/migration/errors.rb +2 -0
  46. data/lib/familia/migration/model.rb +1 -1
  47. data/lib/familia/migration/pipeline.rb +1 -1
  48. data/lib/familia/migration/rake_tasks.rb +11 -17
  49. data/lib/familia/migration/registry.rb +4 -0
  50. data/lib/familia/migration/runner.rb +2 -0
  51. data/lib/familia/migration/script.rb +14 -1
  52. data/lib/familia/migration.rb +2 -0
  53. data/lib/familia/secure_identifier.rb +32 -21
  54. data/lib/familia/settings.rb +66 -8
  55. data/lib/familia/utils.rb +15 -0
  56. data/lib/familia/verifiable_identifier.rb +31 -7
  57. data/lib/familia/version.rb +1 -1
  58. data/lib/familia.rb +1 -0
  59. data/try/bug_fixes/class_destroy_index_cleanup_try.rb +50 -0
  60. data/try/bug_fixes/permission_query_try.rb +64 -0
  61. data/try/bug_fixes/sorted_set_members_count_try.rb +40 -0
  62. data/try/bug_fixes/stale_unique_index_try.rb +54 -0
  63. data/try/features/atomic_write_watch_try.rb +108 -18
  64. data/try/features/cross_model_atomic_write_try.rb +402 -0
  65. data/try/features/encryption/aes_gcm_salt_rotation_try.rb +219 -0
  66. data/try/features/encryption/config_persistence_try.rb +35 -0
  67. data/try/features/encryption/request_cache_try.rb +15 -0
  68. data/try/features/external_identifier/external_identifier_try.rb +73 -0
  69. data/try/features/relationships/index_introspection_try.rb +304 -0
  70. data/try/features/relationships/multi_index_each_record_try.rb +211 -0
  71. data/try/features/relationships/participation_each_record_try.rb +247 -0
  72. data/try/features/relationships/participation_membership_security_try.rb +64 -0
  73. data/try/features/relationships/participation_reverse_methods_try.rb +4 -2
  74. data/try/integration/verifiable_identifier_try.rb +24 -3
  75. data/try/investigation/cross_model_atomic_poc_try.rb +130 -0
  76. data/try/support/stress/atomic_write_ownership_stress.rb +152 -0
  77. data/try/thread_safety/atomic_write_ownership_race_try.rb +107 -104
  78. data/try/unit/core/securerandom_polyfill_try.rb +61 -0
  79. data/try/unit/data_types/each_record_try.rb +1 -1
  80. data/try/unit/horreum/auto_indexing_on_save_try.rb +9 -8
  81. metadata +20 -3
  82. data/docs/guides/writing-migrations.md +0 -345
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab706939f766966f0471ff1285db5def4d14bfc9b0d428c5b0caf481595d57c2
4
- data.tar.gz: 6d08805e52f5acd4a90fc117bb8a6b2e352c036daa9916c0a19079af827ddfec
3
+ metadata.gz: c322fb98599ca1596829b047f78d414a3216a4e94a61ded4c6d3a4355dd7780a
4
+ data.tar.gz: a0af405d479ba7547b2fe65fe17e7a9b0fc5fb04cfe8b5563ce10726187d0396
5
5
  SHA512:
6
- metadata.gz: b328108ed4793a4c94ddd1ef1447e759bd369647fc81aca87bafe92eb1f23a71a53ca1d763726da5a631af19e520966075bb65ab40ff29d4a4563ac1a4b1e34a
7
- data.tar.gz: 652d0a36301b9321eba9225ec658f3d72b0aa449bc0133b038055900475fe546fca36e2df2133680f69a938409afd6b637d0541be44dd956f51cf2cb50b205b2
6
+ metadata.gz: ac3aac1967a9730670b6f43f464c001d6c0acad7e599e528d94edb5b60b7939fb59dc5ef320962bfaacd1c204603e36d09647670f2511320a98c9b4fc9f0d715
7
+ data.tar.gz: f5f7e14e48be039962af4bcda0ac6c61248143353f773239c5b4ac0a9e9af498f64618d2f18dc48f903c7349d47e375f6e87c6ed686d7e0bbc6ebb444a4456b7
@@ -23,11 +23,8 @@ jobs:
23
23
  strategy:
24
24
  fail-fast: false
25
25
  matrix:
26
- ruby: ["3.4", "3.5"]
26
+ ruby: ["3.2", "3.3", "3.4", "3.5", "4.0"]
27
27
  continue-on-error: [false]
28
- include:
29
- - ruby: "4.0"
30
- continue-on-error: true
31
28
 
32
29
  services:
33
30
  redis:
@@ -0,0 +1,161 @@
1
+ # Release Gem Workflow
2
+ #
3
+ # Automatically builds and publishes the familia gem to RubyGems.org when a
4
+ # GitHub release is published with a semantic version tag (vMAJOR.MINOR.PATCH,
5
+ # e.g. v2.10.1, v3.0.0).
6
+ #
7
+ # Follows the canonical RubyGems Trusted Publishing workflow:
8
+ # https://guides.rubygems.org/trusted-publishing/
9
+ #
10
+ # ============================================================================
11
+ # SETUP INSTRUCTIONS (one-time)
12
+ # ============================================================================
13
+ #
14
+ # This workflow uses RubyGems Trusted Publishing (OIDC). No long-lived API
15
+ # key needs to be stored in GitHub.
16
+ #
17
+ # ----------------------------------------------------------------------------
18
+ # 1. Configure the trusted publisher on RubyGems.org
19
+ # ----------------------------------------------------------------------------
20
+ #
21
+ # a. Sign in to https://rubygems.org and enable MFA on your account
22
+ # (required for trusted publishing).
23
+ #
24
+ # b. Open the gem's trusted publisher page (works for both existing gems
25
+ # and the first-ever publish):
26
+ # Existing gem: https://rubygems.org/gems/familia/trusted_publishers
27
+ # First publish: https://rubygems.org/profile/oidc/pending_trusted_publishers/new
28
+ #
29
+ # c. Click "Create" and fill in:
30
+ # Publisher type: GitHub Actions
31
+ # Repository owner: delano
32
+ # Repository name: familia
33
+ # Workflow filename: release-gem.yml
34
+ # Environment: rubygems.org (must match `environment.name` below)
35
+ #
36
+ # d. Save. RubyGems will now accept short-lived OIDC tokens issued by this
37
+ # workflow running in this repository.
38
+ #
39
+ # ----------------------------------------------------------------------------
40
+ # 2. Configure the GitHub environment
41
+ # ----------------------------------------------------------------------------
42
+ #
43
+ # a. In GitHub: Settings -> Environments -> New environment
44
+ # Name: `rubygems.org` (must match `environment.name` below)
45
+ #
46
+ # b. Under "Deployment branches and tags", restrict to tags matching:
47
+ # v*.*.*
48
+ # This prevents the environment (and its OIDC token) from being used
49
+ # from any branch or non-release tag.
50
+ #
51
+ # c. Optional but recommended: add required reviewers to gate publishes
52
+ # behind manual approval.
53
+ #
54
+ # No GitHub secrets are required - OIDC handles authentication.
55
+ #
56
+ # ----------------------------------------------------------------------------
57
+ # 3. Cutting a release
58
+ # ----------------------------------------------------------------------------
59
+ #
60
+ # a. Bump Familia::VERSION in lib/familia/version.rb (semver: MAJOR.MINOR.PATCH).
61
+ # b. Update CHANGELOG.rst and commit on main.
62
+ # c. On GitHub, draft a Release with tag `vX.Y.Z` (matching the constant)
63
+ # and publish it. This workflow runs automatically: it verifies the tag
64
+ # matches the gemspec version, builds the gem, and pushes it.
65
+ #
66
+ # ----------------------------------------------------------------------------
67
+ # Fallback: API key (only if Trusted Publishing isn't an option)
68
+ # ----------------------------------------------------------------------------
69
+ #
70
+ # 1. Create an API key at https://rubygems.org/profile/api_keys with scope
71
+ # "Push rubygem" restricted to the `familia` gem.
72
+ # 2. Store it as a repo secret named `RUBYGEMS_API_KEY`.
73
+ # 3. Drop the `id-token: write` permission and `environment:` block, and
74
+ # replace the `rubygems/release-gem` step with:
75
+ #
76
+ # - name: Publish to RubyGems
77
+ # env:
78
+ # GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
79
+ # run: gem push familia-*.gem
80
+ #
81
+ # ----------------------------------------------------------------------------
82
+ # Action provenance (SHA pins)
83
+ # ----------------------------------------------------------------------------
84
+ #
85
+ # All third-party actions below are pinned to a full commit SHA, per GitHub's
86
+ # secure-use guidance for release-critical workflows. The accompanying
87
+ # comment records the tag the SHA was resolved from so renovate/dependabot
88
+ # can keep them current.
89
+ #
90
+ # actions/checkout - official GitHub action
91
+ # ruby/setup-ruby - official Ruby org action (GitHub-verified creator)
92
+ # rubygems/release-gem - official RubyGems action for trusted publishing
93
+ #
94
+ # ============================================================================
95
+
96
+ name: Release Gem
97
+
98
+ on:
99
+ release:
100
+ types: [published]
101
+
102
+ # Coarse default. Each job re-declares the narrowest permissions it needs.
103
+ permissions:
104
+ contents: read
105
+
106
+ # Don't allow two release runs to race - one published tag, one publish.
107
+ concurrency:
108
+ group: release-gem-${{ github.event.release.tag_name }}
109
+ cancel-in-progress: false
110
+
111
+ jobs:
112
+ release:
113
+ name: Build and push gem
114
+ runs-on: ubuntu-latest
115
+ timeout-minutes: 10
116
+
117
+ environment:
118
+ name: rubygems.org
119
+ url: https://rubygems.org/gems/familia
120
+
121
+ permissions:
122
+ contents: write # rubygems/release-gem attaches built .gem to the GitHub release
123
+ id-token: write # OIDC token for RubyGems Trusted Publishing
124
+
125
+ steps:
126
+ - name: Checkout
127
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
128
+ with:
129
+ persist-credentials: false
130
+
131
+ - name: Set up Ruby
132
+ uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
133
+ with:
134
+ bundler-cache: true
135
+ # Pinned to 3.2, the oldest non-experimental Ruby in ci.yml's matrix
136
+ # and the gemspec's required_ruby_version floor, so the release build
137
+ # matches the minimum supported configuration that CI exercises.
138
+ ruby-version: "3.2"
139
+
140
+ - name: Verify release tag matches Familia::VERSION
141
+ env:
142
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
143
+ run: |
144
+ set -euo pipefail
145
+ case "${RELEASE_TAG}" in
146
+ v[0-9]*.[0-9]*.[0-9]*) ;;
147
+ *)
148
+ echo "Release tag '${RELEASE_TAG}' is not a vMAJOR.MINOR.PATCH semver tag." >&2
149
+ exit 1
150
+ ;;
151
+ esac
152
+ tag_version="${RELEASE_TAG#v}"
153
+ gem_version="$(ruby -r ./lib/familia/version.rb -e 'print Familia::VERSION')"
154
+ if [ "${tag_version}" != "${gem_version}" ]; then
155
+ echo "Release tag ${RELEASE_TAG} (${tag_version}) != Familia::VERSION (${gem_version})." >&2
156
+ exit 1
157
+ fi
158
+ echo "Releasing familia ${gem_version} from tag ${RELEASE_TAG}"
159
+
160
+ - name: Build and push gem to RubyGems
161
+ uses: rubygems/release-gem@6317d8d1f7e28c24d28f6eff169ea854948bd9f7 # v1.2.0
data/.rubocop.yml CHANGED
@@ -135,9 +135,11 @@ Style/StringLiterals:
135
135
  Enabled: true
136
136
  EnforcedStyle: single_quotes
137
137
 
138
+ # Every lib/ file carries `# frozen_string_literal: true`; require it rather
139
+ # than flag it (the previous `never` contradicted the entire codebase).
138
140
  Style/FrozenStringLiteralComment:
139
141
  Enabled: true
140
- EnforcedStyle: never
142
+ EnforcedStyle: always_true
141
143
 
142
144
  Naming/MemoizedInstanceVariableName:
143
145
  Enabled: false
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,166 @@ 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.11.0:
11
+
12
+ 2.11.0 — 2026-06-22
13
+ ===================
14
+
15
+ Added
16
+ -----
17
+
18
+ - Project-wide relationship introspection: ``Familia.index_descriptors``,
19
+ ``Familia.unique_indexes``, ``Familia.multi_indexes``, and
20
+ ``Familia.participation_descriptors`` aggregate index/participation metadata
21
+ across all loaded ``Horreum`` subclasses, returning ``Familia::IndexDescriptor``
22
+ objects (``coordinate``, ``each_record``, ``rebuild!``, ``stale_format?``).
23
+
24
+ - ``Familia.stale_indexes`` and ``Familia.assert_indexes_current!`` detect
25
+ class-level unique indexes still holding pre-2.10.0 JSON-encoded identifiers and
26
+ fail fast (or warn) at boot. Rebuild with ``Familia.stale_indexes.each(&:rebuild!)``.
27
+
28
+ - ``Familia.legacy_json_encoded?`` predicate for detecting legacy-format
29
+ identifiers.
30
+
31
+ Changed
32
+ -------
33
+
34
+ - New ``encryption_hkdf_salt`` and ``encryption_hkdf_salt_history`` settings
35
+ configure the AES-GCM HKDF salt, decoupled from ``encryption_personalization``
36
+ (now used only by the XChaCha20/BLAKE2b providers, still capped at 16 bytes). The
37
+ salt has no length limit and supports rotation; no data migration is required
38
+ (issue #311).
39
+
40
+ - ``feature :external_identifier`` accepts a callable ``secret:``, resolved lazily
41
+ at first use (issue #311).
42
+
43
+ - The opt-in request-scoped key cache keys on the resolved salt, so an encrypt and a
44
+ later decrypt of the same value within one request share a single derived key
45
+ (issue #311).
46
+
47
+ Fixed
48
+ -----
49
+
50
+ - ``SortedSet#members(n)`` / ``#revmembers(n)`` returned one fewer element than
51
+ requested.
52
+
53
+ - Participation permission queries (``<collection>_with_permission``) now return
54
+ matching members instead of raising on a non-existent ``zrangebyscore`` call.
55
+
56
+ - Class-level ``Model.destroy!(id)`` now removes the record's unique-index entries
57
+ instead of leaving them orphaned, so a stale ``find_by_<field>`` can no longer
58
+ resolve a deleted record.
59
+
60
+ - Changing a ``unique_index`` field value and saving now removes the old value's
61
+ index entry in the same transaction (``multi_index`` keeps add-only semantics).
62
+
63
+ Security
64
+ --------
65
+
66
+ - ``Familia::VerifiableIdentifier`` now requires ``VERIFIABLE_ID_HMAC_SECRET``
67
+ (issue #310, S1). The committed fallback secret, which allowed identifier
68
+ forgery, is removed; the secret is read lazily, so requiring the file without it
69
+ set does not raise.
70
+
71
+ - AES-GCM keys derive from a per-deployment ``encryption_hkdf_salt`` instead of a
72
+ static library salt (issue #310, S2). A blank salt or personalization now raises
73
+ rather than silently using a weak/global value; existing ciphertext still
74
+ decrypts (issue #311).
75
+
76
+ - External identifiers derive via SHA-256, or keyed HMAC-SHA256 with a ``secret:``,
77
+ instead of a Mersenne-Twister PRNG seeded from a truncated digest (issue #310, S3).
78
+
79
+ - ``ParticipationMembership#target_instance`` resolves class names through the
80
+ ``Familia.resolve_class`` allowlist instead of ``Object.const_get`` (issue #310, S4).
81
+
82
+ - The request-scoped key cache is wiped on entry to ``with_request_cache`` as well
83
+ as on exit, so a reused fiber cannot inherit another request's keys (issue #310, S6).
84
+
85
+ Documentation
86
+ -------------
87
+
88
+ - Documented the relationship introspection API and stale-index boot guard across
89
+ the relationships guide and methods reference. Renamed
90
+ ``docs/migrating/v2.10.0.md`` to ``docs/migrating/v2.10.md``.
91
+
92
+ - Clarified that the migration ``Script`` SHA-1 is the Redis ``EVALSHA`` identity,
93
+ not a security checksum (issue #310, S5).
94
+
95
+ AI Assistance
96
+ -------------
97
+
98
+ - The 2.11.0 changes, their tryouts, and this changelog were drafted with AI
99
+ assistance.
100
+
101
+ .. _changelog-2.10.1:
102
+
103
+ 2.10.1 — 2026-06-06
104
+ ===================
105
+
106
+ Added
107
+ -----
108
+
109
+ - ``record_class:`` option for collection DataTypes (``list``/``set``/
110
+ ``sorted_set``/``hashkey``). This loading-only hint tells ``each_record`` which
111
+ class to hydrate via ``load_multi`` without changing how the collection
112
+ serializes or deserializes reads. Use this when you want ``each_record`` lookup
113
+ behavior but no changes to read behavior. Issue #297
114
+
115
+ - ``Familia.atomic_write(*instances)`` persists multiple Horreum instances in a
116
+ single ``MULTI/EXEC``. Includes an optional ``watch_keys:``/``pre_check:``
117
+ variant for race-safe, write-once semantics. All participating instances must
118
+ resolve to the same logical database (raising ``Familia::CrossDatabaseError``
119
+ otherwise) and must share a hash slot on Redis Cluster. #296
120
+
121
+ Changed
122
+ -------
123
+
124
+ - ``participates_in`` / ``class_participates_in`` collections now default to
125
+ using ``record_class:``. This change requires **no data migration and causes no
126
+ behavior changes**: existing collections already stored raw identifiers, and
127
+ read operations (``members``, ``to_a``, ``member?``, ``score``) behave exactly
128
+ as before. The only difference is that ``each_record`` is now supported. Pre-
129
+ declared collections are left untouched. Issue #297
130
+
131
+ Fixed
132
+ -----
133
+
134
+ - Enabled ``each_record`` on ``participates_in`` and ``class_participates_in``
135
+ collections by automatically declaring them with ``record_class: <participant
136
+ class>``. This resolves ``Familia::Problem`` exceptions and loads participant
137
+ records via ``load_multi`` across all collection types. Issue #297
138
+
139
+ - Suppressed per-member ``[deserialize] Raw fallback`` warning storm when
140
+ iterating ``record_class`` collections with non-JSON identifiers (such as UUIDs
141
+ or prefixed IDs). These expected raw values are now logged at the debug level
142
+ instead of warnings. Issue #297
143
+
144
+ - Resolved a connection-pooling bug where the ``WATCH``-based optimistic lock
145
+ in ``atomic_write(watch_keys:)``, ``save_if_not_exists!``, and ``create!`` was
146
+ silent/inert. The ``WATCH`` and ``MULTI/EXEC`` commands are now driven through
147
+ the same connection, ensuring concurrent modifications correctly abort and raise
148
+ as
149
+ documented. #296
150
+
151
+ AI Assistance
152
+ -------------
153
+
154
+ - AI diagnosed the participation iteration bug and identified that ``reference: true``
155
+ introduced unintended read-behavior changes. Designed and implemented the
156
+ ``record_class:`` option to decouple ``each_record`` lookup from read deserialization,
157
+ suppressed a resulting per-member deserialize warning storm, kept intentional
158
+ raw-string semantics on ``instances`` and ``unique_index``, updated stale
159
+ flowcharts in ``datatype-collections.md``, and added regression coverage. Issue #297
160
+
161
+ - Root-caused and fixed a split-connection defect with Claude Code: implemented a
162
+ single-connection ``execute_watched_transaction`` primitive (avoiding fiber-pinning
163
+ that degrades atomic commands) and added real concurrent-modification tests to
164
+ replace simulated aborts. #296
165
+
166
+ - Designed and built multi-model atomic writes on top of the new ``WATCH`` primitive:
167
+ implemented the same-database guard, orchestration logic, and a test suite covering
168
+ two-model commits, rollback on error, cross-database rejection, and race conditions. #296
169
+
10
170
  .. _changelog-2.10.0:
11
171
 
12
172
  2.10.0 — 2026-06-04
data/Gemfile CHANGED
@@ -15,6 +15,12 @@ end
15
15
  group :development, :test do
16
16
  gem 'benchmark', '~> 0.4', require: false
17
17
  gem 'debug', require: false
18
+ # reek pulls in dry-configurable transitively (via dry-schema). Its 1.4.0
19
+ # release bumped the minimum Ruby to 3.3, which breaks `bundle install` on
20
+ # Ruby 3.2 (our gemspec's required_ruby_version floor). 1.4.0 only adds
21
+ # Config#to_data, which we don't use (we don't use Dry::Configurable at all),
22
+ # so cap below 1.4 to keep the dev bundle installable on Ruby 3.2.
23
+ gem 'dry-configurable', '>= 1.3', '< 1.5', require: false
18
24
  gem 'irb', '~> 1.15.2', require: false
19
25
  gem 'json_schemer', '~> 2.0', require: false
20
26
  gem 'rake', '~> 13.0', require: false
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (2.10.0)
4
+ familia (2.11.0)
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
32
  dry-configurable (1.3.0)
33
33
  dry-core (~> 1.1)
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.2.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
@@ -201,6 +204,7 @@ DEPENDENCIES
201
204
  benchmark (~> 0.4)
202
205
  concurrent-ruby (~> 1.3.6)
203
206
  debug
207
+ dry-configurable (>= 1.3, < 1.5)
204
208
  familia!
205
209
  irb (~> 1.15.2)
206
210
  json_schemer (~> 2.0)
data/README.md CHANGED
@@ -475,6 +475,32 @@ Contributions are welcome! Please feel free to submit a Pull Request.
475
475
  4. Push to the branch (`git push origin feature/amazing-feature`)
476
476
  5. Open a Pull Request
477
477
 
478
+ ### Running the Tests
479
+
480
+ Familia's test suite uses the [Tryouts](https://github.com/delano/tryouts)
481
+ framework. The simplest way to run it:
482
+
483
+ ```bash
484
+ bundle install
485
+ bundle exec rake test # full suite, with a guaranteed UTF-8 locale
486
+ ```
487
+
488
+ `rake test` runs the suite in a child process with a UTF-8 locale, which the
489
+ suite needs: some tryouts contain UTF-8 source (e.g. Unicode cases) and assert on
490
+ string encodings, so when no locale is set Ruby falls back to
491
+ `Encoding.default_external = US-ASCII`, the runner aborts with `invalid byte
492
+ sequence in US-ASCII`, and encoding specs fail spuriously. (A locale you already
493
+ have is kept; most shells and CI provide a UTF-8 one.)
494
+
495
+ To invoke the runner directly — e.g. for a single file — ensure your shell has a
496
+ UTF-8 locale first:
497
+
498
+ ```bash
499
+ export LANG=C.UTF-8 # or en_US.UTF-8; only needed if unset
500
+ bundle exec try -vf # full suite
501
+ bundle exec try try/path/to/foo_try.rb # a single file
502
+ ```
503
+
478
504
  ### PR Compliance Checks
479
505
 
480
506
  Pull requests are automatically reviewed by [Qodo Merge](https://qodo.ai) with compliance checks for:
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Run the Tryouts test suite with a guaranteed UTF-8 locale.
4
+ #
5
+ # The suite's source and specs are UTF-8. The tryouts runner reads each test file
6
+ # with File.read, which honours Encoding.default_external; when the caller's
7
+ # locale is unset that defaults to US-ASCII and the run aborts on non-ASCII source
8
+ # ("invalid byte sequence in US-ASCII"), with encoding-sensitive specs failing
9
+ # spuriously. Running the suite in a child process with a UTF-8 locale makes it
10
+ # work regardless of the caller's environment. A caller that already has a UTF-8
11
+ # locale (CI, most shells) keeps it -- the defaults below only fill in when unset.
12
+ desc 'Run the Tryouts test suite (ensures a UTF-8 locale)'
13
+ task :test do
14
+ ENV['LANG'] ||= 'C.UTF-8'
15
+ ENV['LC_ALL'] ||= 'C.UTF-8'
16
+ sh 'bundle exec try -vf'
17
+ end
18
+
19
+ task default: :test