vial 0.0.0 → 0.2026.1.1.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: 1b37575d4301362c15f474046030d04e71fdf4a78ce6da1d5e10f9c14d25333a
4
- data.tar.gz: a643a19d4c75049bf1b36ab7b078fb7c6ffa2c9a3028c0d67f3ed4d8f4414586
3
+ metadata.gz: f026c068653b089c9e389a48269f61b166df03350c2c483644d01d20449d75ac
4
+ data.tar.gz: ba3eb55ff916a330b79a7e23a33c64664384ac4b4c53bd9b3ca8c2cff48ddf01
5
5
  SHA512:
6
- metadata.gz: 185493864794e252304dfb5aaf4aa9ce7d57b7d46ed7f2c5a6f60f2c95bf891cc7b335fac21801448ea2fd339075b43cdf86858aa93df9f8618149dfd98e0fe9
7
- data.tar.gz: e9b49107485759a879c451dff0f6292dfbde946b3471daafd1f853d80b0f43d370a61472eb6e27bf28277ed12fa4186867c20a39db88588d978907ab1ed76825
6
+ metadata.gz: eb253aae25c559b82e53fce01af43eec1b378062b7d88163fb9967e3856bc35c4636213c8f08143e0f3baca7840f5d5c5df2d441f6e4cbd4cb3f537ed05496f8
7
+ data.tar.gz: 6c05034b6d5b9d9e39de6f61b59cd4e9e00d0b1acc88f36dc222ce7903a23a4728baa80813ca37b22b85d2eb9153f5ae2d42a638c516d81d507852af5c34abac
data/README.md CHANGED
@@ -1,27 +1,318 @@
1
1
  # Vial: Fixtures, Reinvented
2
2
 
3
- Vial revolutionizes test fixtures with intelligent data management, dynamic generation, and seamless integration. Create maintainable test data with smart relationships, factories, and scenarios.
3
+ Vial is a Ruby gem for Rails 8.1+ on Ruby 4.0+ that compiles programmable fixture intent into explicit, deterministic fixture files.
4
+
5
+ ## Requirements
6
+
7
+ - Ruby 4.0+ (Ruby Box optional via `RUBY_BOX=1`)
8
+ - Rails 8.1+
9
+
10
+ ## Support Policy
11
+
12
+ - Ruby 4.0+ only
13
+ - Rails 8.1+ only
14
+ - No backward compatibility guarantees for older Ruby/Rails
15
+
16
+ ## Features
17
+
18
+ - **Vial Compiler**: Ruby DSL input → explicit YAML fixtures output
19
+ - **Fixture Discovery**: Automatically finds all YAML fixtures across configured paths
20
+ - **Model Mapping Analysis**: Determines which models fixtures belong to using Rails conventions
21
+ - **Validation**: Identifies orphaned fixtures that don't have corresponding models
22
+ - **Multiple Detection Methods**: Supports all Rails fixture-to-model mapping methods
4
23
 
5
24
  ## Installation
6
25
 
