evilution 0.25.0 → 0.27.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 +15 -0
- data/.claude/prompts/architect.md +14 -1
- data/.claude/skills/create-issue/SKILL.md +55 -0
- data/.rubocop_todo.yml +7 -0
- data/CHANGELOG.md +38 -0
- data/README.md +57 -3
- data/lib/evilution/ast/constant_names.rb +34 -0
- data/lib/evilution/cache.rb +2 -0
- data/lib/evilution/child_output.rb +24 -0
- data/lib/evilution/cli/commands/run.rb +9 -0
- data/lib/evilution/cli/commands/version.rb +2 -0
- data/lib/evilution/cli/parser/options_builder.rb +16 -2
- data/lib/evilution/compare/invalid_input.rb +12 -0
- data/lib/evilution/compare.rb +1 -10
- data/lib/evilution/config/builders/spec_resolver.rb +15 -0
- data/lib/evilution/config/builders/spec_selector.rb +16 -0
- data/lib/evilution/config/builders.rb +4 -0
- data/lib/evilution/config/env_loader.rb +12 -0
- data/lib/evilution/config/file_loader.rb +22 -0
- data/lib/evilution/config/sources.rb +14 -0
- data/lib/evilution/config/validators/base.rb +37 -0
- data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
- data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
- data/lib/evilution/config/validators/fail_fast.rb +11 -0
- data/lib/evilution/config/validators/hooks.rb +12 -0
- data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
- data/lib/evilution/config/validators/integration.rb +11 -0
- data/lib/evilution/config/validators/isolation.rb +19 -0
- data/lib/evilution/config/validators/jobs.rb +9 -0
- data/lib/evilution/config/validators/preload.rb +13 -0
- data/lib/evilution/config/validators/spec_mappings.rb +56 -0
- data/lib/evilution/config/validators/spec_pattern.rb +12 -0
- data/lib/evilution/config/validators.rb +4 -0
- data/lib/evilution/config.rb +78 -268
- data/lib/evilution/feedback/detector.rb +15 -0
- data/lib/evilution/feedback/messages.rb +42 -0
- data/lib/evilution/feedback.rb +5 -0
- data/lib/evilution/integration/base.rb +4 -155
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +49 -0
- data/lib/evilution/integration/loading/constant_pinner.rb +24 -0
- data/lib/evilution/integration/loading/mutation_applier.rb +52 -0
- data/lib/evilution/integration/loading/redefinition_recovery.rb +54 -0
- data/lib/evilution/integration/loading/source_evaluator.rb +15 -0
- data/lib/evilution/integration/loading/syntax_validator.rb +19 -0
- data/lib/evilution/integration/loading.rb +6 -0
- data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
- data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
- data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
- data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
- data/lib/evilution/integration/rspec/result_builder.rb +40 -0
- data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
- data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +35 -0
- data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
- data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard.rb +40 -0
- data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
- data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
- data/lib/evilution/integration/rspec.rb +61 -232
- data/lib/evilution/isolation/fork.rb +7 -2
- data/lib/evilution/load_path/subpath_resolver.rb +25 -0
- data/lib/evilution/load_path.rb +4 -0
- data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
- data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
- data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
- data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
- data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
- data/lib/evilution/mcp/info_tool/actions.rb +16 -0
- data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
- data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
- data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
- data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
- data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
- data/lib/evilution/mcp/info_tool.rb +43 -261
- data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
- data/lib/evilution/mcp/mutate_tool.rb +5 -2
- data/lib/evilution/mutator/operator/block_removal.rb +1 -1
- data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
- data/lib/evilution/parallel/work_queue/channel/frame.rb +21 -0
- data/lib/evilution/parallel/work_queue/channel.rb +23 -0
- data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
- data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
- data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators.rb +6 -0
- data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
- data/lib/evilution/parallel/work_queue/worker.rb +114 -0
- data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
- data/lib/evilution/parallel/work_queue.rb +42 -327
- data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
- data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
- data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
- data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
- data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
- data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
- data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
- data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
- data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
- data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
- data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
- data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
- data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
- data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
- data/lib/evilution/reporter/cli/pct.rb +9 -0
- data/lib/evilution/reporter/cli/section.rb +13 -0
- data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
- data/lib/evilution/reporter/cli/trailer.rb +22 -0
- data/lib/evilution/reporter/cli.rb +79 -162
- data/lib/evilution/runner/isolation_resolver.rb +20 -2
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +32 -0
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +16 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +46 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +75 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +69 -0
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +48 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +39 -0
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +80 -0
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +34 -0
- data/lib/evilution/runner/mutation_executor.rb +58 -289
- data/lib/evilution/runner/subject_pipeline.rb +18 -8
- data/lib/evilution/runner.rb +21 -0
- data/lib/evilution/version.rb +1 -1
- metadata +125 -5
- data/lib/evilution/mcp/session_diff_tool.rb +0 -63
- data/lib/evilution/mcp/session_list_tool.rb +0 -50
- data/lib/evilution/mcp/session_show_tool.rb +0 -57
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12d485d3cce9569229a95a1e8f29403fbb11f8f067e0c438256919521f6d82dc
|
|
4
|
+
data.tar.gz: 11a1adeca7c61fa905757137d6a737c6e389eae584b37fca91a8c9eed57926d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce9208fa3f3ed3160d2844cdd5e1479cf7b2fa90985371cb95d6978177f371d38ec4bdccf473f79af318cd6c617fda2b1962efe4f389eab1912ab348cc48b475
|
|
7
|
+
data.tar.gz: 57ff4d971829430d5d47525ac0bc5e488b326e6916657aaf9acb3faf0e8795e909523ec8238b6b9fbaace13bdd60c0f437f30d94efbfa82861b42da802a54aac
|
data/.beads/interactions.jsonl
CHANGED
|
@@ -230,3 +230,18 @@
|
|
|
230
230
|
{"id":"int-94972aae","kind":"field_change","created_at":"2026-04-20T16:13:12.727090894Z","actor":"Denis Kiselev","issue_id":"EV-0fgx","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #813 (commit 262b9ae). Integration spec spec/evilution/compare/integration_spec.rb + e2e fixtures landed. 15/15 examples pass."}}
|
|
231
231
|
{"id":"int-4d639e4e","kind":"field_change","created_at":"2026-04-20T16:57:42.95949164Z","actor":"Denis Kiselev","issue_id":"EV-ynlo","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #816. SourceAstCache content-keyed LRU + ExampleFilter integration + BaselineRunner wiring."}}
|
|
232
232
|
{"id":"int-c15ed3e8","kind":"field_change","created_at":"2026-04-20T17:49:38.782150309Z","actor":"Denis Kiselev","issue_id":"EV-toid","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
233
|
+
{"id":"int-842a1d3a","kind":"field_change","created_at":"2026-04-21T05:01:55.401885343Z","actor":"Denis Kiselev","issue_id":"EV-h8pw","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
234
|
+
{"id":"int-c92539a9","kind":"field_change","created_at":"2026-04-21T05:04:50.605824487Z","actor":"Denis Kiselev","issue_id":"EV-vkq9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
235
|
+
{"id":"int-2abceed3","kind":"field_change","created_at":"2026-04-24T05:18:13.096916772Z","actor":"Denis Kiselev","issue_id":"EV-kjac","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
236
|
+
{"id":"int-03bc2f7f","kind":"field_change","created_at":"2026-04-24T05:40:10.83959826Z","actor":"Denis Kiselev","issue_id":"EV-hklf","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
237
|
+
{"id":"int-2781044c","kind":"field_change","created_at":"2026-04-24T05:40:11.975030418Z","actor":"Denis Kiselev","issue_id":"EV-cpku","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Folded into EV-hklf — error rename 'no method found' → 'no subject matched' shipped in PR #872."}}
|
|
238
|
+
{"id":"int-3fd6fda2","kind":"field_change","created_at":"2026-04-24T17:15:49.060178577Z","actor":"Denis Kiselev","issue_id":"EV-3ew5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"PR #884 merged to master"}}
|
|
239
|
+
{"id":"int-95f8d2f1","kind":"field_change","created_at":"2026-04-25T14:49:14.844074734Z","actor":"Denis Kiselev","issue_id":"EV-owgh","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #890"}}
|
|
240
|
+
{"id":"int-d786895b","kind":"field_change","created_at":"2026-04-25T15:46:53.977607499Z","actor":"Denis Kiselev","issue_id":"EV-a6de","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #891"}}
|
|
241
|
+
{"id":"int-a297634a","kind":"field_change","created_at":"2026-04-25T16:56:08.483381206Z","actor":"Denis Kiselev","issue_id":"EV-ilu3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #892"}}
|
|
242
|
+
{"id":"int-0647d42f","kind":"field_change","created_at":"2026-04-25T17:06:39.083006592Z","actor":"Denis Kiselev","issue_id":"EV-aa4x","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #893"}}
|
|
243
|
+
{"id":"int-86ea8eca","kind":"field_change","created_at":"2026-04-25T17:28:36.516431743Z","actor":"Denis Kiselev","issue_id":"EV-6x6g","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #894"}}
|
|
244
|
+
{"id":"int-811fef5f","kind":"field_change","created_at":"2026-04-25T17:39:31.325092593Z","actor":"Denis Kiselev","issue_id":"EV-6c51","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #895"}}
|
|
245
|
+
{"id":"int-9d182026","kind":"field_change","created_at":"2026-04-25T17:54:26.23542935Z","actor":"Denis Kiselev","issue_id":"EV-67yh","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #897"}}
|
|
246
|
+
{"id":"int-7ede49f5","kind":"field_change","created_at":"2026-04-25T18:16:18.358350457Z","actor":"Denis Kiselev","issue_id":"EV-zvhp","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #898"}}
|
|
247
|
+
{"id":"int-c28f7ee0","kind":"field_change","created_at":"2026-04-26T02:28:38.321564801Z","actor":"Denis Kiselev","issue_id":"EV-vev8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #899"}}
|
|
@@ -66,17 +66,30 @@ Evilution
|
|
|
66
66
|
::AST::SourceSurgeon # Text-level mutation at byte offsets
|
|
67
67
|
::Mutator::Base # Abstract operator base (Prism::Visitor)
|
|
68
68
|
::Mutator::Registry # Node type → operator mapping
|
|
69
|
-
::Mutator::Operator::* #
|
|
69
|
+
::Mutator::Operator::* # 72 concrete mutation operators
|
|
70
70
|
::Isolation::Fork # Fork + pipe per mutation
|
|
71
|
+
::Integration::Base # Template-method orchestrator; delegates mutation apply to Loading::MutationApplier
|
|
71
72
|
::Integration::RSpec # RSpec programmatic test runner
|
|
73
|
+
::Integration::Minitest # Minitest programmatic test runner
|
|
74
|
+
::Integration::Loading::MutationApplier # Composes the mutation-apply pipeline
|
|
75
|
+
::Integration::Loading::SyntaxValidator # Prism parse check
|
|
76
|
+
::Integration::Loading::SourceEvaluator # eval w/ TOPLEVEL_BINDING + absolute path
|
|
77
|
+
::Integration::Loading::ConstantPinner # const_get top-level constants to defeat Zeitwerk re-autoload
|
|
78
|
+
::Integration::Loading::RedefinitionRecovery # Strip constants + retry on "already defined"
|
|
79
|
+
::Integration::Loading::ConcernStateCleaner # Clear AS::Concern @_included_block / @_prepended_block
|
|
80
|
+
::AST::ConstantNames # Prism walk → fully-qualified class/module names
|
|
81
|
+
::LoadPath::SubpathResolver # Shortest $LOAD_PATH-relative path for a file
|
|
72
82
|
::Coverage::Collector # Ruby Coverage module wrapper
|
|
73
83
|
::Coverage::TestMap # Source line → test file mapping
|
|
84
|
+
::Compare::Categorizer # Fixed / new / persistent / flaky / reintroduced bucketing
|
|
85
|
+
::Compare::Normalizer # Canonicalize mutation records for cross-run compare
|
|
74
86
|
::Diff::Parser # Git diff output parser
|
|
75
87
|
::Diff::FileFilter # Filter subjects to changed code
|
|
76
88
|
::Result::MutationResult # Single mutation outcome
|
|
77
89
|
::Result::Summary # Aggregated results
|
|
78
90
|
::Reporter::JSON # Structured output for AI agents
|
|
79
91
|
::Reporter::CLI # Human-readable terminal output
|
|
92
|
+
::Reporter::HTML # Section-based HTML report
|
|
80
93
|
::Reporter::Suggestion # Actionable fix hint generator
|
|
81
94
|
```
|
|
82
95
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-issue
|
|
3
|
+
description: Create a beads issue and a matching GitHub issue, cross-linked via external-ref and description
|
|
4
|
+
argument-hint: "--title <title> --description <desc> --type <type> --priority <0-4>"
|
|
5
|
+
user-invocable: true
|
|
6
|
+
disable-model-invocation: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Create Cross-Linked Issue (Beads + GitHub)
|
|
10
|
+
|
|
11
|
+
Create a beads issue and a matching GitHub issue in Port-Royal/vanilla-mafia, with each linking to the other.
|
|
12
|
+
|
|
13
|
+
## Arguments
|
|
14
|
+
|
|
15
|
+
All arguments are passed as `$ARGUMENTS`. Parse them as bd-style flags:
|
|
16
|
+
|
|
17
|
+
- `--title` (required): Issue title
|
|
18
|
+
- `--description` (required): Why this issue exists and what needs to be done
|
|
19
|
+
- `--type` (required): `bug`, `feature`, or `task`
|
|
20
|
+
- `--priority` (required): 0-4 (0=critical, 4=backlog)
|
|
21
|
+
- `--parent` (optional): Parent beads issue ID for sub-tasks
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
```
|
|
25
|
+
/create-issue --title "Fix login redirect" --description "After login, users are redirected to /dashboard instead of the page they came from" --type bug --priority 1
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Steps
|
|
29
|
+
|
|
30
|
+
1. **Create the GitHub issue first** (to get the GH number):
|
|
31
|
+
```
|
|
32
|
+
gh issue create --repo Port-Royal/vanilla-mafia --title "<title>" --body "<description>"
|
|
33
|
+
```
|
|
34
|
+
Extract the issue number from the output.
|
|
35
|
+
|
|
36
|
+
2. **Create the beads issue** with `--external-ref gh-<number>`:
|
|
37
|
+
```
|
|
38
|
+
bd create --title "<title>" --description "<description>\n\nGH: #<gh-number>" --type <type> --priority <priority> --external-ref gh-<number>
|
|
39
|
+
```
|
|
40
|
+
Extract the beads issue ID from the output.
|
|
41
|
+
|
|
42
|
+
3. **Update the GitHub issue body** to include the beads ID:
|
|
43
|
+
```
|
|
44
|
+
gh api repos/Port-Royal/vanilla-mafia/issues/<number> --method PATCH -f body="<description>
|
|
45
|
+
|
|
46
|
+
Beads: <beads-id>"
|
|
47
|
+
```
|
|
48
|
+
Use `gh api` (not `gh issue edit`) to avoid the Projects Classic GraphQL error.
|
|
49
|
+
|
|
50
|
+
4. **If `--parent` was provided**, set the parent on the beads issue:
|
|
51
|
+
```
|
|
52
|
+
bd update <beads-id> --parent <parent-id>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
5. **Display the result**: show both IDs in format `<beads-id> (GH #<number>)`.
|
data/.rubocop_todo.yml
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.27.0] - 2026-04-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`prism` declared as a runtime dependency (`>= 1.5, < 2`)** — Rails 7.1 stacks pin `prism 0.19` (which lacks `IfNode#subsequent`), causing a `NoMethodError` on the first `if` evilution mutated. Bundler now refuses incompatible prism versions at install time instead of crashing at runtime (#876, PR #891)
|
|
8
|
+
- **`--[no-]incremental` CLI flag** — `--no-incremental` overrides `incremental: true` from the config file for one invocation (cold-cache debugging, CI escape hatch). Last flag wins when both forms are given (#878, PR #897)
|
|
9
|
+
- **`--quiet-children` and `--quiet-children-dir DIR` flags** — redirect each forked worker's stdout/stderr to per-pid files under `tmp/evilution_children/<pid>.{out,err}` (configurable). Keeps parent output clean when app initializers (Datadog, Bullet, etc.) emit warnings on every fork. Trade-off: live worker errors only appear in the side files (`tail -f tmp/evilution_children/*.err`) (#880, PR #899)
|
|
10
|
+
- **Preload autodetect chain extended** — `Runner::IsolationResolver` now probes `spec/rails_helper.rb` → `spec/spec_helper.rb` → `test/test_helper.rb` (was: rails_helper + test_helper only). Rails projects that consolidate everything into `spec/spec_helper.rb` no longer need an explicit `preload:` setting. When the chain finds nothing under a Rails project, raises a `ConfigError` listing every path tried and pointing at `--preload` / `--no-preload` (#879, PR #898)
|
|
11
|
+
- **`evilution version` prints the bundled `mcp` gem version** — second line shows `mcp gem X.Y.Z (server compatibility)`. Run inside the same bundle the MCP server uses to confirm what's loaded after a `bundle update` (#883, PR #894)
|
|
12
|
+
- **Public feedback channel exposed across CLI and MCP surfaces** — when a run hits friction (`errored`, `unparseable`, or `unresolved` buckets > 0; baseline failure; MCP error response), evilution surfaces a single GitHub Discussions URL so agents can suggest filing feedback. CLI text reports gain a one-line footer; the CLI error-exit path emits the same line on stderr (both suppressed by `--quiet`, `--format=json`, `--format=html`). MCP `evilution-mutate` responses embed `feedback_url` + `feedback_hint` on friction (and always on error payloads); the `minimal` verbosity contract is preserved (no extra keys). New MCP `evilution-info action=feedback` returns `{ discussion_url, version, guidance_for_agent }` on demand for any feedback intent including missing-capability requests on clean runs. Tool descriptions and the README MCP "Feedback channel" subsection prominently document the **explicit user-consent gate** (agents must never post on the user's behalf without explicit approval) and **privacy expectations** (never include secrets, env vars, project name, file paths, source code, or class/method names from user code — the channel is public). By construction, no run-derived data is embedded in any feedback field — agent + user compose any actual payload themselves (#900, PR #901)
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`Cache#store` crashed with `TypeError: no implicit conversion of nil into String` when `incremental: true` and `jobs >= 2`** — `Strategy::Parallel#run_batch` called `batch.each(&:strip_sources!)` before `@cache.store`, leaving `mutation.original_source = nil` for `Digest::SHA256.hexdigest`. Reordered so store runs before strip; added a defensive nil-source guard in `Cache#store` mirroring the existing one in `Cache#fetch` (#875, PR #890)
|
|
17
|
+
- **`block_removal` produced unparseable mutations on block-pass arguments (`map!(&:sym)`, `index_by(&:id)`, `flat_map(&block)`)** — operator stripped the `BlockArgumentNode` from inside the call's parens, leaving a dangling open paren. Operator now skips emission when `node.block.is_a?(Prism::BlockArgumentNode)`; explicit `{}` / `do..end` blocks are unaffected (#881, PR #895)
|
|
18
|
+
- **`method_body_replacement` errored at runtime when generating the `super`-replacement on methods whose enclosing class had no parent implementation** — `super`-replacement now only emitted when the original body already calls `super` (`SuperNode` or `ForwardingSuperNode`), using that as a heuristic that a super target exists in this context. Methods without `super` get only the `nil` and `self` replacements (#877, PR #892)
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
|
|
22
|
+
- **README "Installing on Rails 7.1 + Ruby 3.3" section** — covers the `cgi 0.5.0` (Rails-pinned) vs `cgi 0.5.1` (Ruby 3.3 default-gem) Bundler activation conflict, the sidecar `Gemfile.local` workaround (`eval_gemfile("Gemfile")` + add evilution + prism), the `BUNDLE_GEMFILE=Gemfile.local` invocation, and guidance on whether to commit or `.gitignore` the resulting `Gemfile.local.lock` (#882, PR #893)
|
|
23
|
+
- **README MCP "After upgrading the gem: restart the MCP server" subsection** — explains that the MCP server is a long-lived stdio process the agent host spawns; `bundle update evilution` swaps the gem on disk but the running process keeps the old code in memory until restart. Symptom is opaque "Internal error" responses to flags the old build doesn't recognize (#883, PR #894)
|
|
24
|
+
|
|
25
|
+
## [0.26.0] - 2026-04-24
|
|
26
|
+
|
|
27
|
+
### Removed
|
|
28
|
+
|
|
29
|
+
- **Deprecated MCP session tools removed** — `evilution-session-list`, `evilution-session-show`, `evilution-session-diff` shims (deprecated since #637 after consolidation into `evilution-session`) deleted (#686, PR #851)
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`preload` silently ignored under `:in_process` isolation** — `Runner::IsolationResolver#perform_preload` gated the preload on `resolve_isolation == :fork`, so non-Rails projects (auto-resolving to `:in_process`) silently skipped `preload:` from `.evilution.yml` or `--preload`. Preload now runs for `:in_process` too (#868, PR #871)
|
|
34
|
+
- **`--target ClassName` silently narrowed to git-changed files** — when only a class/method target was given with no file scope, `Runner::SubjectPipeline#target_files` fell back to `Git::ChangedFiles`, producing a misleading `no method found matching 'X'` error when the class file was not in the working-tree diff. Target files now resolve from the configured source when a method/class target is given without explicit file scope (#869, PR #872)
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **Internal `Evilution::Compare` refactor** — `lib/evilution/compare.rb` split into one class per file under `lib/evilution/compare/` (`Categorizer`, `Detector`, `Fingerprint`, `InvalidInput`, `Normalizer`, `Record`); `rubocop:disable Style/OneClassPerFile` removed (#825, PR #852)
|
|
39
|
+
- **Internal `Evilution::Integration::Base` refactor** — decomposed into focused collaborators under `Evilution::Integration::Loading::*` (`SyntaxValidator`, `SourceEvaluator`, `ConstantPinner`, `RedefinitionRecovery`, `ConcernStateCleaner`, `MutationApplier`) plus shared helpers `Evilution::AST::ConstantNames` and `Evilution::LoadPath::SubpathResolver`. `Integration::Base` now delegates mutation application to an injectable `MutationApplier`; no user-visible behavior change (#845, PR #873)
|
|
40
|
+
|
|
3
41
|
## [0.25.0] - 2026-04-21
|
|
4
42
|
|
|
5
43
|
### Added
|
data/README.md
CHANGED
|
@@ -21,6 +21,44 @@ Then: `bundle install`
|
|
|
21
21
|
|
|
22
22
|
Or standalone: `gem install evilution`
|
|
23
23
|
|
|
24
|
+
Requires `prism >= 1.5, < 2`. Older Rails apps (e.g. Rails 7.1 pins `prism 0.19`) must upgrade prism — the gemspec constraint forces bundler to resolve a compatible 1.x version. If your app pins `prism 2.x`, bundler will reject the install until evilution widens its upper bound.
|
|
25
|
+
|
|
26
|
+
### Installing on Rails 7.1 + Ruby 3.3
|
|
27
|
+
|
|
28
|
+
Two Bundler conflicts hit fresh installs on this stack:
|
|
29
|
+
|
|
30
|
+
1. **`cgi` activation conflict.** Rails 7.1's `Gemfile.lock` pins `cgi 0.5.0`. Ruby 3.3.x ships `cgi 0.5.1` as a default gem. Loading evilution via Bundler aborts with:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Gem::LoadError: can't activate cgi-0.5.1, already activated cgi-0.5.0.
|
|
34
|
+
Make sure all dependencies are added to Gemfile.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
2. **`prism` pin.** Same lockfile pins `prism 0.19`, which lacks the `IfNode#subsequent` accessor evilution uses (older Prism releases exposed it as `consequent`). Symptom: `NoMethodError: undefined method 'subsequent' for an instance of Prism::IfNode`.
|
|
38
|
+
|
|
39
|
+
Both resolve cleanly with a sidecar `Gemfile.local` that re-evaluates the project Gemfile and adds evilution + prism on top — no edits to the main `Gemfile.lock`:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# Gemfile.local
|
|
43
|
+
eval_gemfile("Gemfile")
|
|
44
|
+
|
|
45
|
+
group :test, :development do
|
|
46
|
+
gem "evilution"
|
|
47
|
+
gem "prism", "~> 1.5"
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then invoke evilution against that Gemfile:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
BUNDLE_GEMFILE=Gemfile.local bundle install
|
|
55
|
+
BUNDLE_GEMFILE=Gemfile.local bundle exec evilution run lib/foo.rb
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The first command writes a sibling `Gemfile.local.lock`. Decide whether to commit or `.gitignore` it the same way you would for any developer-only Gemfile — typically gitignored when only one or two engineers run mutation testing locally, committed when CI also runs evilution against the sidecar Gemfile.
|
|
59
|
+
|
|
60
|
+
The evilution gemspec already declares `prism >= 1.5, < 2`, so adding the `gem "prism"` line above is only necessary on stacks that also pin prism in `Gemfile.lock`.
|
|
61
|
+
|
|
24
62
|
## Command Reference
|
|
25
63
|
|
|
26
64
|
```
|
|
@@ -67,11 +105,13 @@ The shorter alias `evil` ships alongside `evilution` and accepts identical argum
|
|
|
67
105
|
| `-q`, `--quiet` | Boolean | false | Suppress output. |
|
|
68
106
|
| `--stdin` | Boolean | false | Read target file paths from stdin (one per line). |
|
|
69
107
|
| `--integration NAME` | String | `rspec` | Test framework integration: `rspec` or `minitest`. |
|
|
70
|
-
| `--incremental`
|
|
108
|
+
| `--[no-]incremental` | Boolean | false | Cache killed/timeout results; skip unchanged mutations on re-runs. Pass `--no-incremental` to override `incremental: true` from the config file for one invocation (e.g. cold-cache debugging). Last flag wins when both are given. |
|
|
71
109
|
| `--save-session` | Boolean | false | Persist results as timestamped JSON under `.evilution/results/`. |
|
|
72
110
|
| `--no-progress` | Boolean | _(enabled)_ | Disable the TTY progress bar. |
|
|
111
|
+
| `--quiet-children` | Boolean | false | Redirect each forked worker's stdout/stderr to per-pid files under `tmp/evilution_children/<pid>.{out,err}` so noisy app initializers (Datadog, Bullet, etc.) don't merge with parent output. Trade-off: live worker errors only appear in the side files, not the terminal — `tail -f tmp/evilution_children/*.err` to watch them. |
|
|
112
|
+
| `--quiet-children-dir DIR` | String | `tmp/evilution_children` | Override the directory used by `--quiet-children`. |
|
|
73
113
|
| `--isolation MODE` | String | `auto` | Isolation strategy: `auto`, `fork`, or `in_process`. `auto` selects `fork` for Rails projects. See [docs/isolation.md](docs/isolation.md). |
|
|
74
|
-
| `--preload FILE` | String | _(auto)_ | File to require in parent before forking workers
|
|
114
|
+
| `--preload FILE` | String | _(auto)_ | File to require in parent before forking workers. Auto-detect chain for Rails projects: `spec/rails_helper.rb` → `spec/spec_helper.rb` → `test/test_helper.rb`. Errors with the full chain listed if none exist; pass `--no-preload` to opt out. |
|
|
75
115
|
| `--no-preload` | Boolean | _(enabled)_ | Disable parent-process preload. |
|
|
76
116
|
| `--skip-heredoc-literals` | Boolean | false | Skip all string literal mutations inside heredocs. |
|
|
77
117
|
| `--show-disabled` | Boolean | false | Report mutations skipped by `# evilution:disable` comments. |
|
|
@@ -323,7 +363,7 @@ The server exposes the following tools:
|
|
|
323
363
|
|---|---|
|
|
324
364
|
| `evilution-mutate` | Run mutation testing on target files with structured JSON results |
|
|
325
365
|
| `evilution-session` | Inspect mutation testing history — `action: list` browses saved sessions, `action: show` displays one, `action: diff` compares two (fixed/new/persistent survivors, score delta) |
|
|
326
|
-
| `evilution-info` | Discovery before mutation — `action: subjects` lists mutatable methods with mutation counts, `action: tests` resolves which specs cover given sources, `action: environment` dumps the effective config |
|
|
366
|
+
| `evilution-info` | Discovery before mutation — `action: subjects` lists mutatable methods with mutation counts, `action: tests` resolves which specs cover given sources, `action: environment` dumps the effective config, `action: statuses` returns the mutation-result status glossary, `action: feedback` returns the public Discussions URL plus consent + privacy guidance for posting feedback |
|
|
327
367
|
|
|
328
368
|
### Verbosity Control
|
|
329
369
|
|
|
@@ -375,6 +415,20 @@ Pass `skip_config: true` to ignore the project config file. This skips loading `
|
|
|
375
415
|
|
|
376
416
|
> **Note**: `.mcp.json` is gitignored by default since it is a local editor/agent configuration file.
|
|
377
417
|
|
|
418
|
+
### After upgrading the gem: restart the MCP server
|
|
419
|
+
|
|
420
|
+
The MCP server is a long-lived stdio process spawned by the agent host. `bundle update evilution` swaps the gem on disk but the running process keeps the old code in memory — symptom is opaque "Internal error" responses to flags or shapes the old build doesn't recognize. Restart the server (reload the workspace in Claude Code / Copilot / etc.) so the new gem loads.
|
|
421
|
+
|
|
422
|
+
`evilution version` prints the gem version and the bundled `mcp` gem version on separate lines — run it in the same bundle the MCP server uses to confirm what's loaded.
|
|
423
|
+
|
|
424
|
+
### Feedback channel
|
|
425
|
+
|
|
426
|
+
When evilution causes friction (errors, usage problems, missing capabilities you wish were there), the MCP responses include a `feedback_url` plus `feedback_hint`. The `evilution-info` tool also exposes `action=feedback`, which returns the channel URL and posting guidance on demand.
|
|
427
|
+
|
|
428
|
+
**Agents must never post on the user's behalf without explicit user permission.** Show the user exactly what you would post, get explicit approval, then post. Never include secrets, environment variables, the project name, file paths, source code, or class/method names from user code — the feedback channel is public.
|
|
429
|
+
|
|
430
|
+
Discussion URL: <https://github.com/marinazzio/evilution/discussions>
|
|
431
|
+
|
|
378
432
|
## Recommended Workflows for AI Agents
|
|
379
433
|
|
|
380
434
|
### 1. Full project scan
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../ast"
|
|
5
|
+
|
|
6
|
+
# Walks a Prism AST and returns every class/module constant declared, nested
|
|
7
|
+
# names rendered fully-qualified (e.g. "Foo::Bar"). Order is source order:
|
|
8
|
+
# outer declarations precede their nested children.
|
|
9
|
+
class Evilution::AST::ConstantNames
|
|
10
|
+
def call(source)
|
|
11
|
+
result = Prism.parse(source)
|
|
12
|
+
return [] if result.failure?
|
|
13
|
+
|
|
14
|
+
collect(result.value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def collect(node, nesting = [])
|
|
20
|
+
names = []
|
|
21
|
+
case node
|
|
22
|
+
when Prism::ModuleNode, Prism::ClassNode
|
|
23
|
+
const = node.constant_path.full_name
|
|
24
|
+
qualified = nesting.any? && !const.include?("::") ? "#{nesting.join("::")}::#{const}" : const
|
|
25
|
+
names << qualified
|
|
26
|
+
names.concat(collect(node.body, nesting + [const])) if node.body
|
|
27
|
+
when Prism::ProgramNode
|
|
28
|
+
names.concat(collect(node.statements, nesting)) if node.statements
|
|
29
|
+
when Prism::StatementsNode
|
|
30
|
+
node.body.each { |child| names.concat(collect(child, nesting)) }
|
|
31
|
+
end
|
|
32
|
+
names
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/evilution/cache.rb
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../evilution"
|
|
4
|
+
|
|
5
|
+
module Evilution::ChildOutput
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :log_dir
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Per-run truncation happens once in the parent (Runner#configure_child_output);
|
|
13
|
+
# within a run, multiple forks reusing the same PID (pool worker recycle, per-mutation
|
|
14
|
+
# forks) append so cross-fork output isn't lost.
|
|
15
|
+
def redirect!
|
|
16
|
+
return unless log_dir
|
|
17
|
+
|
|
18
|
+
pid = Process.pid
|
|
19
|
+
$stdout.reopen(File.join(log_dir, "#{pid}.out"), "a")
|
|
20
|
+
$stderr.reopen(File.join(log_dir, "#{pid}.err"), "a")
|
|
21
|
+
$stdout.sync = true
|
|
22
|
+
$stderr.sync = true
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -10,6 +10,7 @@ require_relative "../../runner"
|
|
|
10
10
|
require_relative "../../hooks"
|
|
11
11
|
require_relative "../../hooks/registry"
|
|
12
12
|
require_relative "../../hooks/loader"
|
|
13
|
+
require_relative "../../feedback/messages"
|
|
13
14
|
|
|
14
15
|
class Evilution::CLI::Commands::Run < Evilution::CLI::Command
|
|
15
16
|
def call
|
|
@@ -34,10 +35,18 @@ class Evilution::CLI::Commands::Run < Evilution::CLI::Command
|
|
|
34
35
|
@stdout.puts(JSON.generate(error_payload(error)))
|
|
35
36
|
Evilution::CLI::Result.new(exit_code: 2, error: error, error_rendered: true)
|
|
36
37
|
else
|
|
38
|
+
@stderr.puts(Evilution::Feedback::Messages.cli_footer) unless quiet?(config, file_options)
|
|
37
39
|
Evilution::CLI::Result.new(exit_code: 2, error: error)
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
|
|
43
|
+
def quiet?(config, file_options)
|
|
44
|
+
return config.quiet unless config.nil?
|
|
45
|
+
return true if @options[:quiet]
|
|
46
|
+
|
|
47
|
+
file_options && file_options[:quiet]
|
|
48
|
+
end
|
|
49
|
+
|
|
41
50
|
def build_hooks(config)
|
|
42
51
|
return nil if config.hooks.empty?
|
|
43
52
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "mcp"
|
|
3
4
|
require_relative "../commands"
|
|
4
5
|
require_relative "../command"
|
|
5
6
|
require_relative "../dispatcher"
|
|
@@ -10,6 +11,7 @@ class Evilution::CLI::Commands::Version < Evilution::CLI::Command
|
|
|
10
11
|
|
|
11
12
|
def perform
|
|
12
13
|
@stdout.puts(Evilution::VERSION)
|
|
14
|
+
@stdout.puts("mcp gem #{::MCP::VERSION} (server compatibility)")
|
|
13
15
|
0
|
|
14
16
|
end
|
|
15
17
|
end
|
|
@@ -70,15 +70,29 @@ class Evilution::CLI::Parser::OptionsBuilder
|
|
|
70
70
|
opts.on("--fail-fast", "Stop after N surviving mutants " \
|
|
71
71
|
"(default: disabled; if provided without N, uses 1; use --fail-fast=N)") { @options[:fail_fast] ||= 1 }
|
|
72
72
|
opts.on("--no-baseline", "Skip baseline test suite check") { @options[:baseline] = false }
|
|
73
|
-
opts.on("--incremental",
|
|
73
|
+
opts.on("--[no-]incremental",
|
|
74
|
+
"Cache killed/timeout results; skip re-running them on unchanged files. " \
|
|
75
|
+
"Use --no-incremental to override `incremental: true` from the config file for one run.") do |v|
|
|
76
|
+
@options[:incremental] = v
|
|
77
|
+
end
|
|
74
78
|
opts.on("--integration NAME", "Test integration: rspec, minitest (default: rspec)") { |i| @options[:integration] = i }
|
|
75
79
|
opts.on("--isolation STRATEGY", "Isolation: auto, fork, in_process (default: auto)") { |s| @options[:isolation] = s }
|
|
76
80
|
opts.on("--preload FILE", "Preload FILE in the parent process before forking " \
|
|
77
|
-
"(default: auto-detect spec/rails_helper.rb
|
|
81
|
+
"(default: auto-detect spec/rails_helper.rb -> spec/spec_helper.rb -> " \
|
|
82
|
+
"test/test_helper.rb for Rails projects)") { |f| @options[:preload] = f }
|
|
78
83
|
opts.on("--no-preload", "Disable parent-process preload even for Rails projects") { @options[:preload] = false }
|
|
79
84
|
opts.on("--stdin", "Read target file paths from stdin (one per line)") { @options[:stdin] = true }
|
|
80
85
|
opts.on("--suggest-tests", "Generate concrete test code in suggestions (RSpec or Minitest)") { @options[:suggest_tests] = true }
|
|
81
86
|
opts.on("--no-progress", "Disable progress bar") { @options[:progress] = false }
|
|
87
|
+
opts.on("--quiet-children",
|
|
88
|
+
"Redirect each child process's stdout/stderr to per-pid files under " \
|
|
89
|
+
"tmp/evilution_children (or --quiet-children-dir DIR), keeping parent output clean.") do
|
|
90
|
+
@options[:quiet_children] = true
|
|
91
|
+
end
|
|
92
|
+
opts.on("--quiet-children-dir DIR",
|
|
93
|
+
"Directory for --quiet-children per-pid log files (default: tmp/evilution_children)") do |d|
|
|
94
|
+
@options[:quiet_children_dir] = d
|
|
95
|
+
end
|
|
82
96
|
end
|
|
83
97
|
|
|
84
98
|
def add_extra_flag_options(opts)
|
data/lib/evilution/compare.rb
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# rubocop:disable Style/OneClassPerFile
|
|
4
3
|
module Evilution::Compare
|
|
5
4
|
end
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
attr_reader :index
|
|
9
|
-
|
|
10
|
-
def initialize(message, index: nil)
|
|
11
|
-
super(message)
|
|
12
|
-
@index = index
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
# rubocop:enable Style/OneClassPerFile
|
|
6
|
+
require_relative "compare/invalid_input"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../builders"
|
|
4
|
+
require_relative "../../spec_resolver"
|
|
5
|
+
|
|
6
|
+
class Evilution::Config::Builders::SpecResolver
|
|
7
|
+
def self.call(integration:)
|
|
8
|
+
case integration
|
|
9
|
+
when :minitest
|
|
10
|
+
Evilution::SpecResolver.new(test_dir: "test", test_suffix: "_test.rb", request_dir: "integration")
|
|
11
|
+
else
|
|
12
|
+
Evilution::SpecResolver.new
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../builders"
|
|
4
|
+
require_relative "spec_resolver"
|
|
5
|
+
require_relative "../../spec_selector"
|
|
6
|
+
|
|
7
|
+
class Evilution::Config::Builders::SpecSelector
|
|
8
|
+
def self.call(spec_files:, spec_mappings:, spec_pattern:, integration:)
|
|
9
|
+
Evilution::SpecSelector.new(
|
|
10
|
+
spec_files: spec_files,
|
|
11
|
+
spec_mappings: spec_mappings,
|
|
12
|
+
spec_pattern: spec_pattern,
|
|
13
|
+
spec_resolver: Evilution::Config::Builders::SpecResolver.call(integration: integration)
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Evilution::Config::EnvLoader
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def load
|
|
7
|
+
opts = {}
|
|
8
|
+
val = ENV.fetch("EV_DISABLE_EXAMPLE_TARGETING", nil)
|
|
9
|
+
opts[:example_targeting] = false if val && !val.empty? && val != "0"
|
|
10
|
+
opts
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Evilution::Config::FileLoader
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def load
|
|
9
|
+
Evilution::Config::CONFIG_FILES.each do |path|
|
|
10
|
+
next unless File.exist?(path)
|
|
11
|
+
|
|
12
|
+
data = YAML.safe_load_file(path, symbolize_names: true)
|
|
13
|
+
return data.is_a?(Hash) ? data : {}
|
|
14
|
+
rescue Psych::SyntaxError, Psych::DisallowedClass => e
|
|
15
|
+
raise Evilution::ConfigError.new("failed to parse config file #{path}: #{e.message}", file: path)
|
|
16
|
+
rescue SystemCallError => e
|
|
17
|
+
raise Evilution::ConfigError.new("cannot read config file #{path}: #{e.message}", file: path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
{}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "file_loader"
|
|
4
|
+
require_relative "env_loader"
|
|
5
|
+
|
|
6
|
+
module Evilution::Config::Sources
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def merge(explicit:, skip_file:)
|
|
10
|
+
file = skip_file ? {} : Evilution::Config::FileLoader.load
|
|
11
|
+
env = Evilution::Config::EnvLoader.load
|
|
12
|
+
Evilution::Config::DEFAULTS.merge(file).merge(env).merge(explicit)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validators"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::Base
|
|
6
|
+
def self.call(_value)
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def coerce_symbol!(value, allowed:, name:)
|
|
14
|
+
raise Evilution::ConfigError, "#{name} must be #{allowed.join(" or ")}, got nil" if value.nil?
|
|
15
|
+
|
|
16
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
|
17
|
+
raise Evilution::ConfigError, "#{name} must be #{allowed.join(" or ")}, got #{value.inspect}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sym = value.to_sym
|
|
21
|
+
return sym if allowed.include?(sym)
|
|
22
|
+
|
|
23
|
+
raise Evilution::ConfigError, "#{name} must be #{allowed.join(" or ")}, got #{sym.inspect}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def coerce_positive_int!(value, name:)
|
|
27
|
+
raise Evilution::ConfigError, "#{name} must be a positive integer, got #{value.inspect}" if value.is_a?(Float)
|
|
28
|
+
|
|
29
|
+
int = Integer(value)
|
|
30
|
+
raise Evilution::ConfigError, "#{name} must be a positive integer, got #{int}" unless int >= 1
|
|
31
|
+
|
|
32
|
+
int
|
|
33
|
+
rescue ::ArgumentError, ::TypeError
|
|
34
|
+
raise Evilution::ConfigError, "#{name} must be a positive integer, got #{value.inspect}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::ExampleTargetingCache < Evilution::Config::Validators::Base
|
|
6
|
+
def self.call(value)
|
|
7
|
+
raise Evilution::ConfigError, "example_targeting_cache must be a Hash, got #{value.class}" unless value.is_a?(Hash)
|
|
8
|
+
|
|
9
|
+
normalized = normalize_keys(value)
|
|
10
|
+
merged = Evilution::Config::DEFAULTS[:example_targeting_cache].merge(normalized)
|
|
11
|
+
require_positive_int!(merged, :max_files)
|
|
12
|
+
require_positive_int!(merged, :max_blocks)
|
|
13
|
+
merged
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def normalize_keys(value)
|
|
20
|
+
value.each_with_object({}) do |(k, v), acc|
|
|
21
|
+
unless k.is_a?(String) || k.is_a?(Symbol)
|
|
22
|
+
raise Evilution::ConfigError,
|
|
23
|
+
"example_targeting_cache keys must be Strings or Symbols, got #{k.inspect}"
|
|
24
|
+
end
|
|
25
|
+
acc[k.to_sym] = v
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def require_positive_int!(cache, key)
|
|
30
|
+
v = cache[key]
|
|
31
|
+
return if v.is_a?(Integer) && v >= 1
|
|
32
|
+
|
|
33
|
+
raise Evilution::ConfigError,
|
|
34
|
+
"example_targeting_cache.#{key} must be a positive integer, got #{v.inspect}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|