kumi 0.0.9 → 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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +18 -258
  4. data/README.md +188 -121
  5. data/docs/AST.md +1 -1
  6. data/docs/FUNCTIONS.md +52 -8
  7. data/docs/VECTOR_SEMANTICS.md +286 -0
  8. data/docs/compiler_design_principles.md +86 -0
  9. data/docs/features/README.md +15 -2
  10. data/docs/features/hierarchical-broadcasting.md +349 -0
  11. data/docs/features/javascript-transpiler.md +148 -0
  12. data/docs/features/performance.md +1 -3
  13. data/docs/features/s-expression-printer.md +2 -2
  14. data/docs/schema_metadata.md +7 -7
  15. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
  16. data/examples/game_of_life.rb +2 -4
  17. data/lib/kumi/analyzer.rb +34 -14
  18. data/lib/kumi/compiler.rb +4 -283
  19. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +717 -66
  20. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
  21. data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
  22. data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -99
  23. data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
  24. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
  25. data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
  26. data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
  27. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +28 -0
  28. data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
  29. data/lib/kumi/core/analyzer/passes/type_checker.rb +9 -5
  30. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
  31. data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
  32. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +92 -48
  33. data/lib/kumi/core/analyzer/plans.rb +52 -0
  34. data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
  35. data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
  36. data/lib/kumi/core/compiler/access_builder.rb +36 -0
  37. data/lib/kumi/core/compiler/access_planner.rb +219 -0
  38. data/lib/kumi/core/compiler/accessors/base.rb +69 -0
  39. data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
  40. data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
  41. data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
  42. data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
  43. data/lib/kumi/core/compiler_base.rb +137 -0
  44. data/lib/kumi/core/error_reporter.rb +6 -5
  45. data/lib/kumi/core/errors.rb +4 -0
  46. data/lib/kumi/core/explain.rb +157 -205
  47. data/lib/kumi/core/export/node_builders.rb +2 -2
  48. data/lib/kumi/core/export/node_serializers.rb +1 -1
  49. data/lib/kumi/core/function_registry/collection_functions.rb +100 -6
  50. data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
  51. data/lib/kumi/core/function_registry/function_builder.rb +142 -53
  52. data/lib/kumi/core/function_registry/logical_functions.rb +173 -3
  53. data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
  54. data/lib/kumi/core/function_registry.rb +138 -98
  55. data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
  56. data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
  57. data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
  58. data/lib/kumi/core/ir/execution_engine.rb +50 -0
  59. data/lib/kumi/core/ir.rb +58 -0
  60. data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
  61. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
  62. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +37 -16
  63. data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
  64. data/lib/kumi/core/ruby_parser/parser.rb +1 -1
  65. data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
  66. data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
  67. data/lib/kumi/errors.rb +2 -0
  68. data/lib/kumi/js.rb +23 -0
  69. data/lib/kumi/registry.rb +17 -22
  70. data/lib/kumi/runtime/executable.rb +213 -0
  71. data/lib/kumi/schema.rb +15 -4
  72. data/lib/kumi/schema_metadata.rb +2 -2
  73. data/lib/kumi/support/ir_dump.rb +491 -0
  74. data/lib/kumi/support/s_expression_printer.rb +17 -16
  75. data/lib/kumi/syntax/array_expression.rb +6 -6
  76. data/lib/kumi/syntax/call_expression.rb +4 -4
  77. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  78. data/lib/kumi/syntax/case_expression.rb +4 -4
  79. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  80. data/lib/kumi/syntax/hash_expression.rb +4 -4
  81. data/lib/kumi/syntax/input_declaration.rb +6 -5
  82. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  83. data/lib/kumi/syntax/input_reference.rb +5 -5
  84. data/lib/kumi/syntax/literal.rb +4 -4
  85. data/lib/kumi/syntax/location.rb +5 -0
  86. data/lib/kumi/syntax/node.rb +33 -34
  87. data/lib/kumi/syntax/root.rb +6 -6
  88. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  89. data/lib/kumi/syntax/value_declaration.rb +4 -4
  90. data/lib/kumi/version.rb +1 -1
  91. data/lib/kumi.rb +6 -15
  92. data/scripts/analyze_broadcast_methods.rb +68 -0
  93. data/scripts/analyze_cascade_methods.rb +74 -0
  94. data/scripts/check_broadcasting_coverage.rb +51 -0
  95. data/scripts/find_dead_code.rb +114 -0
  96. metadata +36 -9
  97. data/docs/features/array-broadcasting.md +0 -170
  98. data/lib/kumi/cli.rb +0 -449
  99. data/lib/kumi/core/compiled_schema.rb +0 -43
  100. data/lib/kumi/core/evaluation_wrapper.rb +0 -40
  101. data/lib/kumi/core/schema_instance.rb +0 -111
  102. data/lib/kumi/core/vectorization_metadata.rb +0 -110
  103. data/migrate_to_core_iterative.rb +0 -938
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Kumi
4
4
  module Syntax
