active_version 1.3.0 → 1.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f1b2d07a134e66e4e4330b9d195b32d831d3a2ce0599c324bed86157eb2e3fc
4
- data.tar.gz: f612e466b59f335183016fc5d9d601005a4b66b07efa6276b54c1260bbd08736
3
+ metadata.gz: '08398a92e326503be43e704b7681562a6c3c4d220bd7cf934e295ff3afada135'
4
+ data.tar.gz: a3678d669239918343732b02b8ff8285b32e549bb44ae6eae1cffde75abf0ae1
5
5
  SHA512:
6
- metadata.gz: '068727dace361f83992e6b6e92b25e2dcb50cc5128e5cbb80114cfd4d48fa7f4f406f0fe216de0518becbe75f9186ab18637211452ce2a98b66825ea513172f7'
7
- data.tar.gz: 6d09e28cbb0b651a4aaa89fb879a48e978bb9d9fcde7d3c312226ca256963ed119b9aba9e016eaddbe6a0a613d2e10140c5474f280d545f318fdb80282007930
6
+ metadata.gz: 158e15ac369fcbe7f730cf6e11ef638f888b5e14fb996be1f555321ddb0b08f11975c96809df18980d66b32402175dd660651791ffd8f3959798cb66b285a882
7
+ data.tar.gz: d0b501f02a783beaab992849dd91420980ccb592d2f56b8b7c810f49ff0952c1e61bc60cb8b8a202cc8af269928924f947243b15349606b8ddc0c4a0f33d414c
data/CHANGELOG.md CHANGED
@@ -1,9 +1,19 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.4.0 (2026-07-13)
4
+
5
+ - Add `revision_error_behavior` configuration (`:exception`, `:log`, `:silent`) and per-model `has_revisions error_behavior:` override for revision snapshot failures.
6
+ - Skip post-update revision bookkeeping when snapshot creation returns nil under `:log` or `:silent` revision error handling.
7
+ - Route audit warning output through `ActiveVersion.logger` so non-Rails apps and custom log sinks receive audit write failures consistently.
8
+ - Align audit writer fallback error behavior with the configuration default (`:exception`) when neither per-model nor global audit error behavior is set.
9
+ - Fix audit combining to refresh the cached audits association instead of reloading the auditable record, and bound combiner queries with COUNT and LIMIT instead of loading the full audit history each iteration.
10
+ - Fix thread-local audited options reader installation on Rails 6.1 models with delegated class methods.
11
+ - Log debug detail for deferred audit setup, instrumentation probes, and unparseable audit payloads when `ActiveVersion.logger` supports debug output.
12
+
3
13
  ## 1.3.0 (2026-04-15)
4
14
 
5
- - Add `polyrun` `~> 1.2.0` development dependency; `bin/polyrun`, `bin/rspec`, `polyrun.yml`, and `config/polyrun_coverage.yml` for parallel RSpec and merged coverage.
6
- - Add `script/create_postgres_shard_databases.sh` for CI and local Postgres shard databases; document in `POLYRUN.md`; set job-level `DATABASE_URL` in GitHub Actions test workflow.
15
+ - Add `polyrun` `~> 1.3.0` development dependency; `bin/polyrun`, `bin/rspec`, `polyrun.yml`, and `config/polyrun_coverage.yml` for parallel RSpec and merged coverage.
16
+ - Add `script/create_postgres_shard_databases.sh` for CI and local Postgres shard databases; document in `spec/README.md`; set job-level `DATABASE_URL` in GitHub Actions test workflow.
7
17
  - Add internal library logger.
8
18
  - Fix audits, Sequel foreign keys, revision debounce, and versions walk edge cases.
9
19
  - Adjust RSpec and RuboCop test configuration; update spec coverage.
data/README.md CHANGED
@@ -305,6 +305,8 @@ rails g active_version:triggers Post --type=audit
305
305
  rails g active_version:triggers Post --type=revision
