familia 2.11.0 → 2.11.2

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +15 -0
  3. data/.github/workflows/ci.yml +2 -2
  4. data/.github/workflows/claude-code-review.yml +33 -9
  5. data/.github/workflows/claude.yml +9 -2
  6. data/.github/workflows/code-smells.yml +2 -2
  7. data/.github/workflows/codeql.yml +101 -0
  8. data/.github/workflows/release-gem.yml +6 -6
  9. data/.github/workflows/ruby-lint.yml +66 -0
  10. data/.github/workflows/yardoc.yml +110 -0
  11. data/.yardopts +1 -1
  12. data/CHANGELOG.rst +150 -0
  13. data/Gemfile +3 -3
  14. data/Gemfile.lock +14 -22
  15. data/Rakefile +10 -0
  16. data/docs/guides/feature-encrypted-fields.md +41 -11
  17. data/docs/investigation/memory-audit.md +465 -0
  18. data/docs/overview.md +1 -1
  19. data/examples/encryption_upgrade_proof/.gitignore +3 -0
  20. data/examples/encryption_upgrade_proof/README.md +84 -0
  21. data/examples/encryption_upgrade_proof/common.rb +83 -0
  22. data/examples/encryption_upgrade_proof/gemfiles/Gemfile.dev-no-libsodium +4 -0
  23. data/examples/encryption_upgrade_proof/gemfiles/Gemfile.dev-with-libsodium +5 -0
  24. data/examples/encryption_upgrade_proof/gemfiles/Gemfile.production-today +4 -0
  25. data/examples/encryption_upgrade_proof/model.rb +32 -0
  26. data/examples/encryption_upgrade_proof/phase0_production_today.rb +100 -0
  27. data/examples/encryption_upgrade_proof/phase1_gem_upgrade_no_libsodium.rb +99 -0
  28. data/examples/encryption_upgrade_proof/phase2_libsodium_enabled.rb +297 -0
  29. data/examples/encryption_upgrade_proof/phase3_rollback_hazard.rb +56 -0
  30. data/examples/encryption_upgrade_proof/run.sh +60 -0
  31. data/familia.gemspec +1 -1
  32. data/lib/familia/data_type/database_commands.rb +8 -1
  33. data/lib/familia/data_type/types/stringkey.rb +10 -3
  34. data/lib/familia/encryption/provider.rb +9 -0
  35. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +4 -0
  36. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -0
  37. data/lib/familia/encryption/registry.rb +47 -5
  38. data/lib/familia/encryption.rb +19 -37
  39. data/lib/familia/features/encrypted_fields/concealed_string.rb +27 -0
  40. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +21 -3
  41. data/lib/familia/features/encrypted_fields.rb +27 -4
  42. data/lib/familia/horreum/database_commands.rb +7 -1
  43. data/lib/familia/horreum/persistence.rb +114 -19
  44. data/lib/familia/horreum/serialization.rb +16 -2
  45. data/lib/familia/verifiable_identifier.rb +24 -11
  46. data/lib/familia/version.rb +1 -1
  47. data/try/bug_fixes/stringkey_exists_try.rb +153 -0
  48. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +15 -24
  49. data/try/features/encrypted_fields/per_field_algorithm_try.rb +164 -0
  50. data/try/features/encrypted_fields/re_encrypt_fields_try.rb +7 -7
  51. data/try/features/encryption/algorithm_upgrade_try.rb +116 -0
  52. data/try/features/encryption/registry_algorithm_resolution_try.rb +125 -0
  53. data/try/integration/database_consistency_try.rb +4 -3
  54. data/try/integration/nil_field_absence_try.rb +160 -0
  55. data/try/integration/verifiable_identifier_try.rb +66 -2
  56. data/try/investigation/memory_leak_proof.rb +415 -0
  57. data/try/investigation/process_memory_leak_proof.rb +205 -0
  58. data/try/unit/data_types/enumerable_consistency/large_scale_consistency_try.rb +34 -8
  59. data/try/unit/horreum/serialization_try.rb +6 -3
  60. metadata +33 -4
  61. data/.github/workflows/docs.yml +0 -87
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c322fb98599ca1596829b047f78d414a3216a4e94a61ded4c6d3a4355dd7780a
4
- data.tar.gz: a0af405d479ba7547b2fe65fe17e7a9b0fc5fb04cfe8b5563ce10726187d0396
3
+ metadata.gz: d7b0eed1c3632c68c8c4c91617830f0b15a10907da5c7af0bbc14b9cf67a5336
4
+ data.tar.gz: 4a123b805e826b8ede218675c16781f428fab6c3194968c5a3840323b949b28e
5
5
  SHA512:
