evilution 0.35.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.beads/interactions.jsonl +11 -0
- data/CHANGELOG.md +13 -0
- data/README.md +12 -1
- data/docs/architecture.md +220 -0
- data/docs/public_api.md +50 -0
- data/docs/versioning.md +3 -3
- data/lib/evilution/cli.rb +1 -0
- data/lib/evilution/config.rb +1 -1
- data/lib/evilution/integration/loading/redefinition_recovery.rb +3 -1
- data/lib/evilution/related_spec_heuristic.rb +0 -2
- data/lib/evilution/runner.rb +1 -0
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +6 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 26a7a721749a19cb1f10d77b46a9d78d394ce5c112ba5d5fab8963de4a1a5f14
|
|
4
|
+
data.tar.gz: 65ee839279c74e07dd84d153015bfff6e29daf56d87929da75991fdef67e49c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 587598737cb636180b40cfb2466ecd0aac34c827eddb5432b543fc33b2b7e94b2e5b2a03915598fb9c41e9174bab8baad06ffab620f4174d8f686f9173f8fd1b
|
|
7
|
+
data.tar.gz: 85be8b922c45fab5e0e50fd8f17a6d5a572a9b9a2139181f0ca0283dc52c241bcd54b512e0cb3a5be9588edc31cd8b46fedd10437e8caf0c3e3104229c1f7127
|
data/.beads/interactions.jsonl
CHANGED
|
@@ -437,3 +437,14 @@
|
|
|
437
437
|
{"id":"int-555de29c","kind":"field_change","created_at":"2026-06-16T07:54:17.438977358Z","actor":"Denis Kiselev","issue_id":"EV-z03y","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
|
|
438
438
|
{"id":"int-616ccd27","kind":"field_change","created_at":"2026-06-16T09:31:30.217250542Z","actor":"Denis Kiselev","issue_id":"EV-z03y","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #1375 (master). Dep on EV-rxob is provenance only; code fix shipped."}}
|
|
439
439
|
{"id":"int-6d2f9b35","kind":"field_change","created_at":"2026-06-16T10:00:18.493013642Z","actor":"Denis Kiselev","issue_id":"EV-rxob","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"1.0 real-world validation complete. Rounds 2-4 ran 30+ gems across rspec/minitest/test-unit; no crash/memory blowup. All findings filed+fixed+merged: EV-z7f5/#1347, EV-gl1e/#1329, EV-52hf/#1341, EV-5hk5/#1373, EV-bi41/#1374, EV-z03y/#1375. Round-4 OOB re-verify confirmed fixes compound (state_machines 0->0.922, factory_bot/webmock/http real OOB scores, errors=0). Residual resolution-coverage gaps (behavior-named/flat-prefixed/non-std helper) tracked low-pri: EV-ajby/#1376, EV-6rbd/#1377, EV-no5u/#1378."}}
|
|
440
|
+
{"id":"int-46080989","kind":"field_change","created_at":"2026-07-15T12:59:28.484284407Z","actor":"Denis Kiselev","issue_id":"EV-jj18","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
441
|
+
{"id":"int-f2064cbf","kind":"field_change","created_at":"2026-07-15T13:25:07.606150972Z","actor":"Denis Kiselev","issue_id":"EV-fkw0","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
442
|
+
{"id":"int-eb2c5880","kind":"field_change","created_at":"2026-07-15T13:33:14.202699471Z","actor":"Denis Kiselev","issue_id":"EV-axze","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
443
|
+
{"id":"int-fece2dce","kind":"field_change","created_at":"2026-07-15T13:33:14.634226625Z","actor":"Denis Kiselev","issue_id":"EV-7ab0","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
444
|
+
{"id":"int-46a5d7e3","kind":"field_change","created_at":"2026-07-15T13:33:52.254966218Z","actor":"Denis Kiselev","issue_id":"EV-2bx6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
445
|
+
{"id":"int-2b8ea434","kind":"field_change","created_at":"2026-07-15T13:33:52.673568305Z","actor":"Denis Kiselev","issue_id":"EV-dgjv","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
446
|
+
{"id":"int-9f4fbaea","kind":"field_change","created_at":"2026-07-15T13:33:53.013705815Z","actor":"Denis Kiselev","issue_id":"EV-t1qg","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
447
|
+
{"id":"int-608d35ec","kind":"field_change","created_at":"2026-07-15T13:33:53.352696649Z","actor":"Denis Kiselev","issue_id":"EV-vlbh","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
448
|
+
{"id":"int-616f39e1","kind":"field_change","created_at":"2026-07-15T13:33:53.68007233Z","actor":"Denis Kiselev","issue_id":"EV-y194","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
449
|
+
{"id":"int-abcaeb3a","kind":"field_change","created_at":"2026-07-15T13:33:54.363899883Z","actor":"Denis Kiselev","issue_id":"EV-j2kz","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
450
|
+
{"id":"int-b8027311","kind":"field_change","created_at":"2026-07-15T13:34:47.737536249Z","actor":"Denis Kiselev","issue_id":"EV-w07i","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Versioning policy: see [docs/versioning.md](docs/versioning.md).
|
|
4
4
|
|
|
5
|
+
## [1.0.0] - 2026-07-15
|
|
6
|
+
|
|
7
|
+
First stable release. From `1.0.0` onward evilution follows [Semantic Versioning](https://semver.org): the public contract — CLI commands and flags, `.evilution.yml` configuration keys, session JSON files, the MCP tool schemas, and process exit codes — is frozen and covered by the SemVer guarantees and deprecation cycle in [docs/versioning.md](docs/versioning.md). The `1.0.0` milestone is the culmination of the readiness work that shipped across the `0.31`–`0.35` line (config and session-JSON schema versioning, MCP tool-contract stabilization, the CLI flag deprecation sweep, real-world Rails validation, the parallel/isolation stress suite, and running evilution against its own suite to a mutation-score target).
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Contributor architecture guide (`docs/architecture.md`)** — a single internals map for contributors: a high-level flow diagram, a module map of every `Evilution::` namespace, the end-to-end data flow from source file to scored result, and step-by-step recipes for adding a new mutator, reporter, or test integration. Linked from the README "Development" and "Internals" sections (EV-fkw0, PR #1407, GH #863)
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **Public Ruby API frozen as internal (`docs/public_api.md`)** — evilution has no public Ruby API. The entire `Evilution::` namespace is internal and may change in any release; the supported, SemVer-governed surface is the CLI, config file, session JSON, MCP tools, and exit codes. The top-level `Evilution` module and the primary entry points (`Runner`, `CLI`, `Config`) carry an `@api private` YARD marker, the new [docs/public_api.md](docs/public_api.md) documents the contract surfaces, and [docs/versioning.md](docs/versioning.md) is updated to match (the MINOR trigger now covers "introducing a public Ruby facade where none exists" and the deprecation cycle no longer references a Ruby `@deprecated` path) (EV-jj18, PR #1406, GH #854)
|
|
16
|
+
- **Dependency bumps** — `mcp` 0.21.0 → 0.24.0 (PRs #1402/#1403), plus CI/tooling updates (`rubocop` 1.88.2, `ruby/setup-ruby` 1.318.0, `actions/checkout` 7)
|
|
17
|
+
|
|
5
18
|
## [0.35.0] - 2026-06-24
|
|
6
19
|
|
|
7
20
|
### Added
|
data/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* **License**: MIT (free, no commercial restrictions)
|
|
12
12
|
* **Language**: Ruby >= 3.3
|
|
13
13
|
* **Parser**: Prism (Ruby's official AST parser, ships with Ruby 3.3+)
|
|
14
|
-
* **Test frameworks**: RSpec and
|
|
14
|
+
* **Test frameworks**: RSpec, Minitest, and Test::Unit
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
@@ -783,6 +783,12 @@ When Evilution detects a parallel run against a SQLite-backed `config/database.y
|
|
|
783
783
|
|
|
784
784
|
## Development
|
|
785
785
|
|
|
786
|
+
### Architecture
|
|
787
|
+
|
|
788
|
+
New to the codebase? [docs/architecture.md](docs/architecture.md) is the
|
|
789
|
+
contributor guide: the end-to-end mutation flow (source → result), a module map,
|
|
790
|
+
and step-by-step recipes for adding a mutator, a reporter, or a test integration.
|
|
791
|
+
|
|
786
792
|
### Memory leak check
|
|
787
793
|
|
|
788
794
|
Run before releasing to verify no memory regressions:
|
|
@@ -798,6 +804,9 @@ Tests 4 paths (InProcess isolation, Fork isolation, mutation generation + stripp
|
|
|
798
804
|
|
|
799
805
|
## Internals (for context, not for direct use)
|
|
800
806
|
|
|
807
|
+
For the full contributor architecture — module map, data flow, and extension
|
|
808
|
+
points — see [docs/architecture.md](docs/architecture.md).
|
|
809
|
+
|
|
801
810
|
1. **Parse** — Prism parses Ruby files into ASTs with exact byte offsets
|
|
802
811
|
2. **Extract** — Methods are identified as mutation subjects
|
|
803
812
|
3. **Filter** — Disable comments, Sorbet `sig` blocks, and AST ignore patterns exclude mutations before execution
|
|
@@ -811,6 +820,8 @@ Tests 4 paths (InProcess isolation, Fork isolation, mutation generation + stripp
|
|
|
811
820
|
|
|
812
821
|
`evilution` follows [Semantic Versioning](https://semver.org). The full policy — what counts as the public contract, what triggers a major bump, how deprecations work — is documented in [docs/versioning.md](docs/versioning.md).
|
|
813
822
|
|
|
823
|
+
There is **no public Ruby API**: the entire `Evilution::` namespace is internal and may change in any release. Drive evilution through the CLI, `.evilution.yml`, the MCP tools, or hooks. See [docs/public_api.md](docs/public_api.md).
|
|
824
|
+
|
|
814
825
|
## Repository
|
|
815
826
|
|
|
816
827
|
https://github.com/marinazzio/evilution
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Architecture (contributor guide)
|
|
2
|
+
|
|
3
|
+
How evilution turns a source file into a mutation-testing report, and where to
|
|
4
|
+
plug in new behavior. This is an internals map for contributors — none of the
|
|
5
|
+
classes named here are a public API (see [public_api.md](public_api.md)); they
|
|
6
|
+
can move or change in any release.
|
|
7
|
+
|
|
8
|
+
For deeper dives on two subsystems that have their own docs, see
|
|
9
|
+
[isolation.md](isolation.md) (fork vs in-process, sandboxing, signal safety) and
|
|
10
|
+
[integrations.md](integrations.md) (RSpec / Minitest / Test::Unit specifics).
|
|
11
|
+
|
|
12
|
+
## The one-paragraph version
|
|
13
|
+
|
|
14
|
+
Evilution parses each target file with [Prism](https://github.com/ruby/prism)
|
|
15
|
+
into an AST with exact byte offsets. Every method becomes a *subject*. Each
|
|
16
|
+
mutation *operator* walks a subject's AST and emits *mutations* — byte-range
|
|
17
|
+
edits applied by source-level surgery (no AST unparsing). For every mutation,
|
|
18
|
+
evilution copies the file, applies the edit, runs the covering specs in an
|
|
19
|
+
isolated worker, and records whether the tests noticed (`killed`) or not
|
|
20
|
+
(`survived`). Results are aggregated into a `Summary`, scored, and handed to a
|
|
21
|
+
reporter.
|
|
22
|
+
|
|
23
|
+
## High-level flow
|
|
24
|
+
|
|
25
|
+
```mermaid
|
|
26
|
+
flowchart TD
|
|
27
|
+
CLI["exe/evilution → Evilution::CLI<br/>Commands::Run"] --> CFG["Config<br/>(YAML + CLI + env, frozen)"]
|
|
28
|
+
CFG --> RUN["Runner#call<br/>(orchestrator)"]
|
|
29
|
+
RUN --> SUB["SubjectPipeline<br/>AST::Parser → Subject per method"]
|
|
30
|
+
SUB --> BASE["BaselineRunner<br/>record already-failing specs"]
|
|
31
|
+
SUB --> PLAN["MutationPlanner<br/>Mutator::Registry → Mutation objects<br/>dedupe · disable · sig · equivalent"]
|
|
32
|
+
PLAN --> EXEC["MutationExecutor<br/>jobs=1 Sequential · jobs>1 Parallel::Pool"]
|
|
33
|
+
BASE --> EXEC
|
|
34
|
+
EXEC --> ISO["Isolation::Fork / InProcess<br/>apply mutation → Integration runs specs"]
|
|
35
|
+
ISO --> STAT["classify_status<br/>killed / survived / error / timeout / unresolved"]
|
|
36
|
+
STAT --> SUM["Result::Summary<br/>counts + score"]
|
|
37
|
+
SUM --> REP["ReportPublisher → Reporter::{CLI,JSON,HTML}"]
|
|
38
|
+
REP --> OUT["stdout / evilution-report.html<br/>+ optional Session::Store"]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Module map
|
|
42
|
+
|
|
43
|
+
Everything lives under `lib/evilution/`.
|
|
44
|
+
|
|
45
|
+
| Namespace | Responsibility | Key entry points |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `CLI`, `CLI::Commands::*` | Parse argv, build `Config`, dispatch subcommands (`run`, `session`, `compare`), map result to exit code. | `cli.rb`, `cli/commands/run.rb` |
|
|
48
|
+
| `Config`, `Config::*` | Merge `.evilution.yml` + CLI flags + env, validate, freeze. | `config.rb`, `config/sources.rb`, `config/validators/*` |
|
|
49
|
+
| `Runner`, `Runner::*` | Orchestrate the whole run. Each stage is its own collaborator. | `runner.rb`, `runner/*` |
|
|
50
|
+
| `AST`, `Subject` | Prism parse, find method subjects, source surgery, pattern matching, heredoc spans. | `ast/parser.rb`, `ast/source_surgeon.rb`, `subject.rb` |
|
|
51
|
+
| `Mutator`, `Mutator::Operator::*` | 74 operators (default profile) that emit byte-edits; registry + profiles. | `mutator/base.rb`, `mutator/registry.rb`, `mutator/operator/*` |
|
|
52
|
+
| `Mutation` | An immutable mutation record (original/mutated sources, slice, location, parse status). | `mutation.rb` |
|
|
53
|
+
| `SpecResolver`, `SpecSelector` | Map a source file to its covering spec files (layout heuristics + explicit mappings). | `spec_resolver.rb`, `spec_selector.rb` |
|
|
54
|
+
| `Isolation::{Fork,InProcess}`, `ProcessSupervisor` | Run one mutation's tests in isolation; process-group lifecycle, sandboxing, TERM/KILL ladder. | `isolation/fork.rb`, `process_supervisor.rb` |
|
|
55
|
+
| `Integration::{RSpec,Minitest,TestUnit}` | Apply a mutation and run the configured test framework; report the raw outcome. | `integration/base.rb`, `integration/rspec.rb` |
|
|
56
|
+
| `Parallel::{Pool,WorkQueue}` | Fan mutations across worker processes for `jobs > 1`. | `parallel/pool.rb`, `parallel/work_queue.rb` |
|
|
57
|
+
| `Result::{MutationResult,Summary}` | Per-mutation result + aggregated, scored summary. | `result/mutation_result.rb`, `result/summary.rb` |
|
|
58
|
+
| `Reporter::{CLI,JSON,HTML,Suggestion}` | Render a `Summary` to text / JSON / HTML. | `reporter/*` |
|
|
59
|
+
| `Session`, `Compare` | Persist runs to `.evilution/results/*.json`; diff two sessions. | `session/store.rb`, `compare.rb` |
|
|
60
|
+
| `Coverage`, `Equivalent`, `Baseline`, `Cache`, `Hooks`, `MCP` | Coverage-based example targeting, equivalent-mutation detection, baseline capture, incremental cache, lifecycle hooks, MCP server. | respective dirs |
|
|
61
|
+
|
|
62
|
+
## Data flow, source → result
|
|
63
|
+
|
|
64
|
+
Driven entirely by `Evilution::Runner#call` (`runner.rb:24`). Each step names the
|
|
65
|
+
class that owns it.
|
|
66
|
+
|
|
67
|
+
1. **Config** — `Config#initialize` merges YAML + CLI + env (`Config::Sources.merge`),
|
|
68
|
+
applies `DEFAULTS`, freezes. `Runner#initialize` builds the `AST::Parser`,
|
|
69
|
+
`Mutator::Registry.for_profile(config.profile)`, and (if `incremental`) a `Cache`.
|
|
70
|
+
2. **Subjects** — `Runner::SubjectPipeline#call` resolves target files (explicit,
|
|
71
|
+
`source:<glob>`, or `Git::ChangedFiles`), then `AST::Parser#call` runs
|
|
72
|
+
`Prism.parse` and `AST::SubjectFinder` (a `Prism::Visitor`) emits one
|
|
73
|
+
`Evilution::Subject` per `def` node. Optional descendant/target/line-range filters follow.
|
|
74
|
+
3. **Baseline** — `Runner::BaselineRunner#call` builds the integration from
|
|
75
|
+
`Runner::INTEGRATIONS` (`rspec`/`minitest`/`test_unit`) and records spec files
|
|
76
|
+
that already fail *before* any mutation, so their mutations aren't miscounted.
|
|
77
|
+
An optional `Runner::Canary` proves the pipeline can observe a known mutation.
|
|
78
|
+
4. **Mutations** — `Runner::MutationPlanner#call` flat-maps subjects through
|
|
79
|
+
`Mutator::Registry#mutations_for`. The registry instantiates each operator and
|
|
80
|
+
runs `operator.call(subject, filter:)`; each operator subclasses
|
|
81
|
+
`Mutator::Base` and calls `add_mutation`, which runs `AST::SourceSurgeon` and
|
|
82
|
+
builds an immutable `Evilution::Mutation`. The planner then **deduplicates**
|
|
83
|
+
(by `file_path` + mutated source), and filters **disabled** (`DisableComment`),
|
|
84
|
+
**Sorbet `sig`** (`AST::SorbetSigDetector`), and **equivalent**
|
|
85
|
+
(`Equivalent::Detector`) mutations. Equivalent ones become `:equivalent`
|
|
86
|
+
results directly.
|
|
87
|
+
5. **Spec resolution** — per subject, `SpecSelector#call(source_path)` picks specs
|
|
88
|
+
(explicit `spec_files` → `spec_mappings` → `SpecResolver#resolve_specs` layout
|
|
89
|
+
heuristics). Example-level targeting narrows to examples that reference the
|
|
90
|
+
mutated token (`ExampleFilter` / `CoverageExampleFilter`).
|
|
91
|
+
6. **Execute** — `Runner::MutationExecutor#call` picks a strategy by `config.jobs`:
|
|
92
|
+
`Strategy::Sequential` for `jobs == 1`, `Strategy::Parallel` (via
|
|
93
|
+
`Parallel::Pool` / `WorkQueue`) for `jobs > 1`. Either way each mutation reaches
|
|
94
|
+
`@isolator.call(mutation:, test_command:, timeout:)`.
|
|
95
|
+
7. **Isolate + test** — `Isolation::Fork#call` (default for Rails/gems) spawns a
|
|
96
|
+
sandboxed worker through `ProcessSupervisor#spawn` (own process group, TERM →
|
|
97
|
+
grace → KILL ladder, sandbox reap), applies the mutation
|
|
98
|
+
(`Integration::Loading::MutationApplier`), and runs `Integration::Base#call` →
|
|
99
|
+
`run_tests`. The child's result is marshaled back over a length-prefixed pipe.
|
|
100
|
+
`Isolation::InProcess` is the lighter path for plain-Ruby projects.
|
|
101
|
+
8. **Status** — outcome flags come from the integration
|
|
102
|
+
(`Integration::RSpec::ResultBuilder`: passed / test_crashed / examples-loaded
|
|
103
|
+
guard / unresolved); the final symbol is chosen by
|
|
104
|
+
`Isolation::Fork#classify_status`: `:timeout` → `:killed` (crash) →
|
|
105
|
+
`:unresolved` → `:error` → `:survived` (tests passed) → default `:killed`.
|
|
106
|
+
A `NeutralizationPipeline` can reclassify results whose covering spec already
|
|
107
|
+
failed at baseline into `:neutral`.
|
|
108
|
+
9. **Aggregate + report** — `Result::Summary` counts each status and computes
|
|
109
|
+
`score = killed / score_denominator` (total minus error/neutral/equivalent/
|
|
110
|
+
unresolved/unparseable). `Runner::ReportPublisher#publish` selects a reporter by
|
|
111
|
+
`config.format` and writes it; `Session::Store` optionally persists the run.
|
|
112
|
+
`Commands::Run` maps `summary.success?(min_score:)` to exit code `0`/`1` (`2` on error).
|
|
113
|
+
|
|
114
|
+
## How to add a new mutator
|
|
115
|
+
|
|
116
|
+
A mutator is a `Prism::Visitor` subclass that emits byte-range edits.
|
|
117
|
+
|
|
118
|
+
1. **Create the operator** in `lib/evilution/mutator/operator/<name>.rb`:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
# frozen_string_literal: true
|
|
122
|
+
|
|
123
|
+
require_relative "../operator"
|
|
124
|
+
|
|
125
|
+
class Evilution::Mutator::Operator::MyThing < Evilution::Mutator::Base
|
|
126
|
+
def visit_integer_node(node) # a Prism visit_*_node hook
|
|
127
|
+
add_mutation(
|
|
128
|
+
offset: node.location.start_offset,
|
|
129
|
+
length: node.location.length,
|
|
130
|
+
replacement: "42", # the source text to splice in
|
|
131
|
+
node: node
|
|
132
|
+
)
|
|
133
|
+
super # ALWAYS call super to keep walking
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- Implement one or more `visit_<node_type>_node(node)` methods (the Prism node
|
|
139
|
+
names — `visit_call_node`, `visit_if_node`, …). Call `super` so child nodes
|
|
140
|
+
are still visited.
|
|
141
|
+
- Emit edits only through `add_mutation(offset:, length:, replacement:, node:)`.
|
|
142
|
+
It handles heredoc-span extension, the mutation filter, source surgery, and
|
|
143
|
+
builds the immutable `Mutation`. Never edit source strings directly.
|
|
144
|
+
- The operator's registered name is auto-derived from the class name
|
|
145
|
+
(`MyThing` → `my_thing`); that string is the `operator` field in JSON output
|
|
146
|
+
and is part of the public contract, so name it deliberately.
|
|
147
|
+
|
|
148
|
+
2. **Require it** in `lib/evilution.rb` alongside the other
|
|
149
|
+
`require_relative "evilution/mutator/operator/..."` lines.
|
|
150
|
+
|
|
151
|
+
3. **Register it** in `lib/evilution/mutator/registry.rb`: add the class to the
|
|
152
|
+
`default` list (runs in every profile) — or, for an aggressive operator meant
|
|
153
|
+
only for `profile: strict`, to `STRICT_EXTRA_OPERATORS` instead.
|
|
154
|
+
|
|
155
|
+
4. **Test it** under `spec/evilution/mutator/operator/<name>_spec.rb`, mirroring an
|
|
156
|
+
existing operator spec. Adding an operator to the `default` profile shifts
|
|
157
|
+
mutation scores and is a **MINOR** version bump (see [versioning.md](versioning.md)).
|
|
158
|
+
|
|
159
|
+
## How to add a new reporter
|
|
160
|
+
|
|
161
|
+
A reporter is `.new(**opts)` + `#call(summary) -> String`. It consumes a frozen
|
|
162
|
+
`Evilution::Result::Summary` and returns the rendered report body.
|
|
163
|
+
|
|
164
|
+
1. **Create the reporter** in `lib/evilution/reporter/<format>.rb`:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
# frozen_string_literal: true
|
|
168
|
+
|
|
169
|
+
require_relative "../reporter"
|
|
170
|
+
|
|
171
|
+
class Evilution::Reporter::Csv
|
|
172
|
+
def initialize(integration: :rspec) # keyword args only; keep them optional
|
|
173
|
+
@integration = integration
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def call(summary) # returns a String
|
|
177
|
+
# read summary.results, summary.score, summary.killed, summary.survived,
|
|
178
|
+
# summary.survived_results, etc. — see Result::Summary for the full API
|
|
179
|
+
"..."
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The `Summary` API a reporter reads includes: counts (`total`, `killed`,
|
|
185
|
+
`survived`, `errors`, `neutral`, `equivalent`, `unresolved`, `unparseable`,
|
|
186
|
+
`timed_out`), metrics (`score`, `score_denominator`, `success?(min_score:)`,
|
|
187
|
+
`efficiency`, `peak_memory_mb`), the raw `results`, and filtered lists
|
|
188
|
+
(`survived_results`, `killed_results`, …). Each result exposes `mutation`
|
|
189
|
+
(`operator_name`, `file_path`, `line`, `diff`), `status`, `duration`, and
|
|
190
|
+
status predicates (`killed?`, `survived?`, …).
|
|
191
|
+
|
|
192
|
+
2. **Require it** in `lib/evilution.rb` (next to the other `reporter/*` requires)
|
|
193
|
+
**and** in `lib/evilution/runner.rb` (the runner requires reporters
|
|
194
|
+
independently, near the bottom of the file).
|
|
195
|
+
|
|
196
|
+
3. **Wire the dispatch** in `Runner::ReportPublisher#build_reporter`
|
|
197
|
+
(`runner/report_publisher.rb`): add a `when :csv` branch returning your class,
|
|
198
|
+
and a matching `require_relative "../reporter/csv"` at the top. If the output
|
|
199
|
+
should go to a file rather than stdout, extend the `#publish` write branch
|
|
200
|
+
(only `:html` writes a file today; every other format is `$stdout.puts`ed).
|
|
201
|
+
|
|
202
|
+
4. **Advertise the value.** There is no strict format allowlist to update —
|
|
203
|
+
`config.format` is only `to_sym`'d, not validated — but update the
|
|
204
|
+
`--format` help string in `cli/parser/options_builder.rb` so `csv` is
|
|
205
|
+
discoverable. (The `compare` subcommand keeps its *own* `SUPPORTED_FORMATS`
|
|
206
|
+
list; touch it only if `compare` should support the new format too.)
|
|
207
|
+
|
|
208
|
+
Adding a new format is additive — a **MINOR** bump.
|
|
209
|
+
|
|
210
|
+
## How to add a new test integration (bonus)
|
|
211
|
+
|
|
212
|
+
Integrations subclass `Evilution::Integration::Base` (`integration/base.rb`),
|
|
213
|
+
which defines the contract: class methods `baseline_runner` / `baseline_options`,
|
|
214
|
+
and instance methods `run_tests(mutation)`, `ensure_framework_loaded`,
|
|
215
|
+
`build_args(mutation)`, `reset_state` (each `raise NotImplementedError` until
|
|
216
|
+
overridden). `Base#call` already handles applying the mutation and firing
|
|
217
|
+
`mutation_insert_pre/post` hooks. Register the new class in
|
|
218
|
+
`Runner::INTEGRATIONS` (`runner/baseline_runner.rb`) and allow its symbol in the
|
|
219
|
+
integration config validator. See [integrations.md](integrations.md) for the
|
|
220
|
+
framework-specific details.
|
data/docs/public_api.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Public API
|
|
2
|
+
|
|
3
|
+
This document defines evilution's public, SemVer-governed surface for `1.x`.
|
|
4
|
+
|
|
5
|
+
## There is no public Ruby API
|
|
6
|
+
|
|
7
|
+
**The entire `Evilution::` Ruby namespace is internal.** Every class, module,
|
|
8
|
+
method, and constant under `Evilution::` may change, move, or disappear in any
|
|
9
|
+
release — including patch releases — with no deprecation cycle. Do not `require`
|
|
10
|
+
evilution and call into it programmatically expecting stability; internal
|
|
11
|
+
refactors will break you without warning.
|
|
12
|
+
|
|
13
|
+
Evilution is consumed through its CLI, its configuration file, its session
|
|
14
|
+
output, and its MCP tools — not as a library you embed. The top-level
|
|
15
|
+
`Evilution` module and the primary entry points (`Evilution::Runner`,
|
|
16
|
+
`Evilution::CLI`, `Evilution::Config`) carry an `@api private` YARD marker; the
|
|
17
|
+
namespace as a whole is internal by this declaration, whether or not an
|
|
18
|
+
individual class is tagged.
|
|
19
|
+
|
|
20
|
+
## What *is* the public contract
|
|
21
|
+
|
|
22
|
+
These surfaces are stable under the [versioning policy](versioning.md). They are
|
|
23
|
+
the supported way to drive evilution:
|
|
24
|
+
|
|
25
|
+
| Surface | Authoritative reference |
|
|
26
|
+
|---|---|
|
|
27
|
+
| **CLI commands and flags** | README "Command Reference" |
|
|
28
|
+
| **`.evilution.yml` configuration keys** | README "Configuration" |
|
|
29
|
+
| **Session JSON files** (`.evilution/results/*.json`) | README "JSON Output Schema" |
|
|
30
|
+
| **MCP tool schemas** (`evilution-mutate`, `evilution-session`, `evilution-info`) | README "MCP Server" → "Contract stability" |
|
|
31
|
+
| **Process exit codes** (`0` pass, `1` fail, `2` error) | README "Exit Codes" |
|
|
32
|
+
| **Hook events and payload keys** (configured via the `hooks:` config key) | README "Configuration" / hooks docs |
|
|
33
|
+
|
|
34
|
+
Anything not in that table — and in particular anything reachable only by calling
|
|
35
|
+
Ruby methods on `Evilution::` objects — is internal.
|
|
36
|
+
|
|
37
|
+
## Why no Ruby API at 1.0
|
|
38
|
+
|
|
39
|
+
Evilution's job is to run mutation testing from the command line (and from an MCP
|
|
40
|
+
agent). The value users depend on is the behaviour of `evilution run`, the shape
|
|
41
|
+
of the report it writes, and the config that tunes it — none of which requires a
|
|
42
|
+
frozen object graph. Keeping the Ruby internals unfrozen lets the isolation,
|
|
43
|
+
parallelism, and mutation-operator internals evolve freely across `1.x` without
|
|
44
|
+
spending major-version budget. If a genuine embedding use case emerges, a small
|
|
45
|
+
public facade can be introduced additively in a MINOR release.
|
|
46
|
+
|
|
47
|
+
## See also
|
|
48
|
+
|
|
49
|
+
- [Versioning & Upgrade Policy](versioning.md) — what each SemVer bump covers and
|
|
50
|
+
how deprecations work.
|
data/docs/versioning.md
CHANGED
|
@@ -7,14 +7,14 @@ This document defines what `evilution` promises across releases.
|
|
|
7
7
|
| Bump | Triggered by |
|
|
8
8
|
|---------------|-------------------------------------------------------------------------------|
|
|
9
9
|
| MAJOR (`2.0`) | Removing or renaming anything in the public contract; changing semantics; tightening input validation. |
|
|
10
|
-
| MINOR (`1.X`) | Adding a new CLI flag, config key, mutation operator,
|
|
10
|
+
| MINOR (`1.X`) | Adding a new CLI flag, config key, mutation operator, or session/MCP field; introducing a public Ruby facade where none exists today; relaxing validation; adding an operator to the `default` profile (whether brand-new or promoted from `strict`). |
|
|
11
11
|
| PATCH (`1.X.Y`) | Bug fix, performance improvement, documentation, internal refactor with no observable contract effect. |
|
|
12
12
|
|
|
13
13
|
## Public contract surface
|
|
14
14
|
|
|
15
15
|
The following surfaces are covered by the SemVer guarantees above:
|
|
16
16
|
|
|
17
|
-
- **Public Ruby API** —
|
|
17
|
+
- **Public Ruby API** — there is none. The entire `Evilution::` namespace is internal and may change in any release. See [docs/public_api.md](public_api.md).
|
|
18
18
|
- **CLI flags and commands** — the README "Command Reference" tables are the authoritative list.
|
|
19
19
|
- **`.evilution.yml` configuration keys** — see the README "Configuration" section.
|
|
20
20
|
- **Session JSON files** (`.evilution/results/*.json`) — see the README "JSON Output Schema" section.
|
|
@@ -27,7 +27,7 @@ Anything not on this list is internal. It can change in any release without a de
|
|
|
27
27
|
|
|
28
28
|
When a feature on the public contract surface is deprecated:
|
|
29
29
|
|
|
30
|
-
1. It is marked with
|
|
30
|
+
1. It is marked with a deprecation note in the relevant doc table (CLI flags, config keys, session/MCP fields).
|
|
31
31
|
2. Where the call site is reachable at runtime, a one-line warning is emitted to stderr.
|
|
32
32
|
3. The deprecated form remains functional for the entire `1.x` line. A feature deprecated in any `1.X` release continues to work in every subsequent `1.X+N` release.
|
|
33
33
|
4. The earliest release that may remove the feature is the next major (`2.0`), per the SemVer table above.
|
data/lib/evilution/cli.rb
CHANGED
data/lib/evilution/config.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "yaml"
|
|
|
4
4
|
require_relative "spec_resolver"
|
|
5
5
|
require_relative "spec_selector"
|
|
6
6
|
|
|
7
|
+
# @api private
|
|
7
8
|
class Evilution::Config
|
|
8
9
|
CONFIG_FILES = %w[.evilution.yml config/evilution.yml].freeze
|
|
9
10
|
CURRENT_SCHEMA_VERSION = 1
|
|
@@ -117,7 +118,6 @@ class Evilution::Config
|
|
|
117
118
|
FileLoader.load
|
|
118
119
|
end
|
|
119
120
|
|
|
120
|
-
# Generates a default config file template.
|
|
121
121
|
def self.default_template
|
|
122
122
|
<<~YAML
|
|
123
123
|
# Evilution configuration
|
|
@@ -63,7 +63,9 @@ class Evilution::Integration::Loading::RedefinitionRecovery
|
|
|
63
63
|
|
|
64
64
|
def idempotency_violation?(error)
|
|
65
65
|
msg = error.message
|
|
66
|
-
|
|
66
|
+
# `msg` is a String and patterns are substrings, so `include?` is a
|
|
67
|
+
# substring test — `intersect?` would compare array elements, not text.
|
|
68
|
+
IDEMPOTENCY_PATTERNS.any? { |pat| msg.include?(pat) } # rubocop:disable Style/ArrayIntersect
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
def superclass_mismatch?(error)
|
|
@@ -31,14 +31,12 @@ class Evilution::RelatedSpecHeuristic
|
|
|
31
31
|
def extract_domain(file_path)
|
|
32
32
|
normalized = normalize_path(file_path)
|
|
33
33
|
|
|
34
|
-
# Strip common prefixes and get the relative path under app/ or lib/
|
|
35
34
|
relative = normalized
|
|
36
35
|
.delete_prefix("app/controllers/")
|
|
37
36
|
.delete_prefix("app/models/")
|
|
38
37
|
.delete_prefix("app/")
|
|
39
38
|
.delete_prefix("lib/")
|
|
40
39
|
|
|
41
|
-
# Remove .rb extension and _controller suffix
|
|
42
40
|
basename = relative.sub(/\.rb\z/, "")
|
|
43
41
|
basename = basename.sub(/_controller\z/, "")
|
|
44
42
|
|
data/lib/evilution/runner.rb
CHANGED
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -129,6 +129,12 @@ require_relative "evilution/cli"
|
|
|
129
129
|
require_relative "evilution/disable_comment"
|
|
130
130
|
require_relative "evilution/runner"
|
|
131
131
|
|
|
132
|
+
# Evilution has no public Ruby API. The entire `Evilution::` namespace is
|
|
133
|
+
# internal and may change in any release, including patches. The stable,
|
|
134
|
+
# SemVer-governed contract is the CLI, `.evilution.yml` config keys, session
|
|
135
|
+
# JSON files, the MCP tool schemas, and process exit codes. See
|
|
136
|
+
# docs/public_api.md and docs/versioning.md.
|
|
137
|
+
# @api private
|
|
132
138
|
module Evilution
|
|
133
139
|
# Captured at load time, before any isolator can chdir into a per-mutation
|
|
134
140
|
# sandbox. Used as the anchor for resolving project-relative paths (spec
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: evilution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Denis Kiselev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|
|
@@ -108,11 +108,13 @@ files:
|
|
|
108
108
|
- comparison_results/baseline_2026-04-09.md
|
|
109
109
|
- comparison_results/operator_classification.md
|
|
110
110
|
- comparison_results/operator_prioritization.md
|
|
111
|
+
- docs/architecture.md
|
|
111
112
|
- docs/ast_pattern_syntax.md
|
|
112
113
|
- docs/integrations.md
|
|
113
114
|
- docs/isolation.md
|
|
114
115
|
- docs/migration-from-mutant.md
|
|
115
116
|
- docs/mutation_density_benchmark.md
|
|
117
|
+
- docs/public_api.md
|
|
116
118
|
- docs/versioning.md
|
|
117
119
|
- exe/evil
|
|
118
120
|
- exe/evilution
|