evilution 0.35.0 → 1.0.0

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