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,106 @@
1
+ # Evilution Gem DevOps Specialist
2
+
3
+ You are a DevOps specialist for evilution — a Ruby gem for mutation testing. Your focus is CI/CD, gem publishing, and development infrastructure.
4
+
5
+ ## Git Workflow
6
+
7
+ Before starting any new task:
8
+ 1. `git checkout master && git pull`
9
+ 2. `git checkout -b <descriptive-branch-name>`
10
+ 3. Do all work on the feature branch
11
+
12
+ ## Core Responsibilities
13
+
14
+ 1. **CI/CD**: GitHub Actions for testing across Ruby versions
15
+ 2. **Gem Publishing**: RubyGems release process
16
+ 3. **Code Quality**: RuboCop, bundler-audit
17
+ 4. **Versioning**: Semantic versioning for gem releases
18
+
19
+ ## GitHub Actions CI
20
+
21
+ ```yaml
22
+ # .github/workflows/ci.yml
23
+ name: CI
24
+
25
+ on:
26
+ push:
27
+ branches: [master]
28
+ pull_request:
29
+
30
+ jobs:
31
+ test:
32
+ runs-on: ubuntu-latest
33
+ strategy:
34
+ matrix:
35
+ ruby: ['3.2', '3.3', '4.0']
36
+
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+ - name: Set up Ruby
40
+ uses: ruby/setup-ruby@v1
41
+ with:
42
+ ruby-version: ${{ matrix.ruby }}
43
+ bundler-cache: true
44
+
45
+ - name: Run specs
46
+ run: bundle exec rspec
47
+
48
+ - name: Run linter
49
+ run: bundle exec rubocop
50
+ ```
51
+
52
+ ## Gem Release Process
53
+
54
+ 1. Update version in `lib/evilution/version.rb`
55
+ 2. Update `CHANGELOG.md`
56
+ 3. Commit: `git commit -m "Bump version to X.Y.Z"`
57
+ 4. Tag: `git tag vX.Y.Z`
58
+ 5. Build and push:
59
+ ```bash
60
+ gem build evilution.gemspec
61
+ gem push evilution-X.Y.Z.gem
62
+ ```
63
+ 6. Or use bundler's rake task: `bundle exec rake release`
64
+
65
+ ## Versioning Strategy
66
+
67
+ Follow semantic versioning:
68
+ - **PATCH** (0.1.x): Bug fixes, new mutation operators
69
+ - **MINOR** (0.x.0): New features (new integrations, new output formats)
70
+ - **MAJOR** (x.0.0): Breaking API changes
71
+
72
+ ## Security
73
+
74
+ - No credentials in gem package — gemspec excludes sensitive files
75
+ - `bundler-audit` for dependency vulnerability scanning
76
+ - No `eval` of user-provided input (only eval of mutation-generated code in forked processes)
77
+
78
+ ## Dependencies
79
+
80
+ Runtime dependencies must be minimal:
81
+ - `diff-lcs` — diff computation for reports
82
+ - Prism ships with Ruby 3.3+ (declared as gemspec dependency for 3.2)
83
+
84
+ Dev dependencies:
85
+ - `rspec` — testing
86
+ - `rubocop` — linting
87
+ - `rake` — task runner
88
+
89
+ ## Project Structure Awareness
90
+
91
+ ```
92
+ evilution.gemspec # Gem metadata and dependencies
93
+ Gemfile # Dev dependency management
94
+ Rakefile # Default task: spec + rubocop
95
+ exe/evilution # CLI executable
96
+ lib/ # Gem source code
97
+ spec/ # RSpec specs
98
+ .github/workflows/ # CI configuration
99
+ ```
100
+
101
+ ## Key Principles
102
+
103
+ - Keep CI fast — run specs and rubocop only
104
+ - Multi-Ruby matrix — test on 3.2, 3.3, and 4.0
105
+ - No Docker needed — this is a pure Ruby gem
106
+ - No database, no external services
@@ -0,0 +1,160 @@
1
+ # Evilution Gem Testing Specialist
2
+
3
+ You are an RSpec testing specialist ensuring comprehensive test coverage for evilution — a Ruby mutation testing gem.
4
+
5
+ ## Git Workflow
6
+
7
+ Before starting any new task:
8
+ 1. `git checkout master && git pull`
9
+ 2. `git checkout -b <descriptive-branch-name>`
10
+ 3. Do all work on the feature branch
11
+
12
+ ## Core Responsibilities
13
+
14
+ 1. **Test Coverage**: Write comprehensive specs for all gem classes
15
+ 2. **Test Types**: Unit specs, integration specs, fixture-based specs
16
+ 3. **Test Quality**: Tests must be meaningful — verify behavior, not implementation
17
+ 4. **Test Performance**: Keep the suite fast (no network, no heavy I/O)
18
+ 5. **TDD**: Write specs first, then implement
19
+
20
+ ## Project Rules
21
+
22
+ - Self-documenting test names — no comments in spec files
23
+ - One spec file per class, mirroring the lib/ directory structure
24
+ - Use fixture Ruby files for parser and mutation operator tests
25
+ - No FactoryBot — plain Ruby objects and fixture files
26
+ - No database, no Rails — this is a pure Ruby gem
27
+
28
+ ## Testing Framework: RSpec
29
+
30
+ ### Unit Spec Example
31
+
32
+ ```ruby
33
+ RSpec.describe Evilution::AST::SourceSurgeon do
34
+ describe ".apply" do
35
+ it "replaces text at the given byte offset" do
36
+ source = "age >= 18"
37
+ result = described_class.apply(source, offset: 4, length: 2, replacement: ">")
38
+
39
+ expect(result).to eq("age > 18")
40
+ end
41
+ end
42
+ end
43
+ ```
44
+
45
+ ### Mutation Operator Spec Example
46
+
47
+ ```ruby
48
+ RSpec.describe Evilution::Mutator::Operator::ComparisonReplacement do
49
+ let(:source) { "def adult?(age)\n age >= 18\nend" }
50
+ let(:subject_under_test) { build_subject(source: source, file: "example.rb") }
51
+
52
+ describe "#call" do
53
+ it "generates mutations for >= operator" do
54
+ mutations = described_class.new.call(subject_under_test)
55
+
56
+ mutated_sources = mutations.map(&:mutated_source)
57
+ expect(mutated_sources).to include(
58
+ "def adult?(age)\n age > 18\nend",
59
+ "def adult?(age)\n age == 18\nend"
60
+ )
61
+ end
62
+ end
63
+ end
64
+ ```
65
+
66
+ ### Integration Spec Example
67
+
68
+ ```ruby
69
+ RSpec.describe "End-to-end mutation run" do
70
+ it "detects surviving mutants in poorly tested code" do
71
+ result = Evilution::Runner.new(
72
+ files: [fixture_path("poorly_tested.rb")],
73
+ spec_files: [fixture_path("poorly_tested_spec.rb")]
74
+ ).call
75
+
76
+ expect(result.summary.survived).to be > 0
77
+ expect(result.summary.score).to be < 1.0
78
+ end
79
+ end
80
+ ```
81
+
82
+ ## TDD Cycle
83
+
84
+ 1. **Write a spec** for the class/method being built
85
+ 2. **Watch it fail** — confirm it fails for the right reason
86
+ 3. **Write minimal implementation** to make the spec pass
87
+ 4. **Refactor** while keeping specs green
88
+ 5. **Repeat** for the next behavior
89
+
90
+ ## Test Organization
91
+
92
+ ```
93
+ spec/
94
+ ├── spec_helper.rb
95
+ ├── support/
96
+ │ ├── helpers.rb # Shared test helpers (build_subject, fixture_path)
97
+ │ └── fixtures/
98
+ │ ├── simple_class.rb # Fixture Ruby files for parsing
99
+ │ ├── comparison.rb # Fixture with comparison operators
100
+ │ ├── arithmetic.rb # Fixture with arithmetic operators
101
+ │ └── conditionals.rb # Fixture with if/else
102
+ ├── evilution/
103
+ │ ├── config_spec.rb
104
+ │ ├── runner_spec.rb
105
+ │ ├── subject_spec.rb
106
+ │ ├── mutation_spec.rb
107
+ │ ├── cli_spec.rb
108
+ │ ├── ast/
109
+ │ │ ├── parser_spec.rb
110
+ │ │ └── source_surgeon_spec.rb
111
+ │ ├── mutator/
112
+ │ │ ├── base_spec.rb
113
+ │ │ ├── registry_spec.rb
114
+ │ │ └── operator/
115
+ │ │ ├── comparison_replacement_spec.rb
116
+ │ │ ├── arithmetic_replacement_spec.rb
117
+ │ │ └── ...
118
+ │ ├── isolation/
119
+ │ │ └── fork_spec.rb
120
+ │ ├── integration/
121
+ │ │ └── rspec_spec.rb
122
+ │ ├── result/
123
+ │ │ ├── mutation_result_spec.rb
124
+ │ │ └── summary_spec.rb
125
+ │ └── reporter/
126
+ │ ├── json_spec.rb
127
+ │ └── cli_spec.rb
128
+ ```
129
+
130
+ ## Testing Patterns
131
+
132
+ ### Arrange-Act-Assert
133
+ 1. **Arrange**: Set up fixture files, build Subject objects, configure as needed
134
+ 2. **Act**: Call the method under test
135
+ 3. **Assert**: Verify the expected output
136
+
137
+ ### Fixture Files
138
+ - Store fixture Ruby files in `spec/support/fixtures/`
139
+ - Each fixture exercises a specific category of mutations
140
+ - Keep fixtures minimal — only the code needed for the test
141
+
142
+ ### Testing Mutation Operators
143
+ For each operator, verify:
144
+ - Correct mutations are generated for target node types
145
+ - Non-target nodes are left alone
146
+ - Edge cases: empty bodies, nested nodes, multiple occurrences
147
+ - Generated mutations are valid Ruby (parseable by Prism)
148
+
149
+ ### Testing Isolation::Fork
150
+ - Verify child process gets clean state
151
+ - Verify timeout kills the child
152
+ - Verify results are marshalled correctly via pipe
153
+ - Use simple test commands (not full RSpec) in specs
154
+
155
+ ### Edge Cases
156
+ Always test:
157
+ - Empty/nil inputs
158
+ - Boundary conditions (single-statement bodies, nested methods)
159
+ - Invalid source code (parser errors)
160
+ - Files with no mutable nodes
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-03-02
4
+
5
+ ### Added
6
+
7
+ - **18 mutation operators**: ArithmeticReplacement, ComparisonReplacement, BooleanOperatorReplacement, BooleanLiteralReplacement, NilReplacement, IntegerLiteral, FloatLiteral, StringLiteral, ArrayLiteral, HashLiteral, SymbolLiteral, ConditionalNegation, ConditionalBranch, StatementDeletion, MethodBodyReplacement, NegationInsertion, ReturnValueRemoval, CollectionReplacement
8
+ - **Prism-based AST parsing** with source-level surgery via byte offsets
9
+ - **Fork-based isolation** for safe mutation execution
10
+ - **RSpec integration** for test execution
11
+ - **Parallel execution** with configurable worker count (`--jobs`)
12
+ - **Diff-based targeting** to mutate only changed code (`--diff HEAD~1`)
13
+ - **Coverage-based filtering** — skip mutations on lines no test exercises
14
+ - **JSON output** for AI agents and CI pipelines (`--format json`)
15
+ - **CLI output** with human-readable mutation testing results
16
+ - **Actionable suggestions** for surviving mutants
17
+ - **Configuration file** support (`.evilution.yml`)
18
+ - **`evilution init`** command to generate default config
19
+ - **Error handling** with clean exit codes (0=pass, 1=fail, 2=error)
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "evilution" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["denis.kiselyov@gmail.com"](mailto:"denis.kiselyov@gmail.com").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Denis Kiselev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # Evilution — Mutation Testing for Ruby
2
+
3
+ > **Purpose**: Validate test suite quality by injecting small code changes (mutations) and checking whether tests detect them. Surviving mutations indicate gaps in test coverage.
4
+
5
+ **License**: MIT (free, no commercial restrictions)
6
+ **Language**: Ruby >= 3.3
7
+ **Parser**: Prism (Ruby's official AST parser, ships with Ruby 3.3+)
8
+ **Test framework**: RSpec (currently the only supported integration)
9
+
10
+ ## Installation
11
+
12
+ Add to `Gemfile`:
13
+
14
+ ```ruby
15
+ gem "evilution", group: :test
16
+ ```
17
+
18
+ Then: `bundle install`
19
+
20
+ Or standalone: `gem install evilution`
21
+
22
+ ## Command Reference
23
+
24
+ ```
25
+ evilution [command] [options] [files...]
26
+ ```
27
+
28
+ ### Commands
29
+
30
+ | Command | Description | Default |
31
+ |-----------|------------------------------------------|---------|
32
+ | `run` | Execute mutation testing against files | Yes |
33
+ | `init` | Generate `.evilution.yml` config file | |
34
+ | `version` | Print version string | |
35
+
36
+ ### Options (for `run` command)
37
+
38
+ | Flag | Type | Default | Description |
39
+ |-------------------------|---------|--------------|---------------------------------------------------|
40
+ | `-j`, `--jobs N` | Integer | CPU cores | Parallel worker count. Use `1` for sequential. |
41
+ | `-t`, `--timeout N` | Integer | 10 | Per-mutation timeout in seconds. |
42
+ | `-f`, `--format FORMAT` | String | `text` | Output format: `text` or `json`. |
43
+ | `--diff BASE` | String | _(none)_ | Git ref. Only mutate methods whose definition line changed since BASE. |
44
+ | `--min-score FLOAT` | Float | 0.0 | Minimum mutation score (0.0–1.0) to pass. |
45
+ | `--no-coverage` | Boolean | false | Reserved; currently has no effect. |
46
+ | `-v`, `--verbose` | Boolean | false | Verbose output. |
47
+ | `-q`, `--quiet` | Boolean | false | Suppress output. |
48
+
49
+ ### Exit Codes
50
+
51
+ | Code | Meaning | Agent action |
52
+ |------|-----------------------------------------------|---------------------------------------|
53
+ | 0 | Mutation score meets or exceeds `--min-score` | Success. No action needed. |
54
+ | 1 | Mutation score below `--min-score` | Parse output, fix surviving mutants. |
55
+ | 2 | Tool error (bad config, parse failure, etc.) | Check stderr, fix invocation. |
56
+
57
+ ## Configuration
58
+
59
+ Generate default config: `bundle exec evilution init`
60
+
61
+ Creates `.evilution.yml`:
62
+
63
+ ```yaml
64
+ # jobs: 4 # parallel workers
65
+ # timeout: 10 # seconds per mutation
66
+ # format: text # text | json
67
+ # min_score: 0.0 # 0.0–1.0
68
+ # integration: rspec # test framework
69
+ # coverage: true # skip mutations on uncovered lines
70
+ ```
71
+
72
+ **Precedence**: CLI flags override `.evilution.yml` values.
73
+
74
+ ## JSON Output Schema
75
+
76
+ Use `--format json` for machine-readable output. Schema:
77
+
78
+ ```json
79
+ {
80
+ "version": "string — gem version",
81
+ "timestamp": "string — ISO 8601 timestamp of the report",
82
+ "summary": {
83
+ "total": "integer — total mutations generated",
84
+ "killed": "integer — mutations detected by tests (test failed = good)",
85
+ "survived": "integer — mutations NOT detected (test passed = gap in coverage)",
86
+ "timed_out": "integer — mutations that exceeded timeout",
87
+ "errors": "integer — mutations that caused unexpected errors",
88
+ "score": "float — killed / (total - errors), range 0.0-1.0, rounded to 4 decimals",
89
+ "duration": "float — total wall-clock seconds, rounded to 4 decimals"
90
+ },
91
+ "survived": [
92
+ {
93
+ "operator": "string — mutation operator name (see Operators table)",
94
+ "file": "string — relative path to mutated file",
95
+ "line": "integer — line number of the mutation",
96
+ "status": "string — result status: 'survived', 'killed', 'timeout', or 'error'",
97
+ "duration": "float — seconds this mutation took, rounded to 4 decimals",
98
+ "diff": "string — unified diff snippet",
99
+ "suggestion": "string — actionable hint for surviving mutants (survived only)"
100
+ }
101
+ ],
102
+ "killed": ["... same shape as survived entries ..."],
103
+ "timed_out": ["... same shape as survived entries ..."],
104
+ "errors": ["... same shape as survived entries ..."]
105
+ }
106
+ ```
107
+
108
+ **Key metric**: `summary.score` — the mutation score. Higher is better. 1.0 means all mutations were caught.
109
+
110
+ ## Mutation Operators (18 total)
111
+
112
+ Each operator name is stable and appears in JSON output under `survived[].operator`.
113
+
114
+ | Operator | What it does | Example |
115
+ |---------------------------|-------------------------------------------|------------------------------------|
116
+ | `arithmetic_replacement` | Swap arithmetic operators | `a + b` -> `a - b` |
117
+ | `comparison_replacement` | Swap comparison operators | `a >= b` -> `a > b` |
118
+ | `boolean_operator_replacement` | Swap `&&` / `\|\|` | `a && b` -> `a \|\| b` |
119
+ | `boolean_literal_replacement` | Flip boolean literals | `true` -> `false` |
120
+ | `nil_replacement` | Replace expression with `nil` | `expr` -> `nil` |
121
+ | `integer_literal` | Boundary-value integer mutations | `n` -> `0`, `1`, `n+1`, `n-1` |
122
+ | `float_literal` | Boundary-value float mutations | `f` -> `0.0`, `1.0` |
123
+ | `string_literal` | Empty the string | `"str"` -> `""` |
124
+ | `array_literal` | Empty the array | `[a, b]` -> `[]` |
125
+ | `hash_literal` | Empty the hash | `{k: v}` -> `{}` |
126
+ | `symbol_literal` | Replace with sentinel symbol | `:foo` -> `:__evilution_mutated__` |
127
+ | `conditional_negation` | Replace condition with `true`/`false` | `if cond` -> `if true` |
128
+ | `conditional_branch` | Remove if/else branch | Deletes branch body |
129
+ | `statement_deletion` | Remove statements from method bodies | Deletes a statement |
130
+ | `method_body_replacement` | Replace entire method body with `nil` | Method body -> `nil` |
131
+ | `negation_insertion` | Negate predicate methods | `x.empty?` -> `!x.empty?` |
132
+ | `return_value_removal` | Strip return values | `return x` -> `return` |
133
+ | `collection_replacement` | Swap collection methods | `map` -> `each`, `select` <-> `reject` |
134
+
135
+ ## Recommended Workflows for AI Agents
136
+
137
+ ### 1. Full project scan
138
+
139
+ ```bash
140
+ bundle exec evilution run lib/ --format json --jobs 4 --min-score 0.8
141
+ ```
142
+
143
+ Parse JSON output. Exit code 0 = pass, 1 = surviving mutants to address.
144
+
145
+ ### 2. PR / diff-only scan (fast feedback)
146
+
147
+ ```bash
148
+ bundle exec evilution run lib/ --format json --diff main --min-score 0.9
149
+ ```
150
+
151
+ Mutates methods whose definition (starting) line is changed compared to `main` (the diff filter is based on the method’s first line, not any line in its body). Use this for incremental checks — it's fast and focused on newly added or moved methods and changed signatures.
152
+
153
+ ### 3. Single-file targeted scan
154
+
155
+ ```bash
156
+ bundle exec evilution run lib/specific_file.rb --format json
157
+ ```
158
+
159
+ Use when you know which file was modified and want to verify its test coverage.
160
+
161
+ ### 4. Fixing surviving mutants
162
+
163
+ For each entry in `survived[]`:
164
+ 1. Read `file` at `line` to understand the code context
165
+ 2. Read `operator` to understand what was changed
166
+ 3. Read `suggestion` for a hint on what test to write
167
+ 4. Write a test that would fail if the mutation were applied
168
+ 5. Re-run evilution on just that file to verify the mutant is now killed
169
+
170
+ ### 5. CI gate
171
+
172
+ ```bash
173
+ bundle exec evilution run lib/ --format json --min-score 0.8 --quiet
174
+ # Exit code 0 = pass, 1 = fail, 2 = error
175
+ ```
176
+
177
+ Note: `--quiet` suppresses all stdout output (including JSON). Use it in CI only when you care about the exit code and do not need JSON output.
178
+
179
+ ## Internals (for context, not for direct use)
180
+
181
+ 1. **Parse** — Prism parses Ruby files into ASTs with exact byte offsets
182
+ 2. **Extract** — Methods are identified as mutation subjects
183
+ 3. **Mutate** — Operators produce text replacements at precise byte offsets (source-level surgery, no AST unparsing)
184
+ 4. **Isolate** — Each mutation runs in a `fork()`-ed child process (no test pollution)
185
+ 5. **Test** — RSpec executes against the mutated source
186
+ 6. **Report** — Results aggregated into text or JSON
187
+
188
+ ## Repository
189
+
190
+ https://github.com/marinazzio/evilution
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/claude-swarm.yml ADDED
@@ -0,0 +1,28 @@
1
+ version: 1
2
+ swarm:
3
+ name: "Evilution Ruby gem Development Team"
4
+ main: architect
5
+
6
+ instances:
7
+ architect:
8
+ description: "Ruby architect coordinating full-stack development for Evilution Ruby gem"
9
+ directory: .
10
+ model: opus
11
+ connections: [tests, devops]
12
+ prompt_file: .claude/prompts/architect.md
13
+ vibe: true
14
+
15
+ tests:
16
+ description: "RSpec testing, factories, and test coverage specialist"
17
+ directory: ./spec
18
+ model: opus
19
+ allowed_tools: [Read, Edit, Write, Bash, Grep, Glob, LS]
20
+ prompt_file: .claude/prompts/tests.md
21
+
22
+
23
+ devops:
24
+ description: "Deployment, Docker, CI/CD, and production configuration specialist"
25
+ directory: ./.github
26
+ model: opus
27
+ allowed_tools: [Read, Edit, Write, Bash, Grep, Glob, LS]
28
+ prompt_file: .claude/prompts/devops.md
data/exe/evilution ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "evilution"
5
+
6
+ exit Evilution::CLI.new(ARGV).call
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Evilution
6
+ module AST
7
+ class Parser
8
+ def call(file_path)
9
+ raise ParseError, "file not found: #{file_path}" unless File.exist?(file_path)
10
+
11
+ source = File.read(file_path)
12
+ result = Prism.parse(source)
13
+
14
+ raise ParseError, "failed to parse #{file_path}: #{result.errors.map(&:message).join(", ")}" if result.failure?
15
+
16
+ extract_subjects(result.value, source, file_path)
17
+ end
18
+
19
+ private
20
+
21
+ def extract_subjects(tree, source, file_path)
22
+ finder = SubjectFinder.new(source, file_path)
23
+ finder.visit(tree)
24
+ finder.subjects
25
+ end
26
+ end
27
+
28
+ class SubjectFinder < Prism::Visitor
29
+ attr_reader :subjects
30
+
31
+ def initialize(source, file_path)
32
+ @source = source
33
+ @file_path = file_path
34
+ @subjects = []
35
+ @context = []
36
+ end
37
+
38
+ def visit_module_node(node)
39
+ @context.push(constant_name(node.constant_path))
40
+ super
41
+ @context.pop
42
+ end
43
+
44
+ def visit_class_node(node)
45
+ @context.push(constant_name(node.constant_path))
46
+ super
47
+ @context.pop
48
+ end
49
+
50
+ def visit_def_node(node)
51
+ scope = @context.join("::")
52
+ name = if scope.empty?
53
+ "##{node.name}"
54
+ else
55
+ "#{scope}##{node.name}"
56
+ end
57
+
58
+ loc = node.location
59
+ method_source = @source[loc.start_offset...loc.end_offset]
60
+
61
+ @subjects << Subject.new(
62
+ name: name,
63
+ file_path: @file_path,
64
+ line_number: loc.start_line,
65
+ source: method_source,
66
+ node: node
67
+ )
68
+
69
+ super
70
+ end
71
+
72
+ private
73
+
74
+ def constant_name(node)
75
+ if node.respond_to?(:full_name)
76
+ node.full_name
77
+ else
78
+ node.name.to_s
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module AST
5
+ module SourceSurgeon
6
+ def self.apply(source, offset:, length:, replacement:)
7
+ result = source.dup
8
+ result[offset, length] = replacement
9
+ result
10
+ end
11
+ end
12
+ end
13
+ end