7
- Add this line to your application's Gemfile:
26
+ Add this gem to your Gemfile in the development/test group:
27
+
28
+ ```ruby
29
+ group :development, :test do
30
+ gem 'vial'
31
+ end
32
+ ```
33
+
34
+ Then run:
35
+
36
+ ```bash
37
+ bundle install
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### Compile Vial Sources
43
+
44
+ Vial reads `*.vial.rb` files from `test/vials` and writes YAML fixtures to `test/fixtures`:
45
+
46
+ ```bash
47
+ bin/rails vial:compile
48
+ ```
49
+
50
+ If `RUBY_BOX=1` is set, Vial evaluates vial sources inside Ruby::Box for isolation.
51
+
52
+ Compile only specific vials:
53
+
54
+ ```bash
55
+ bin/rails vial:compile:only[users,posts]
56
+ ```
57
+
58
+ Note: `vial:compile:only` still validates the full dataset to catch global ID collisions; it only limits which files are written.
59
+
60
+ Dry run (validate and show output targets without writing files):
61
+
62
+ ```bash
63
+ bin/rails vial:compile:dry_run
64
+ ```
65
+
66
+ Example `test/vials/users.vial.rb`:
67
+
68
+ ```ruby
69
+ vial :users do
70
+ base do
71
+ active true
72
+ country "MA"
73
+ end
74
+
75
+ variant :admin do
76
+ role "admin"
77
+ end
78
+
79
+ variant :guest do
80
+ role "guest"
81
+ active false
82
+ end
83
+
84
+ sequence(:email) { |i| "user#{i}@test.local" }
85
+
86
+ generate 10, :admin
87
+ generate 50, :guest
88
+ end
89
+ ```
90
+
91
+ Output: `test/fixtures/users.yml` with deterministic IDs and explicit records.
92
+
93
+ ### Global Validation
94
+
95
+ `vial:compile` validates the entire dataset before writing any fixtures. Any collision or duplicate label fails the compile with a precise error.
96
+
97
+ ### Composition
98
+
99
+ ```ruby
100
+ vial :base_users, abstract: true do
101
+ base do
102
+ active true
103
+ country "MA"
104
+ end
105
+ end
106
+
107
+ vial :admin_users do
108
+ include_vial :base_users do
109
+ override :role, "admin"
110
+ end
111
+
112
+ generate 2
113
+ end
114
+ ```
115
+
116
+ `include_vial` composes base attributes (and sequences) only; variants are not imported.
117
+ Abstract Vials are compile-time only and do not emit fixture files.
118
+
119
+ ### ID Strategy
120
+
121
+ Derived IDs are deterministic integers based on the identity tuple:
122
+
123
+ ```
124
+ [vial_name, record_type, label, variants[], index]
125
+ ```
126
+
127
+ You can set a namespace range per Vial:
8
128
 
9
129
  ```ruby
10
- gem 'vial'
130
+ vial :users, id_base: 100_000, id_range: 90_000 do
131
+ base do
132
+ role "user"
133
+ end
134
+
135
+ generate 3
136
+ end
11
137
  ```
12
138
 
13
- And then execute:
139
+ Explicit IDs are supported and never rewritten:
140
+
141
+ ```ruby
142
+ vial :users do
143
+ base do
144
+ id 1001
145
+ role "system"
146
+ end
14
147
 