5
- HashExpression = Struct.new(:pairs) do
6
- include Node
5
+ HashExpression = Struct.new(:pairs) do
6
+ include Node
7
7
 
8
- def children = pairs.flatten
9
- end
8
+ def children = pairs.flatten
9
+ end
10
10
  end
11
11
  end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Kumi
4
4
  module Syntax
5
- # For field metadata declarations inside input blocks
6
- InputDeclaration = Struct.new(:name, :domain, :type, :children) do
7
- include Node
5
+ # For field metadata declarations inside input blocks
6
+ InputDeclaration = Struct.new(:name, :domain, :type, :children, :access_mode) do
7
+ include Node
8
8
 
9
- def children = self[:children] || []
10
- end
9
+ def children = self[:children] || []
10
+ def access_mode = self[:access_mode]
11
+ end
11
12
  end
12
13
  end
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Kumi
4
4
  module Syntax
5
- # For field usage/reference in expressions (input.field_name)
6
- InputElementReference = Struct.new(:path) do
7
- include Node
5
+ # For field usage/reference in expressions (input.field_name)
6
+ InputElementReference = Struct.new(:path) do
7
+ include Node
8
8
 
9
- def children = []
10
- end
9
+ def children = []
10
+ end
11
11
  end
12
12
  end
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Kumi
4
4
  module Syntax
5
- # For field usage/reference in expressions (input.field_name)
6
- InputReference = Struct.new(:name) do
7
- include Node
5
+ # For field usage/reference in expressions (input.field_name)
6
+ InputReference = Struct.new(:name) do
7
+ include Node
8
8
 
9
- def children = []
10
- end
9
+ def children = []
10
+ end
11
11
  end
12
12
  end
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Kumi
4
4
  module Syntax
5
- Literal = Struct.new(:value) do
6
- include Node
5
+ Literal = Struct.new(:value) do
6
+ include Node
7
7
 
8
- def children = []
9
- end
8
+ def children = []
9
+ end
10
10
  end
11
11
  end
@@ -0,0 +1,5 @@
1
+ module Kumi
2
+ module Syntax
3
+ Location = Struct.new(:file, :line, :column, keyword_init: true)
4
+ end
5
+ end
@@ -2,45 +2,44 @@
2
2
 
3
3
  module Kumi
4
4
  module Syntax
5
- # A struct to hold standardized source location information.
6
- Location = Struct.new(:file, :line, :column, keyword_init: true)
5
+ # A struct to hold standardized source location information.
7
6
 
8
- # Base module included by all AST nodes to provide a standard
9
- # interface for accessing source location information..
10
- module Node
11
- attr_accessor :loc
7
+ # Base module included by all AST nodes to provide a standard
8
+ # interface for accessing source location information..
9
+ module Node
10
+ attr_accessor :loc
12
11
 
