kumi 0.0.10 โ†’ 0.0.11

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +7 -231
  4. data/README.md +1 -1
  5. data/docs/VECTOR_SEMANTICS.md +286 -0
  6. data/docs/features/hierarchical-broadcasting.md +1 -1
  7. data/docs/features/s-expression-printer.md +2 -2
  8. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
  9. data/lib/kumi/analyzer.rb +34 -12
  10. data/lib/kumi/compiler.rb +2 -12
  11. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +157 -64
  12. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
  13. data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
  14. data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -101
  15. data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
  16. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
  17. data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
  18. data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
  19. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +2 -1
  20. data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
  21. data/lib/kumi/core/analyzer/passes/type_checker.rb +3 -3
  22. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
  23. data/lib/kumi/core/analyzer/passes/{type_inferencer.rb โ†’ type_inferencer_pass.rb} +4 -4
  24. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +2 -2
  25. data/lib/kumi/core/analyzer/plans.rb +52 -0
  26. data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
  27. data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
  28. data/lib/kumi/core/compiler/access_builder.rb +36 -0
  29. data/lib/kumi/core/compiler/access_planner.rb +219 -0
  30. data/lib/kumi/core/compiler/accessors/base.rb +69 -0
  31. data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
  32. data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
  33. data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
  34. data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
  35. data/lib/kumi/core/compiler_base.rb +2 -2
  36. data/lib/kumi/core/error_reporter.rb +6 -5
  37. data/lib/kumi/core/errors.rb +4 -0
  38. data/lib/kumi/core/explain.rb +157 -205
  39. data/lib/kumi/core/export/node_builders.rb +2 -2
  40. data/lib/kumi/core/export/node_serializers.rb +1 -1
  41. data/lib/kumi/core/function_registry/collection_functions.rb +21 -10
  42. data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
  43. data/lib/kumi/core/function_registry/function_builder.rb +142 -55
  44. data/lib/kumi/core/function_registry/logical_functions.rb +5 -5
  45. data/lib/kumi/core/function_registry/stat_functions.rb +2 -2
  46. data/lib/kumi/core/function_registry.rb +126 -108
  47. data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
  48. data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
  49. data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
  50. data/lib/kumi/core/ir/execution_engine.rb +50 -0
  51. data/lib/kumi/core/ir.rb +58 -0
  52. data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
  53. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
  54. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +36 -15
  55. data/lib/kumi/core/ruby_parser/input_builder.rb +5 -5
  56. data/lib/kumi/core/ruby_parser/parser.rb +1 -1
  57. data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
  58. data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
  59. data/lib/kumi/registry.rb +14 -79
  60. data/lib/kumi/runtime/executable.rb +213 -0
  61. data/lib/kumi/schema.rb +14 -3
  62. data/lib/kumi/schema_metadata.rb +2 -2
  63. data/lib/kumi/support/ir_dump.rb +491 -0
  64. data/lib/kumi/support/s_expression_printer.rb +1 -1
  65. data/lib/kumi/syntax/location.rb +5 -0
  66. data/lib/kumi/syntax/node.rb +0 -1
  67. data/lib/kumi/syntax/root.rb +2 -2
  68. data/lib/kumi/version.rb +1 -1
  69. data/lib/kumi.rb +6 -15
  70. metadata +26 -15
  71. data/lib/kumi/core/cascade_executor_builder.rb +0 -132
  72. data/lib/kumi/core/compiled_schema.rb +0 -43
  73. data/lib/kumi/core/compiler/expression_compiler.rb +0 -146
  74. data/lib/kumi/core/compiler/function_invoker.rb +0 -55
  75. data/lib/kumi/core/compiler/path_traversal_compiler.rb +0 -158
  76. data/lib/kumi/core/compiler/reference_compiler.rb +0 -46
  77. data/lib/kumi/core/evaluation_wrapper.rb +0 -40
  78. data/lib/kumi/core/nested_structure_utils.rb +0 -78
  79. data/lib/kumi/core/schema_instance.rb +0 -115
  80. data/lib/kumi/core/vectorized_function_builder.rb +0 -88
  81. data/lib/kumi/js/compiler.rb +0 -878
  82. data/lib/kumi/js/function_registry.rb +0 -333
  83. data/migrate_to_core_iterative.rb +0 -938
