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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 492fc3dd9f7676eaf9c263b1342602250561a382afe2aa9f5871daa04ce842a7
4
- data.tar.gz: 8982ff01289997d8abaa3d5cb122de065ef0ebfcc1c043767e423d97a7d714f6
3
+ metadata.gz: 71f26de1d4437f62a01f5d5784a3a67ffdc914418ae952cb279de392ec401ecb
4
+ data.tar.gz: 92d18647ec19836335c5eaa3156ce6da7db1857b285b66f49efc2ed66de49ed0
5
5
  SHA512:
6
- metadata.gz: 6653347d505820a813fa673d60013806738b1e37e9f268c4c9c429c531644998a6e9d27bac2dfe93613eaa4aca6b8a257ed44867d9a1d92d4f4c5a27a16f93fa
7
- data.tar.gz: d6da79725f113a9e11ac33da8e835ecf58db554db34efdd5b74a015b190bc4717864718f04462993801a9b44d606ff53de303a32dbcb7bf7fe15d5b4d71cb6b8
6
+ metadata.gz: 0174eb2e10182274ed8cbda806172a8d4956caeff7af844c409daa6f3de711b709a450be89cf4dc3a08b36e7df84ca0c0473a66d21909d8752a83bce4309aab2
7
+ data.tar.gz: 8bf2f8cfce0633f537186cd08aaf6abd66c06869d56308647f0be34352d6d94c06912b8c1c0e8128ca19b45acc60536b5a6f35028996ce25ca2a0501bdc1b508
@@ -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 Minitest
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 74 stable operators registered in `Mutator::Registry.default`. Suitable for everyday CI runs; balances coverage signal against survivor noise.
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 (74 total)
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** — 74 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
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.
@@ -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, public Ruby method, or session/MCP field; relaxing validation; adding an operator to the `default` profile (whether brand-new or promoted from `strict`). |
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** — classes and methods explicitly documented as public. Everything else is internal and may change in any release.
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 the YARD `@deprecated` tag (Ruby API), or with a deprecation note in the relevant doc table (CLI flags, config keys, MCP fields).
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
@@ -19,6 +19,7 @@ require_relative "cli/commands/session_gc"
19
19
  require_relative "cli/commands/compare"
20
20
  require_relative "cli/commands/run"
21
21
 
22
+ # @api private
22
23
  class Evilution::CLI
23
24
  def initialize(argv, stdin: $stdin)
24
25
  parsed = Parser.new(argv, stdin: stdin).parse
@@ -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
- IDEMPOTENCY_PATTERNS.any? { |pat| msg.include?(pat) }
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
@@ -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::Server.new(
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]