6
- metadata.gz: ac3aac1967a9730670b6f43f464c001d6c0acad7e599e528d94edb5b60b7939fb59dc5ef320962bfaacd1c204603e36d09647670f2511320a98c9b4fc9f0d715
7
- data.tar.gz: f5f7e14e48be039962af4bcda0ac6c61248143353f773239c5b4ac0a9e9af498f64618d2f18dc48f903c7349d47e375f6e87c6ed686d7e0bbc6ebb444a4456b7
6
+ metadata.gz: 6c245370d354eaa0d8af0b0994cd44ea10f8dec3b260c9750d3cceb66563c51ee8f4390062429c8aced6cd578d919c7a806a0eef2e5be0f9d90bbc00dcf87b06
7
+ data.tar.gz: a824edcd1af5d3535168600d90a3e189b0203c884fe17c7382cafb22ebc6c19a36c68011a468e9dd8b16fed386b902eac16709625004f73b7f4085b0ae0a601e
@@ -0,0 +1,15 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "github-actions"
9
+ directory: "/" # Location of package manifests
10
+ schedule:
11
+ interval: "weekly"
12
+ - package-ecosystem: "bundler"
13
+ directory: "/" # Location of package manifests
14
+ schedule:
15
+ interval: "weekly"
@@ -41,7 +41,7 @@ jobs:
41
41
  - 2525:6379
42
42
 
43
43
  steps:
44
- - uses: actions/checkout@v4
44
+ - uses: actions/checkout@v7
45
45
  - name: Set up Ruby
46
46
  uses: ruby/setup-ruby@v1
47
47
  with:
@@ -53,7 +53,7 @@ jobs:
53
53
  bundler-cache: true
54
54
 
55
55
  - name: Setup tmate session
56
- uses: mxschmitt/action-tmate@7b6a61a73bbb9793cb80ad69b8dd8ac19261834c # v3
56
+ uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3
57
57
  if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
58
58
  with:
59
59
  detached: true
@@ -4,14 +4,34 @@ on:
4
4
  pull_request:
5
5
  types: [opened, synchronize, labeled]
6
6
  workflow_dispatch:
7
+ inputs:
8
+ model:
9
+ description: "Claude model for this run (overrides the CLAUDE_MODEL repo variable)"
10
+ type: choice
11
+ required: false
12
+ default: claude-opus-4-6
13
+ options:
14
+ - claude-opus-4-6
15
+ - claude-sonnet-4-6
16
+ - claude-haiku-4-5-20251001
7
17
 
8
18
  jobs:
9
19
  claude-review:
10
- # Run on opened/synchronize, but only on 'labeled' if label is 'claude-review'
11
- if: |
12
- github.event.action == 'opened' ||
13
- github.event.action == 'synchronize' ||
14
- (github.event.action == 'labeled' && github.event.label.name == 'claude-review')
20
+ # Run only when ALL of the following hold:
21
+ # 1. Author is not a bot (renovate, dependabot, etc. surface as 'name[bot]').
22
+ # 2. PR head is not from a fork. Fork PRs run without repository secrets, so
23
+ # CLAUDE_CODE_OAUTH_TOKEN is empty and the action would fail.
24
+ # 3. The event is a freshly opened PR, a 'claude-review' label add, or a push
25
+ # to a PR that already carries the 'claude-review' label.
26
+ if: ${{
27
+ !endsWith(github.actor, '[bot]') &&
28
+ !github.event.pull_request.head.repo.fork &&
29
+ (
30
+ (github.event.action == 'opened') ||
31
+ (github.event.action == 'labeled' && github.event.label.name == 'claude-review') ||
32
+ (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'claude-review'))
33
+ )
34
+ }}
15
35
 