@@ -1,938 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require "fileutils"
5
- require "open3"
6
- require "json"
7
-
8
- class IterativeKumiCoreMigrator
9
- attr_reader :errors, :metadata, :change_tracker
10
-
11
- def initialize
12
- @errors = []
13
- @change_tracker = {}
14
- @change_thresholds = {
15
- warning: 10,
16
- critical: 25,
17
- suspicious: 50
18
- }
19
- @start_time = Time.now
20
- @phases = []
21
- @rollback_points = []
22
- @current_phase = nil
23
- @stats = {
24
- files_to_migrate: 0,
25
- files_updated: 0,
26
- files_moved: 0,
27
- total_changes: 0
28
- }
29
- end
30
-
31
- def migrate!
32
- log_phase("๐Ÿš€ Starting Iterative Kumi to Kumi::Core Migration")
33
-
34
- begin
35
- # Iterative approach to handle Zeitwerk properly
36
- run_phase("Phase 1: Setup & Analysis") { phase_1_setup_and_analysis }
37
- run_phase("Phase 2: Update Module Declarations In-Place") { phase_2_update_modules_in_place }
38
- run_phase("Phase 3: Move Files to Core") { phase_3_move_files_to_core }
39
- run_phase("Phase 4: Update External References") { phase_4_update_references }
40
- run_phase("Phase 5: Final Validation") { phase_5_final_validation }
41
-
42
- finalize_migration
43
- log_completion_summary
44
- log_phase("โœ… Migration completed successfully!", :success)
45
- rescue StandardError => e
46
- log_phase("โŒ Migration failed: #{e.message}", :error)
47
- handle_failure(e)
48
- restore_to_initial_state
49
- raise e
50
- end
51
- end
52
-
53
- private
54
-
55
- # ====================
56
- # PHASE MANAGEMENT
57
- # ====================
58
-
59
- def run_phase(phase_name, &block)
60
- @current_phase = phase_name
61
- log_phase("Starting #{phase_name}")
62
-
63
- # Create rollback point before each phase
64
- create_rollback_point(phase_name)
65
-
66
- begin
67
- result = yield
68
- log_phase("โœ… #{phase_name} completed successfully")
69
- @phases << { name: phase_name, status: :success }
70
- result
71
- rescue StandardError => e
72
- log_phase("โŒ #{phase_name} FAILED: #{e.message}", :error)
73
- @phases << { name: phase_name, status: :failed, error: e.message }
74
- raise e
75
- end
76
- end
77
-
78
- def log_phase(message, level = :info)
79
- timestamp = Time.now.strftime("%H:%M:%S")
80
- icon = case level
81
- when :success then "โœ…"
82
- when :error then "โŒ"
83
- when :warning then "โš ๏ธ"
84
- else "โ„น๏ธ"
85
- end
86
-
87
- puts "[#{timestamp}] #{icon} #{message}"
88
- end
89
-
90
- def create_rollback_point(phase_name)
91
- # Store initial commit for potential rollback
92
- if @rollback_points.empty?
93
- initial_commit = `git rev-parse HEAD`.strip
94
- @rollback_points << { phase: "Initial State", commit: initial_commit }
95
- log_phase("๐Ÿ“ Initial commit stored: #{initial_commit[0..7]}")
96
- end
97
-
98
- commit_msg = "Rollback point before #{phase_name}"
99
- system("git add -A && git commit -m '#{commit_msg}' --allow-empty")
100
- commit_hash = `git rev-parse HEAD`.strip
101
-
102
- @rollback_points << { phase: phase_name, commit: commit_hash }
103
-
104
- log_phase("๐Ÿ“ Created rollback point: #{commit_hash[0..7]} for #{phase_name}")
105
- end
106
-
107
- # ====================
108
- # PHASE 1: SETUP & ANALYSIS
109
- # ====================
110
-
111
- def phase_1_setup_and_analysis
112
- log_phase("๐Ÿ“‹ Analyzing current structure...")
113
-
114
- # Analyze current files
115
- files_to_migrate = analyze_files_to_migrate
116
- @stats[:files_to_migrate] = files_to_migrate.length
117
- log_phase("๐Ÿ“ Found #{files_to_migrate.length} files to migrate to core/")
118
-
119
- # Show file breakdown by type
120
- file_types = files_to_migrate.group_by { |f| f.split("/")[2] || "root" }
121
- file_types.each do |type, files|
122
- log_phase(" โ””โ”€ #{files.length} #{type} files")
123
- end
124
-
125
- # Analyze Zeitwerk expectations
126
- expected_constants = analyze_zeitwerk_structure
127
- log_phase("๐Ÿ” Zeitwerk will expect #{expected_constants} new Core:: constants")
128
-
129
- # Create core directory
130
- FileUtils.mkdir_p("lib/kumi/core")
131
- log_phase("๐Ÿ“‚ Created lib/kumi/core/ directory structure")
132
-
133
- # Test that everything still works before changes
134
- log_phase("๐Ÿงช Running pre-migration tests...")
135
- run_basic_tests("Phase 1 - Pre-migration baseline")
136
- log_phase("โœ… Baseline tests passed - ready to proceed")
137
- end
138
-
139
- def analyze_files_to_migrate
140
- exclude_files = [
141
- "lib/kumi/cli.rb",
142
- "lib/kumi/version.rb",
143
- "lib/kumi/schema.rb"
144
- ]
145
-
146
- Dir.glob("lib/kumi/**/*.rb").reject do |file|
147
- exclude_files.include?(file) ||
148
- file.include?("/core/") ||
149
- File.directory?(file)
150
- end
151
- end
152
-
153
- def analyze_zeitwerk_structure
154
- # Check what constants Zeitwerk will expect after file moves
155
- expected_count = 0
156
-
157
- Dir.glob("lib/kumi/**/*.rb").each do |file|
158
- next if file.include?("/core/")
159
- next if ["cli.rb", "version.rb", "schema.rb"].any? { |skip| file.end_with?(skip) }
160
-
161
- expected_count += 1
162
- end
163
-
164
- expected_count
165
- end
166
-
167
- def camelize(string)
168
- string.split("/").map do |part|
169
- part.split("_").map(&:capitalize).join
170
- end.join("::")
171
- end
172
-
173
- # ====================
174
- # PHASE 2: UPDATE MODULE DECLARATIONS IN-PLACE
175
- # ====================
176
-
177
- def phase_2_update_modules_in_place
178
- log_phase("๐Ÿ”„ Updating module declarations in-place (Zeitwerk compatibility)...")
179
-
180
- files_to_update = Dir.glob("lib/kumi/**/*.rb").reject do |file|
181
- file.include?("/core/") ||
182
- ["cli.rb", "version.rb", "schema.rb"].any? { |skip| file.end_with?(skip) }
183
- end
184
-
185
- log_phase(" Processing #{files_to_update.length} files...")
186
-
187
- files_updated = 0
188
- files_to_update.each_with_index do |file, index|
189
- files_updated += 1 if update_file_module_declaration(file)
190
- end
191
-
192
- @stats[:files_updated] = files_updated
193
- log_phase("โœ… Updated #{files_updated} files: Kumi:: โ†’ Kumi::Core::")
194
-
195
- # Critical test - ensure Zeitwerk can load updated modules
196
- log_phase("๐Ÿงช Testing Zeitwerk compatibility...")
197
- run_basic_tests("Phase 2 - Zeitwerk compatibility check")
198
- log_phase("โœ… Zeitwerk autoloading working correctly")
199
- end
200
-
201
- def update_file_module_declaration(file)
202
- content = File.read(file)
203
- original_content = content.dup
204
-
205
- # Pattern 1: Simple "module Kumi" -> "module Kumi::Core"
206
- content.gsub!(/^(\s*)module Kumi(\s*$)/) { "#{::Regexp.last_match(1)}module Kumi::Core#{::Regexp.last_match(2)}" }
207
- content.gsub!(/^(\s*)module Kumi(\s*#.*)$/) { "#{::Regexp.last_match(1)}module Kumi::Core#{::Regexp.last_match(2)}" }
208
-
209
- # Pattern 2: Nested modules "module Kumi::Something" -> "module Kumi::Core::Something"
210
- content.gsub!(/^(\s*)module Kumi::([A-Z]\w*)/) { "#{::Regexp.last_match(1)}module Kumi::Core::#{::Regexp.last_match(2)}" }
211
-
212
- # INLINE VALIDATION: Fix common issues while reading
213
- content = apply_inline_fixes(content, file, :namespace_update)
214
-
215
- if content != original_content
216
- track_file_changes(file, original_content, content, :in_place_module_update)
217
- File.write(file, content)
218
- return true
219
- end
220
-
221
- false
222
- end
223
-
224
- # ====================
225
- # PHASE 3: MOVE FILES TO CORE
226
- # ====================
227
-
228
- def phase_3_move_files_to_core
229
- log_phase("Moving files to core directory...")
230
-
231
- files_to_move = analyze_files_to_migrate
232
- moved_count = 0
233
-
234
- files_to_move.each do |file|
235
- moved_count += 1 if move_file_to_core_with_git(file)
236
- end
237
-
238
- # Clean up empty directories
239
- clean_empty_directories
240
-
241
- log_phase("Moved #{moved_count} files to core")
242
-
243
- # CRITICAL: Re-apply module declarations to moved files
244
- # (git mv preserves original content, so we need to re-update the modules)
245
- log_phase("๐Ÿ”„ Re-applying module declarations to moved files...")
246
- reapply_module_declarations_to_core_files
247
-
248
- # Test that Zeitwerk can find all the moved modules
249
- run_basic_tests("Phase 3 - After file moves")
250
-
251
- @stats[:files_moved] = moved_count
252
- end
253
-
254
- def move_file_to_core_with_git(file)
255
- relative_path = file.sub("lib/kumi/", "")
256
- new_path = "lib/kumi/core/#{relative_path}"
257
-
258
- # Create directory if needed
259
- FileUtils.mkdir_p(File.dirname(new_path))
260
-
261
- # Use git mv to preserve history
262
- result = system("git mv '#{file}' '#{new_path}' 2>/dev/null")
263
- if result
264
- log_phase(" Moved #{file} -> #{new_path}")
265
- true
266
- else
267
- record_error("Failed to move #{file} to #{new_path}")
268
- false
269
- end
270
- end
271
-
272
- def reapply_module_declarations_to_core_files
273
- core_files = Dir.glob("lib/kumi/core/**/*.rb")
274
- updated_count = 0
275
-
276
- core_files.each do |file|
277
- updated_count += 1 if update_file_module_declaration(file)
278
- end
279
-
280
- log_phase(" โœ… Re-applied module declarations to #{updated_count} core files")
281
- end
282
-
283
- def clean_empty_directories
284
- Dir.glob("lib/kumi/*/").each do |dir|
285
- next if dir.include?("/core/")
286
-
287
- if Dir.empty?(dir)
288
- Dir.rmdir(dir)
289
- log_phase(" Removed empty directory #{dir}")
290
- end
291
- end
292
- end
293
-
294
- # ====================
295
- # PHASE 4: UPDATE EXTERNAL REFERENCES
296
- # ====================
297
-
298
- def phase_4_update_references
299
- log_phase("Updating external references...")
300
-
301
- # Update public interface files
302
- update_public_interface_files
303
-
304
- # Update spec files
305
- update_spec_files
306
-
307
- # Update other files (examples, scripts, etc.)
308
- update_other_files
309
-
310
- # Test that all references work
311
- run_basic_tests("Phase 4 - After reference updates")
312
- end
313
-
314
- def update_public_interface_files
315
- log_phase(" Updating public interface files...")
316
-
317
- core_modules = %w[
318
- Analyzer Compiler Types Syntax Export Input Domain RubyParser
319
- Kumi::Registry.SchemaInstance SchemaMetadata Explain CompiledSchema
320
- EvaluationWrapper ErrorReporter ErrorReporting VectorizationMetadata
321
- JsonSchema AtomUnsatSolver ConstraintRelationshipSolver
322
- ]
323
-
324
- ["lib/kumi/schema.rb", "lib/kumi.rb"].each do |file|
325
- next unless File.exist?(file)
326
-
327
- update_file_references(file, core_modules, :public_interface_update)
328
- end
329
- end
330
-
331
- def update_spec_files
332
- log_phase(" Updating spec files...")
333
-
334
- spec_files = Dir.glob("{spec,test}/**/*.rb")
335
- spec_files.each { |file| update_spec_file_references(file) }
336
- end
337
-
338
- def update_other_files
339
- log_phase(" Updating other files...")
340
-
341
- other_files = Dir.glob("{examples,docs,scripts}/**/*.rb")
342
- other_files.each { |file| update_spec_file_references(file) if File.exist?(file) }
343
- end
344
-
345
- def update_file_references(file, core_modules, change_type)
346
- content = File.read(file)
347
- original_content = content.dup
348
-
349
- core_modules.each do |mod|
350
- content.gsub!(/\b#{mod}\./) { "Core::#{mod}." }
351
- content.gsub!(/\b#{mod}::/) { "Core::#{mod}::" }
352
- content.gsub!(/(\W)#{mod}(\s*\.)/) { "#{::Regexp.last_match(1)}Core::#{mod}#{::Regexp.last_match(2)}" }
353
- end
354
-
355
- # INLINE VALIDATION: Apply additional fixes
356
- content = apply_inline_fixes(content, file, :reference_update)
357
-
358
- return unless content != original_content
359
-
360
- track_file_changes(file, original_content, content, change_type)
361
- File.write(file, content)
362
- end
363
-
364
- def update_spec_file_references(file)
365
- content = File.read(file)
366
- original_content = content.dup
367
-
368
- core_modules = %w[
369
- Analyzer Compiler Types Syntax Export Input Domain RubyParser
370
- Kumi::Registry.SchemaInstance SchemaMetadata Explain CompiledSchema
371
- EvaluationWrapper ErrorReporter ErrorReporting VectorizationMetadata
372
- JsonSchema AtomUnsatSolver ConstraintRelationshipSolver
373
- ]
374
-
375
- core_modules.each do |mod|
376
- # Apply Core prefix, but skip VERSION patterns
377
- content.gsub!(/\bKumi::#{mod}(?!::[A-Z][a-zA-Z0-9]*::VERSION\b|::VERSION\b)/) { "Kumi::Core::#{mod}" }
378
- end
379
-
380
- # Special cases
381
- content.gsub!("include Kumi::Syntax", "include Kumi::Core::Syntax")
382
- content.gsub!("include Kumi::ErrorReporting", "include Kumi::Core::ErrorReporting")
383
-
384
- core_modules.each do |mod|
385
- content.gsub!(/^(\s*)#{mod}\./) { "#{::Regexp.last_match(1)}Kumi::Core::#{mod}." }
386
- end
387
-
388
- # INLINE VALIDATION: Apply additional fixes
389
- content = apply_inline_fixes(content, file, :spec_update)
390
-
391
- return unless content != original_content
392
-
393
- track_file_changes(file, original_content, content, :spec_reference_update)
394
- File.write(file, content)
395
- end
396
-
397
- # ====================
398
- # PHASE 5: FINAL VALIDATION
399
- # ====================
400
-
401
- def phase_5_final_validation
402
- log_phase("Running comprehensive validation...")
403
-
404
- # Test 1: Basic loading
405
- run_basic_tests("Final - Basic loading")
406
-
407
- # Test 2: Core module structure
408
- test_core_module_structure
409
-
410
- # Test 3: Final cleanup pass for Errors references
411
- fix_remaining_errors_references
412
-
413
- # Test 4: Full test suite (after cleanup)
414
- run_full_test_suite
415
-
416
- # Analyze change statistics
417
- analyze_final_statistics
418
- end
419
-
420
- def test_core_module_structure
421
- log_phase(" Testing core module structure...")
422
-
423
- test_script = <<~RUBY
424
- require "./lib/kumi"
425
-
426
- # Test that Core module exists
427
- raise "Kumi::Core not defined" unless defined?(Kumi::Core)
428
-
429
- # Test a few key modules
430
- core_modules = %w[Analyzer Compiler Syntax]
431
- core_modules.each do |mod|
432
- const_name = "Kumi::Core::\#{mod}"
433
- raise "\#{const_name} not available" unless Object.const_defined?(const_name)
434
- end
435
-
436
- puts "โœ… Core module structure validated"
437
- RUBY
438
-
439
- result = system("ruby -e '#{test_script}' 2>/dev/null")
440
- return if result
441
-
442
- record_error("Core module structure validation failed")
443
- raise "Core module structure is invalid"
444
- end
445
-
446
- def fix_remaining_errors_references
447
- log_phase(" Final cleanup pass for VERSION references...")
448
-
449
- fixed_count = 0
450
-
451
- # Find all files that might have incorrect VERSION references
452
- files_to_check = Dir.glob("lib/**/*.rb") + Dir.glob("spec/**/*.rb") + Dir.glob("examples/**/*.rb")
453
-
454
- files_to_check.each do |file|
455
- content = File.read(file)
456
- original_content = content.dup
457
-
458
- # Only fix VERSION references like Kumi::Core::Export::Serializer::VERSION -> Kumi::VERSION
459
- content.gsub!(/Kumi::Core::[A-Z]\w*(?:::[A-Z]\w*)*::VERSION\b/, "Kumi::VERSION")
460
-
461
- if content != original_content
462
- File.write(file, content)
463
- fixed_count += 1
464
- end
465
- end
466
-
467
- if fixed_count > 0
468
- log_phase(" ๐Ÿ”ง Fixed VERSION references in #{fixed_count} files")
469
- else
470
- log_phase(" โœ… No incorrect VERSION references found")
471
- end
472
- end
473
-
474
- def run_full_test_suite
475
- log_phase("๐Ÿงช Running full test suite...")
476
-
477
- stdout, stderr, status = Open3.capture3("bundle exec rspec")
478
-
479
- if status.success?
480
- # Extract test summary from output
481
- summary_line = stdout.lines.find { |line| line.include?("examples") && line.include?("failures") }
482
- log_phase(" โœ… Full test suite passed!")
483
- log_phase(" ๐Ÿ“Š #{summary_line.chomp}") if summary_line
484
- else
485
- log_phase(" โŒ Full test suite FAILED", :error)
486
-
487
- # Analyze uninitialized constant errors before writing logs
488
- test_output = stderr.empty? ? stdout : stderr
489
- analyze_uninitialized_constant_errors(test_output)
490
-
491
- # Write failed test logs to test_rspec.logs
492
- File.write("test_rspec.logs", test_output)
493
- log_phase(" ๐Ÿ“ Test failure logs written to test_rspec.logs")
494
-
495
- record_error("Full test suite failed")
496
- raise "Full test suite failed"
497
- end
498
- end
499
-
500
- def analyze_uninitialized_constant_errors(test_output)
501
- log_phase(" ๐Ÿ” Analyzing uninitialized constant errors...")
502
-
503
- # Extract all uninitialized constant errors
504
- constant_errors = []
505
- test_output.lines.each do |line|
506
- if line.match?(/NameError.*uninitialized constant ([A-Z][a-zA-Z0-9:]*[A-Z][a-zA-Z0-9]*)/)
507
- constant = line.match(/NameError.*uninitialized constant ([A-Z][a-zA-Z0-9:]*[A-Z][a-zA-Z0-9]*)/)[1]
508
- constant_errors << constant
509
- end
510
- end
511
-
512
- if constant_errors.any?
513
- unique_errors = constant_errors.uniq.sort
514
- log_phase(" ๐Ÿ“‹ Found #{constant_errors.length} uninitialized constant errors (#{unique_errors.length} unique):")
515
- unique_errors.each do |const|
516
- count = constant_errors.count(const)
517
- log_phase(" #{count}x #{const}")
518
- end
519
- else
520
- log_phase(" โœ… No uninitialized constant errors found")
521
- end
522
- end
523
-
524
- def analyze_final_statistics
525
- return if @change_tracker.empty?
526
-
527
- @stats[:total_changes] = @change_tracker.values.sum { |stats| stats[:total_changes] }
528
- total_files = @change_tracker.length
529
-
530
- log_phase("๐Ÿ“Š Change Statistics:")
531
- log_phase(" Files modified: #{total_files}")
532
- log_phase(" Total changes: #{@stats[:total_changes]}")
533
-
534
- # Show files with high change counts
535
- flagged_files = []
536
- @change_tracker.each do |file, stats|
537
- total = stats[:total_changes]
538
-
539
- if total >= @change_thresholds[:suspicious]
540
- flagged_files << { file: file, count: total, level: :suspicious }
541
- elsif total >= @change_thresholds[:critical]
542
- flagged_files << { file: file, count: total, level: :critical }
543
- elsif total >= @change_thresholds[:warning]
544
- flagged_files << { file: file, count: total, level: :warning }
545
- end
546
- end
547
-
548
- return unless flagged_files.any?
549
-
550
- log_phase("๐Ÿšจ #{flagged_files.length} files need review:")
551
- flagged_files.each { |f| log_phase(" #{f[:file]} (#{f[:count]} changes)") }
552
- end
553
-
554
- def run_cleanup_validation
555
- log_phase("๐Ÿ” Running cleanup validation...")
556
-
557
- issues_found = 0
558
-
559
- # Check for double Core:: patterns
560
- issues_found += fix_double_core_patterns
561
-
562
- # Check for missing Core:: in moved files
563
- issues_found += fix_missing_core_references
564
-
565
- if issues_found > 0
566
- log_phase("๐Ÿ”ง Fixed #{issues_found} reference issues automatically")
567
-
568
- # Re-test basic loading after fixes
569
- run_basic_tests("Post-cleanup validation")
570
- else
571
- log_phase("โœ… No cleanup issues found")
572
- end
573
- end
574
-
575
- def fix_double_core_patterns
576
- log_phase(" Checking for double Core:: patterns...")
577
- fixes = 0
578
-
579
- # Fix module declarations: Kumi::Core::Core -> Kumi::Core
580
- Dir.glob("lib/kumi/core/**/*.rb").each do |file|
581
- content = File.read(file)
582
- original_content = content.dup
583
-
584
- # Fix double Core in module declarations
585
- content.gsub!("module Kumi::Core::Core", "module Kumi::Core")
586
-
587
- # Fix double Core in references
588
- content.gsub!("Kumi::Core::Core::", "Kumi::Core::")
589
- content.gsub!("Core::Core::", "Core::")
590
-
591
- next unless content != original_content
592
-
593
- File.write(file, content)
594
- fixes += 1
595
- end
596
-
597
- # Also check public interface files
598
- ["lib/kumi/schema.rb"].each do |file|
599
- next unless File.exist?(file)
600
-
601
- content = File.read(file)
602
- original_content = content.dup
603
-
604
- content.gsub!("Core::Core::", "Core::")
605
-
606
- next unless content != original_content
607
-
608
- File.write(file, content)
609
- fixes += 1
610
- end
611
-
612
- fixes
613
- end
614
-
615
- def fix_missing_core_references
616
- log_phase(" Checking for missing Core:: references in moved files...")
617
- fixes = 0
618
-
619
- # Pattern: files in core/ should reference other core modules with Core::
620
- Dir.glob("lib/kumi/core/**/*.rb").each do |file|
621
- content = File.read(file)
622
- original_content = content.dup
623
-
624
- # Core modules that moved and need Core:: prefix when referenced
625
- core_modules = %w[
626
- Types Syntax Export Input Domain FunctionRegistry
627
- SchemaInstance SchemaMetadata Explain CompiledSchema
628
- EvaluationWrapper ErrorReporter ErrorReporting VectorizationMetadata
629
- JsonSchema AtomUnsatSolver ConstraintRelationshipSolver
630
- ]
631
-
632
- # Fix bare Kumi::ModuleName references (but not Kumi::Core::)
633
- core_modules.each do |mod|
634
- # Only fix if it's clearly a reference to a moved module
635
- content.gsub!(/\bKumi::#{mod}(?!::)/, "Kumi::Core::#{mod}")
636
- end
637
-
638
- # Fix string literal references (in eval, using statements, etc.)
639
- content.gsub!(/"using Kumi::([A-Z]\w+(?:::[A-Z]\w+)*)"/) do |match|
640
- module_path = ::Regexp.last_match(1)
641
- "\"using Kumi::Core::#{module_path}\""
642
- end
643
-
644
- # Fix other Kumi:: references in string literals
645
- content.gsub!(/(['"])([^'"]*?)Kumi::([A-Z]\w+(?:::[A-Z]\w+)*)([^'"]*?)\1/) do |match|
646
- quote = ::Regexp.last_match(1)
647
- prefix = ::Regexp.last_match(2)
648
- module_path = ::Regexp.last_match(3)
649
- suffix = ::Regexp.last_match(4)
650
- "#{quote}#{prefix}Kumi::Core::#{module_path}#{suffix}#{quote}"
651
- end
652
-
653
- next unless content != original_content
654
-
655
- File.write(file, content)
656
- fixes += 1
657
- end
658
-
659
- fixes
660
- end
661
-
662
- def log_completion_summary
663
- duration = Time.now - @start_time
664
-
665
- log_phase("=" * 60)
666
- log_phase("๐ŸŽ‰ MIGRATION COMPLETED SUCCESSFULLY!")
667
- log_phase("=" * 60)
668
- log_phase("๐Ÿ“ˆ Summary:")
669
- log_phase(" Files migrated: #{@stats[:files_to_migrate]}")
670
- log_phase(" Files moved: #{@stats[:files_moved]}")
671
- log_phase(" Total changes: #{@stats[:total_changes]}")
672
- log_phase(" Duration: #{duration.round(2)}s")
673
- log_phase(" Phases completed: #{@phases.count { |p| p[:status] == :success }}/#{@phases.length}")
674
- end
675
-
676
- def handle_failure(error)
677
- log_phase("๐Ÿ”„ Handling migration failure...")
678
-
679
- # Show failure summary
680
- log_phase("=" * 60)
681
- log_phase("โŒ MIGRATION FAILED")
682
- log_phase("=" * 60)
683
- log_phase("๐Ÿ’ฅ Error: #{error.message}")
684
-
685
- return unless @phases.any?
686
-
687
- log_phase("๐Ÿ“‹ Phase Status:")
688
- @phases.each do |phase|
689
- status_icon = phase[:status] == :success ? "โœ…" : "โŒ"
690
- log_phase(" #{status_icon} #{phase[:name]}")
691
- end
692
- rescue StandardError => e
693
- log_phase("โš ๏ธ Error during failure handling: #{e.message}", :error)
694
- @errors << "Error during failure handling: #{e.message}"
695
- end
696
-
697
- def restore_to_initial_state
698
- log_phase("๐Ÿ”„ Restoring to initial state...")
699
-
700
- # Stash the migration script to avoid restoring it
701
- script_name = File.basename(__FILE__)
702
- if File.exist?(script_name)
703
- system("cp #{script_name} #{script_name}.backup")
704
- log_phase("๐Ÿ“‹ Backed up migration script")
705
- end
706
-
707
- return
708
- # Find the initial commit from the first rollback point
709
- if @rollback_points.any?
710
- initial_commit = @rollback_points.first[:commit]
711
- log_phase("๐Ÿ“ Rolling back to initial commit #{initial_commit[0..7]}...")
712
- result = system("git reset --hard #{initial_commit}")
713
- if result
714
- log_phase("โœ… Repository restored to initial state")
715
-
716
- # Restore the migration script
717
- if File.exist?("#{script_name}.backup")
718
- system("mv #{script_name}.backup #{script_name}")
719
- log_phase("๐Ÿ“‹ Restored migration script")
720
- end
721
- else
722
- log_phase("โŒ Failed to restore to initial state", :error)
723
- end
724
- else
725
- log_phase("โš ๏ธ No initial commit stored - cannot restore")
726
- end
727
- end
728
-
729
- # ====================
730
- # INLINE VALIDATION
731
- # ====================
732
-
733
- def apply_inline_fixes(content, file_path, context = :general)
734
- original_content = content.dup
735
-
736
- # Fix 1: Double Core:: patterns
737
- content.gsub!("module Kumi::Core::Core", "module Kumi::Core")
738
- content.gsub!("Kumi::Core::Core::", "Kumi::Core::")
739
- content.gsub!("Core::Core::", "Core::")
740
-
741
- # Fix 2: For files in the core directory, be more selective about what gets the Core:: prefix
742
- if file_path.include?("/core/")
743
- # Only apply Core:: to modules that are actually in Core
744
- core_modules = %w[
745
- Analyzer Compiler Types Syntax Export Input Domain RubyParser
746
- Kumi::Registry.SchemaInstance SchemaMetadata Explain CompiledSchema
747
- EvaluationWrapper ErrorReporter ErrorReporting VectorizationMetadata
748
- JsonSchema AtomUnsatSolver ConstraintRelationshipSolver
749
- ]
750
-
751
- # Fix references to other core modules
752
- core_modules.each do |mod|
753
- # Replace Kumi::ModuleName with Core prefix, but skip VERSION references
754
- content.gsub!(/\bKumi::#{mod}(?!::[A-Z][a-zA-Z0-9]*::VERSION\b|::VERSION\b)/) { "Kumi::Core::#{mod}" }
755
- end
756
- else
757
- # For non-core files, apply broader fixes but exclude only top-level non-core modules
758
- content.gsub!(/(?<!module\s)(?<!class\s)(?<!struct\s)\bKumi::([A-Z][a-zA-Z0-9]*(?:::[A-Z][a-zA-Z0-9]*)*)/) do |match|
759
- module_path = ::Regexp.last_match(1)
760
- # Skip if already has Core:: or if it's a root-level non-core module
761
- first_part = module_path.split("::").first
762
- if module_path.start_with?("Core::") ||
763
- %w[Schema CLI VERSION].include?(first_part)
764
- match
765
- else
766
- "Kumi::Core::#{module_path}"
767
- end
768
- end
769
- end
770
-
771
- # Fix 3: String literal references - be selective here too
772
- content.gsub!(/"using Kumi::([A-Z]\w+(?:::[A-Z]\w+)*)"/) do |match|
773
- module_path = ::Regexp.last_match(1)
774
- # Don't move top-level non-core modules
775
- if module_path.start_with?("Core::") || %w[Schema CLI VERSION].include?(module_path.split("::").first)
776
- "\"using Kumi::#{module_path}\""
777
- else
778
- "\"using Kumi::Core::#{module_path}\""
779
- end
780
- end
781
-
782
- content
783
- end
784
-
785
- def count_line_differences(original, updated)
786
- original_lines = original.lines
787
- updated_lines = updated.lines
788
-
789
- differences = 0
790
- max_lines = [original_lines.length, updated_lines.length].max
791
-
792
- (0...max_lines).each do |i|
793
- orig_line = original_lines[i]&.strip
794
- new_line = updated_lines[i]&.strip
795
- differences += 1 if orig_line != new_line
796
- end
797
-
798
- differences
799
- end
800
-
801
- # ====================
802
- # UTILITIES
803
- # ====================
804
-
805
- def run_basic_tests(context)
806
- log_phase("๐Ÿงช Running basic tests (#{context})...")
807
-
808
- test_script = 'require "./lib/kumi"; puts "โœ… Basic load successful"'
809
- stdout, stderr, status = Open3.capture3("ruby -e '#{test_script}'")
810
-
811
- if status.success?
812
- log_phase(" โœ… Basic loading test passed")
813
- else
814
- log_phase(" โŒ Basic loading test FAILED", :error)
815
- log_test_error("Basic Load Test", stderr, stdout)
816
- record_error("Basic test failed in #{context}")
817
- raise "Basic test failed in #{context}"
818
- end
819
- end
820
-
821
- def log_test_error(test_name, stderr, stdout)
822
- log_phase("๐Ÿšจ #{test_name} Error Details:", :error)
823
-
824
- if stderr && !stderr.empty?
825
- # Clean and extract key error information
826
- error_lines = stderr.lines.first(5)
827
- error_lines.each do |line|
828
- cleaned_line = clean_path_from_error(line.chomp)
829
- log_phase(" #{cleaned_line}", :error)
830
- end
831
- end
832
-
833
- return unless stdout && !stdout.empty? && stdout != stderr
834
-
835
- cleaned_output = clean_path_from_error(stdout.chomp)
836
- log_phase(" Output: #{cleaned_output}")
837
- end
838
-
839
- def clean_path_from_error(message)
840
- # Remove the current working directory from paths to make them relative
841
- current_dir = Dir.pwd
842
- message.gsub(current_dir + "/", "")
843
- .gsub(current_dir, ".")
844
- end
845
-
846
- def track_file_changes(file_path, original_content, new_content, change_type)
847
- return 0 if original_content == new_content
848
-
849
- @change_tracker[file_path] ||= {
850
- total_changes: 0,
851
- change_types: Hash.new(0),
852
- phases: []
853
- }
854
-
855
- # Simple line-diff count
856
- original_lines = original_content.lines
857
- new_lines = new_content.lines
858
-
859
- changes_count = 0
860
- max_lines = [original_lines.length, new_lines.length].max
861
- (0...max_lines).each do |i|
862
- orig_line = original_lines[i]&.strip
863
- new_line = new_lines[i]&.strip
864
- changes_count += 1 if orig_line != new_line
865
- end
866
-
867
- @change_tracker[file_path][:total_changes] += changes_count
868
- @change_tracker[file_path][:change_types][change_type] += changes_count
869
- @change_tracker[file_path][:phases] << {
870
- phase: @current_phase,
871
- type: change_type,
872
- count: changes_count,
873
- timestamp: Time.now
874
- }
875
-
876
- changes_count
877
- end
878
-
879
- def flag_file(file, change_count, severity)
880
- icon = case severity
881
- when :warning then "โš ๏ธ"
882
- when :critical then "๐Ÿšจ"
883
- when :suspicious then "๐Ÿ”ด"
884
- end
885
-
886
- log_phase("#{icon} #{file}: #{change_count} changes (#{severity})", severity)
887
- end
888
-
889
- def finalize_migration
890
- log_phase("Finalizing migration...")
891
-
892
- system("git add -A")
893
- commit_msg = "Complete Kumi to Kumi::Core migration\n\n" \
894
- "Iterative migration completed successfully:\n" \
895
- "- #{@metadata[:files_to_migrate]} files migrated\n" \
896
- "- All tests passing\n" \
897
- "- Zeitwerk autoloading working correctly"
898
-
899
- system("git commit -m '#{commit_msg}'")
900
-
901
- final_commit = `git rev-parse HEAD`.strip
902
- @metadata[:final_commit] = final_commit
903
- @metadata[:final_status] = :success
904
-
905
- log_phase("Migration committed as #{final_commit[0..7]}")
906
- end
907
-
908
- def record_error(message)
909
- @errors << {
910
- timestamp: Time.now,
911
- message: message,
912
- phase: @current_phase
913
- }
914
- end
915
-
916
- def save_migration_metadata
917
- @metadata[:end_time] = Time.now
918
- @metadata[:duration] = @metadata[:end_time] - @metadata[:start_time]
919
- @metadata[:errors] = @errors
920
- @metadata[:change_statistics] = @change_tracker
921
-
922
- File.write("migration_metadata_iterative.json", JSON.pretty_generate(@metadata))
923
- log_phase("Migration metadata saved")
924
- end
925
- end
926
-
927
- # Run migration if script is executed directly
928
- if __FILE__ == $0
929
- begin
930
- migrator = IterativeKumiCoreMigrator.new
931
- migrator.migrate!
932
-
933
- # Success summary already logged by log_completion_summary
934
- rescue StandardError => e
935
- # Error details already logged by handle_failure
936
- exit 1
937
- end
938
- end