13
- def initialize(*args, loc: nil, **kwargs)
14
- @loc = loc
15
- super(*args, **kwargs)
16
- freeze
17
- end
12
+ def initialize(*args, loc: nil, **kwargs)
13
+ @loc = loc
14
+ super(*args, **kwargs)
15
+ freeze
16
+ end
18
17
 
19
- def ==(other)
20
- other.is_a?(self.class) &&
21
- # for Struct-based nodes
22
- (if respond_to?(:members)
23
- members.all? { |m| self[m] == other[m] }
24
- else
25
- instance_variables.reject { |iv| iv == :@loc }
26
- .all? do |iv|
27
- instance_variable_get(iv) ==
28
- other.instance_variable_get(iv)
29
- end
18
+ def ==(other)
19
+ other.is_a?(self.class) &&
20
+ # for Struct-based nodes
21
+ (if respond_to?(:members)
22
+ members.all? { |m| self[m] == other[m] }
23
+ else
24
+ instance_variables.reject { |iv| iv == :@loc }
25
+ .all? do |iv|
26
+ instance_variable_get(iv) ==
27
+ other.instance_variable_get(iv)
30
28
  end
31
- )
32
- end
33
- alias eql? ==
29
+ end
30
+ )
31
+ end
32
+ alias eql? ==
34
33
 
35
- def hash
36
- values = if respond_to?(:members)
37
- members.map { |m| self[m] }
38
- else
39
- instance_variables.reject { |iv| iv == :@loc }
40
- .map { |iv| instance_variable_get(iv) }
41
- end
42
- [self.class, *values].hash
43
- end
34
+ def hash
35
+ values = if respond_to?(:members)
36
+ members.map { |m| self[m] }
37
+ else
38
+ instance_variables.reject { |iv| iv == :@loc }
39
+ .map { |iv| instance_variable_get(iv) }
40
+ end
41
+ [self.class, *values].hash
44
42
  end
43
+ end
45
44
  end
46
45
  end
@@ -2,12 +2,12 @@
2
2
 
3
3
  module Kumi
4
4
  module Syntax
5
- # Represents the root of the Abstract Syntax Tree.
6
- # It holds all the top-level declarations parsed from the source.
7
- Root = Struct.new(:inputs, :attributes, :traits) do
8
- include Node
5
+ # Represents the root of the Abstract Syntax Tree.
6
+ # It holds all the top-level declarations parsed from the source.
7
+ Root = Struct.new(:inputs, :values, :traits) do
8
+ include Node
9
9
 
10
- def children = [inputs, attributes, traits]
11
- end
10
+ def children = [inputs, values, traits]
11
+ end
12
12
  end
13
13
  end
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Kumi
4
4
  module Syntax
5
- TraitDeclaration = Struct.new(:name, :expression) do
6
- include Node
5
+ TraitDeclaration = Struct.new(:name, :expression) do
6
+ include Node
7
7
 
8
- def children = [expression]
9
- end
8
+ def children = [expression]
9
+ end
10
10
  end
11
11
  end
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Kumi
4
4
  module Syntax
5
- ValueDeclaration = Struct.new(:name, :expression) do
6
- include Node
5
+ ValueDeclaration = Struct.new(:name, :expression) do
6
+ include Node
7
7
 
8
- def children = [expression]
9
- end
8
+ def children = [expression]
9
+ end
10
10
  end
11
11
  end
data/lib/kumi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kumi
4
- VERSION = "0.0.9"
4
+ VERSION = "0.0.11"
5
5
  end
data/lib/kumi.rb CHANGED
@@ -5,22 +5,13 @@ require "zeitwerk"
5
5
 
6
6
  loader = Zeitwerk::Loader.for_gem
7
7
  loader.ignore("#{__dir__}/kumi-cli")
8
+ loader.inflector.inflect(
9
+ "lower_to_ir_pass" => "LowerToIRPass",
10
+ "vm" => "VM",
11
+ "ir" => "IR",
12
+ 'ir_dump' => 'IRDump',
13
+ )
8
14
  loader.setup
9
15
 
10
16
  module Kumi
