evilution 0.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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.beads/.gitignore +51 -0
  3. data/.beads/.migration-hint-ts +1 -0
  4. data/.beads/README.md +81 -0
  5. data/.beads/config.yaml +67 -0
  6. data/.beads/interactions.jsonl +0 -0
  7. data/.beads/issues.jsonl +68 -0
  8. data/.beads/metadata.json +4 -0
  9. data/.claude/prompts/architect.md +98 -0
  10. data/.claude/prompts/devops.md +106 -0
  11. data/.claude/prompts/tests.md +160 -0
  12. data/CHANGELOG.md +19 -0
  13. data/CODE_OF_CONDUCT.md +10 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +190 -0
  16. data/Rakefile +12 -0
  17. data/claude-swarm.yml +28 -0
  18. data/exe/evilution +6 -0
  19. data/lib/evilution/ast/parser.rb +83 -0
  20. data/lib/evilution/ast/source_surgeon.rb +13 -0
  21. data/lib/evilution/cli.rb +78 -0
  22. data/lib/evilution/config.rb +98 -0
  23. data/lib/evilution/coverage/collector.rb +47 -0
  24. data/lib/evilution/coverage/test_map.rb +25 -0
  25. data/lib/evilution/diff/file_filter.rb +29 -0
  26. data/lib/evilution/diff/parser.rb +47 -0
  27. data/lib/evilution/integration/base.rb +11 -0
  28. data/lib/evilution/integration/rspec.rb +184 -0
  29. data/lib/evilution/isolation/fork.rb +70 -0
  30. data/lib/evilution/mutation.rb +45 -0
  31. data/lib/evilution/mutator/base.rb +54 -0
  32. data/lib/evilution/mutator/operator/arithmetic_replacement.rb +37 -0
  33. data/lib/evilution/mutator/operator/array_literal.rb +22 -0
  34. data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +31 -0
  35. data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +50 -0
  36. data/lib/evilution/mutator/operator/collection_replacement.rb +37 -0
  37. data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -0
  38. data/lib/evilution/mutator/operator/conditional_branch.rb +36 -0
  39. data/lib/evilution/mutator/operator/conditional_negation.rb +36 -0
  40. data/lib/evilution/mutator/operator/float_literal.rb +26 -0
  41. data/lib/evilution/mutator/operator/hash_literal.rb +22 -0
  42. data/lib/evilution/mutator/operator/integer_literal.rb +45 -0
  43. data/lib/evilution/mutator/operator/method_body_replacement.rb +22 -0
  44. data/lib/evilution/mutator/operator/negation_insertion.rb +22 -0
  45. data/lib/evilution/mutator/operator/nil_replacement.rb +20 -0
  46. data/lib/evilution/mutator/operator/return_value_removal.rb +22 -0
  47. data/lib/evilution/mutator/operator/statement_deletion.rb +24 -0
  48. data/lib/evilution/mutator/operator/string_literal.rb +22 -0
  49. data/lib/evilution/mutator/operator/symbol_literal.rb +20 -0
  50. data/lib/evilution/mutator/registry.rb +55 -0
  51. data/lib/evilution/parallel/pool.rb +98 -0
  52. data/lib/evilution/parallel/worker.rb +24 -0
  53. data/lib/evilution/reporter/cli.rb +72 -0
  54. data/lib/evilution/reporter/json.rb +59 -0
  55. data/lib/evilution/reporter/suggestion.rb +51 -0
  56. data/lib/evilution/result/mutation_result.rb +37 -0
  57. data/lib/evilution/result/summary.rb +54 -0
  58. data/lib/evilution/runner.rb +139 -0
  59. data/lib/evilution/subject.rb +20 -0
  60. data/lib/evilution/version.rb +5 -0
  61. data/lib/evilution.rb +51 -0
  62. data/sig/evilution.rbs +4 -0
  63. metadata +130 -0
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class ArithmeticReplacement < Base
7
+ REPLACEMENTS = {
8
+ :+ => [:-],
9
+ :- => [:+],
10
+ :* => [:/],
11
+ :/ => [:*],
12
+ :% => [:*],
13
+ :** => [:*]
14
+ }.freeze
15
+
16
+ def visit_call_node(node)
17
+ replacements = REPLACEMENTS[node.name]
18
+ return super unless replacements
19
+
20
+ loc = node.message_loc
21
+ return super unless loc
22
+
23
+ replacements.each do |replacement|
24
+ add_mutation(
25
+ offset: loc.start_offset,
26
+ length: loc.length,
27
+ replacement: replacement.to_s,
28
+ node: node
29
+ )
30
+ end
31
+
32
+ super
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class ArrayLiteral < Base
7
+ def visit_array_node(node)
8
+ if node.opening_loc && node.elements.any?
9
+ add_mutation(
10
+ offset: node.location.start_offset,
11
+ length: node.location.length,
12
+ replacement: "[]",
13
+ node: node
14
+ )
15
+ end
16
+
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class BooleanLiteralReplacement < Base
7
+ def visit_true_node(node)
8
+ add_mutation(
9
+ offset: node.location.start_offset,
10
+ length: node.location.length,
11
+ replacement: "false",
12
+ node: node
13
+ )
14
+
15
+ super
16
+ end
17
+
18
+ def visit_false_node(node)
19
+ add_mutation(
20
+ offset: node.location.start_offset,
21
+ length: node.location.length,
22
+ replacement: "true",
23
+ node: node
24
+ )
25
+
26
+ super
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class BooleanOperatorReplacement < Base
7
+ REPLACEMENTS = {
8
+ "&&" => "||",
9
+ "||" => "&&",
10
+ "and" => "or",
11
+ "or" => "and"
12
+ }.freeze
13
+
14
+ def visit_and_node(node)
15
+ loc = node.operator_loc
16
+ operator = loc.slice
17
+ replacement = REPLACEMENTS[operator]
18
+
19
+ if replacement
20
+ add_mutation(
21
+ offset: loc.start_offset,
22
+ length: loc.length,
23
+ replacement: replacement,
24
+ node: node
25
+ )
26
+ end
27
+
28
+ super
29
+ end
30
+
31
+ def visit_or_node(node)
32
+ loc = node.operator_loc
33
+ operator = loc.slice
34
+ replacement = REPLACEMENTS[operator]
35
+
36
+ if replacement
37
+ add_mutation(
38
+ offset: loc.start_offset,
39
+ length: loc.length,
40
+ replacement: replacement,
41
+ node: node
42
+ )
43
+ end
44
+
45
+ super
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class CollectionReplacement < Base
7
+ REPLACEMENTS = {
8
+ map: [:each],
9
+ each: [:map],
10
+ select: [:reject],
11
+ reject: [:select],
12
+ flat_map: [:map],
13
+ collect: [:each]
14
+ }.freeze
15
+
16
+ def visit_call_node(node)
17
+ replacements = REPLACEMENTS[node.name]
18
+ return super unless replacements
19
+
20
+ loc = node.message_loc
21
+ return super unless loc
22
+
23
+ replacements.each do |replacement|
24
+ add_mutation(
25
+ offset: loc.start_offset,
26
+ length: loc.length,
27
+ replacement: replacement.to_s,
28
+ node: node
29
+ )
30
+ end
31
+
32
+ super
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class ComparisonReplacement < Base
7
+ REPLACEMENTS = {
8
+ :> => %i[>= ==],
9
+ :< => %i[<= ==],
10
+ :>= => %i[> ==],
11
+ :<= => %i[< ==],
12
+ :== => [:!=],
13
+ :!= => [:==]
14
+ }.freeze
15
+
16
+ def visit_call_node(node)
17
+ replacements = REPLACEMENTS[node.name]
18
+ return super unless replacements
19
+
20
+ loc = node.message_loc
21
+ return super unless loc
22
+
23
+ replacements.each do |replacement|
24
+ add_mutation(
25
+ offset: loc.start_offset,
26
+ length: loc.length,
27
+ replacement: replacement.to_s,
28
+ node: node
29
+ )
30
+ end
31
+
32
+ super
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class ConditionalBranch < Base
7
+ def visit_if_node(node)
8
+ if node.statements && node.subsequent.nil?
9
+ add_mutation(
10
+ offset: node.statements.location.start_offset,
11
+ length: node.statements.location.length,
12
+ replacement: "nil",
13
+ node: node
14
+ )
15
+ elsif node.statements && node.subsequent&.statements
16
+ add_mutation(
17
+ offset: node.statements.location.start_offset,
18
+ length: node.statements.location.length,
19
+ replacement: "nil",
20
+ node: node
21
+ )
22
+
23
+ add_mutation(
24
+ offset: node.subsequent.statements.location.start_offset,
25
+ length: node.subsequent.statements.location.length,
26
+ replacement: "nil",
27
+ node: node
28
+ )
29
+ end
30
+
31
+ super
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class ConditionalNegation < Base
7
+ def visit_if_node(node)
8
+ mutate_predicate(node)
9
+ super
10
+ end
11
+
12
+ def visit_unless_node(node)
13
+ mutate_predicate(node)
14
+ super
15
+ end
16
+
17
+ private
18
+
19
+ def mutate_predicate(node)
20
+ add_mutation(
21
+ offset: node.predicate.location.start_offset,
22
+ length: node.predicate.location.length,
23
+ replacement: "true",
24
+ node: node
25
+ )
26
+ add_mutation(
27
+ offset: node.predicate.location.start_offset,
28
+ length: node.predicate.location.length,
29
+ replacement: "false",
30
+ node: node
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class FloatLiteral < Base
7
+ def visit_float_node(node)
8
+ replacement = case node.value
9
+ when 0.0 then "1.0"
10
+ when 1.0 then "0.0"
11
+ else "0.0"
12
+ end
13
+
14
+ add_mutation(
15
+ offset: node.location.start_offset,
16
+ length: node.location.length,
17
+ replacement: replacement,
18
+ node: node
19
+ )
20
+
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class HashLiteral < Base
7
+ def visit_hash_node(node)
8
+ if node.elements.any?
9
+ add_mutation(
10
+ offset: node.location.start_offset,
11
+ length: node.location.length,
12
+ replacement: "{}",
13
+ node: node
14
+ )
15
+ end
16
+
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class IntegerLiteral < Base
7
+ def visit_integer_node(node)
8
+ value = node.value
9
+
10
+ if value.zero?
11
+ add_mutation(
12
+ offset: node.location.start_offset,
13
+ length: node.location.length,
14
+ replacement: "1",
15
+ node: node
16
+ )
17
+ elsif value == 1
18
+ add_mutation(
19
+ offset: node.location.start_offset,
20
+ length: node.location.length,
21
+ replacement: "0",
22
+ node: node
23
+ )
24
+ else
25
+ add_mutation(
26
+ offset: node.location.start_offset,
27
+ length: node.location.length,
28
+ replacement: "0",
29
+ node: node
30
+ )
31
+
32
+ add_mutation(
33
+ offset: node.location.start_offset,
34
+ length: node.location.length,
35
+ replacement: (node.value + 1).to_s,
36
+ node: node
37
+ )
38
+ end
39
+
40
+ super
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class MethodBodyReplacement < Base
7
+ def visit_def_node(node)
8
+ if node.body
9
+ add_mutation(
10
+ offset: node.body.location.start_offset,
11
+ length: node.body.location.length,
12
+ replacement: "nil",
13
+ node: node
14
+ )
15
+ end
16
+
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class NegationInsertion < Base
7
+ def visit_call_node(node)
8
+ if node.name.to_s.end_with?("?")
9
+ add_mutation(
10
+ offset: node.location.start_offset,
11
+ length: 0,
12
+ replacement: "!",
13
+ node: node
14
+ )
15
+ end
16
+
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class NilReplacement < Base
7
+ def visit_nil_node(node)
8
+ add_mutation(
9
+ offset: node.location.start_offset,
10
+ length: node.location.length,
11
+ replacement: "true",
12
+ node: node
13
+ )
14
+
15
+ super
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class ReturnValueRemoval < Base
7
+ def visit_return_node(node)
8
+ if node.arguments
9
+ add_mutation(
10
+ offset: node.location.start_offset,
11
+ length: node.location.length,
12
+ replacement: "return",
13
+ node: node
14
+ )
15
+ end
16
+
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class StatementDeletion < Base
7
+ def visit_statements_node(node)
8
+ if node.body.length > 1
9
+ node.body.each do |child|
10
+ add_mutation(
11
+ offset: child.location.start_offset,
12
+ length: child.location.length,
13
+ replacement: "",
14
+ node: child
15
+ )
16
+ end
17
+ end
18
+
19
+ super
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class StringLiteral < Base
7
+ def visit_string_node(node)
8
+ replacement = node.content.empty? ? '"mutation"' : '""'
9
+
10
+ add_mutation(
11
+ offset: node.location.start_offset,
12
+ length: node.location.length,
13
+ replacement: replacement,
14
+ node: node
15
+ )
16
+
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ module Operator
6
+ class SymbolLiteral < Base
7
+ def visit_symbol_node(node)
8
+ add_mutation(
9
+ offset: node.location.start_offset,
10
+ length: node.location.length,
11
+ replacement: ":__evilution_mutated__",
12
+ node: node
13
+ )
14
+
15
+ super
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Mutator
5
+ class Registry
6
+ def self.default
7
+ registry = new
8
+ [
9
+ Operator::ComparisonReplacement,
10
+ Operator::ArithmeticReplacement,
11
+ Operator::BooleanOperatorReplacement,
12
+ Operator::BooleanLiteralReplacement,
13
+ Operator::NilReplacement,
14
+ Operator::IntegerLiteral,
15
+ Operator::FloatLiteral,
16
+ Operator::StringLiteral,
17
+ Operator::ArrayLiteral,
18
+ Operator::HashLiteral,
19
+ Operator::SymbolLiteral,
20
+ Operator::ConditionalNegation,
21
+ Operator::ConditionalBranch,
22
+ Operator::StatementDeletion,
23
+ Operator::MethodBodyReplacement,
24
+ Operator::NegationInsertion,
25
+ Operator::ReturnValueRemoval,
26
+ Operator::CollectionReplacement
27
+ ].each { |op| registry.register(op) }
28
+ registry
29
+ end
30
+
31
+ def initialize
32
+ @operators = []
33
+ end
34
+
35
+ def register(operator_class)
36
+ @operators << operator_class
37
+ self
38
+ end
39
+
40
+ def mutations_for(subject)
41
+ @operators.flat_map do |operator_class|
42
+ operator_class.new.call(subject)
43
+ end
44
+ end
45
+
46
+ def operator_count
47
+ @operators.length
48
+ end
49
+
50
+ def operators
51
+ @operators.dup
52
+ end
53
+ end
54
+ end
55
+ end