16
36
  runs-on: ubuntu-latest
17
37
  permissions:
@@ -22,7 +42,7 @@ jobs:
22
42
 
23
43
  steps:
24
44
  - name: Checkout repository
25
- uses: actions/checkout@v4
45
+ uses: actions/checkout@v7
26
46
  with:
27
47
  fetch-depth: 1
28
48
 
@@ -32,6 +52,13 @@ jobs:
32
52
  with:
33
53
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
34
54
 
55
+ # Primary model precedence: the workflow_dispatch "model" input (manual
56
+ # runs) -> the CLAUDE_MODEL repo variable -> this built-in default.
57
+ # Pinning a current id avoids the action's frozen default, which 404s
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' }}"
61
+
35
62
  # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
36
63
  use_sticky_comment: true
37
64
 
@@ -50,6 +77,3 @@ jobs:
50
77
  # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
51
78
  # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
52
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:*)"
53
-
54
- # Allow bot-initiated workflows (e.g., qodo, dependabot)
55
- allowed_bots: "*"
@@ -26,16 +26,23 @@ jobs:
26
26
  actions: read # Required for Claude to read CI results on PRs
27
27
  steps:
28
28
  - name: Checkout repository
29
- uses: actions/checkout@v4
29
+ uses: actions/checkout@v7
30
30
  with:
31
31
  fetch-depth: 1
32
32
 
33
33
  - name: Run Claude Code
34
34
  id: claude
35
- uses: anthropics/claude-code-action@v1
35
+ uses: anthropics/claude-code-action@beta
36
36
  with:
37
37
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
38
 
39
+ # Primary model: set the CLAUDE_MODEL repo variable to override without
40
+ # editing this file. Pinning a current id avoids the action's frozen
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' }}"
45
+
39
46
  # This is an optional setting that allows Claude to read CI results on PRs
40
47
  additional_permissions: |
41
48
  actions: read
@@ -21,7 +21,7 @@ jobs:
21
21
 
22
22
  steps:
23
23
  - name: Checkout code
24
- uses: actions/checkout@v4
24
+ uses: actions/checkout@v7
25
25
 
26
26
  - name: Set up Ruby
27
27
  uses: ruby/setup-ruby@v1
@@ -91,7 +91,7 @@ jobs:
91
91
 
92
92
  steps:
93
93
  - name: Checkout code
94
- uses: actions/checkout@v4
94
+ uses: actions/checkout@v7
95
95
 
96
96
  - name: Set up Ruby
97
97
  uses: ruby/setup-ruby@v1
