evilution 0.35.0 → 1.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.
- checksums.yaml +4 -4
- data/.beads/interactions.jsonl +32 -0
- data/CHANGELOG.md +39 -0
- data/README.md +23 -6
- 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/mcp/complete_result_server.rb +41 -0
- data/lib/evilution/mcp/server.rb +2 -1
- data/lib/evilution/mutator/base.rb +14 -2
- data/lib/evilution/mutator/operator/boolean_expression_to_nil.rb +22 -0
- data/lib/evilution/mutator/operator/boolean_operand_promotion.rb +31 -0
- data/lib/evilution/mutator/operator/case_in.rb +64 -0
- data/lib/evilution/mutator/operator/case_when.rb +72 -1
- data/lib/evilution/mutator/operator/conditional_branch.rb +20 -7
- data/lib/evilution/mutator/operator/if_branch_swap.rb +48 -0
- data/lib/evilution/mutator/operator/loop_body_to_raise.rb +68 -0
- data/lib/evilution/mutator/operator/pattern_predicate.rb +28 -0
- data/lib/evilution/mutator/primitives.rb +52 -0
- data/lib/evilution/mutator/registry.rb +6 -0
- 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 +12 -0
- data/scripts/compare_targeting +4 -2
- metadata +12 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 71f26de1d4437f62a01f5d5784a3a67ffdc914418ae952cb279de392ec401ecb
|
|
4
|
+
data.tar.gz: 92d18647ec19836335c5eaa3156ce6da7db1857b285b66f49efc2ed66de49ed0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0174eb2e10182274ed8cbda806172a8d4956caeff7af844c409daa6f3de711b709a450be89cf4dc3a08b36e7df84ca0c0473a66d21909d8752a83bce4309aab2
|
|
7
|
+
data.tar.gz: 8bf2f8cfce0633f537186cd08aaf6abd66c06869d56308647f0be34352d6d94c06912b8c1c0e8128ca19b45acc60536b5a6f35028996ce25ca2a0501bdc1b508
|
data/.beads/interactions.jsonl
CHANGED
|
@@ -437,3 +437,35 @@
|
|
|
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"}}
|
|
451
|
+
{"id":"int-e7b1f137","kind":"field_change","created_at":"2026-08-14T05:36:05.821384373Z","actor":"Denis Kiselev","issue_id":"EV-4ild","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
452
|
+
{"id":"int-c003e7fa","kind":"field_change","created_at":"2026-08-22T04:23:28.006710286Z","actor":"Denis Kiselev","issue_id":"EV-170m.1","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
|
|
453
|
+
{"id":"int-f912a48c","kind":"field_change","created_at":"2026-08-22T15:26:32.589708305Z","actor":"Denis Kiselev","issue_id":"EV-170m.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
454
|
+
{"id":"int-d74d2ac8","kind":"field_change","created_at":"2026-08-22T17:21:54.118527965Z","actor":"Denis Kiselev","issue_id":"EV-170m.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
455
|
+
{"id":"int-d694451a","kind":"field_change","created_at":"2026-08-22T17:21:59.468788556Z","actor":"Denis Kiselev","issue_id":"EV-170m.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
456
|
+
{"id":"int-9cd4dc49","kind":"field_change","created_at":"2026-08-22T17:22:05.341826256Z","actor":"Denis Kiselev","issue_id":"EV-170m.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
457
|
+
{"id":"int-9a64fd8d","kind":"field_change","created_at":"2026-08-23T05:54:32.093591437Z","actor":"Denis Kiselev","issue_id":"EV-170m.14","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
458
|
+
{"id":"int-990ddf2a","kind":"field_change","created_at":"2026-08-23T05:54:35.35056636Z","actor":"Denis Kiselev","issue_id":"EV-170m.15","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
459
|
+
{"id":"int-aa07e9f8","kind":"field_change","created_at":"2026-08-23T05:54:38.655416932Z","actor":"Denis Kiselev","issue_id":"EV-170m.16","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
460
|
+
{"id":"int-e7ba9d2f","kind":"field_change","created_at":"2026-08-23T05:54:41.957571538Z","actor":"Denis Kiselev","issue_id":"EV-170m.10","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
461
|
+
{"id":"int-c4cf46e8","kind":"field_change","created_at":"2026-08-23T05:54:43.220109392Z","actor":"Denis Kiselev","issue_id":"EV-170m.11","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
462
|
+
{"id":"int-46ac995b","kind":"field_change","created_at":"2026-08-23T05:54:46.601240327Z","actor":"Denis Kiselev","issue_id":"EV-170m.12","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
463
|
+
{"id":"int-ee1da7ed","kind":"field_change","created_at":"2026-08-23T05:54:58.551793942Z","actor":"Denis Kiselev","issue_id":"EV-170m.8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
464
|
+
{"id":"int-a2659f70","kind":"field_change","created_at":"2026-08-23T06:17:48.940199465Z","actor":"Denis Kiselev","issue_id":"EV-zfh6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
465
|
+
{"id":"int-3ef2119d","kind":"field_change","created_at":"2026-08-23T06:22:32.612716343Z","actor":"Denis Kiselev","issue_id":"EV-9mrs","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
466
|
+
{"id":"int-4f24cfc1","kind":"field_change","created_at":"2026-08-23T06:22:36.754337802Z","actor":"Denis Kiselev","issue_id":"EV-170m.4","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
467
|
+
{"id":"int-db1a15c4","kind":"field_change","created_at":"2026-08-23T06:22:40.226188047Z","actor":"Denis Kiselev","issue_id":"EV-170m.6","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
468
|
+
{"id":"int-ee6c5593","kind":"field_change","created_at":"2026-08-23T06:22:43.665223358Z","actor":"Denis Kiselev","issue_id":"EV-170m.7","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
469
|
+
{"id":"int-a9687ce8","kind":"field_change","created_at":"2026-08-23T06:22:47.016572566Z","actor":"Denis Kiselev","issue_id":"EV-170m.9","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
470
|
+
{"id":"int-6979f453","kind":"field_change","created_at":"2026-08-23T06:22:50.346314707Z","actor":"Denis Kiselev","issue_id":"EV-170m.13","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
471
|
+
{"id":"int-c0c4ce61","kind":"field_change","created_at":"2026-08-23T06:22:53.658409885Z","actor":"Denis Kiselev","issue_id":"EV-170m.17","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
Versioning policy: see [docs/versioning.md](docs/versioning.md).
|
|
4
4
|
|
|
5
|
+
## [1.1.0] - 2026-08-23
|
|
6
|
+
|
|
7
|
+
Control-flow and pattern-matching operator expansion: the `default` profile grows from 74 to 80 operators, and three existing operators gain mutations they were silently missing. Adding operators to `default` is a MINOR change under [docs/versioning.md](docs/versioning.md), and mutation scores will move — every new operator produces mutants your suite has never been measured against. Pin the gem version and the operator profile if you need a stable score across runs.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Six new mutation operators for control flow and pattern matching (`default` profile: 74 -> 80)** — closing the value-level and branch-level gaps in an operator set that previously mutated control-flow nodes only at the keyword level (epic EV-170m, GH #1418):
|
|
12
|
+
- **`boolean_operand_promotion`** — drops one side of a compound boolean: `a && b` becomes `a` and `b`, and the same for `||` and the `and` / `or` keyword forms. Kills tests that only ever exercise one side of a condition, which `boolean_operator_replacement` cannot: it swaps the operator but always keeps both operands (EV-170m.2, PR #1565, GH #1431)
|
|
13
|
+
- **`boolean_expression_to_nil`** — replaces a whole compound boolean with `nil`, targeting conditions that are run for their side effects rather than their value (EV-170m.3, PR #1566, GH #1432)
|
|
14
|
+
- **`if_branch_swap`** — replaces the if-branch with the else body and drops the else: `if c; x; else; y; end` becomes `if c; y; end`. Both outcomes of the condition change, which is out of reach of `conditional_negation` (pins the predicate to one branch) and `conditional_branch` (blanks one body to `nil`) (EV-170m.5, PR #1567, GH #1434)
|
|
15
|
+
- **`loop_body_to_raise`** — replaces a `while` / `until` body with a bare `raise`, so a survivor means no test ever enters the loop. The raise ends the loop on its first iteration, so the mutant cannot spin (EV-170m.8, PR #1569, GH #1437)
|
|
16
|
+
- **`case_in`** — the first operator to visit Ruby's pattern-matching grammar (`Prism::CaseMatchNode`): drops one `in` clause from a `case/in`, or drops its `else`. Input that used to match then falls through to a later arm, to the `else`, or — with neither — raises `NoMatchingPatternError` (EV-170m.14 / EV-170m.15, PRs #1573 / #1574, GH #1443 / #1444)
|
|
17
|
+
- **`pattern_predicate`** — one-line pattern match to `false`: `x in Integer` becomes `false`. The mutant differs from the original only on inputs the pattern actually matches, so a survivor means no test ever feeds it a matching value (EV-170m.16, PR #1575, GH #1445)
|
|
18
|
+
- **`case_when` covers two more shapes** — an empty `when` arm now gets a `raise` inserted, and a multi-condition arm is shortened one value at a time (`when a, b` becomes `when a` and `when b`). Neither was previously reachable: dropping an empty arm is indistinguishable from falling through to a missing `else`, and whole-arm removal cannot tell which of several listed values a test exercises (EV-170m.11 / EV-170m.12, PRs #1571 / #1572, GH #1440 / #1441)
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **`conditional_branch` ignored `unless` entirely** — the operator defined only `visit_if_node`, so neither `unless c; x; end` nor `unless c; x; else; y; end` received any branch-body mutation, while the equivalent `if` forms received one per branch. Prism gives `unless` its own node type and names the else slot `else_clause` rather than `IfNode`'s `subsequent`; both forms now share the branch handling, so `unless` bodies — including the modifier form — get the same branch-to-`nil` mutants (EV-9mrs, PR #1578, GH #1568)
|
|
23
|
+
- **MCP list responses omitted the mandatory `resultType` field** — protocol revision 2026-07-28 (SEP-2322) makes `resultType` mandatory on list results, and the `mcp` gem echoes back that revision while still omitting the field. Strict clients therefore rejected the whole list and saw a server exposing no tools at all. `Evilution::MCP::CompleteResultServer` now marks every list result `"complete"` (PR #1417)
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Post-form loops (`begin ... end while c`) verified across the loop operators** — `loop_body_to_raise` reaches through the `BeginNode` that Prism reports as the loop's statements, so the wrapper, and with it the guaranteed first iteration, survives the mutation; editing the outer span would have rewritten the loop as `raise while c`, which tests the condition first. `loop_flip` and `begin_unwrap` gained the post-form coverage they never had, including the fact that unwrapping such a loop legitimately drops its run-once guarantee. A sweep of every operator over post-form loops found no mutation that fails to parse (EV-170m.10, PR #1570, GH #1439)
|
|
28
|
+
- **Shared operator primitives (`Mutator::Primitives`)** — `mutate_to_nil` and `promote_child`, built on `Base#add_mutation(skip_unparseable: true)`. Operators built on them skip rather than emit when the result would not parse in its surrounding context, so a promotion that cannot parse no longer reaches the `unparseable` bucket that the point operators still populate (EV-170m.1, PR #1564, GH #1430)
|
|
29
|
+
- **Dependency bumps** — `mcp` 0.24.0 -> 1.2.0 (PRs #1411, #1413, #1414, #1563), CI Ruby versions and gem dependencies (PR #1415), `ruby/setup-ruby` 1.318.0 -> 1.321.0 (PRs #1409, #1412), `actions/checkout` 7.0.0 -> 7.0.1 (PR #1410), `rubygems/release-gem` 1.4.0 -> 1.4.1 (PR #1562)
|
|
30
|
+
|
|
31
|
+
## [1.0.0] - 2026-07-15
|
|
32
|
+
|
|
33
|
+
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).
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **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)
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **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)
|
|
42
|
+
- **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)
|
|
43
|
+
|
|
5
44
|
## [0.35.0] - 2026-06-24
|
|
6
45
|
|
|
7
46
|
### 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
|
|
|
@@ -157,7 +157,7 @@ Every command, subcommand, and flag listed in this section is part of evilution'
|
|
|
157
157
|
|
|
158
158
|
Two profiles ship out of the box:
|
|
159
159
|
|
|
160
|
-
- **`default`** — the
|
|
160
|
+
- **`default`** — the 80 stable operators registered in `Mutator::Registry.default`. Suitable for everyday CI runs; balances coverage signal against survivor noise.
|
|
161
161
|
- **`strict`** — adds extra truthiness mutators on top of `default`. Currently `PredicateToNil` (replaces every `x.predicate?` call with `nil` to surface tests that only assert truthiness rather than exact return values). Use for pre-merge audits where you want maximum sensitivity at the cost of more survivors.
|
|
162
162
|
|
|
163
163
|
Set via `--profile=strict`, the `--strict` shortcut, or `profile: strict` in `.evilution.yml`.
|
|
@@ -390,7 +390,7 @@ Compatibility policy for the `1.x` gem line:
|
|
|
390
390
|
|
|
391
391
|
Unresolved mutations indicate a missing test mapping — the file has no corresponding test file that the resolver could find (for example, an RSpec `_spec.rb` file or a Minitest `_test.rb` file, depending on configuration). The resolver searches the `lib/`-mirrored path, common non-mirrored buckets (`spec/unit`, `spec/lib`, `test/unit`, `test/lib`), and the flat `test_`-prefixed Minitest/Test::Unit convention (`test/test_connection_pool_timed_stack.rb`), so a high unresolved rate usually means a genuinely missing or unconventionally-placed test; a run that leaves many mutations unresolved prints an unresolved-rate warning with a best-guess spec path per source file. They are reported separately so you can act on them (add a test, adjust test naming, pass `--spec`, or opt in to the full-suite fallback) without inflating the error count.
|
|
392
392
|
|
|
393
|
-
## Mutation Operators (
|
|
393
|
+
## Mutation Operators (80 total)
|
|
394
394
|
|
|
395
395
|
Each operator name is stable and appears in JSON output under `survived[].operator`.
|
|
396
396
|
|
|
@@ -399,6 +399,8 @@ Each operator name is stable and appears in JSON output under `survived[].operat
|
|
|
399
399
|
| `arithmetic_replacement` | Swap arithmetic operators | `a + b` -> `a - b` |
|
|
400
400
|
| `comparison_replacement` | Swap comparison operators | `a >= b` -> `a > b` |
|
|
401
401
|
| `boolean_operator_replacement` | Swap `&&` / `\|\|` | `a && b` -> `a \|\| b` |
|
|
402
|
+
| `boolean_operand_promotion` | Drop one side of `&&` / `\|\|` | `a && b` -> `a`, `b` |
|
|
403
|
+
| `boolean_expression_to_nil` | Replace a whole compound boolean with `nil` | `a && b` -> `nil` |
|
|
402
404
|
| `boolean_literal_replacement` | Flip boolean literals | `true` -> `false` |
|
|
403
405
|
| `nil_replacement` | Replace `nil` with `true`, `false`, `0`, `""` | `nil` -> `true` |
|
|
404
406
|
| `integer_literal` | Boundary-value integer mutations | `n` -> `0`, `1`, `n+1`, `n-1` |
|
|
@@ -408,7 +410,8 @@ Each operator name is stable and appears in JSON output under `survived[].operat
|
|
|
408
410
|
| `hash_literal` | Empty the hash | `{k: v}` -> `{}` |
|
|
409
411
|
| `symbol_literal` | Replace with sentinel symbol | `:foo` -> `:__evilution_mutated__` |
|
|
410
412
|
| `conditional_negation` | Replace condition with `true`/`false` | `if cond` -> `if true` |
|
|
411
|
-
| `conditional_branch` | Remove if/else branch | Deletes branch body |
|
|
413
|
+
| `conditional_branch` | Remove if/unless/else branch | Deletes branch body |
|
|
414
|
+
| `if_branch_swap` | Replace the if-branch with the else body, drop the else | `if c; x; else; y; end` -> `if c; y; end` |
|
|
412
415
|
| `conditional_flip` | Flip `if` to `unless` and vice versa | `if cond` -> `unless cond` |
|
|
413
416
|
| `statement_deletion` | Remove statements from method bodies | Deletes a statement |
|
|
414
417
|
| `method_body_replacement` | Replace entire method body | Method body -> `nil`, `self`, `super` |
|
|
@@ -460,9 +463,12 @@ Each operator name is stable and appears in JSON output under `survived[].operat
|
|
|
460
463
|
| `defined_check` | Replace `defined?` with `true` | `defined?(x)` -> `true` |
|
|
461
464
|
| `regex_capture` | Swap or nil-ify capture refs | `$1` -> `$2`, `$1` -> `nil` |
|
|
462
465
|
| `loop_flip` | Swap while/until loops | `while cond` -> `until cond` |
|
|
466
|
+
| `loop_body_to_raise` | Replace a loop body with `raise` | `while c; body; end` -> `while c; raise; end` |
|
|
463
467
|
| `string_interpolation` | Replace interpolation content with nil | `"hello #{name}"` -> `"hello #{nil}"` |
|
|
464
468
|
| `retry_removal` | Remove retry statements | `retry` -> `nil` |
|
|
465
|
-
| `case_when` | Remove/replace case/when branches | Remove `when` branch, body -> `nil`, remove `else` |
|
|
469
|
+
| `case_when` | Remove/replace case/when branches | Remove `when` branch, drop one condition from `when a, b`, body -> `nil`, empty body -> `raise`, remove `else` |
|
|
470
|
+
| `case_in` | Drop one `in` clause from a `case/in`, or its `else` | `case x; in Integer; …; in String; …; end` -> drops an arm; `else` -> `NoMatchingPatternError` |
|
|
471
|
+
| `pattern_predicate` | One-line pattern match -> `false` | `x in Integer` -> `false` |
|
|
466
472
|
| `predicate_replacement` | Replace predicate calls with booleans | `x.empty?` -> `true`, `x.empty?` -> `false` |
|
|
467
473
|
| `equality_to_identity` | Replace equality with identity check | `a == b` -> `a.equal?(b)` |
|
|
468
474
|
| `lambda_body` | Replace lambda body with nil | `-> { expr }` -> `-> { nil }` |
|
|
@@ -783,6 +789,12 @@ When Evilution detects a parallel run against a SQLite-backed `config/database.y
|
|
|
783
789
|
|
|
784
790
|
## Development
|
|
785
791
|
|
|
792
|
+
### Architecture
|
|
793
|
+
|
|
794
|
+
New to the codebase? [docs/architecture.md](docs/architecture.md) is the
|
|
795
|
+
contributor guide: the end-to-end mutation flow (source → result), a module map,
|
|
796
|
+
and step-by-step recipes for adding a mutator, a reporter, or a test integration.
|
|
797
|
+
|
|
786
798
|
### Memory leak check
|
|
787
799
|
|
|
788
800
|
Run before releasing to verify no memory regressions:
|
|
@@ -798,10 +810,13 @@ Tests 4 paths (InProcess isolation, Fork isolation, mutation generation + stripp
|
|
|
798
810
|
|
|
799
811
|
## Internals (for context, not for direct use)
|
|
800
812
|
|
|
813
|
+
For the full contributor architecture — module map, data flow, and extension
|
|
814
|
+
points — see [docs/architecture.md](docs/architecture.md).
|
|
815
|
+
|
|
801
816
|
1. **Parse** — Prism parses Ruby files into ASTs with exact byte offsets
|
|
802
817
|
2. **Extract** — Methods are identified as mutation subjects
|
|
803
818
|
3. **Filter** — Disable comments, Sorbet `sig` blocks, and AST ignore patterns exclude mutations before execution
|
|
804
|
-
4. **Mutate** —
|
|
819
|
+
4. **Mutate** — 80 operators produce text replacements at precise byte offsets (source-level surgery, no AST unparsing); heredoc literal text is skipped by default. Identical byte-mutations from different operators are deduplicated by `(file_path, mutated_source)` so the count is not inflated by overlap
|
|
805
820
|
5. **Isolate** — Mutations are applied to temporary file copies (never modifying originals); load-path redirection ensures `require` resolves the mutated copy. Default isolation is in-process for plain Ruby projects (no gemspec) and fork for Rails projects and packaged gems (auto-detected); `--isolation fork` forces forked child processes. Both sequential and parallel (`--jobs N`) modes respect the configured isolation strategy
|
|
806
821
|
6. **Test** — The configured test framework (RSpec, Minitest, or Test::Unit) executes against the mutated source
|
|
807
822
|
7. **Collect** — Source strings and AST nodes are released after use to minimize memory retention
|
|
@@ -811,6 +826,8 @@ Tests 4 paths (InProcess isolation, Fork isolation, mutation generation + stripp
|
|
|
811
826
|
|
|
812
827
|
`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
828
|
|
|
829
|
+
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).
|
|
830
|
+
|
|
814
831
|
## Repository
|
|
815
832
|
|
|
816
833
|
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::*` | 80 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? { msg.include?(_1) }
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
def superclass_mismatch?(error)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
require_relative "../mcp"
|
|
6
|
+
|
|
7
|
+
# MCP protocol revision 2026-07-28 (SEP-2322) makes `resultType` mandatory on list results: a
|
|
8
|
+
# client may only treat a missing field as "complete" for servers that negotiated an earlier
|
|
9
|
+
# revision. The mcp gem echoes back any client-offered revision it supports — 2026-07-28
|
|
10
|
+
# included — yet its list handlers still omit the field, so strict clients reject the whole
|
|
11
|
+
# list and the server appears to expose no tools at all.
|
|
12
|
+
#
|
|
13
|
+
# Every list result this server produces is final (there is no `input_required` round trip in
|
|
14
|
+
# the evilution tool surface), so the field is always `"complete"`. The value is written as a
|
|
15
|
+
# literal rather than `MCP::ResultType::COMPLETE` because the gem dependency allows versions
|
|
16
|
+
# that predate that constant. Older revisions ignore the extra key.
|
|
17
|
+
class Evilution::MCP::CompleteResultServer < MCP::Server
|
|
18
|
+
RESULT_TYPE_COMPLETE = "complete"
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def list_tools(request)
|
|
23
|
+
mark_complete(super)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def list_prompts(request)
|
|
27
|
+
mark_complete(super)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def list_resources(request)
|
|
31
|
+
mark_complete(super)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def list_resource_templates(request)
|
|
35
|
+
mark_complete(super)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def mark_complete(result)
|
|
39
|
+
result.merge(resultType: RESULT_TYPE_COMPLETE)
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/evilution/mcp/server.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "mcp"
|
|
4
4
|
require_relative "../version"
|
|
5
|
+
require_relative "complete_result_server"
|
|
5
6
|
require_relative "mutate_tool"
|
|
6
7
|
require_relative "session_tool"
|
|
7
8
|
require_relative "info_tool"
|
|
@@ -10,7 +11,7 @@ require_relative "../mcp"
|
|
|
10
11
|
|
|
11
12
|
class Evilution::MCP::Server
|
|
12
13
|
def self.build
|
|
13
|
-
::MCP::
|
|
14
|
+
Evilution::MCP::CompleteResultServer.new(
|
|
14
15
|
name: "evilution",
|
|
15
16
|
version: Evilution::VERSION,
|
|
16
17
|
tools: [Evilution::MCP::MutateTool, Evilution::MCP::SessionTool, Evilution::MCP::InfoTool]
|