11
- extend Schema
12
-
13
- def self.inspector_from_schema
14
- Inspector.new(@__syntax_tree__, @__analyzer_result__, @__compiled_schema__)
15
- end
16
-
17
- def self.reset!
18
- @__syntax_tree__ = nil
19
- @__analyzer_result__ = nil
20
- @__compiled_schema__ = nil
21
- @__schema_metadata__ = nil
22
- end
23
-
24
- # Reset on require to avoid state leakage in tests
25
- reset!
26
17
  end
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+
6
+ coverage_file = "coverage/.resultset.json"
7
+
8
+ unless File.exist?(coverage_file)
9
+ puts "āŒ No coverage data found. Run tests first:"
10
+ puts " COVERAGE=true bundle exec rspec"
11
+ exit 1
12
+ end
13
+
14
+ data = JSON.parse(File.read(coverage_file))
15
+ coverage = data.values.first["coverage"]
16
+
17
+ broadcast_file = coverage.find { |file, _| file.include?("broadcast_detector.rb") }
18
+
19
+ if broadcast_file
20
+ file_path, line_data = broadcast_file
21
+ lines = line_data["lines"]
22
+
23
+ puts "šŸ” BroadcastDetector Coverage Analysis:"
24
+ puts "File: #{File.basename(file_path)}"
25
+
26
+ total_lines = lines.count { |hits| !hits.nil? }
27
+ covered_lines = lines.count { |hits| hits && hits > 0 }
28
+ coverage_percent = (covered_lines.to_f / total_lines * 100).round(1)
29
+
30
+ puts "Coverage: #{coverage_percent}% (#{covered_lines}/#{total_lines} lines)"
31
+
32
+ # Find uncovered method definitions
33
+ file_content = File.read(file_path)
34
+ uncovered_methods = []
35
+ covered_methods = []
36
+
37
+ file_content.lines.each_with_index do |line, i|
38
+ if line.match?(/^\s*def\s+(\w+)/)
39
+ method_name = line.match(/^\s*def\s+(\w+)/)[1]
40
+ line_hits = lines[i]
41
+
42
+ if line_hits.nil?
43
+ # Line not tracked
44
+ elsif line_hits == 0
45
+ uncovered_methods << "#{method_name} (line #{i+1})"
46
+ else
47
+ covered_methods << "#{method_name} (line #{i+1})"
48
+ end
49
+ end
50
+ end
51
+
52
+ puts "\nšŸ“Š Method Coverage Summary:"
53
+ puts " Total methods: #{covered_methods.length + uncovered_methods.length}"
54
+ puts " Covered methods: #{covered_methods.length}"
55
+ puts " Uncovered methods: #{uncovered_methods.length}"
56
+
57
+ if uncovered_methods.any?
58
+ puts "\n🚨 Uncovered methods (potential dead code):"
59
+ uncovered_methods.each { |m| puts " - #{m}" }
60
+ end
61
+
62
+ if covered_methods.any?
63
+ puts "\nāœ… Covered methods:"
64
+ covered_methods.each { |m| puts " - #{m}" }
65
+ end
66
+ else
67
+ puts "āŒ BroadcastDetector file not found in coverage data"
68
+ end
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+
6
+ coverage_file = "coverage/.resultset.json"
7
+ target_file = "cascade_executor_builder.rb"
8
+
9
+ unless File.exist?(coverage_file)
10
+ puts "āŒ No coverage data found. Run tests first"
11
+ exit 1
12
+ end
13
+
14
+ data = JSON.parse(File.read(coverage_file))
15
+ coverage = data.values.first["coverage"]
16
+
17
+ file_entry = coverage.find { |file, _| file.include?(target_file) }
18
+
19
+ if file_entry
20
+ file_path, line_data = file_entry
21
+ lines = line_data["lines"]
22
+
23
+ puts "šŸ” CascadeExecutorBuilder Method Coverage:"
24
+ puts "=" * 50
25
+
26
+ file_content = File.read(file_path)
27
+ methods = []
28
+
29
+ file_content.lines.each_with_index do |line, i|
30
+ if line.match?(/^\s*def\s+/)
31
+ method_match = line.match(/^\s*def\s+(self\.)?(\w+)/)
32
+ if method_match
33
+ method_name = method_match[2]
34
+ is_class_method = !method_match[1].nil?
35
+ line_hits = lines[i]
36
+
37
+ status = if line_hits.nil?
38
+ "⚪ NOT TRACKED"
39
+ elsif line_hits == 0
40
+ "🚨 UNCOVERED"
41
+ else
42
+ "āœ… COVERED (#{line_hits} hits)"
43
+ end
44
+
45
+ prefix = is_class_method ? "self." : ""
46
+ methods << {
47
+ name: "#{prefix}#{method_name}",
48
+ line: i + 1,
49
+ hits: line_hits,
50
+ status: status
51
+ }
52
+ end
53
+ end
54
+ end
55
+
56
+ methods.each do |method|
57
+ printf " %-25s (line %3d) %s\n", method[:name], method[:line], method[:status]
58
+ end
59
+
60
+ uncovered = methods.select { |m| m[:hits] == 0 }
61
+ covered = methods.select { |m| m[:hits] && m[:hits] > 0 }
62
+
63
+ puts "\nšŸ“Š Summary:"
64
+ puts " Total methods: #{methods.length}"
65
+ puts " Covered: #{covered.length}"
66
+ puts " Uncovered: #{uncovered.length}"
67
+
68
+ if uncovered.any?
69
+ puts "\n🚨 Uncovered methods (potential dead code):"
70
+ uncovered.each { |m| puts " #{m[:name]} (line #{m[:line]})" }
71
+ end
72
+ else
73
+ puts "āŒ #{target_file} not found in coverage data"
74
+ end
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+
6
+ coverage_file = "coverage/.resultset.json"
7
+
8
+ unless File.exist?(coverage_file)
9
+ puts "āŒ No coverage data found. Run tests first"
10
+ exit 1
11
+ end
12
+
13
+ data = JSON.parse(File.read(coverage_file))
14
+ coverage = data.values.first["coverage"]
15
+
16
+ broadcasting_files = [
17
+ "cascade_executor_builder.rb",
18
+ "vectorized_function_builder.rb",
19
+ "nested_structure_utils.rb",
20
+ "broadcast_detector.rb"
21
+ ]
22
+
23
+ puts "šŸ” Broadcasting Files Coverage:"
24
+ puts "=" * 60
25
+
26
+ broadcasting_files.each do |filename|
27
+ file_entry = coverage.find { |file, _| file.include?(filename) }
28
+
29
+ if file_entry
30
+ file_path, line_data = file_entry
31
+ lines = line_data["lines"]
32
+
33
+ total_lines = lines.count { |hits| !hits.nil? }
34
+ covered_lines = lines.count { |hits| hits && hits > 0 }
35
+ coverage_percent = total_lines > 0 ? (covered_lines.to_f / total_lines * 100).round(1) : 0
36
+
37
+ status = if coverage_percent == 0
38
+ "🚨 DEAD"
39
+ elsif coverage_percent < 20
40
+ "šŸ”“ LOW"
41
+ elsif coverage_percent < 50
42
+ "🟔 MEDIUM"
43
+ else
44
+ "āœ… GOOD"
45
+ end
46
+
47
+ printf "%-35s %s %6.1f%% (%d/%d lines)\n", filename, status, coverage_percent, covered_lines, total_lines
48
+ else
49
+ printf "%-35s āŒ NOT FOUND\n", filename
50
+ end
51
+ end
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+
6
+ class SimpleDeadCodeFinder
7
+ COVERAGE_JSON = "coverage/.resultset.json"
8
+
9
+ def run
10
+ unless File.exist?(COVERAGE_JSON)
11
+ puts "āŒ No coverage data found. Run tests first:"
12
+ puts " COVERAGE=true bundle exec rspec"
13
+ exit 1
14
+ end
15
+
16
+ puts "šŸ” Analyzing SimpleCov data for dead code..."
17
+
18
+ coverage_data = JSON.parse(File.read(COVERAGE_JSON))
19
+ coverage = coverage_data.values.first["coverage"]
20
+
21
+ find_dead_files(coverage)
22
+ find_low_coverage_files(coverage)
23
+
24
+ puts "\nšŸ’” Next steps:"
25
+ puts " 1. Open coverage/index.html to see detailed line-by-line coverage"
26
+ puts " 2. Review uncovered files to see if they can be removed"
27
+ puts " 3. Look at uncovered methods in low-coverage files"
28
+ end
29
+
30
+ private
31
+
32
+ def find_dead_files(coverage)
33
+ puts "\n🚨 COMPLETELY UNCOVERED FILES (potential dead code):"
34
+ puts "=" * 60
35
+
36
+ dead_files = []
37
+
38
+ coverage.each do |file, line_data|
39
+ next unless file.include?("/lib/")
40
+
41
+ # Extract lines array from SimpleCov format
42
+ lines = line_data["lines"]
43
+ next unless lines
44
+
45
+ total_lines = lines.count { |hits| !hits.nil? }
46
+ covered_lines = lines.count { |hits| hits && hits > 0 }
47
+
48
+ if total_lines > 0 && covered_lines == 0
49
+ relative_path = file.sub("#{Dir.pwd}/", "")
50
+ dead_files << relative_path
51
+ end
52
+ end
53
+
54
+ if dead_files.empty?
55
+ puts "āœ… No completely uncovered files found!"
56
+ else
57
+ puts "Found #{dead_files.length} completely uncovered files:"
58
+ dead_files.sort.each { |f| puts " šŸ“„ #{f}" }
59
+
60
+ puts "\nāš ļø These files are candidates for removal, but verify:"
61
+ puts " • Check if they're loaded via autoloading/zeitwerk"
62
+ puts " • Look for usage in examples/ or external code"
63
+ puts " • Consider if they're part of public API"
64
+ end
65
+ end
66
+
67
+ def find_low_coverage_files(coverage)
68
+ puts "\nšŸ” LOW COVERAGE FILES (<20% - check for dead methods):"
69
+ puts "=" * 20
70
+
71
+ low_coverage = []
72
+
73
+ coverage.each do |file, line_data|
74
+ next unless file.include?("/lib/")
75
+
76
+ # Extract lines array from SimpleCov format
77
+ lines = line_data["lines"]
78
+ next unless lines
79
+
80
+ total_lines = lines.count { |hits| !hits.nil? }
81
+ covered_lines = lines.count { |hits| hits && hits > 0 }
82
+
83
+ next if total_lines == 0
84
+
85
+ coverage_percent = (covered_lines.to_f / total_lines * 100)
86
+
87
+ next unless coverage_percent > 0 && coverage_percent < 20
88
+
89
+ relative_path = file.sub("#{Dir.pwd}/", "")
90
+ low_coverage << {
91
+ file: relative_path,
92
+ percent: coverage_percent,
93
+ covered: covered_lines,
94
+ total: total_lines
95
+ }
96
+ end
97
+
98
+ if low_coverage.empty?
99
+ puts "āœ… No files with very low coverage found!"
100
+ else
101
+ low_coverage.sort_by { |f| f[:percent] }.each do |info|
102
+ printf " šŸ“„ %-50s %5.1f%% (%d/%d lines)\n",
103
+ info[:file], info[:percent], info[:covered], info[:total]
104
+ end
105
+
106
+ puts "\nšŸ’” These files likely contain dead methods:"
107
+ puts " • Open coverage/index.html and click on these files"
108
+ puts " • Red lines are uncovered - potential dead code"
109
+ puts " • Look for entire uncovered methods/classes"
110
+ end
111
+ end
112
+ end
113
+
114
+ SimpleDeadCodeFinder.new.run if __FILE__ == $0