@@ -0,0 +1,101 @@
1
+ # For most projects, this workflow file will not need changing; you simply need
2
+ # to commit it to your repository.
3
+ #
4
+ # You may wish to alter this file to override the set of languages analyzed,
5
+ # or to provide custom queries or build logic.
6
+ #
7
+ # ******** NOTE ********
8
+ # We have attempted to detect the languages in your repository. Please check
9
+ # the `language` matrix defined below to confirm you have the correct set of
10
+ # supported CodeQL languages.
11
+ #
12
+ name: "CodeQL Advanced"
13
+
14
+ on:
15
+ push:
16
+ branches: [ "main" ]
17
+ pull_request:
18
+ branches: [ "main" ]
19
+ schedule:
20
+ - cron: '30 0 * * 6'
21
+
22
+ jobs:
23
+ analyze:
24
+ name: Analyze (${{ matrix.language }})
25
+ # Runner size impacts CodeQL analysis time. To learn more, please see:
26
+ # - https://gh.io/recommended-hardware-resources-for-running-codeql
27
+ # - https://gh.io/supported-runners-and-hardware-resources
28
+ # - https://gh.io/using-larger-runners (GitHub.com only)
29
+ # Consider using larger runners or machines with greater resources for possible analysis time improvements.
30
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
31
+ permissions:
32
+ # required for all workflows
33
+ security-events: write
34
+
35
+ # required to fetch internal or private CodeQL packs
36
+ packages: read
37
+
38
+ # only required for workflows in private repositories
39
+ actions: read
40
+ contents: read
41
+
42
+ strategy:
43
+ fail-fast: false
44
+ matrix:
45
+ include:
46
+ - language: actions
47
+ build-mode: none
48
+ - language: ruby
49
+ build-mode: none
50
+ # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
51
+ # Use `c-cpp` to analyze code written in C, C++ or both
52
+ # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
53
+ # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
54
+ # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
55
+ # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
56
+ # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
57
+ # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
58
+ steps:
59
+ - name: Checkout repository
60
+ uses: actions/checkout@v7
61
+
62
+ # Add any setup steps before running the `github/codeql-action/init` action.
63
+ # This includes steps like installing compilers or runtimes (`actions/setup-node`
64
+ # or others). This is typically only required for manual builds.
65
+ # - name: Setup runtime (example)
66
+ # uses: actions/setup-example@v1
67
+
68
+ # Initializes the CodeQL tools for scanning.
69
+ - name: Initialize CodeQL
70
+ uses: github/codeql-action/init@v4
71
+ with:
72
+ languages: ${{ matrix.language }}
73
+ build-mode: ${{ matrix.build-mode }}
74
+ # If you wish to specify custom queries, you can do so here or in a config file.
75
+ # By default, queries listed here will override any specified in a config file.
76
+ # Prefix the list here with "+" to use these queries and those in the config file.
77
+
78
+ # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
79
+ # queries: security-extended,security-and-quality
80
+
81
+ # If the analyze step fails for one of the languages you are analyzing with
82
+ # "We were unable to automatically build your code", modify the matrix above
83
+ # to set the build mode to "manual" for that language. Then modify this step
84
+ # to build your code.
85
+ # ℹ️ Command-line programs to run using the OS shell.
86
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
87
+ - name: Run manual build steps
88
+ if: matrix.build-mode == 'manual'
89
+ shell: bash
90
+ run: |
91
+ echo 'If you are using a "manual" build mode for one or more of the' \
92
+ 'languages you are analyzing, replace this with the commands to build' \
93
+ 'your code, for example:'
94
+ echo ' make bootstrap'
95
+ echo ' make release'
96
+ exit 1
97
+
98
+ - name: Perform CodeQL Analysis
99
+ uses: github/codeql-action/analyze@v4
100
+ with:
101
+ category: "/language:${{matrix.language}}"
@@ -124,7 +124,7 @@ jobs:
124
124
 
125
125
  steps:
126
126
  - name: Checkout
127
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
127
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
128
128
  with:
129
129
  persist-credentials: false
130
130
 
@@ -132,10 +132,10 @@ jobs:
132
132
  uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
133
133
  with:
134
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"
135
+ # Pinned to 3.3 to match the otto/rhales release builds. The gem is
136
+ # pure Ruby, so the build Ruby is independent of the supported range
137
+ # declared in the gemspec (>= 3.2).
138
+ ruby-version: "3.3"
139
139
 
140
140
  - name: Verify release tag matches Familia::VERSION
141
141
  env:
@@ -158,4 +158,4 @@ jobs:
158
158
  echo "Releasing familia ${gem_version} from tag ${RELEASE_TAG}"
159
159
 
160
160
  - name: Build and push gem to RubyGems