15
- $ bundle install
148
+ generate 1
149
+ end
150
+ ```
151
+
152
+ Explain a derived ID:
153
+
154
+ ```bash
155
+ bin/rails vial:explain_id['users.admin.eu[3]']
156
+ ```
16
157
 
17
- Or install it yourself as:
158
+ ### Determinism & Faker
18
159
 
19
- $ gem install vial
160
+ Vial seeds Ruby's RNG with `Vial.config.seed` (default: 1). As long as you avoid non-deterministic sources
161
+ (`Time.now`, `SecureRandom`, etc.), recompiles produce identical output.
162
+
163
+ If you use Faker, seed it explicitly:
164
+
165
+ ```ruby
166
+ Faker::Config.random = Random.new(Vial.config.seed)
167
+ ```
168
+
169
+ ### Troubleshooting
170
+
171
+ - `Vial: no vial definitions found` → No `*.vial.rb` files were found under `Vial.config.source_paths`.
172
+ - `Vial: no records generated` → All vials are abstract or missing `generate` calls.
173
+ - Fixture ID standardization skipped for ERB → ERB fixtures are not rewritten to avoid altering dynamic content.
174
+ - Output path looks wrong → `Vial.config.output_path` defaults to the first `ActiveSupport::TestCase.fixture_paths` entry.
175
+
176
+ ### Configuration
177
+
178
+ ```ruby
179
+ Vial.configure do |config|
180
+ config.source_paths = [Rails.root.join("test/vials")]
181
+ config.output_path = Rails.root.join("test/fixtures")
182
+ config.seed = 1
183
+ end
184
+ ```
185
+
186
+ Example Vial sources live in `examples/`:
187
+ - `examples/users.vial.rb`
188
+ - `examples/company__users.vial.rb`
189
+ - `examples/base_users.vial.rb`
190
+ - `examples/admin_users.vial.rb`
191
+
192
+ Use `erb("...")` inside a Vial file to emit raw ERB (for associations or special IDs).
193
+
194
+ Vial provides several rake tasks for fixture analysis:
195
+
196
+ ### Count Fixtures
197
+
198
+ Get an overview of all YAML fixtures in your project:
199
+
200
+ ```bash
201
+ bin/rails vial:count_fixtures
202
+ ```
203
+
204
+ This shows:
205
+ - Total number of fixture files
206
+ - Distribution by directory
207
+ - Configured fixture paths
208
+
209
+ Note: `Vial.config.output_path` defaults to the first configured `fixture_paths` when available.
210
+
211
+ ### Clean Stale Vial Fixtures
212
+
213
+ Remove fixture files previously generated by Vial that no longer have a matching vial definition:
214
+
215
+ ```bash
216
+ bin/rails vial:clean
217
+ ```
218
+
219
+ Vial tracks generated fixture files in `test/fixtures/.vial_manifest.yml` and only removes files listed there.
220
+
221
+ ### Analyze Fixtures
222
+
223
+ Get detailed fixture-to-model mapping information:
224
+
225
+ ```bash
226
+ bin/rails vial:analyze_fixtures
227
+ ```
228
+
229
+ This shows:
230
+ - Mapped fixtures with their corresponding models
231
+ - Detection method used (fixture directive, set_fixture_class, or path inference)
232
+ - Unmapped fixtures that may need attention
233
+ - Any errors encountered during analysis
234
+
235
+ ### Validate Fixtures
236
+
237
+ Check for orphaned fixtures without models:
238
+
239
+ ```bash
240
+ bin/rails vial:validate_fixtures
241
+ ```
242
+
243
+ This task:
244
+ - Returns success (exit 0) if all fixtures are mapped
245
+ - Returns failure (exit 1) if unmapped fixtures are found
246
+ - Provides suggestions for fixing unmapped fixtures
247
+
248
+ ### Analyze Fixture IDs
249
+
250
+ Check if fixtures use proper `ActiveRecord::FixtureSet.identify`:
251
+
252
+ ```bash
253
+ bin/rails vial:analyze_fixture_ids
254
+ ```
255
+
256
+ This shows:
257
+ - Primary key type distribution (UUID, integer, string, etc.)
258
+ - Fixtures that need ID standardization
259
+ - Current vs expected ID values
260
+
261
+ ### Standardize Fixture IDs
262
+
263
+ Convert hardcoded IDs to use `ActiveRecord::FixtureSet.identify`:
264
+
265
+ ```bash
266
+ # Preview changes without applying
267
+ bin/rails vial:standardize_fixture_ids:dry_run
268
+
269
+ # Apply changes (with confirmation prompt)
270
+ bin/rails vial:standardize_fixture_ids
271
+ ```
272
+
273
+ This will:
274
+ - Replace hardcoded integer IDs with `<%= ActiveRecord::FixtureSet.identify(:label) %>`
275
+ - Replace hardcoded UUIDs with `<%= ActiveRecord::FixtureSet.identify(:label, :uuid) %>`
276
+ - Skip string primary keys (often manually managed like slugs)
277
+ - Ensure consistent, deterministic IDs across all fixtures
278
+
279
+ ## How Fixture Detection Works
280
+
281
+ Vial uses three methods to determine which model a fixture belongs to:
282
+
283
+ 1. **_fixture Directive**: Checks for `_fixture: model_class:` in YAML files
284
+ 2. **set_fixture_class**: Uses mappings from `ActiveSupport::TestCase.set_fixture_class`
285
+ 3. **Path Inference**: Infers models from fixture paths using Rails naming conventions
286
+
287
+ ## Example Output
288
+
289
+ ```
290
+ === Vial Fixture Analysis ===
291
+
292
+ Total fixtures found: 114
293
+ Mapped to models: 93
294
+ Unmapped fixtures: 21
295
+
296
+ === Mapped Fixtures ===
297
+ platform/countries:
298
+ Model: Platform::Country
299
+ Table: platform_countries
300
+ Detection: path_inference
301
+
302
+ active_llm_llms:
303
+ Model: ActiveLLM::LLM
304
+ Table: active_llm_llms
305
+ Detection: set_fixture_class
306
+ ```
20
307
 
21
- ## Note
308
+ ## Contributing
22
309
 
23
- This is a placeholder gem. The full implementation is coming soon.
310
+ 1. Fork it
311
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
312
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
313
+ 4. Push to the branch (`git push origin my-new-feature`)
314
+ 5. Create new Pull Request
24
315
 
25
316
  ## License
26
317
 
27
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
318
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ vial :admin_users do
4
+ include_vial :base_users do
5
+ override :role, "admin"
6
+ end
7
+
8
+ generate 2
9
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ vial :base_users, abstract: true do
4
+ base do
5
+ active true
6
+ country "MA"
7
+ email sequence(:email)
8
+ end
9
+
10
+ sequence(:email) { |i| "base#{i}@test.local" }
11
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ vial :company__users do
4
+ base do
5
+ active true
6
+ company_id erb("<%= ActiveRecord::FixtureSet.identify(:acme) %>")
7
+ end
8
+
9
+ generate 1
10
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ vial :users do
4
+ base do
5
+ active true
6
+ country "MA"
7
+ email sequence(:email)
8
+ end
9
+
10
+ variant :admin do
11
+ role "admin"
12
+ end
13
+
14
+ variant :guest do
15
+ role "guest"
16
+ active false
17
+ end
18
+
19
+ sequence(:email) { |i| "user#{i}@test.local" }
20
+
21
+ generate 2, :admin
22
+ generate 3, :guest
23
+ end
@@ -0,0 +1,303 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :vial do
4
+ desc 'Compile Vial sources into Rails fixtures'
5
+ task :compile, [:only] => :environment do |_, args|
6
+ only = args[:only] || ENV['ONLY']
7
+ result = Vial.compile!(only: only)
8
+ if result && result.status == :compiled
9
+ puts "Vial: fixtures compiled to #{Vial.config.output_path}"
10
+ end
11
+ end
12
+
13
+ desc 'Compile Vial sources without writing files (dry run)'
14
+ task 'compile:dry_run', [:only] => :environment do |_, args|
15
+ only = args[:only] || ENV['ONLY']
16
+ result = Vial.compile!(dry_run: true, only: only)
17
+ return unless result
18
+
19
+ if result.status == :dry_run
20
+ puts "Vial: dry run - would write #{result.files.size} fixture files to #{result.output_path}"
21
+ end
22
+ end
23
+
24
+ desc 'Compile only selected vials (comma-separated, e.g. vial:compile:only[users,posts])'
25
+ task 'compile:only', [:names] => :environment do |_, args|
26
+ names = args[:names] || ENV['ONLY']
27
+ if names.nil? || names.strip.empty?
28
+ abort "Usage: bin/rails vial:compile:only[users,posts]"
29
+ end
30
+
31
+ result = Vial.compile!(only: names)
32
+ if result && result.status == :compiled
33
+ puts "Vial: fixtures compiled to #{Vial.config.output_path}"
34
+ end
35
+ end
36
+
37
+ desc 'Remove fixture files generated by Vial that no longer have a matching vial definition'
38
+ task clean: :environment do
39
+ result = Vial.clean!
40
+ return unless result
41
+
42
+ if result.status == :cleaned
43
+ if result.removed.empty?
44
+ puts "Vial: no stale fixtures to remove"
45
+ else
46
+ puts "Vial: removed #{result.removed.size} stale fixture files"
47
+ end
48
+ end
49
+ end
50
+
51
+ desc 'Explain a derived ID (format: vial.label.variant[index])'
52
+ task :explain_id, [:query] => :environment do |_, args|
53
+ query = args[:query] || ENV['QUERY']
54
+ if query.nil? || query.strip.empty?
55
+ abort "Usage: bin/rails vial:explain_id['users.admin.eu[3]']"
56
+ end
57
+
58
+ begin
59
+ vial_name, label, variants, index = Vial::ExplainId.parse(query)
60
+ rescue ArgumentError => e
61
+ abort e.message
62
+ end
63
+ Vial.load_sources!
64
+ definition = Vial.registry[vial_name]
65
+ abort "Unknown vial: #{vial_name}" unless definition
66
+
67
+ info = definition.explain_id(label: label, variant_stack: variants, index: index)
68
+ puts info[:normalized]
69
+ puts "hash: #{info[:hash]}"
70
+ puts "base: #{info[:base]}"
71
+ puts "range: #{info[:range]}"
72
+ puts "final: #{info[:final]}"
73
+ end
74
+
75
+ desc 'Count YAML fixture files across all configured fixture paths'
76
+ task count_fixtures: :environment do
77
+ # Need to load test environment to get fixture configuration
78
+ require Rails.root.join('test/test_helper')
79
+
80
+ # Rails 8+ uses fixture_paths as an attribute
81
+ fixture_paths = ActiveSupport::TestCase.fixture_paths
82
+
83
+ puts "Vial: Scanning fixture paths..."
84
+ puts "Configured paths: #{fixture_paths.join(', ')}"
85
+ puts
86
+
87
+ total_count = 0
88
+ fixture_counts = {}
89
+
90
+ fixture_paths.each do |path|
91
+ next unless File.directory?(path)
92
+
93
+ yaml_files = Dir.glob(File.join(path, '**', '*.yml'))
94
+ yaml_files += Dir.glob(File.join(path, '**', '*.yaml'))
95
+
96
+ count = yaml_files.size
97
+ total_count += count
98
+
99
+ puts "#{path}: #{count} YAML files"
100
+
101
+ # Group by subdirectory for better visibility
102
+ yaml_files.each do |file|
103
+ relative_path = Pathname.new(file).relative_path_from(Pathname.new(path))
104
+ dir = relative_path.dirname.to_s
105
+ fixture_counts[dir] ||= 0
106
+ fixture_counts[dir] += 1
107
+ end
108
+ end
109
+
110
+ puts
111
+ puts "Fixture distribution by directory:"
112
+ fixture_counts.sort.each do |dir, count|
113
+ puts " #{dir == '.' ? '(root)' : dir}: #{count} files"
114
+ end
115
+
116
+ puts
117
+ puts "Total YAML fixtures found: #{total_count}"
118
+ end
119
+
120
+ desc 'Analyze fixtures and their model mappings'
121
+ task analyze_fixtures: :environment do
122
+ # Ensure test environment is loaded to get fixture configuration
123
+ require Rails.root.join('test/test_helper')
124
+
125
+ analyzer = Vial::FixtureAnalyzer.new
126
+ analyzer.analyze
127
+
128
+ puts "=== Vial Fixture Analysis ==="
129
+ puts
130
+
131
+ summary = analyzer.summary
132
+ puts "Total fixtures found: #{summary[:total_fixtures]}"
133
+ puts "Mapped to models: #{summary[:mapped_fixtures]}"
134
+ puts "Unmapped fixtures: #{summary[:unmapped_fixtures]}"
135
+ puts
136
+
137
+ if analyzer.mapped_fixtures.any?
138
+ puts "=== Mapped Fixtures ==="
139
+ analyzer.mapped_fixtures.sort_by { |k, _| k }.each do |fixture_name, info|
140
+ puts "#{fixture_name}:"
141
+ puts " Model: #{info.model_name}"
142
+ puts " Table: #{info.table_name}"
143
+ puts " Detection: #{info.detection_method}"
144
+ end
145
+ puts
146
+ end
147
+
148
+ if analyzer.unmapped_fixtures.any?
149
+ puts "=== Unmapped Fixtures (Potential Issues) ==="
150
+ analyzer.unmapped_fixtures.sort_by { |k, _| k }.each do |fixture_name, info|
151
+ puts "#{fixture_name}:"
152
+ puts " File: #{info.file}"
153
+ puts " Status: No model found"
154
+ end
155
+ puts
156
+ end
157
+
158
+ if summary[:errors].any?
159
+ puts "=== Errors ==="
160
+ summary[:errors].each do |error|
161
+ puts "File: #{error[:file]}"
162
+ puts "Error: #{error[:error]}"
163
+ puts
164
+ end
165
+ end
166
+ end
167
+
168
+ desc 'Validate fixtures - check for orphaned fixtures without models'
169
+ task validate_fixtures: :environment do
170
+ require Rails.root.join('test/test_helper')
171
+
172
+ analyzer = Vial::FixtureAnalyzer.new
173
+ analyzer.analyze
174
+
175
+ unmapped = analyzer.unmapped_fixtures
176
+
177
+ if unmapped.empty?
178
+ puts "✅ All fixtures are properly mapped to models!"
179
+ exit 0
180
+ else
181
+ puts "❌ Found #{unmapped.size} unmapped fixtures:"
182
+ unmapped.sort_by { |k, _| k }.each do |fixture_name, info|
183
+ puts " - #{fixture_name}"
184
+ end
185
+ puts
186
+ puts "These fixtures may:"
187
+ puts " 1. Need a _fixture: model_class directive in the YAML"
188
+ puts " 2. Need to be added to set_fixture_class in test_helper.rb"
189
+ puts " 3. Be orphaned and should be removed"
190
+ puts " 4. Have a model that doesn't follow Rails naming conventions"
191
+
192
+ exit 1
193
+ end
194
+ end
195
+
196
+ desc 'Analyze fixture IDs and check if they use proper ActiveRecord::FixtureSet.identify'
197
+ task analyze_fixture_ids: :environment do
198
+ require Rails.root.join('test/test_helper')
199
+
200
+ standardizer = Vial::FixtureIdStandardizer.new
201
+ standardizer.analyze
202
+
203
+ summary = standardizer.summary
204
+
205
+ puts "=== Vial Fixture ID Analysis ==="
206
+ puts
207
+ puts "Total fixtures analyzed: #{summary[:total_fixtures]}"
208
+ puts "Fixtures needing ID updates: #{summary[:fixtures_needing_updates]}"
209
+ puts "Total records to update: #{summary[:records_to_update]}"
210
+ puts
211
+
212
+ if summary[:primary_key_types].any?
213
+ puts "Primary key types:"
214
+ summary[:primary_key_types].sort.each do |type, count|
215
+ puts " #{type}: #{count} fixtures"
216
+ end
217
+ puts
218
+ end
219
+
220
+ if standardizer.updates_needed.any?
221
+ puts "=== Fixtures Needing ID Standardization ==="
222
+ standardizer.updates_needed.each do |update|
223
+ puts
224
+ puts "File: #{update.file}"
225
+ puts "Model: #{update.model_class.name}"
226
+ puts "Primary Key: #{update.primary_key} (#{update.primary_key_type})"
227
+ puts "Changes needed:"
228
+ update.changes.each do |change|
229
+ puts " #{change.label}:"
230
+ puts " Current: #{change.current_id.inspect}"
231
+ puts " Should be: #{change.new_id}"
232
+ end
233
+ end
234
+ else
235
+ puts "✅ All fixture IDs are properly standardized!"
236
+ end
237
+
238
+ if summary[:errors].any?
239
+ puts
240
+ puts "=== Errors ==="
241
+ summary[:errors].each do |error|
242
+ puts "File: #{error[:file]}"
243
+ puts "Error: #{error[:error]}"
244
+ end
245
+ end
246
+ end
247
+
248
+ desc 'Standardize fixture IDs to use ActiveRecord::FixtureSet.identify'
249
+ task standardize_fixture_ids: :environment do
250
+ require Rails.root.join('test/test_helper')
251
+
252
+ standardizer = Vial::FixtureIdStandardizer.new
253
+ standardizer.analyze
254
+
255
+ summary = standardizer.summary
256
+
257
+ if summary[:records_to_update] == 0
258
+ puts "✅ All fixture IDs are already standardized!"
259
+ exit 0
260
+ end
261
+
262
+ puts "Will update #{summary[:records_to_update]} records across #{summary[:fixtures_needing_updates]} files."
263
+ puts
264
+ print "Continue? (y/N): "
265
+
266
+ response = STDIN.gets.chomp.downcase
267
+
268
+ if response == 'y'
269
+ puts
270
+ standardizer.standardize!(dry_run: false)
271
+ puts
272
+ puts "✅ Fixture IDs have been standardized!"
273
+ else
274
+ puts "Cancelled."
275
+ end
276
+ end
277
+
278
+ desc 'Dry run of fixture ID standardization (preview changes without applying)'
279
+ task 'standardize_fixture_ids:dry_run' => :environment do
280
+ require Rails.root.join('test/test_helper')
281
+
282
+ standardizer = Vial::FixtureIdStandardizer.new
283
+ standardizer.analyze
284
+
285
+ summary = standardizer.summary
286
+
287
+ puts "=== Vial Fixture ID Standardization (DRY RUN) ==="
288
+ puts
289
+ puts "Would update #{summary[:records_to_update]} records across #{summary[:fixtures_needing_updates]} files."
290
+ puts
291
+
292
+ standardizer.standardize!(dry_run: true)
293
+
294
+ if summary[:errors].any?
295
+ puts
296
+ puts "=== Errors ==="
297
+ summary[:errors].each do |error|
298
+ puts "File: #{error[:file]}"
299
+ puts "Error: #{error[:error]}"
300
+ end
301
+ end
302
+ end
303
+ end