evilution 1.1.0 → 1.3.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 +58 -0
- data/.rubocop_todo.yml +9 -0
- data/CHANGELOG.md +86 -0
- data/README.md +202 -14
- data/docs/architecture.md +54 -5
- data/docs/isolation.md +3 -4
- data/exe/evil +8 -1
- data/exe/evilution +8 -1
- data/lib/evilution/ast/local_reads.rb +43 -0
- data/lib/evilution/ast/parser.rb +1 -0
- data/lib/evilution/ast/pattern/filter.rb +1 -0
- data/lib/evilution/ast/pattern/parser.rb +1 -0
- data/lib/evilution/ast/pattern.rb +2 -0
- data/lib/evilution/ast/sorbet_sig_detector.rb +1 -0
- data/lib/evilution/baseline.rb +37 -15
- data/lib/evilution/child_output.rb +1 -1
- data/lib/evilution/cli/command.rb +1 -0
- data/lib/evilution/cli/commands/tests_list.rb +4 -3
- data/lib/evilution/cli/commands.rb +2 -0
- data/lib/evilution/cli/dispatcher.rb +2 -0
- data/lib/evilution/cli/exit_guard.rb +61 -0
- data/lib/evilution/cli/parsed_args.rb +2 -0
- data/lib/evilution/cli/parser/command_extractor.rb +2 -0
- data/lib/evilution/cli/parser/file_args.rb +2 -0
- data/lib/evilution/cli/parser/options_builder.rb +4 -0
- data/lib/evilution/cli/parser/stdin_reader.rb +1 -0
- data/lib/evilution/cli/parser.rb +1 -0
- data/lib/evilution/cli/printers/tests_list.rb +5 -4
- data/lib/evilution/cli/printers.rb +2 -0
- data/lib/evilution/cli/result.rb +2 -0
- data/lib/evilution/cli.rb +6 -0
- data/lib/evilution/config/builders.rb +2 -0
- data/lib/evilution/config/env_loader.rb +2 -0
- data/lib/evilution/config/file_loader.rb +1 -0
- data/lib/evilution/config/sources.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_cache.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_fallback.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_strategy.rb +1 -0
- data/lib/evilution/config/validators/fail_fast.rb +1 -0
- data/lib/evilution/config/validators/hooks.rb +1 -0
- data/lib/evilution/config/validators/ignore_patterns.rb +1 -0
- data/lib/evilution/config/validators/integration.rb +1 -0
- data/lib/evilution/config/validators/isolation.rb +1 -0
- data/lib/evilution/config/validators/jobs.rb +1 -0
- data/lib/evilution/config/validators/preload.rb +1 -0
- data/lib/evilution/config/validators/profile.rb +1 -0
- data/lib/evilution/config/validators/spec_mappings.rb +1 -0
- data/lib/evilution/config/validators/spec_pattern.rb +1 -0
- data/lib/evilution/config/validators/warmup.rb +17 -0
- data/lib/evilution/config/validators.rb +2 -0
- data/lib/evilution/config.rb +7 -5
- data/lib/evilution/coverage.rb +1 -1
- data/lib/evilution/coverage_example_filter.rb +1 -1
- data/lib/evilution/diagnostic.rb +22 -0
- data/lib/evilution/example_filter.rb +1 -1
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +10 -2
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +20 -4
- data/lib/evilution/integration/loading/reeval_warning_filter.rb +72 -0
- data/lib/evilution/integration/loading/source_evaluator.rb +4 -1
- data/lib/evilution/integration/loading/test_load_path.rb +21 -13
- data/lib/evilution/integration/minitest.rb +2 -1
- data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +9 -1
- data/lib/evilution/integration/rspec/state_guard/configuration_state.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/configuration_streams.rb +5 -1
- data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/internals.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard.rb +6 -0
- data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +2 -1
- data/lib/evilution/integration/rspec.rb +48 -2
- data/lib/evilution/integration/test_unit/test_file_resolver.rb +2 -1
- data/lib/evilution/isolation/fork.rb +29 -8
- data/lib/evilution/mcp/info_tool/actions/environment.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/feedback.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/statuses.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/tests.rb +1 -0
- data/lib/evilution/mutation.rb +1 -1
- data/lib/evilution/mutator/operator/argument_list_removal.rb +52 -0
- data/lib/evilution/mutator/operator/argument_propagation.rb +54 -0
- data/lib/evilution/mutator/operator/array_coercion_to_literal.rb +34 -0
- data/lib/evilution/mutator/operator/attribute_write_to_read.rb +34 -0
- data/lib/evilution/mutator/operator/bang_method.rb +11 -3
- data/lib/evilution/mutator/operator/binary_operand_promotion.rb +63 -0
- data/lib/evilution/mutator/operator/block_body_promotion.rb +45 -0
- data/lib/evilution/mutator/operator/block_body_to_nil.rb +52 -0
- data/lib/evilution/mutator/operator/block_body_to_raise.rb +34 -0
- data/lib/evilution/mutator/operator/block_destructuring_expansion.rb +85 -0
- data/lib/evilution/mutator/operator/block_parameter_drop.rb +96 -0
- data/lib/evilution/mutator/operator/call_to_nil.rb +34 -0
- data/lib/evilution/mutator/operator/coercion_emptying.rb +60 -0
- data/lib/evilution/mutator/operator/collection_replacement.rb +19 -9
- data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -12
- data/lib/evilution/mutator/operator/const_get_to_constant_path.rb +50 -0
- data/lib/evilution/mutator/operator/dig_to_fetch_chain.rb +45 -0
- data/lib/evilution/mutator/operator/double_negation_removal.rb +22 -0
- data/lib/evilution/mutator/operator/dynamic_dispatch_resolution.rb +79 -0
- data/lib/evilution/mutator/operator/forwarding_super_to_explicit.rb +71 -0
- data/lib/evilution/mutator/operator/inequality_to_negated_identity.rb +43 -0
- data/lib/evilution/mutator/operator/keyword_argument_removal.rb +47 -0
- data/lib/evilution/mutator/operator/method_body_replacement.rb +10 -1
- data/lib/evilution/mutator/operator/method_body_to_raise.rb +59 -0
- data/lib/evilution/mutator/operator/method_body_to_super.rb +150 -0
- data/lib/evilution/mutator/operator/optional_default_injection.rb +71 -0
- data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +41 -0
- data/lib/evilution/mutator/operator/proc_to_lambda.rb +43 -0
- data/lib/evilution/mutator/operator/receiver_constructor_swap.rb +51 -0
- data/lib/evilution/mutator/operator/reduce_to_sum.rb +58 -0
- data/lib/evilution/mutator/operator/regexp_anchor_to_predicate.rb +136 -0
- data/lib/evilution/mutator/operator/safe_navigation_removal.rb +67 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +42 -6
- data/lib/evilution/mutator/operator/symbol_to_proc_replacement.rb +59 -0
- data/lib/evilution/mutator/operator/to_i_to_integer.rb +38 -0
- data/lib/evilution/mutator/operator/typed_default_return.rb +84 -0
- data/lib/evilution/mutator/primitives.rb +25 -0
- data/lib/evilution/mutator/registry.rb +32 -1
- data/lib/evilution/parallel/pool.rb +1 -0
- data/lib/evilution/parallel_db_warning.rb +1 -1
- data/lib/evilution/process_supervisor.rb +20 -8
- data/lib/evilution/rails_warmup.rb +61 -0
- data/lib/evilution/reporter/cli/item_formatters/neutral_group.rb +23 -0
- data/lib/evilution/reporter/cli/item_formatters/subject_score.rb +42 -0
- data/lib/evilution/reporter/cli/item_formatters/subject_score_group.rb +16 -0
- data/lib/evilution/reporter/cli/line_formatters/infra_retry_notice.rb +19 -0
- data/lib/evilution/reporter/cli/line_formatters/result_line.rb +27 -3
- data/lib/evilution/reporter/cli/line_formatters/score.rb +19 -1
- data/lib/evilution/reporter/cli/line_formatters/unresolved_targets.rb +35 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +4 -0
- data/lib/evilution/reporter/cli/trailer.rb +11 -7
- data/lib/evilution/reporter/cli.rb +20 -4
- data/lib/evilution/reporter/html/report.rb +1 -0
- data/lib/evilution/reporter/json/subjects.rb +29 -0
- data/lib/evilution/reporter/json.rb +23 -1
- data/lib/evilution/result/coverage_gap_grouper.rb +1 -0
- data/lib/evilution/result/mutation_result.rb +3 -2
- data/lib/evilution/result/neutral_reason.rb +35 -0
- data/lib/evilution/result/subject_score.rb +28 -0
- data/lib/evilution/result/subject_scorer.rb +38 -0
- data/lib/evilution/result/summary.rb +44 -2
- data/lib/evilution/runner/baseline_runner.rb +17 -7
- data/lib/evilution/runner/canary.rb +52 -5
- data/lib/evilution/runner/isolation_resolver.rb +23 -2
- data/lib/evilution/runner/mutation_executor/infra_retry.rb +54 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +23 -10
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +18 -1
- data/lib/evilution/runner/mutation_executor/result_cache.rb +11 -0
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +19 -1
- data/lib/evilution/runner/mutation_executor.rb +32 -4
- data/lib/evilution/runner/report_publisher.rb +31 -9
- data/lib/evilution/runner/target_spec_audit.rb +43 -0
- data/lib/evilution/runner.rb +10 -1
- data/lib/evilution/source_ast_cache.rb +1 -1
- data/lib/evilution/spec_ast_cache.rb +1 -1
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +35 -0
- metadata +50 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 171e27cbec4014afbffb3c058ecc0b666caa8f849c33532ed55f84e2ecee3a87
|
|
4
|
+
data.tar.gz: bff682f7afeb96e4b565ae916950ad8b526f5eca2dc00bb88f17a28382b5e9d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c272e9c1cb1c5d0af0bb85856b02a59eb14dfad81bc4285d035eb693826eb8559acf803ac14482acf736da98e531c6b1444bb269fa8d0d3ecb340c16f2942f9e
|
|
7
|
+
data.tar.gz: b5a31b2aa40568c6f43ab9147d8a3cde7c8d2ab9205466a3d459eaca5395b7b31cf2a94a1e997a2e6f982cc4ef8fd189be268f2bfc52c0f897a963eebc9f0718
|
data/.beads/interactions.jsonl
CHANGED
|
@@ -469,3 +469,61 @@
|
|
|
469
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
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
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"}}
|
|
472
|
+
{"id":"int-9fe2b310","kind":"field_change","created_at":"2026-08-23T15:26:14.173757324Z","actor":"Denis Kiselev","issue_id":"EV-secd","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
473
|
+
{"id":"int-e3df2816","kind":"field_change","created_at":"2026-08-23T15:26:14.758723969Z","actor":"Denis Kiselev","issue_id":"EV-v2rc","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
474
|
+
{"id":"int-4964c024","kind":"field_change","created_at":"2026-08-23T16:53:26.159738103Z","actor":"Denis Kiselev","issue_id":"EV-df7u","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
475
|
+
{"id":"int-09aacdba","kind":"field_change","created_at":"2026-08-23T17:31:02.425168724Z","actor":"Denis Kiselev","issue_id":"EV-65nf","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
476
|
+
{"id":"int-f248abda","kind":"field_change","created_at":"2026-09-19T02:33:35.442608624Z","actor":"Denis Kiselev","issue_id":"EV-0y0p.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
477
|
+
{"id":"int-2295820f","kind":"field_change","created_at":"2026-09-19T05:36:52.469484387Z","actor":"Denis Kiselev","issue_id":"EV-0y0p.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
478
|
+
{"id":"int-c34dface","kind":"field_change","created_at":"2026-09-19T06:40:49.582010237Z","actor":"Denis Kiselev","issue_id":"EV-0y0p.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
479
|
+
{"id":"int-b54a1b1b","kind":"field_change","created_at":"2026-09-19T09:16:07.408034725Z","actor":"Denis Kiselev","issue_id":"EV-0y0p.7","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
480
|
+
{"id":"int-d07f50fb","kind":"field_change","created_at":"2026-09-20T05:54:55.207588665Z","actor":"Denis Kiselev","issue_id":"EV-0y0p.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
481
|
+
{"id":"int-8c521e96","kind":"field_change","created_at":"2026-09-20T07:27:33.837283926Z","actor":"Denis Kiselev","issue_id":"EV-0y0p.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
482
|
+
{"id":"int-0082d40c","kind":"field_change","created_at":"2026-09-20T11:10:19.819992902Z","actor":"Denis Kiselev","issue_id":"EV-0y0p.6","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
483
|
+
{"id":"int-b1fbb9d9","kind":"field_change","created_at":"2026-09-20T12:09:22.863657064Z","actor":"Denis Kiselev","issue_id":"EV-0y0p.8","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
484
|
+
{"id":"int-0341459f","kind":"field_change","created_at":"2026-09-20T15:42:35.81801432Z","actor":"Denis Kiselev","issue_id":"EV-p4sm","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
485
|
+
{"id":"int-37d6d94c","kind":"field_change","created_at":"2026-09-20T16:28:47.475124108Z","actor":"Denis Kiselev","issue_id":"EV-39t1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
486
|
+
{"id":"int-3c77e0d2","kind":"field_change","created_at":"2026-09-20T17:08:50.170870939Z","actor":"Denis Kiselev","issue_id":"EV-j0bv","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
487
|
+
{"id":"int-9a5aa97e","kind":"field_change","created_at":"2026-09-21T05:03:01.265493211Z","actor":"Denis Kiselev","issue_id":"EV-g8ya","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
488
|
+
{"id":"int-2b83bb6b","kind":"field_change","created_at":"2026-09-21T06:59:17.164808817Z","actor":"Denis Kiselev","issue_id":"EV-nlx1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
489
|
+
{"id":"int-9a7df3ab","kind":"field_change","created_at":"2026-09-21T07:43:13.101312655Z","actor":"Denis Kiselev","issue_id":"EV-5pob","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
490
|
+
{"id":"int-6c08ef8f","kind":"field_change","created_at":"2026-09-21T08:47:57.19015326Z","actor":"Denis Kiselev","issue_id":"EV-f8h3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
491
|
+
{"id":"int-41e28161","kind":"field_change","created_at":"2026-09-21T09:13:10.822154641Z","actor":"Denis Kiselev","issue_id":"EV-vk1f","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
492
|
+
{"id":"int-a85e9307","kind":"field_change","created_at":"2026-09-24T08:59:06.431936036Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
493
|
+
{"id":"int-0146928b","kind":"field_change","created_at":"2026-09-24T08:59:57.400402587Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Won't do: self. insertion is equivalent on Ruby >= 2.7 (private calls with literal self allowed); gem requires >= 3.3"}}
|
|
494
|
+
{"id":"int-d13277bb","kind":"field_change","created_at":"2026-09-24T10:11:27.25351867Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
495
|
+
{"id":"int-98f94055","kind":"field_change","created_at":"2026-09-24T11:09:10.574795113Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
496
|
+
{"id":"int-cfc17855","kind":"field_change","created_at":"2026-09-25T04:10:24.329240246Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
497
|
+
{"id":"int-ff65a252","kind":"field_change","created_at":"2026-09-25T06:05:35.905280887Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
498
|
+
{"id":"int-005be0d5","kind":"field_change","created_at":"2026-09-25T06:05:41.780650066Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.7","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Covered by argument_list_removal (EV-tsi4.6, PR #1639)"}}
|
|
499
|
+
{"id":"int-99b3d64d","kind":"field_change","created_at":"2026-09-25T09:43:15.271882913Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.28","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
500
|
+
{"id":"int-f94cb5b9","kind":"field_change","created_at":"2026-09-25T10:05:26.030418343Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.15","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
501
|
+
{"id":"int-ab93e0ea","kind":"field_change","created_at":"2026-09-25T11:09:58.712099704Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
502
|
+
{"id":"int-f54216bc","kind":"field_change","created_at":"2026-09-25T11:28:25.015240322Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
503
|
+
{"id":"int-78532509","kind":"field_change","created_at":"2026-09-25T12:03:26.712050666Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.10","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
504
|
+
{"id":"int-b04e4ada","kind":"field_change","created_at":"2026-09-25T12:35:14.598132892Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.27","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
505
|
+
{"id":"int-878aed06","kind":"field_change","created_at":"2026-09-25T16:34:22.646996954Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.11","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
506
|
+
{"id":"int-a5be78a4","kind":"field_change","created_at":"2026-09-25T17:15:19.778644918Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.12","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
507
|
+
{"id":"int-b5b54c8c","kind":"field_change","created_at":"2026-09-25T17:47:12.91754374Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.13","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
508
|
+
{"id":"int-0915250f","kind":"field_change","created_at":"2026-09-26T03:50:18.7581253Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.26","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
509
|
+
{"id":"int-01656f0a","kind":"field_change","created_at":"2026-09-26T04:02:44.194731168Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.14","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
510
|
+
{"id":"int-f5cfcd38","kind":"field_change","created_at":"2026-09-26T04:18:06.728081454Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.25","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
511
|
+
{"id":"int-f611c7c0","kind":"field_change","created_at":"2026-09-26T06:33:30.975279097Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.16","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
512
|
+
{"id":"int-dbccf56c","kind":"field_change","created_at":"2026-09-26T06:52:37.666559599Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.17","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
513
|
+
{"id":"int-3ace0c73","kind":"field_change","created_at":"2026-09-26T07:07:52.345696388Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.18","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
514
|
+
{"id":"int-df7a3ad8","kind":"field_change","created_at":"2026-09-26T07:19:58.301755963Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.24","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
515
|
+
{"id":"int-ec1508f5","kind":"field_change","created_at":"2026-09-26T07:41:16.328539681Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.19","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
516
|
+
{"id":"int-4fb31878","kind":"field_change","created_at":"2026-09-26T08:00:27.677084584Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.20","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
517
|
+
{"id":"int-451c79e2","kind":"field_change","created_at":"2026-09-26T17:48:34.137710758Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.21","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
518
|
+
{"id":"int-b243b5f3","kind":"field_change","created_at":"2026-09-27T04:19:10.37017749Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.22","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
519
|
+
{"id":"int-561a3e6f","kind":"field_change","created_at":"2026-09-27T04:37:48.03229929Z","actor":"Denis Kiselev","issue_id":"EV-tsi4.23","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
520
|
+
{"id":"int-180ba042","kind":"field_change","created_at":"2026-09-27T05:10:45.908206414Z","actor":"Denis Kiselev","issue_id":"EV-27v9.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
521
|
+
{"id":"int-854c1949","kind":"field_change","created_at":"2026-09-27T05:27:27.19234994Z","actor":"Denis Kiselev","issue_id":"EV-27v9.2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
522
|
+
{"id":"int-d09d6377","kind":"field_change","created_at":"2026-09-27T10:40:02.318630878Z","actor":"Denis Kiselev","issue_id":"EV-27v9.3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
523
|
+
{"id":"int-5bb755e9","kind":"field_change","created_at":"2026-09-27T10:43:06.788713181Z","actor":"Denis Kiselev","issue_id":"EV-27v9.4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Won't do: hoist yields NoMethodError (killed by execution) or duplicates block_body_promotion; signal covered by EV-27v9.3"}}
|
|
524
|
+
{"id":"int-1344bddb","kind":"field_change","created_at":"2026-09-27T10:54:21.511979983Z","actor":"Denis Kiselev","issue_id":"EV-27v9.5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
525
|
+
{"id":"int-8d4fd88a","kind":"field_change","created_at":"2026-09-27T15:12:53.603922398Z","actor":"Denis Kiselev","issue_id":"EV-03as","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
526
|
+
{"id":"int-e2c0e5da","kind":"field_change","created_at":"2026-09-27T16:02:46.866587228Z","actor":"Denis Kiselev","issue_id":"EV-e9ni","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
527
|
+
{"id":"int-356e6b7a","kind":"field_change","created_at":"2026-09-28T02:59:40.442681333Z","actor":"Denis Kiselev","issue_id":"EV-ae7u","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
528
|
+
{"id":"int-a15bfc29","kind":"field_change","created_at":"2026-09-28T03:35:22.865923943Z","actor":"Denis Kiselev","issue_id":"EV-ziy0","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
529
|
+
{"id":"int-c4c3b0ee","kind":"field_change","created_at":"2026-09-28T05:00:30.703915081Z","actor":"Denis Kiselev","issue_id":"EV-qj40","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
data/.rubocop_todo.yml
CHANGED
|
@@ -16,7 +16,16 @@ Metrics/ClassLength:
|
|
|
16
16
|
- "lib/evilution/runner/isolation_resolver.rb"
|
|
17
17
|
- "lib/evilution/cli/parser/options_builder.rb"
|
|
18
18
|
- "lib/evilution/spec_resolver.rb"
|
|
19
|
+
- "lib/evilution/mutator/registry.rb"
|
|
19
20
|
|
|
20
21
|
Metrics/MethodLength:
|
|
21
22
|
Exclude:
|
|
22
23
|
- "lib/evilution/runner.rb"
|
|
24
|
+
|
|
25
|
+
# The empty forward declaration (`class Evilution::CLI; end`) that lets the
|
|
26
|
+
# children nest under the namespace before the full definition counts as a
|
|
27
|
+
# second top-level class. See "Require conventions" in docs/architecture.md.
|
|
28
|
+
Style/OneClassPerFile:
|
|
29
|
+
Exclude:
|
|
30
|
+
- "lib/evilution/cli.rb"
|
|
31
|
+
- "lib/evilution/integration/rspec/state_guard.rb"
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,92 @@
|
|
|
2
2
|
|
|
3
3
|
Versioning policy: see [docs/versioning.md](docs/versioning.md).
|
|
4
4
|
|
|
5
|
+
## [1.3.0] - 2026-09-28
|
|
6
|
+
|
|
7
|
+
Operator expansion release: the `default` profile grows from 88 to 111 operators, mostly around call sites and blocks, and several existing operators gain new replacements. 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
|
+
The fixes come from a field report by [@rubdttcom](https://github.com/rubdttcom) on a Rails 8 / Minitest application (GH #1596–#1599); two of them landed as their own commits.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Nineteen new operators for call sites and message sends (`default` profile)** — call nodes were mutated only by selector swaps, receiver stripping and argument nil-substitution (epic GH #1420):
|
|
14
|
+
- **`call_to_nil`** — a call to `nil`, the broadest value-level probe. Skips calls whose value is discarded, receivers of another call and attribute writes, where the result is noise (PR #1635, GH #1455)
|
|
15
|
+
- **`safe_navigation_removal`** — `a&.b` to `a.b`. A survivor means no test reaches the call with a nil receiver. Skips `self` and literal receivers, which are never nil (PR #1636, GH #1457)
|
|
16
|
+
- **`attribute_write_to_read`** — `a.foo = b` to `a.foo`, and `a[i] = b` to `a[i]`: proves the write is observed (PR #1637, GH #1458)
|
|
17
|
+
- **`argument_propagation`** — `normalize(value)` to `value`: proves the method transforms its input (PR #1638, GH #1459)
|
|
18
|
+
- **`argument_list_removal`** — `foo(a, b)` to `foo`, every argument kind at once; also covers the single-argument case `argument_removal` never reached (PR #1639, GH #1460, #1461)
|
|
19
|
+
- **`keyword_argument_removal`** — drops one `key: value` pair at a call site, which no operator touched before (PR #1642, GH #1462)
|
|
20
|
+
- **`double_negation_removal`** — `!!x` to `x`: distinguishes a truthy object from `true` (PR #1643, GH #1463)
|
|
21
|
+
- **`regexp_anchor_to_predicate`** — `x =~ /^foo/` to `x.start_with?("foo")`, and `$` / `\Z` to `end_with?`. Line anchors also match after a newline, so a survivor means no test feeds multi-line input, or the anchor should have been `\A` / `\z` (PR #1644, GH #1464)
|
|
22
|
+
- **`reduce_to_sum`** — `reduce(:+)` to `sum`: differs on empty collections, non-numeric elements and float precision (PR #1646, GH #1465)
|
|
23
|
+
- **`array_coercion_to_literal`** — `Array(x)` to `[x]`: differs for `nil`, arrays and hashes (PR #1647, GH #1466)
|
|
24
|
+
- **`to_i_to_integer`** — `x.to_i` to `Integer(x)`: `Integer()` rejects malformed input and honours radix prefixes (`"0x1A"`, `"012"`) (PR #1648, GH #1467)
|
|
25
|
+
- **`coercion_emptying`** — `to_a` / `to_h` / `to_s` (and `to_ary` / `to_hash` / `to_str`) to `[]` / `{}` / `""`: proves the content is asserted, not just the type (PR #1650, GH #1468)
|
|
26
|
+
- **`dynamic_dispatch_resolution`** — `obj.send(:m)` to `obj.m`, emitted only where visibility checks can differ (PR #1641, GH #1469)
|
|
27
|
+
- **`const_get_to_constant_path`** — `X.const_get(:Y)` to `X::Y`: differs on top-level and private constants and `inherit = false` (PR #1652, GH #1470)
|
|
28
|
+
- **`proc_to_lambda`** — `proc { }` / `Proc.new { }` to `lambda { }`: arity checks, array auto-splat and `return` semantics (PR #1653, GH #1471)
|
|
29
|
+
- **`dig_to_fetch_chain`** — `x.dig(a, b)` to `x.fetch(a).dig(b)` (PR #1654, GH #1472)
|
|
30
|
+
- **`inequality_to_negated_identity`** — `a != b` to `!a.eql?(b)` and `!a.equal?(b)`; skips `nil`, boolean and symbol literals, where all three agree (PR #1656, GH #1473)
|
|
31
|
+
- **`binary_operand_promotion`** — `a + b` to `a` and `b` for arithmetic and bitwise operators, skipping integer identities such as `a + 0` (PR #1657, GH #1474)
|
|
32
|
+
- **`receiver_constructor_swap`** — `Date` / `DateTime` / `Time.parse` to a stricter sibling (`iso8601`, `strptime`, …), keyed on receiver and selector (PR #1658, GH #1475)
|
|
33
|
+
- **`symbol_to_proc_replacement`** — applies the `send_mutation` and `collection_replacement` selector tables to `&:sym` arguments, which previously got only removal (PR #1640, GH #1482)
|
|
34
|
+
- **Three new operators for blocks** — blocks were only ever removed wholesale (epic GH #1421):
|
|
35
|
+
- **`block_body_to_nil`** — the block body to `nil`, keeping the iteration. Skips `loop` and endless `cycle`, which would never terminate (PR #1661, GH #1483)
|
|
36
|
+
- **`block_body_to_raise`** — the block body to `raise`: a survivor means the block is never invoked. Skips bodies with a `rescue` clause, which would swallow it (PR #1662, GH #1484)
|
|
37
|
+
- **`block_body_promotion`** — `Base.transaction { save! }` to `save!` for parameter-less blocks: exposes wrappers whose effect is never tested, such as transactions whose rollback nothing triggers (PR #1663, GH #1485)
|
|
38
|
+
- **`warmup: rails` / `--warmup rails`** — after `--preload`, warms Rails state that otherwise initialises on each fork's first request (I18n, routes, asset paths, templates when `actionview_precompiler` is installed). Every step is side-effect free and skipped with a note if it does not apply. See "Warming up Rails before forking" in the README (PR #1671, GH #1599)
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **Larger selector tables** — new swaps in `send_mutation` (`bytes`/`chars`, `start_with?`/`end_with?`, `ceil`/`floor`, `to_s` -> `to_str` and the other implicit conversions, `even?`/`odd?`, `pred`/`succ`, `is_a?` -> `instance_of?`, `obj.method(:m)` -> `public_method`, …), `collection_replacement` (`any?` -> `empty?`/`none?`, `find` / `max` / `min` -> `first`/`last`, `fetch` -> `key?`, `delete_if` -> `reject`, `each_with_index` -> `each`, …) and `comparison_replacement` (ordering comparisons to `eql?` / `equal?`, `=~` -> `match?`) (PRs #1649, #1651, #1655, #1659, #1660; GH #1476–#1480)
|
|
43
|
+
- **`bang_method` knows every standard-library bang pair** — the list grew from a 24-name sample to every String, Array, Hash and Set method with an in-place twin (PR #1645, GH #1481)
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **`tests list` resolves specs the way `run` does** — it always used the RSpec layout, so Minitest and Test::Unit projects saw `(no spec found)` for every source, and it ignored `spec_mappings` and `spec_pattern`. A source mapped to several spec files now lists each (PR #1667, GH #1596; fix by @rubdttcom)
|
|
48
|
+
- **A preload outside `test/` or `spec/` no longer shadows gems** — the preload's own directory went onto `$LOAD_PATH`, so `--preload config/evilution_preload.rb` made `require "puma"` load the app's `config/puma.rb` (PR #1668, GH #1597; fix by @rubdttcom)
|
|
49
|
+
- **The baseline follows `fallback_to_full_suite` and `spec_mappings`** — for a source with no resolved test it ran the whole test directory even with the fallback off, and printed "running full suite"; in a real Rails suite that run timed out silently. A source declared only through `spec_mappings` also fell back to the directory, and its survivors were then reported `neutral` against a mapped test that passes. The baseline now uses the same spec selection as `run`, skips unresolved sources unless the run falls back, and reports a timed-out baseline explicitly (PR #1669, GH #1598)
|
|
50
|
+
- **Projects that raise on warnings no longer error every mutation of a file with a neutralized class-body call** — before re-evaluating a mutated file, evilution replaces non-idempotent class-body calls such as `deprecate :lift, :coerce` with `nil`, which Ruby flags under `-w` as "possibly useless use of nil in void context". A suite that raises on warnings (dry-monads) then scored 545 of 546 mutations of `maybe.rb` as errors; the replacement is now `()` (GH #1673)
|
|
51
|
+
|
|
52
|
+
## [1.2.0] - 2026-09-21
|
|
53
|
+
|
|
54
|
+
Reporting honesty release. A run now says what it did **not** measure instead of letting the headline score speak for the whole target set: files that resolved to no spec fail the run, subjects nothing reached are named, neutral results record why, and the printed verdict and the exit code finally share one threshold. The `default` profile also grows from 80 to 88 operators, so 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.
|
|
55
|
+
|
|
56
|
+
Most of this release comes from a field report on 1.1.0 ([discussion #1584](https://github.com/marinazzio/evilution/discussions/1584)): eight months of side-by-side runs against `mutant` on a private Rails application, filed as GH #1603–#1608.
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- **Eight new mutation operators for method definitions (`default` profile: 80 -> 88)** — the operator set mutated method bodies only to `nil`, `self` or `super`, and never touched parameter lists (GH #1419):
|
|
61
|
+
- **`method_body_to_raise`** — whole body to a bare `raise`. Where body-to-nil asks whether the return value matters, this asks the prior question of whether the method is called at all on a path the suite asserts; a method whose result is discarded survives the nil mutation but not this one (PR #1609, GH #1447)
|
|
62
|
+
- **`method_body_to_super`** — whole body to a bare `super`, for plain overrides. `method_body_replacement` emits the same replacement only when the body *already* calls super, so the case worth probing was never reached. Emitted only where a super target exists: an explicit superclass, or `include`/`prepend` for instance methods and `extend` for singleton ones (PR #1610, GH #1448)
|
|
63
|
+
- **`typed_default_return`** — single-expression body to the empty value of the type its trailing call returns (`users.map(&:name)` -> `[]`, `users.count` -> `0`). A survivor means the suite asserts the shape of what comes back but never its content (PR #1611, GH #1449)
|
|
64
|
+
- **`block_parameter_drop`** — drops a block's single parameter: `users.each { |u| touch(u) }` -> `users.each { touch(u) }`. A survivor means the block never ran on an asserted path (PR #1612, GH #1453)
|
|
65
|
+
- **`optional_parameter_to_required`** — `def f(a = 1)` -> `def f(a)`. A survivor means no example calls the method without that argument, so the default is never exercised (PR #1613, GH #1450)
|
|
66
|
+
- **`optional_default_injection`** — overwrites an optional parameter with its own default at the top of the body. The mirror of the above: it asks whether any value *other* than the default is ever asserted (PR #1614, GH #1451)
|
|
67
|
+
- **`block_destructuring_expansion`** — `|(k, v), i|` -> `|k, v, i|`, emitted only where the group has a sibling parameter, since a lone group and a flat list bind identically for a single yielded array (PR #1615, GH #1452)
|
|
68
|
+
- **`forwarding_super_to_explicit`** — bare `super` to `super()`. Bare super forwards the current arguments, `super()` forwards none, so a survivor means the forwarded arguments never reach an assertion (PR #1616, GH #1454)
|
|
69
|
+
- **Per-subject score breakdown** — the run's score is computed per file, which says nothing about a method inside it that no example reaches; a well-tested file that gains untested methods still reported 100%. Every report now scores each subject separately. The text report names the subjects the file-level score does not speak for, and JSON carries every subject under `subjects` with `killed`, `verified`, `score` and `reached` (PR #1621, GH #1605)
|
|
70
|
+
- **Neutral results record why they are neutral** — `neutral` covered both "the spec was already red" and "the test process died on infrastructure", which want opposite responses. Each neutral now carries a reason; the text report groups by it and names the failing spec or the crash class, and JSON entries carry `neutral_reason` as `{ kind, detail }` (PR #1622, GH #1606)
|
|
71
|
+
- **`--output FILE`** — writes the report to a file instead of stdout, for projects whose preloaded spec helper writes to stdout on exit (PR #1620, GH #1608)
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- **A target file that resolves to no spec now fails the run** — previously it contributed a handful of `unresolved` mutations and disappeared behind the other files' score: adding one well-tested file to the same command turned a 0% file into a `PASS`. Such files are now named, and the run exits non-zero whatever the score. `--fallback-full-suite` suppresses this, since nothing goes untested there. JSON carries `summary.unresolved_target_files` (PR #1617, GH #1603)
|
|
76
|
+
- **The printed verdict and the exit code share one threshold** — `Result:` was rendered against a hard-coded 80% while the exit code used `min_score`, whose default is `0.0`, so a run could print `FAIL (score 0.00% < 80.00%)` and exit 0. The reporter now uses the run's own `min_score`, and with no gate configured it prints the score instead of a verdict: `Result: 66.67% (no minimum score set)`. Exit-code semantics are unchanged (PR #1618, GH #1604)
|
|
77
|
+
- **The score line says how much of the run it covers** — `Score: 100.00% (10/10 verified of 17 mutations, 7 neutral)` whenever mutations were left out of the denominator. A run that measured everything still prints the plain pair (PR #1622, GH #1606)
|
|
78
|
+
- **Evilution owns its exit status and, in JSON mode, stdout** — `--preload` loads the project's spec helper into the parent process, and an at-exit hook it installs (SimpleCov calls `exit` with its own status, and prints its coverage report) replaced evilution's exit code and appended text after the JSON document. The executable now installs a guard that has the final word on the status, and JSON mode points stdout at stderr once the document is written (PR #1620, GH #1608)
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
- **Survivors are confirmed against the whole spec file** — per-mutation targeting selects examples by matching the enclosing method's name against example bodies, which keys on incidental identifier text: on evilution's own code a local variable named `row` selected two examples and hid six kills, reporting survivors the suite actually covered. A survivor is now re-run against the whole resolved file before it is reported. Only survivors pay for it, and only where the subset was narrower than the file (PR #1629, GH #1624)
|
|
83
|
+
- **Neutral counts no longer move with `--jobs`** — parallel workers contending on a shared database crash the test process, and those crashes are demoted to `:neutral` so they cannot inflate the kill count (GH #814). Since the contention exists only while the pool runs, the same workload scored 24 killed / 0 neutral at `-j 1` and 6 killed / 18 neutral at `-j 4`. Mutations neutralised that way are now re-run one at a time once the pool is done, and the run reports how many it had to redo. Such crashes are also never written to the `--incremental` cache, where a cached `:killed` would have hidden a real survivor on the next run (PR #1619, GH #1607)
|
|
84
|
+
- **`--format json` is parseable under `in_process` isolation** — RSpec redirects its own output only when the stream its configuration holds is the current `$stdout`; in-process isolation swaps `$stdout` for a null IO, and `--preload` builds the configuration before that swap, so RSpec kept writing to the real stdout ahead of the document. The run now claims RSpec's streams outright (PR #1631, GH #1627)
|
|
85
|
+
- **`method_body_replacement` no longer emits a bare `super` for a method that has no parent to call** — a `super` inside a *nested* def made the enclosing method eligible, producing a mutant that raises `NoMethodError` on contact: a kill that proves nothing (PR #1630, GH #1625)
|
|
86
|
+
- **Canary failures name the underlying error** — the proof-of-life check reported that it failed without saying why (PR #1592)
|
|
87
|
+
- **Advisory warnings bypass `Kernel#warn`** — a project whose `Warning` handler raises could turn an evilution advisory into a fatal error mid-run (PR #1590)
|
|
88
|
+
- **`ConcernStateCleaner` handles ActiveSupport deprecation proxies** — sending messages to a module that intercepts them left concern state uncleared (PR #1587)
|
|
89
|
+
- **Empty results from a child report exit status and signal** — a worker that died without producing a result said only "empty result from child" (PR #1585, GH #1580)
|
|
90
|
+
|
|
5
91
|
## [1.1.0] - 2026-08-23
|
|
6
92
|
|
|
7
93
|
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.
|