161
- uses: rubygems/release-gem@6317d8d1f7e28c24d28f6eff169ea854948bd9f7 # v1.2.0
161
+ uses: rubygems/release-gem@052cc82692552de3ef2b81fd670e41d13cba8092 # v1.4.0
@@ -0,0 +1,66 @@
1
+ # .github/workflows/ruby-lint.yml
2
+
3
+ name: Ruby Lint
4
+
5
+ # Runs RuboCop across the supported Ruby versions. Linting is informational:
6
+ # every dependency/lint step is continue-on-error, so style findings surface as
7
+ # annotations without blocking merges.
8
+
9
+ on:
10
+ push:
11
+ branches:
12
+ - fix/*
13
+ - rel/*
14
+ pull_request:
15
+ branches:
16
+ - main
17
+ - develop
18
+ - feature/*
19
+ workflow_dispatch:
20
+ inputs:
21
+ debug_enabled:
22
+ type: boolean
23
+ description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
24
+ required: false
25
+ default: false
26
+
27
+ permissions:
28
+ contents: read
29
+
30
+ jobs:
31
+ lint:
32
+ timeout-minutes: 10 # prevent hung jobs
33
+
34
+ runs-on: ubuntu-24.04
35
+
36
+ strategy:
37
+ fail-fast: true
38
+ matrix:
39
+ ruby: ['3.2', '3.3', '3.4', '3.5', '4.0']
40
+ continue-on-error: [true]
41
+
42
+ steps:
43
+ - name: Checkout code
44
+ uses: actions/checkout@v7
45
+
46
+ - uses: ruby/setup-ruby@v1
47
+ with:
48
+ ruby-version: ${{ matrix.ruby }}
49
+ bundler-cache: true
50
+
51
+ - name: Setup tmate session
52
+ uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3
53
+ if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
54
+ with:
55
+ detached: true
56
+
57
+ - name: Install dependencies
58
+ continue-on-error: ${{ matrix.continue-on-error }}
59
+ run: |
60
+ bundle config path vendor/bundle
61
+ bundle install --jobs 4 --retry 3
62
+
63
+ - name: Run Rubocop
64
+ continue-on-error: ${{ matrix.continue-on-error }}
65
+ run: |
66
+ bundle exec rubocop --config .rubocop.yml --format json --fail-level warning
@@ -0,0 +1,110 @@
1
+ name: Generate and Deploy YARD Documentation
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - 'lib/**/*'
9
+ - 'docs/**/*'
10
+ - 'README.md'
11
+ - 'CHANGELOG.md'
12
+ - 'CHANGELOG.rst'
13
+ - 'LICENSE.txt'
14
+ - '.yardopts'
15
+ - '.github/workflows/yardoc.yml'
16
+ workflow_dispatch:
17
+
18
+ permissions:
19
+ contents: read
20
+ pages: write
21
+ id-token: write
22
+
23
+ # Allow only one concurrent deployment, skipping runs queued between the run
24
+ # in-progress and latest queued. Do NOT cancel in-progress runs so production
25
+ # deployments can complete.
26
+ concurrency:
27
+ group: "pages"
28
+ cancel-in-progress: false
29
+
30
+ jobs:
31
+ build-docs:
32
+ timeout-minutes: 10
33
+ runs-on: ubuntu-latest
34
+ name: Generate YARD Documentation
35
+
36
+ steps:
37
+ - name: Checkout repository
38
+ uses: actions/checkout@v7
39
+ with:
40
+ fetch-depth: 0
41
+
42
+ - name: Set up Ruby environment
43
+ uses: ruby/setup-ruby@v1
44
+ with:
45
+ ruby-version: '3.4'
46
+ bundler-cache: true
47
+
48
+ - name: Generate documentation
49
+ # Uses the repository's committed .yardopts as the single source of
50
+ # truth for the output dir, includes/excludes, markup, and tags.
51
+ run: |
52
+ echo "::group::YARD Documentation Generation"
53
+ bundle exec yard stats --list-undoc || true
54
+ bundle exec yard doc
55
+ echo "::endgroup::"
56
+
57
+ - name: Disable Jekyll processing
58
+ # YARD emits files and directories that begin with underscores; the
59
+ # .nojekyll marker stops GitHub Pages from stripping them.
60
+ run: touch doc/.nojekyll
61
+
62
+ - name: Validate documentation output
63
+ run: |
64
+ echo "::group::Documentation Validation"
65
+ if [ ! -d "doc" ]; then
66
+ echo "Error: documentation directory 'doc' was not generated." >&2
67
+ echo "Ensure .yardopts sets '--output-dir doc'." >&2
68
+ exit 1
69
+ fi
70
+ if [ ! -f "doc/index.html" ]; then
71
+ echo "Warning: doc/index.html not found"
72
+ fi
73
+ echo "Generated HTML files: $(find doc -name '*.html' | wc -l)"
74
+ echo "Total documentation size: $(du -sh doc/ | cut -f1)"
75
+ echo "::endgroup::"
76
+
77
+ - name: Setup GitHub Pages configuration
78
+ uses: actions/configure-pages@v4
79
+
80
+ - name: Upload documentation artifact
81
+ uses: actions/upload-pages-artifact@v5
82
+ with:
83
+ path: './doc'
84
+
85
+ deploy-pages:
86
+ timeout-minutes: 10
87
+ environment:
88
+ name: github-pages
89
+ url: ${{ steps.deployment.outputs.page_url }}
90
+ runs-on: ubuntu-latest
91
+ needs: build-docs
92
+ outputs:
93
+ page_url: ${{ steps.deployment.outputs.page_url }}
94
+
95
+ steps:
96
+ - name: Deploy to GitHub Pages
97
+ id: deployment
98
+ uses: actions/deploy-pages@v5
99
+
100
+ notify-completion:
101
+ timeout-minutes: 5
102
+ runs-on: ubuntu-latest
103
+ needs: [build-docs, deploy-pages]
104
+ if: success()
105
+
106
+ steps:
107
+ - name: Documentation deployment summary
108
+ run: |
109
+ echo "::notice title=Documentation Deployed::YARD documentation successfully deployed to GitHub Pages"
110
+ echo "::notice title=Access URL::Documentation available at: ${{ needs.deploy-pages.outputs.page_url }}"
data/.yardopts CHANGED
@@ -6,7 +6,7 @@
6
6
  --charset utf-8