306
306
  ```
307
307
 
308
+ PostgreSQL audit triggers capture the full row as JSON. They do not apply Ruby-side `only`, `except`, or `redacted` filters from `has_audits`. Use triggers only when you want complete row snapshots at the database layer, or generate custom trigger SQL that matches your column policy.
309
+
308
310
  ### Infrastructure Ownership
309
311
 
310
312
  ```ruby
@@ -507,7 +509,7 @@ Environment knobs:
507
509
 
508
510
  Bug reports and pull requests are welcome on GitHub at https://github.com/amkisko/active_version.rb.
509
511
 
510
- Development dependencies include [polyrun](https://rubygems.org/gems/polyrun) **`~> 1.2.0`** (declared in `active_version.gemspec`) for **parallel RSpec** (`./bin/polyrun`), coverage, and CI report formats—see [POLYRUN.md](POLYRUN.md).
512
+ Development dependencies include [polyrun](https://rubygems.org/gems/polyrun) (declared in `active_version.gemspec`) for **parallel RSpec** (`./bin/polyrun`), coverage, and CI report formats—see [spec/README.md](spec/README.md).
511
513
 
512
514
  ## License
513
515
 
@@ -16,7 +16,10 @@ module ActiveVersion
16
16
  return value unless value.is_a?(String)
17
17
 
18
18
  JSON.parse(value)
19
- rescue JSON::ParserError
19
+ rescue JSON::ParserError => error
20
+ ActiveVersion.log_debug(
21
+ "[ActiveVersion::Audits::AuditRecord::Serializers::Json] failed to parse audit payload: #{error.message}"
22
+ )
20
23
  value
21
24
  end
22
25
 
@@ -35,7 +38,10 @@ module ActiveVersion
35
38
  return value unless value.is_a?(String)
36
39
 
37
40
  YAML.safe_load(value, permitted_classes: PERMITTED_CLASSES, aliases: false)
38
- rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::AliasesNotEnabled, ArgumentError
41
+ rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::AliasesNotEnabled, ArgumentError => error
42
+ ActiveVersion.log_debug(
43
+ "[ActiveVersion::Audits::AuditRecord::Serializers::Yaml] failed to parse audit payload: #{error.message}"
44
+ )
39
45
  value
40
46
  end
41
47
 
@@ -125,10 +125,10 @@ module ActiveVersion
125
125
  attribute changes_column, :text unless attribute_names.include?(changes_column.to_s)
126
126
  attribute context_column, :text unless attribute_names.include?(context_column.to_s)
127
127
  end
128
- rescue *ActiveVersion::Runtime.active_record_connection_errors => e
129
- if defined?(Rails) && Rails.respond_to?(:logger)
130
- Rails.logger&.debug("[ActiveVersion] Deferred audit attribute setup for #{name}: #{e.class}: #{e.message}")
131
- end
128
+ rescue *ActiveVersion::Runtime.active_record_connection_errors => error
129
+ ActiveVersion.log_debug(
130
+ "[ActiveVersion] Deferred audit attribute setup for #{name}: #{error.class}: #{error.message}"
131
+ )
132
132
  end
133
133
 
134
134
  begin
@@ -156,10 +156,10 @@ module ActiveVersion
156
156
  end
157
157
  rescue NameError
158
158
  # Source class not yet defined, will be set up later
159
- rescue *ActiveVersion::Runtime.active_record_connection_errors => e
160
- if defined?(Rails) && Rails.respond_to?(:logger)
161
- Rails.logger&.debug("[ActiveVersion] Deferred audit association setup for #{name}: #{e.class}: #{e.message}")
162
- end
159
+ rescue *ActiveVersion::Runtime.active_record_connection_errors => error
160
+ ActiveVersion.log_debug(
161
+ "[ActiveVersion] Deferred audit association setup for #{name}: #{error.class}: #{error.message}"
162
+ )
163
163
  end
164
164
  end
165
165
 
@@ -24,54 +24,27 @@ module ActiveVersion
24
24
  iteration += 1
25
25
  break if iteration > max_iterations
26
26
 
27
- # Force reload from database to see any updates
28
- if persisted?
29
- reload
30
- end
31
-
32
- # Clear association cache to ensure we get fresh data from database.
33
- # Avoid calling audits reader directly here to prevent AR 6.1
34
- # delegation edge cases on dynamic models.
27
+ # Clear association cache so the direct audit query below sees SQL updates
28
+ # from prior combine rounds without reloading the auditable record.
35
29
  if respond_to?(:association) && association_cached?(:audits)
36
30
  association(:audits).reset
37
31
  end
38
32
 
39
- # Get all audits fresh from database (not from cache)
40
- # Query directly to ensure we get updated values after SQL updates
41
33
  auditable_type = audited_options[:class_name] || self.class.name
42
34
  auditable_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :auditable)
43
35
  version_column = ActiveVersion.column_mapper.column_for(self.class, :audits, :version)
44
- audit_klass =
45
- if self.class.reflect_on_association(:audits)
46
- association(:audits).klass
47
- else
48
- self.class.audit_class
49
- end
50
- if audit_klass.nil? && self.class.respond_to?(:resolve_audit_class_option, true)
51
- audit_klass = self.class.send(:resolve_audit_class_option, audited_options[:as])
52
- end
36
+ audit_klass = combiner_audit_klass
53
37
  break unless audit_klass
54
- all_audits = audit_klass.where({"#{auditable_column}_type" => auditable_type}.merge(active_version_audit_identity_map))
55
- .order(version_column => :asc)
56
- .to_a
57
38
 
58
- # Filter out combined audits (those with empty changes)
59
- # Check raw column value first (before JSON parsing) for "{}" string
60
- active_audits = all_audits.reject do |audit|
61
- # Check raw column value first - combined audits have "{}" as string
62
- raw_changes = audit.read_attribute(changes_column)
63
-
64
- # If raw value is exactly "{}", it's a combined audit
65
- if raw_changes.is_a?(String) && raw_changes.strip == "{}"
66
- true
67
- else
68
- # Otherwise check parsed value
69
- changes = audit.audited_changes
70
- changes.nil? || (changes.is_a?(Hash) && changes.empty?) || (changes.is_a?(String) && changes.strip.empty?)
71
- end
72
- end
39
+ active_scope = active_audits_scope_for_combiner(
40
+ audit_klass,
41
+ auditable_type,
42
+ auditable_column,
43
+ version_column,
44
+ changes_column
45
+ )
73
46
 
74
- audits_count = active_audits.length
47
+ audits_count = active_scope.count
75
48
  break if audits_count <= max_audits
76
49
 
77
50
  # Calculate how many extra audits we have
@@ -79,7 +52,11 @@ module ActiveVersion
79
52
 
80
53
  # Get the oldest active audits to combine (first extra_count + 1)
81
54
  # The +1 is because we'll merge into the last one in this set
82
- audits_to_combine = active_audits.first(extra_count + 1)
55
+ audits_to_combine = active_scope
56
+ .limit(extra_count + 1)
57
+ .to_a
58
+ .reject { |audit| combined_audit_record?(audit, changes_column) }
59
+ .first(extra_count + 1)
83
60
 
84
61
  # Safety check to prevent infinite loops
85
62
  break if audits_to_combine.empty? || audits_to_combine.length <= 1
@@ -89,6 +66,39 @@ module ActiveVersion
89
66
  end
90
67
  end
91
68
 
69
+ def combiner_audit_klass
70
+ audit_klass =
71
+ if self.class.reflect_on_association(:audits)
72
+ association(:audits).klass
73
+ else
74
+ self.class.audit_class
75
+ end
76
+ if audit_klass.nil? && self.class.respond_to?(:resolve_audit_class_option, true)
77
+ audit_klass = self.class.send(:resolve_audit_class_option, audited_options[:as])
78
+ end
79
+ audit_klass
80
+ end
81
+
82
+ def active_audits_scope_for_combiner(audit_klass, auditable_type, auditable_column, version_column, changes_column)
83
+ audit_klass
84
+ .where({"#{auditable_column}_type" => auditable_type}.merge(active_version_audit_identity_map))
85
+ .where.not(changes_column => nil)
86
+ .where.not(changes_column => "")
87
+ .where.not(changes_column => "{}")
88
+ .order(version_column => :asc)
89
+ end
90
+
91
+ def combined_audit_record?(audit, changes_column)
92
+ raw_changes = audit.read_attribute(changes_column)
93
+
94
+ if raw_changes.is_a?(String) && raw_changes.strip == "{}"
95
+ true
96
+ else
97
+ changes = audit.audited_changes
98
+ changes.nil? || (changes.is_a?(Hash) && changes.empty?) || (changes.is_a?(String) && changes.strip.empty?)
99
+ end
100
+ end
101
+
92
102
  def evaluate_max_audits
93
103
  max_audits = case (option = audited_options[:max_audits])
94
104
  when Proc then option.call
@@ -228,7 +228,7 @@ module ActiveVersion
228
228
 
229
229
  def handle_audit_errors(error, action)
230
230
  ActiveVersion::Instrumentation.instrument_audit_write_failed(self, error: error, action: action)
231
- behavior = audited_options[:error_behavior] || ActiveVersion.config.audit_error_behavior || :log
231
+ behavior = audited_options[:error_behavior] || ActiveVersion.config.audit_error_behavior || :exception
232
232
 
233
233
  case behavior
234
234
  when :log
@@ -241,7 +241,7 @@ module ActiveVersion
241
241
  end
242
242
 
243
243
  def log_audit_errors(error, action)
244
- Rails.logger&.warn(
244
+ ActiveVersion.logger.warn(
245
245
  "Unable to create audit for #{action} of #{self.class.name}" \
246
246
  "##{id}: #{error.message}"
247
247
  )
@@ -419,7 +419,7 @@ module ActiveVersion
419
419
  private
420
420
 
421
421
  def install_thread_local_audited_options_reader!
422
- unless singleton_class.instance_methods(false).include?(:audited_options_without_thread_local)
422
+ unless singleton_class.method_defined?(:audited_options_without_thread_local, false)
423
423
  singleton_class.alias_method :audited_options_without_thread_local, :audited_options
424
424
  end
425
425
 
@@ -217,9 +217,7 @@ module ActiveVersion
217
217
  end
218
218
 
219
219
  def log_debug(message)
220
- if defined?(Rails) && Rails.respond_to?(:logger)
221
- Rails.logger&.debug("[ActiveVersion::Instrumentation] #{message}")
222
- end
220
+ ActiveVersion.log_debug("[ActiveVersion::Instrumentation] #{message}")
223
221
  end
224
222
  end
225
223
  end
@@ -113,30 +113,25 @@ module ActiveVersion
113
113
  pseudo.define_singleton_method(:persisted?) { true }
114
114
  pseudo
115
115
  else
116
-
117
- # Use create! instead of build + save to get better error messages
118
116
  begin
119
117
  revision = create_revision_record_with_version_retry!(revision_attrs, version_column_sym)
120
- rescue ActiveRecord::RecordInvalid => e
121
- ActiveVersion::Instrumentation.instrument_revision_write_failed(self, error: e)
122
- error_msg = "Failed to create revision: #{e.class}"
123
- error_msg += "\nRevision attribute keys: #{revision_attrs.keys.map(&:to_s).sort.join(", ")}"
124
- error_msg += "\nIdentity keys: #{active_version_revision_identity_map.keys.map(&:to_s).sort.join(", ")}"
125
- error_msg += "\nVersion column: #{version_column_sym}, New version: #{new_version}"
126
- error_msg += "\nRevision class: #{self.class.revision_class.name}"
127
- error_msg += "\nSource class: #{self.class.name}"
128
- if e.record
129
- error_msg += "\nRecord error fields: #{e.record.errors.attribute_names.map(&:to_s).uniq.sort.join(", ")}"
130
- error_msg += "\nRecord valid?: #{e.record.valid?}"
131
- end
132
- raise error_msg
133
- rescue => e
134
- ActiveVersion::Instrumentation.instrument_revision_write_failed(self, error: e)
135
- error_msg = "Failed to create revision: #{e.class}"
136
- error_msg += "\nRevision attribute keys: #{revision_attrs.keys.map(&:to_s).sort.join(", ")}"
137
- error_msg += "\nIdentity keys: #{active_version_revision_identity_map.keys.map(&:to_s).sort.join(", ")}"
138
- error_msg += "\nVersion column: #{version_column_sym}, New version: #{new_version}"
139
- raise error_msg
118
+ rescue ActiveRecord::RecordInvalid => error
119
+ revision = handle_revision_errors(
120
+ error,
121
+ revision_attrs: revision_attrs,
122
+ version_column_sym: version_column_sym,
123
+ new_version: new_version,
124
+ invalid_record: error.record
125
+ )
126
+ return revision unless revision
127
+ rescue => error
128
+ revision = handle_revision_errors(
129
+ error,
130
+ revision_attrs: revision_attrs,
131
+ version_column_sym: version_column_sym,
132
+ new_version: new_version
133
+ )
134
+ return revision unless revision
140
135
  end
141
136
 
142
137
  # Force reload association to ensure it's visible
@@ -432,15 +427,60 @@ module ActiveVersion
432
427
  begin
433
428
  attempt += 1
434
429
  revisions.create!(revision_attrs)
435
- rescue ActiveRecord::RecordNotUnique => e
436
- raise e if attempt >= 2
430
+ rescue ActiveRecord::RecordNotUnique => error
431
+ raise error if attempt >= 2
437
432
 
438
433
  max_version = revisions_scope.maximum(version_column_sym) || 0
439
434
  revision_attrs[version_column_sym] = max_version + 1
440
435
  retry
441
436
  end
442
437
  end
443
- private :create_revision_record_with_version_retry!
438
+
439
+ def handle_revision_errors(error, revision_attrs:, version_column_sym:, new_version:, invalid_record: nil)
440
+ ActiveVersion::Instrumentation.instrument_revision_write_failed(self, error: error)
441
+ behavior = self.class.revision_options&.dig(:error_behavior) ||
442
+ ActiveVersion.config.revision_error_behavior ||
443
+ :exception
444
+
445
+ case behavior
446
+ when :exception
447
+ raise build_revision_error_message(
448
+ error,
449
+ revision_attrs: revision_attrs,
450
+ version_column_sym: version_column_sym,
451
+ new_version: new_version,
452
+ invalid_record: invalid_record
453
+ )
454
+ when :log
455
+ ActiveVersion.logger.warn(
456
+ build_revision_error_message(
457
+ error,
458
+ revision_attrs: revision_attrs,
459
+ version_column_sym: version_column_sym,
460
+ new_version: new_version,
461
+ invalid_record: invalid_record
462
+ )
463
+ )
464
+ nil
465
+ when :silent
466
+ nil
467
+ end
468
+ end
469
+
470
+ def build_revision_error_message(error, revision_attrs:, version_column_sym:, new_version:, invalid_record: nil)
471
+ message = "Failed to create revision: #{error.class}"
472
+ message += "\nRevision attribute keys: #{revision_attrs.keys.map(&:to_s).sort.join(", ")}"
473
+ message += "\nIdentity keys: #{active_version_revision_identity_map.keys.map(&:to_s).sort.join(", ")}"
474
+ message += "\nVersion column: #{version_column_sym}, New version: #{new_version}"
475
+ if invalid_record
476
+ message += "\nRevision class: #{self.class.revision_class.name}"
477
+ message += "\nSource class: #{self.class.name}"
478
+ message += "\nRecord error fields: #{invalid_record.errors.attribute_names.map(&:to_s).uniq.sort.join(", ")}"
479
+ message += "\nRecord valid?: #{invalid_record.valid?}"
480
+ end
481
+ message
482
+ end
483
+ private :create_revision_record_with_version_retry!, :handle_revision_errors, :build_revision_error_message
444
484
 
445
485
  def refreshable_column_names
446
486
  @refreshable_column_names ||=
@@ -113,9 +113,6 @@ module ActiveVersion
113
113
 
114
114
  # Set up callbacks based on options
115
115
  setup_revision_callbacks(revision_options)
116
-
117
- # Register with version registry (idempotent)
118
- ActiveVersion.registry.register(self, :revisions, revision_options)
119
116
  end
120
117
 
121
118
  # Check if model has revisions configured
@@ -134,7 +131,8 @@ module ActiveVersion
134
131
  except: Array.wrap(options[:except] || []).map(&:to_s),
135
132
  foreign_key: normalize_identity_columns(options[:foreign_key]),
136
133
  identity_resolver: options[:identity_resolver],
137
- table_name: options[:table_name]
134
+ table_name: options[:table_name],
135
+ error_behavior: options[:error_behavior]
138
136
  }
139
137
  end
140
138
 
@@ -409,6 +407,7 @@ module ActiveVersion
409
407
  return if truncated_forward_history && latest_revision_matches_current_state?
410
408
 
411
409
  result = create_snapshot!(use_old_values: true)
410
+ return unless result
412
411
 
413
412
  # Ensure revision is persisted and visible in association
414
413
  unless result.persisted?
@@ -1,3 +1,3 @@
1
1
  module ActiveVersion
2
- VERSION = "1.3.0"
2
+ VERSION = "1.4.0"
3
3
  end
@@ -338,6 +338,13 @@ module ActiveVersion
338
338
  @logger = default_logger
339
339
  end
340
340
 
341
+ def log_debug(message)
342
+ logger = self.logger
343
+ return if logger.nil?
344
+
345
+ logger.debug(message) if logger.respond_to?(:debug)
346
+ end
347
+
341
348
  def default_logger
342
349
  l = Logger.new($stderr)
343
350
  l.level = Logger::WARN
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_version
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -85,14 +85,14 @@ dependencies:
85
85
  requirements:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
- version: '1.4'
88
+ version: 2.9.5
89
89
  type: :development
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
- version: '1.4'
95
+ version: 2.9.5
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: pg
98
98
  requirement: !ruby/object:Gem::Requirement
@@ -137,18 +137,32 @@ dependencies:
137
137
  version: '0.4'
138
138
  - !ruby/object:Gem::Dependency
139
139
  name: polyrun
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: 2.2.0
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: 2.2.0
152
+ - !ruby/object:Gem::Dependency
153
+ name: prosopite
140
154
  requirement: !ruby/object:Gem::Requirement
141
155
  requirements:
142
156
  - - "~>"
143
157
  - !ruby/object:Gem::Version
144
- version: 1.2.0
158
+ version: '2.0'
145
159
  type: :development
146
160
  prerelease: false
147
161
  version_requirements: !ruby/object:Gem::Requirement
148
162
  requirements:
149
163
  - - "~>"
150
164
  - !ruby/object:Gem::Version
151
- version: 1.2.0
165
+ version: '2.0'
152
166
  - !ruby/object:Gem::Dependency
153
167
  name: standard
154
168
  requirement: !ruby/object:Gem::Requirement
@@ -275,6 +289,20 @@ dependencies:
275
289
  - - "~>"
276
290
  - !ruby/object:Gem::Version
277
291
  version: '2'
292
+ - !ruby/object:Gem::Dependency
293
+ name: bundler-audit
294
+ requirement: !ruby/object:Gem::Requirement
295
+ requirements:
296
+ - - "~>"
297
+ - !ruby/object:Gem::Version
298
+ version: '0.9'
299
+ type: :development
300
+ prerelease: false
301
+ version_requirements: !ruby/object:Gem::Requirement
302
+ requirements:
303
+ - - "~>"
304
+ - !ruby/object:Gem::Version
305
+ version: '0.9'
278
306
  - !ruby/object:Gem::Dependency
279
307
  name: rbs
280
308
  requirement: !ruby/object:Gem::Requirement
@@ -395,7 +423,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
395
423
  - !ruby/object:Gem::Version
396
424
  version: '0'
397
425
  requirements: []
398
- rubygems_version: 3.6.9
426
+ rubygems_version: 4.0.6
399
427
  specification_version: 4
400
428
  summary: Unified versioning library for translations, revisions, and audits
401
429
  test_files: []