7
7
  --markup markdown
8
8
  --markup-provider redcarpet
9
- --output-dir public
9
+ --output-dir doc
10
10
  --embed-mixin ClassMethods
11
11
  --embed-mixin InstanceMethods
12
12
  --embed-mixin Features
data/CHANGELOG.rst CHANGED
@@ -7,6 +7,156 @@ 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.2:
11
+
12
+ 2.11.2 — 2026-07-05
13
+ ===================
14
+
15
+
16
+ Changed
17
+ -------
18
+
19
+ - ``to_h_for_storage`` omits nil fields; every write path removes a field that has
20
+ become nil. ``save``/``commit_fields`` do a full overwrite (the stored hash
21
+ matches the in-memory object, so a field nil in memory is deleted);
22
+ ``save_fields``/``multi_field_update``/``multi_field_fast_write`` delete a named
23
+ field passed as nil. ``to_h`` still returns every declared field, nils included.
24
+
25
+ - Claim caveat: a full ``save``/``commit_fields`` of a stale copy clears a field
26
+ another writer claimed via ``HSETNX``. To claim and update without disturbing
27
+ it, use the targeted writers or ``refresh!`` first.
28
+
29
+ Fixed
30
+ -----
31
+
32
+ - Nil-valued fields are no longer stored as the JSON string ``"null"``. Because a
33
+ hash has no native NULL, this left declared fields perpetually present, breaking
34
+ ``HSETNX``/``HEXISTS`` atomic-claim patterns and wasting memory. Nil fields are
35
+ now omitted, and clearing a field to nil removes it from storage (``HDEL``), so
36
+ absence again means "no value". No migration required: stale ``"null"`` values
37
+ are cleaned up on the next save and already decode back to ``nil`` on read.
38
+
39
+ AI Assistance
40
+ -------------
41
+
42
+ - Investigation, implementation, and tryouts coverage produced with Claude Code.
43
+
44
+ .. _changelog-2.11.1:
45
+
46
+ 2.11.1 — 2026-07-04
47
+ ===================
48
+
49
+ Added
50
+ -----
51
+
52
+ - ``encrypted_field`` now honors a per-field ``algorithm:`` option, pinning that
53
+ field's write algorithm to a specific registered provider (``'aes-256-gcm'`` or
54
+ ``'xchacha20poly1305'``) independent of the registry's default-provider
55
+ priority. The option was previously documented but silently ignored, so writes
56
+ always used the default provider. Decryption stays envelope-driven, so a pin can
57
+ be added, changed, or removed without breaking ciphertext already at rest, and
58
+ ``re_encrypt_fields!`` re-encrypts under the pin rather than the default. This is
59
+ the supported lever for a reader-before-writer format migration: deploy
60
+ ``rbnacl`` fleet-wide so every node can *read* XChaCha20-Poly1305 while keeping
61
+ *writes* pinned to AES-256-GCM until all readers are confirmed capable, then drop
62
+ the pin. Issue #334
63
+
64
+ Changed
65
+ -------
66
+
67
+ - ``Familia::Encryption::Registry.get`` now distinguishes an unknown algorithm
68
+ from a *known* algorithm whose provider is not available on the current node.
69
+ Because ``Registry.register`` only stores providers whose runtime dependency is
70
+ present, pinning ``encrypted_field ..., algorithm: 'xchacha20poly1305'`` on a
71
+ node without ``rbnacl``/libsodium previously raised the misleading
72
+ ``"Unsupported algorithm: xchacha20poly1305"`` -- pointing an operator at a typo
73
+ when the real fix is a missing dependency. It now names the provider, explains
74
+ the dependency is missing, and (because ``get`` runs on both the encrypt and
75
+ decrypt paths) states that installing the dependency is what enables reading
76
+ *and* writing the algorithm, framing an algorithm pin as a write-time
77
+ workaround that cannot decrypt existing ciphertext. Each provider declares its
78
+ own dependency via a new ``Provider.dependency_hint`` class method (nil for
79
+ always-available providers like OpenSSL AES-256-GCM), so the generic error path
80
+ names the correct library as providers are added rather than hardcoding any one
81
+ of them. The set of registerable providers is centralized in
82
+ ``Registry.known_providers``, the single source of truth shared by ``setup!``
83
+ and ``get``. Error-message and internal-refactor only; the resolution of every
84
+ available algorithm is unchanged. Issue #334
85
+
86
+ Fixed
87
+ -----
88
+
89
+ - ``Familia::DataType#exists?`` no longer returns ``true`` for a deleted or
90
+ never-created scalar key (``StringKey``, ``Counter``, ``Lock``,
91
+ ``JsonStringKey``). The check was ``dbclient.exists(dbkey) && !size.zero?``,
92
+ but ``EXISTS`` returns an Integer count and ``0`` is truthy in Ruby, so the
93
+ guard never short-circuited on a missing key -- existence was decided
94
+ entirely by the size check. ``exists?`` now uses a boolean-coerced ``EXISTS``
95
+ count directly. Issue #331
96
+
97
+ - Relatedly, ``StringKey#size``/``#length``/``#empty?`` (and ``Lock``'s) no
98
+ longer reflect the never-nil ``#to_s`` fallback. ``#char_count`` derived from
99
+ ``#to_s.size``, and ``#to_s`` intentionally returns ``Familia::Base``'s
100
+ documented "never nil" inspect-string when the value is absent -- so
101
+ ``#size`` was non-zero (and ``#empty?`` false) for a missing key.
102
+ ``#char_count`` now reads ``#value`` directly; ``#to_s`` is left unchanged.
103
+ Issue #331
104
+
105
+ - ``encrypted_fields_status`` now reports each field's real algorithm for a live
106
+ encrypted value (e.g. ``{ encrypted: true, algorithm: "aes-256-gcm", cleared:
107
+ false }``), honoring any per-field pin. Previously it returned ``{ encrypted:
108
+ false, value: "[CONCEALED]" }`` for every encrypted field, because
109
+ ``ConcealedString`` had no ``concealed?`` predicate for the status check to
110
+ match -- so the algorithm shown in the method's docstring and the guides was
111
+ never actually produced. ``ConcealedString`` gains ``#concealed?`` and
112
+ ``#algorithm`` readers (the latter reads the stored envelope). Issue #334
113
+
114
+ Documentation
115
+ -------------
116
+
117
+ - Added an executable, multi-phase proof (``examples/encryption_upgrade_proof/``)
118
+ demonstrating that installing ``rbnacl`` safely flips new writes to
119
+ XChaCha20-Poly1305 while every existing AES-256-GCM envelope — including
120
+ ciphertext written by the released 2.10.1 gem, under the pre-#310 static HKDF
121
+ salt, and under a retired master key version — keeps decrypting. Also pins,
122
+ as deliberately-passing checks, two operational hazards: the XChaCha
123
+ ``encryption_personalization`` cannot be rotated (no history/fallback like
124
+ ``encryption_hkdf_salt_history``), and once any XChaCha envelope exists,
125
+ every node that may read it needs libsodium installed. PR #330
126
+
127
+ - The encrypted-fields guide previously showed a ``provider: :aes_gcm`` field
128
+ option that was never implemented; those examples now use the real
129
+ ``algorithm: 'aes-256-gcm'`` form, and the ``Familia::Encryption`` facade
130
+ docstring documents the shipped behavior instead of a hypothetical
131
+ implementation sketch. The ``encrypted_fields_status`` output examples across
132
+ the guides and the overview were corrected to match what the method now
133
+ returns. Issue #334
134
+
135
+ - Added a memory-audit investigation (``docs/investigation/memory-audit.md``)
136
+ diagnosing #309's ``<collection>_with_permission`` O(N) query as a transient,
137
+ GC-reclaimable spike rather than a per-process leak, and auditing the rest of
138
+ ``lib/`` for per-process growth (concluding Familia has no unconditional leak).
139
+ Ships two executable proofs in ``try/investigation/`` — a pure-Ruby
140
+ ``process_memory_leak_proof.rb`` and a live-Redis ``memory_leak_proof.rb``.
141
+ Diagnosis only; no runtime behaviour is changed by the investigation. Issue #309
142
+
143
+ AI Assistance
144
+ -------------
145
+
146
+ - The encryption upgrade proof, its regression tryouts, and this changelog
147
+ entry were drafted with AI assistance. PR #330
148
+
149
+ - The ``exists?`` fix and its regression tryouts were drafted with AI
150
+ assistance. Issue #331
151
+
152
+ - The per-field algorithm implementation, the ``encrypted_fields_status`` fix,
153
+ their regression tryouts, the guide corrections, and the
154
+ ``Registry.get`` error-message refinement were drafted with AI assistance.
155
+ Issue #334
156
+
157
+ - The memory-audit investigation and its executable proofs were drafted with AI
158
+ assistance. Issue #309
159
+
10
160
  .. _changelog-2.11.0:
11
161
 
12
162
  2.11.0 — 2026-06-22
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.6', require: false
8
+ gem 'concurrent-ruby', '~> 1.3.7', require: false
9
9
  gem 'ruby-prof'
10
10
  gem 'stackprof'
11
11
  gem 'timecop', require: false
@@ -21,12 +21,12 @@ group :development, :test do
21
21
  # Config#to_data, which we don't use (we don't use Dry::Configurable at all),
22
22
  # so cap below 1.4 to keep the dev bundle installable on Ruby 3.2.
23
23
  gem 'dry-configurable', '>= 1.3', '< 1.5', require: false
24
- gem 'irb', '~> 1.15.2', require: false
24
+ gem 'irb', '~> 1.18.0', require: false
25
25
  gem 'json_schemer', '~> 2.0', require: false
26
26
  gem 'rake', '~> 13.0', require: false
27
27
  gem 'redcarpet', require: false
28
28
  gem 'reek', require: false
29
- gem 'rubocop', '~> 1.85.1', require: false
29
+ gem 'rubocop', '~> 1.88.0', require: false
30
30
  gem 'rubocop-performance', require: false
31
31
  gem 'rubocop-thread_safety', require: false
32
32
  gem 'ruby-lsp', require: false