evilution 0.25.0 → 0.26.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 +5 -0
- data/.claude/prompts/architect.md +14 -1
- data/.claude/skills/create-issue/SKILL.md +55 -0
- data/CHANGELOG.md +16 -0
- data/lib/evilution/ast/constant_names.rb +34 -0
- data/lib/evilution/compare/invalid_input.rb +12 -0
- data/lib/evilution/compare.rb +1 -10
- 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/load_path/subpath_resolver.rb +25 -0
- data/lib/evilution/load_path.rb +4 -0
- data/lib/evilution/runner/isolation_resolver.rb +12 -1
- data/lib/evilution/runner/subject_pipeline.rb +18 -8
- data/lib/evilution/version.rb +1 -1
- metadata +14 -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: 3b561b61697995a22463ea15828357375b1ce9868ad6ee87f3f331336d26b890
|
|
4
|
+
data.tar.gz: 4a0cb24bfe2e9e9478ab5d752f05ea4ea69582e324e736241f1fe6b1e5e581cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 416aaea92d50fc3a969c350fd36d47e5b8b625ba4b1c64b66f208731cfaf1250714b8bc8e494bbc1586922357886dd96026f31e9f96b7e2cd849e43f402de3e3
|
|
7
|
+
data.tar.gz: e81fc9c5edfa9e25b4a7077f2776c2256bf5211bc5d88b53cc604ef735d64366ee3b3a294a480f49d3107c7634ff98459f6f46e0a8ede3248cec2b72007f940e
|
data/.beads/interactions.jsonl
CHANGED
|
@@ -230,3 +230,8 @@
|
|
|
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."}}
|
|
@@ -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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.26.0] - 2026-04-24
|
|
4
|
+
|
|
5
|
+
### Removed
|
|
6
|
+
|
|
7
|
+
- **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)
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`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)
|
|
12
|
+
- **`--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)
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **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)
|
|
17
|
+
- **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)
|
|
18
|
+
|
|
3
19
|
## [0.25.0] - 2026-04-21
|
|
4
20
|
|
|
5
21
|
### Added
|
|
@@ -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/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"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "prism"
|
|
4
3
|
require_relative "../integration"
|
|
4
|
+
require_relative "loading/mutation_applier"
|
|
5
5
|
|
|
6
6
|
class Evilution::Integration::Base
|
|
7
7
|
def self.baseline_runner
|
|
@@ -12,14 +12,15 @@ class Evilution::Integration::Base
|
|
|
12
12
|
raise NotImplementedError, "#{name}.baseline_options must be implemented"
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def initialize(hooks: nil)
|
|
15
|
+
def initialize(hooks: nil, mutation_applier: Evilution::Integration::Loading::MutationApplier.new)
|
|
16
16
|
@hooks = hooks
|
|
17
|
+
@mutation_applier = mutation_applier
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def call(mutation)
|
|
20
21
|
ensure_framework_loaded
|
|
21
22
|
fire_hook(:mutation_insert_pre, mutation: mutation, file_path: mutation.file_path)
|
|
22
|
-
load_error =
|
|
23
|
+
load_error = @mutation_applier.call(mutation)
|
|
23
24
|
return load_error if load_error
|
|
24
25
|
|
|
25
26
|
fire_hook(:mutation_insert_post, mutation: mutation, file_path: mutation.file_path)
|
|
@@ -47,156 +48,4 @@ class Evilution::Integration::Base
|
|
|
47
48
|
def fire_hook(event, **payload)
|
|
48
49
|
@hooks.fire(event, **payload) if @hooks
|
|
49
50
|
end
|
|
50
|
-
|
|
51
|
-
def apply_mutation(mutation)
|
|
52
|
-
prism_error = validate_mutated_syntax(mutation.mutated_source)
|
|
53
|
-
return prism_error if prism_error
|
|
54
|
-
|
|
55
|
-
pin_autoloaded_constants(mutation.original_source)
|
|
56
|
-
clear_concern_state(mutation.file_path)
|
|
57
|
-
with_redefinition_recovery(mutation.original_source) do
|
|
58
|
-
eval_mutated_source(mutation)
|
|
59
|
-
end
|
|
60
|
-
nil
|
|
61
|
-
rescue SyntaxError => e
|
|
62
|
-
{
|
|
63
|
-
passed: false,
|
|
64
|
-
error: "syntax error in mutated source: #{e.message}",
|
|
65
|
-
error_class: e.class.name,
|
|
66
|
-
error_backtrace: Array(e.backtrace).first(5)
|
|
67
|
-
}
|
|
68
|
-
rescue ScriptError, StandardError => e
|
|
69
|
-
{
|
|
70
|
-
passed: false,
|
|
71
|
-
error: "#{e.class}: #{e.message}",
|
|
72
|
-
error_class: e.class.name,
|
|
73
|
-
error_backtrace: Array(e.backtrace).first(5)
|
|
74
|
-
}
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def validate_mutated_syntax(source)
|
|
78
|
-
return nil if Prism.parse(source).success?
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
passed: false,
|
|
82
|
-
error: "mutated source has syntax errors",
|
|
83
|
-
error_class: "SyntaxError",
|
|
84
|
-
error_backtrace: []
|
|
85
|
-
}
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Evaluate the mutated source with __FILE__ set to the original path so
|
|
89
|
-
# that `require_relative` and `__dir__` resolve against the real source
|
|
90
|
-
# tree, where sibling files actually exist.
|
|
91
|
-
def eval_mutated_source(mutation)
|
|
92
|
-
absolute = File.expand_path(mutation.file_path)
|
|
93
|
-
# rubocop:disable Security/Eval
|
|
94
|
-
eval(mutation.mutated_source, TOPLEVEL_BINDING, absolute, 1)
|
|
95
|
-
# rubocop:enable Security/Eval
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def with_redefinition_recovery(original_source)
|
|
99
|
-
yield
|
|
100
|
-
rescue ArgumentError => e
|
|
101
|
-
raise unless redefinition_conflict?(e)
|
|
102
|
-
|
|
103
|
-
remove_defined_constants(original_source)
|
|
104
|
-
yield
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def redefinition_conflict?(error)
|
|
108
|
-
error.message.include?("already defined")
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def pin_autoloaded_constants(source)
|
|
112
|
-
collect_constant_names(Prism.parse(source).value).each do |name|
|
|
113
|
-
Object.const_get(name) if Object.const_defined?(name, false)
|
|
114
|
-
rescue NameError # :nodoc:
|
|
115
|
-
nil
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def collect_constant_names(node, nesting = [])
|
|
120
|
-
names = []
|
|
121
|
-
case node
|
|
122
|
-
when Prism::ModuleNode, Prism::ClassNode
|
|
123
|
-
const = node.constant_path.full_name
|
|
124
|
-
qualified = nesting.any? && !const.include?("::") ? "#{nesting.join("::")}::#{const}" : const
|
|
125
|
-
names << qualified
|
|
126
|
-
names.concat(collect_constant_names(node.body, nesting + [const])) if node.body
|
|
127
|
-
when Prism::ProgramNode
|
|
128
|
-
names.concat(collect_constant_names(node.statements, nesting)) if node.statements
|
|
129
|
-
when Prism::StatementsNode
|
|
130
|
-
node.body.each { |child| names.concat(collect_constant_names(child, nesting)) }
|
|
131
|
-
end
|
|
132
|
-
names
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def remove_defined_constants(source)
|
|
136
|
-
collect_constant_names(Prism.parse(source).value).reverse_each do |name|
|
|
137
|
-
parent_name, _, local_name = name.rpartition("::")
|
|
138
|
-
parent = resolve_loaded_constant_parent(parent_name)
|
|
139
|
-
next unless parent
|
|
140
|
-
next unless parent.const_defined?(local_name, false)
|
|
141
|
-
next if parent.autoload?(local_name)
|
|
142
|
-
|
|
143
|
-
parent.send(:remove_const, local_name.to_sym)
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def resolve_loaded_constant_parent(parent_name)
|
|
148
|
-
return Object if parent_name.empty?
|
|
149
|
-
|
|
150
|
-
parent_name.split("::").reduce(Object) do |mod, part|
|
|
151
|
-
return nil unless mod.const_defined?(part, false)
|
|
152
|
-
return nil if mod.autoload?(part)
|
|
153
|
-
|
|
154
|
-
resolved = mod.const_get(part, false)
|
|
155
|
-
return nil unless resolved.is_a?(Module)
|
|
156
|
-
|
|
157
|
-
resolved
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def clear_concern_state(file_path)
|
|
162
|
-
return unless defined?(ActiveSupport::Concern)
|
|
163
|
-
|
|
164
|
-
absolute = File.expand_path(file_path)
|
|
165
|
-
subpath = resolve_require_subpath(file_path)
|
|
166
|
-
|
|
167
|
-
ObjectSpace.each_object(Module) do |mod|
|
|
168
|
-
next unless mod.singleton_class.ancestors.include?(ActiveSupport::Concern)
|
|
169
|
-
|
|
170
|
-
%i[@_included_block @_prepended_block].each do |ivar|
|
|
171
|
-
next unless mod.instance_variable_defined?(ivar)
|
|
172
|
-
|
|
173
|
-
block = mod.instance_variable_get(ivar)
|
|
174
|
-
block_file = block.source_location&.first
|
|
175
|
-
next unless block_file
|
|
176
|
-
|
|
177
|
-
expanded = File.expand_path(block_file)
|
|
178
|
-
mod.remove_instance_variable(ivar) if source_matches?(expanded, absolute, subpath)
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def source_matches?(block_path, absolute, subpath)
|
|
184
|
-
block_path == absolute || (subpath && block_path.end_with?("/#{subpath}"))
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def resolve_require_subpath(file_path)
|
|
188
|
-
absolute = File.expand_path(file_path)
|
|
189
|
-
best_subpath = nil
|
|
190
|
-
|
|
191
|
-
$LOAD_PATH.each do |entry|
|
|
192
|
-
dir = File.expand_path(entry)
|
|
193
|
-
prefix = dir.end_with?("/") ? dir : "#{dir}/"
|
|
194
|
-
next unless absolute.start_with?(prefix)
|
|
195
|
-
|
|
196
|
-
candidate = absolute.delete_prefix(prefix)
|
|
197
|
-
best_subpath = candidate if best_subpath.nil? || candidate.length < best_subpath.length
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
best_subpath
|
|
201
|
-
end
|
|
202
51
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../loading"
|
|
4
|
+
require_relative "../../load_path/subpath_resolver"
|
|
5
|
+
|
|
6
|
+
# Re-evaluating an `ActiveSupport::Concern` module raises
|
|
7
|
+
# "MultipleIncludedBlocks" because AS::Concern records the block source
|
|
8
|
+
# location on the first include/prepend call. Before a re-eval we clear the
|
|
9
|
+
# `@_included_block` / `@_prepended_block` ivar on modules whose block came
|
|
10
|
+
# from the file we're about to re-eval.
|
|
11
|
+
class Evilution::Integration::Loading::ConcernStateCleaner
|
|
12
|
+
IVARS = %i[@_included_block @_prepended_block].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(subpath_resolver: Evilution::LoadPath::SubpathResolver.new)
|
|
15
|
+
@subpath_resolver = subpath_resolver
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(file_path)
|
|
19
|
+
return unless defined?(ActiveSupport::Concern)
|
|
20
|
+
|
|
21
|
+
absolute = File.expand_path(file_path)
|
|
22
|
+
subpath = @subpath_resolver.call(file_path)
|
|
23
|
+
|
|
24
|
+
ObjectSpace.each_object(Module) do |mod|
|
|
25
|
+
next unless mod.singleton_class.ancestors.include?(ActiveSupport::Concern)
|
|
26
|
+
|
|
27
|
+
clear_concern_ivars(mod, absolute, subpath)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def clear_concern_ivars(mod, absolute, subpath)
|
|
34
|
+
IVARS.each do |ivar|
|
|
35
|
+
next unless mod.instance_variable_defined?(ivar)
|
|
36
|
+
|
|
37
|
+
block = mod.instance_variable_get(ivar)
|
|
38
|
+
block_file = block.source_location&.first
|
|
39
|
+
next unless block_file
|
|
40
|
+
|
|
41
|
+
expanded = File.expand_path(block_file)
|
|
42
|
+
mod.remove_instance_variable(ivar) if source_matches?(expanded, absolute, subpath)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def source_matches?(block_path, absolute, subpath)
|
|
47
|
+
block_path == absolute || (subpath && block_path.end_with?("/#{subpath}"))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../loading"
|
|
4
|
+
require_relative "../../ast/constant_names"
|
|
5
|
+
|
|
6
|
+
# Defeat Zeitwerk's re-autoload hook when we re-eval a file in place. Walking
|
|
7
|
+
# the source AST for top-level class/module names and calling `const_get` on
|
|
8
|
+
# each tells Zeitwerk "this constant is loaded" so our re-eval does not lose
|
|
9
|
+
# state (e.g. `@_included_block`) to a follow-up autoload.
|
|
10
|
+
class Evilution::Integration::Loading::ConstantPinner
|
|
11
|
+
def initialize(constant_names: Evilution::AST::ConstantNames.new)
|
|
12
|
+
@constant_names = constant_names
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(source)
|
|
16
|
+
names = @constant_names.call(source)
|
|
17
|
+
names.each do |name|
|
|
18
|
+
Object.const_get(name) if Object.const_defined?(name, false)
|
|
19
|
+
rescue NameError # :nodoc:
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
names
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../loading"
|
|
4
|
+
require_relative "syntax_validator"
|
|
5
|
+
require_relative "constant_pinner"
|
|
6
|
+
require_relative "concern_state_cleaner"
|
|
7
|
+
require_relative "source_evaluator"
|
|
8
|
+
require_relative "redefinition_recovery"
|
|
9
|
+
|
|
10
|
+
# Composes the load-time pipeline that applies a mutation's new source to the
|
|
11
|
+
# running VM: syntax-validate -> pin top-level constants (beats Zeitwerk) ->
|
|
12
|
+
# clear AS::Concern state -> eval inside a redefinition-recovery wrapper.
|
|
13
|
+
# Returns nil on success or a failure-shaped hash on any error.
|
|
14
|
+
class Evilution::Integration::Loading::MutationApplier
|
|
15
|
+
def initialize(syntax_validator: Evilution::Integration::Loading::SyntaxValidator.new,
|
|
16
|
+
constant_pinner: Evilution::Integration::Loading::ConstantPinner.new,
|
|
17
|
+
concern_state_cleaner: Evilution::Integration::Loading::ConcernStateCleaner.new,
|
|
18
|
+
source_evaluator: Evilution::Integration::Loading::SourceEvaluator.new,
|
|
19
|
+
redefinition_recovery: Evilution::Integration::Loading::RedefinitionRecovery.new)
|
|
20
|
+
@syntax_validator = syntax_validator
|
|
21
|
+
@constant_pinner = constant_pinner
|
|
22
|
+
@concern_state_cleaner = concern_state_cleaner
|
|
23
|
+
@source_evaluator = source_evaluator
|
|
24
|
+
@redefinition_recovery = redefinition_recovery
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(mutation)
|
|
28
|
+
syntax_error = @syntax_validator.call(mutation.mutated_source)
|
|
29
|
+
return syntax_error if syntax_error
|
|
30
|
+
|
|
31
|
+
@constant_pinner.call(mutation.original_source)
|
|
32
|
+
@concern_state_cleaner.call(mutation.file_path)
|
|
33
|
+
@redefinition_recovery.call(mutation.original_source) do
|
|
34
|
+
@source_evaluator.call(mutation.mutated_source, mutation.file_path)
|
|
35
|
+
end
|
|
36
|
+
nil
|
|
37
|
+
rescue SyntaxError => e
|
|
38
|
+
{
|
|
39
|
+
passed: false,
|
|
40
|
+
error: "syntax error in mutated source: #{e.message}",
|
|
41
|
+
error_class: e.class.name,
|
|
42
|
+
error_backtrace: Array(e.backtrace).first(5)
|
|
43
|
+
}
|
|
44
|
+
rescue ScriptError, StandardError => e
|
|
45
|
+
{
|
|
46
|
+
passed: false,
|
|
47
|
+
error: "#{e.class}: #{e.message}",
|
|
48
|
+
error_class: e.class.name,
|
|
49
|
+
error_backtrace: Array(e.backtrace).first(5)
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../loading"
|
|
4
|
+
require_relative "../../ast/constant_names"
|
|
5
|
+
|
|
6
|
+
# Some DSLs (Rails 8 enum, define_method guards) raise ArgumentError on
|
|
7
|
+
# re-declaration. On such a conflict we strip constants declared in the source
|
|
8
|
+
# and retry the load once against a fresh namespace.
|
|
9
|
+
class Evilution::Integration::Loading::RedefinitionRecovery
|
|
10
|
+
def initialize(constant_names: Evilution::AST::ConstantNames.new)
|
|
11
|
+
@constant_names = constant_names
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(source, &block)
|
|
15
|
+
block.call
|
|
16
|
+
rescue ArgumentError => e
|
|
17
|
+
raise unless redefinition_conflict?(e)
|
|
18
|
+
|
|
19
|
+
remove_defined_constants(source)
|
|
20
|
+
block.call
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def redefinition_conflict?(error)
|
|
26
|
+
error.message.include?("already defined")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def remove_defined_constants(source)
|
|
30
|
+
@constant_names.call(source).reverse_each do |name|
|
|
31
|
+
parent_name, _, local_name = name.rpartition("::")
|
|
32
|
+
parent = resolve_loaded_constant_parent(parent_name)
|
|
33
|
+
next unless parent
|
|
34
|
+
next unless parent.const_defined?(local_name, false)
|
|
35
|
+
next if parent.autoload?(local_name)
|
|
36
|
+
|
|
37
|
+
parent.send(:remove_const, local_name.to_sym)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def resolve_loaded_constant_parent(parent_name)
|
|
42
|
+
return Object if parent_name.empty?
|
|
43
|
+
|
|
44
|
+
parent_name.split("::").reduce(Object) do |mod, part|
|
|
45
|
+
return nil unless mod.const_defined?(part, false)
|
|
46
|
+
return nil if mod.autoload?(part)
|
|
47
|
+
|
|
48
|
+
resolved = mod.const_get(part, false)
|
|
49
|
+
return nil unless resolved.is_a?(Module)
|
|
50
|
+
|
|
51
|
+
resolved
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../loading"
|
|
4
|
+
|
|
5
|
+
# Evaluate source with __FILE__ set to the absolute original path so that
|
|
6
|
+
# `require_relative` and `__dir__` resolve against the real source tree, where
|
|
7
|
+
# sibling files actually exist.
|
|
8
|
+
class Evilution::Integration::Loading::SourceEvaluator
|
|
9
|
+
def call(source, file_path)
|
|
10
|
+
absolute = File.expand_path(file_path)
|
|
11
|
+
# rubocop:disable Security/Eval
|
|
12
|
+
eval(source, TOPLEVEL_BINDING, absolute, 1)
|
|
13
|
+
# rubocop:enable Security/Eval
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../loading"
|
|
5
|
+
|
|
6
|
+
class Evilution::Integration::Loading::SyntaxValidator
|
|
7
|
+
ERROR_MESSAGE = "mutated source has syntax errors"
|
|
8
|
+
|
|
9
|
+
def call(source)
|
|
10
|
+
return nil if Prism.parse(source).success?
|
|
11
|
+
|
|
12
|
+
{
|
|
13
|
+
passed: false,
|
|
14
|
+
error: ERROR_MESSAGE,
|
|
15
|
+
error_class: "SyntaxError",
|
|
16
|
+
error_backtrace: []
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../load_path"
|
|
4
|
+
|
|
5
|
+
# Given an absolute (or expandable) file path, returns the shortest path
|
|
6
|
+
# relative to any `$LOAD_PATH` entry the file lives under, or nil if the file
|
|
7
|
+
# is outside every entry. The shortest match wins because a deeper LOAD_PATH
|
|
8
|
+
# entry yields a shorter subpath that better matches `require` resolution.
|
|
9
|
+
class Evilution::LoadPath::SubpathResolver
|
|
10
|
+
def call(file_path)
|
|
11
|
+
absolute = File.expand_path(file_path)
|
|
12
|
+
best_subpath = nil
|
|
13
|
+
|
|
14
|
+
$LOAD_PATH.each do |entry|
|
|
15
|
+
dir = File.expand_path(entry)
|
|
16
|
+
prefix = dir.end_with?("/") ? dir : "#{dir}/"
|
|
17
|
+
next unless absolute.start_with?(prefix)
|
|
18
|
+
|
|
19
|
+
candidate = absolute.delete_prefix(prefix)
|
|
20
|
+
best_subpath = candidate if best_subpath.nil? || candidate.length < best_subpath.length
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
best_subpath
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -30,10 +30,10 @@ class Evilution::Runner::IsolationResolver
|
|
|
30
30
|
|
|
31
31
|
def perform_preload
|
|
32
32
|
return if config.preload == false
|
|
33
|
-
return unless resolve_isolation == :fork
|
|
34
33
|
|
|
35
34
|
path = resolve_preload_path
|
|
36
35
|
return unless path
|
|
36
|
+
return unless should_preload?
|
|
37
37
|
|
|
38
38
|
prepare_load_path_for_preload
|
|
39
39
|
require File.expand_path(path)
|
|
@@ -48,6 +48,17 @@ class Evilution::Runner::IsolationResolver
|
|
|
48
48
|
|
|
49
49
|
attr_reader :config, :hooks
|
|
50
50
|
|
|
51
|
+
# Under :fork, allow preloading — caller resolves whether a path exists (an
|
|
52
|
+
# explicit --preload / preload: value, or an auto-detected rails_helper) and
|
|
53
|
+
# bails early when none does. Under :in_process, only allow preloading when
|
|
54
|
+
# the user explicitly asked via --preload or preload: in YAML — don't
|
|
55
|
+
# auto-load spec/rails_helper.rb for a user who opted out of fork.
|
|
56
|
+
def should_preload?
|
|
57
|
+
return true if resolve_isolation == :fork
|
|
58
|
+
|
|
59
|
+
config.preload.is_a?(String)
|
|
60
|
+
end
|
|
61
|
+
|
|
51
62
|
def target_files
|
|
52
63
|
@target_files ||= @target_files_callback.call
|
|
53
64
|
end
|
|
@@ -20,13 +20,7 @@ class Evilution::Runner::SubjectPipeline
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def target_files
|
|
23
|
-
@target_files ||=
|
|
24
|
-
resolve_source_glob
|
|
25
|
-
elsif !config.target_files.empty?
|
|
26
|
-
config.target_files
|
|
27
|
-
else
|
|
28
|
-
Evilution::Git::ChangedFiles.new.call
|
|
29
|
-
end
|
|
23
|
+
@target_files ||= resolve_target_files
|
|
30
24
|
end
|
|
31
25
|
|
|
32
26
|
private
|
|
@@ -88,11 +82,27 @@ class Evilution::Runner::SubjectPipeline
|
|
|
88
82
|
|
|
89
83
|
def filter_by_target(subjects)
|
|
90
84
|
matched = subjects.select(&target_matcher)
|
|
91
|
-
raise Evilution::Error,
|
|
85
|
+
raise Evilution::Error, build_no_match_error if matched.empty?
|
|
92
86
|
|
|
93
87
|
matched
|
|
94
88
|
end
|
|
95
89
|
|
|
90
|
+
def resolve_target_files
|
|
91
|
+
return resolve_source_glob if source_glob_target?
|
|
92
|
+
return config.target_files unless config.target_files.empty?
|
|
93
|
+
|
|
94
|
+
@used_git_fallback = true
|
|
95
|
+
Evilution::Git::ChangedFiles.new.call
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_no_match_error
|
|
99
|
+
base = "no subject matched '#{config.target}'"
|
|
100
|
+
return base unless @used_git_fallback
|
|
101
|
+
|
|
102
|
+
"#{base}; scanned git-changed files only. Pass file paths or " \
|
|
103
|
+
"--target source:<glob> to scan the full codebase."
|
|
104
|
+
end
|
|
105
|
+
|
|
96
106
|
def target_matcher
|
|
97
107
|
target = config.target
|
|
98
108
|
if target.end_with?("*")
|
data/lib/evilution/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.26.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-04-
|
|
11
|
+
date: 2026-04-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|
|
@@ -72,6 +72,7 @@ files:
|
|
|
72
72
|
- ".claude/prompts/devops.md"
|
|
73
73
|
- ".claude/prompts/tests.md"
|
|
74
74
|
- ".claude/settings.json"
|
|
75
|
+
- ".claude/skills/create-issue/SKILL.md"
|
|
75
76
|
- CHANGELOG.md
|
|
76
77
|
- CODE_OF_CONDUCT.md
|
|
77
78
|
- LICENSE.txt
|
|
@@ -88,6 +89,7 @@ files:
|
|
|
88
89
|
- exe/evilution
|
|
89
90
|
- lib/evilution.rb
|
|
90
91
|
- lib/evilution/ast.rb
|
|
92
|
+
- lib/evilution/ast/constant_names.rb
|
|
91
93
|
- lib/evilution/ast/inheritance_scanner.rb
|
|
92
94
|
- lib/evilution/ast/parser.rb
|
|
93
95
|
- lib/evilution/ast/pattern.rb
|
|
@@ -135,6 +137,7 @@ files:
|
|
|
135
137
|
- lib/evilution/compare/categorizer.rb
|
|
136
138
|
- lib/evilution/compare/detector.rb
|
|
137
139
|
- lib/evilution/compare/fingerprint.rb
|
|
140
|
+
- lib/evilution/compare/invalid_input.rb
|
|
138
141
|
- lib/evilution/compare/normalizer.rb
|
|
139
142
|
- lib/evilution/compare/record.rb
|
|
140
143
|
- lib/evilution/config.rb
|
|
@@ -158,12 +161,21 @@ files:
|
|
|
158
161
|
- lib/evilution/integration.rb
|
|
159
162
|
- lib/evilution/integration/base.rb
|
|
160
163
|
- lib/evilution/integration/crash_detector.rb
|
|
164
|
+
- lib/evilution/integration/loading.rb
|
|
165
|
+
- lib/evilution/integration/loading/concern_state_cleaner.rb
|
|
166
|
+
- lib/evilution/integration/loading/constant_pinner.rb
|
|
167
|
+
- lib/evilution/integration/loading/mutation_applier.rb
|
|
168
|
+
- lib/evilution/integration/loading/redefinition_recovery.rb
|
|
169
|
+
- lib/evilution/integration/loading/source_evaluator.rb
|
|
170
|
+
- lib/evilution/integration/loading/syntax_validator.rb
|
|
161
171
|
- lib/evilution/integration/minitest.rb
|
|
162
172
|
- lib/evilution/integration/minitest_crash_detector.rb
|
|
163
173
|
- lib/evilution/integration/rspec.rb
|
|
164
174
|
- lib/evilution/isolation.rb
|
|
165
175
|
- lib/evilution/isolation/fork.rb
|
|
166
176
|
- lib/evilution/isolation/in_process.rb
|
|
177
|
+
- lib/evilution/load_path.rb
|
|
178
|
+
- lib/evilution/load_path/subpath_resolver.rb
|
|
167
179
|
- lib/evilution/mcp.rb
|
|
168
180
|
- lib/evilution/mcp/info_tool.rb
|
|
169
181
|
- lib/evilution/mcp/mutate_tool.rb
|
|
@@ -174,9 +186,6 @@ files:
|
|
|
174
186
|
- lib/evilution/mcp/mutate_tool/report_trimmer.rb
|
|
175
187
|
- lib/evilution/mcp/mutate_tool/survived_enricher.rb
|
|
176
188
|
- lib/evilution/mcp/server.rb
|
|
177
|
-
- lib/evilution/mcp/session_diff_tool.rb
|
|
178
|
-
- lib/evilution/mcp/session_list_tool.rb
|
|
179
|
-
- lib/evilution/mcp/session_show_tool.rb
|
|
180
189
|
- lib/evilution/mcp/session_tool.rb
|
|
181
190
|
- lib/evilution/memory.rb
|
|
182
191
|
- lib/evilution/memory/leak_check.rb
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "mcp"
|
|
5
|
-
require_relative "../session/store"
|
|
6
|
-
require_relative "../session/diff"
|
|
7
|
-
|
|
8
|
-
require_relative "../mcp"
|
|
9
|
-
|
|
10
|
-
# @deprecated Superseded by {Evilution::MCP::SessionTool} (action: "diff") as of 0.22.8.
|
|
11
|
-
# No longer registered with the MCP server; retained only for direct Ruby callers.
|
|
12
|
-
# Will be removed entirely — tracked by EV-h8pw / GH #686.
|
|
13
|
-
class Evilution::MCP::SessionDiffTool < MCP::Tool
|
|
14
|
-
tool_name "evilution-session-diff"
|
|
15
|
-
description "DEPRECATED: use evilution-session with action: 'diff'. " \
|
|
16
|
-
"Compare two mutation testing sessions and return the diff. " \
|
|
17
|
-
"Shows new regressions, fixed mutations, and persistent survivors."
|
|
18
|
-
input_schema(
|
|
19
|
-
properties: {
|
|
20
|
-
base: {
|
|
21
|
-
type: "string",
|
|
22
|
-
description: "Path to the base (older) session JSON file"
|
|
23
|
-
},
|
|
24
|
-
head: {
|
|
25
|
-
type: "string",
|
|
26
|
-
description: "Path to the head (newer) session JSON file"
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
class << self
|
|
32
|
-
# rubocop:disable Lint/UnusedMethodArgument
|
|
33
|
-
def call(server_context:, base: nil, head: nil)
|
|
34
|
-
return error_response("config_error", "base is required") unless base
|
|
35
|
-
return error_response("config_error", "head is required") unless head
|
|
36
|
-
|
|
37
|
-
store = Evilution::Session::Store.new
|
|
38
|
-
base_data = store.load(base)
|
|
39
|
-
head_data = store.load(head)
|
|
40
|
-
|
|
41
|
-
diff = Evilution::Session::Diff.new
|
|
42
|
-
result = diff.call(base_data, head_data)
|
|
43
|
-
|
|
44
|
-
::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(result.to_h) }])
|
|
45
|
-
rescue Evilution::Error => e
|
|
46
|
-
error_response("not_found", e.message)
|
|
47
|
-
rescue ::JSON::ParserError => e
|
|
48
|
-
error_response("parse_error", e.message)
|
|
49
|
-
rescue SystemCallError => e
|
|
50
|
-
error_response("runtime_error", e.message)
|
|
51
|
-
end
|
|
52
|
-
# rubocop:enable Lint/UnusedMethodArgument
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
def error_response(type, message)
|
|
57
|
-
::MCP::Tool::Response.new(
|
|
58
|
-
[{ type: "text", text: ::JSON.generate({ error: { type: type, message: message } }) }],
|
|
59
|
-
error: true
|
|
60
|
-
)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "mcp"
|
|
5
|
-
require_relative "../session/store"
|
|
6
|
-
|
|
7
|
-
require_relative "../mcp"
|
|
8
|
-
|
|
9
|
-
# @deprecated Superseded by {Evilution::MCP::SessionTool} (action: "list") as of 0.22.8.
|
|
10
|
-
# No longer registered with the MCP server; retained only for direct Ruby callers.
|
|
11
|
-
# Will be removed entirely — tracked by EV-h8pw / GH #686.
|
|
12
|
-
class Evilution::MCP::SessionListTool < MCP::Tool
|
|
13
|
-
tool_name "evilution-session-list"
|
|
14
|
-
description "DEPRECATED: use evilution-session with action: 'list'. " \
|
|
15
|
-
"List past mutation testing sessions with summary statistics. " \
|
|
16
|
-
"Returns sessions in reverse chronological order."
|
|
17
|
-
input_schema(
|
|
18
|
-
properties: {
|
|
19
|
-
results_dir: {
|
|
20
|
-
type: "string",
|
|
21
|
-
description: "Session results directory (default: .evilution/results)"
|
|
22
|
-
},
|
|
23
|
-
limit: {
|
|
24
|
-
type: "integer",
|
|
25
|
-
description: "Return only the N most recent sessions"
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
class << self
|
|
31
|
-
# rubocop:disable Lint/UnusedMethodArgument
|
|
32
|
-
def call(server_context:, results_dir: nil, limit: nil)
|
|
33
|
-
store_opts = {}
|
|
34
|
-
store_opts[:results_dir] = results_dir if results_dir
|
|
35
|
-
store = Evilution::Session::Store.new(**store_opts)
|
|
36
|
-
entries = store.list
|
|
37
|
-
entries = entries.first(limit) if limit
|
|
38
|
-
|
|
39
|
-
payload = entries.map { |e| stringify_keys(e) }
|
|
40
|
-
::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(payload) }])
|
|
41
|
-
end
|
|
42
|
-
# rubocop:enable Lint/UnusedMethodArgument
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
def stringify_keys(hash)
|
|
47
|
-
hash.transform_keys(&:to_s)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "mcp"
|
|
5
|
-
require_relative "../session/store"
|
|
6
|
-
|
|
7
|
-
require_relative "../mcp"
|
|
8
|
-
|
|
9
|
-
# @deprecated Superseded by {Evilution::MCP::SessionTool} (action: "show") as of 0.22.8.
|
|
10
|
-
# No longer registered with the MCP server; retained only for direct Ruby callers.
|
|
11
|
-
# Will be removed entirely — tracked by EV-h8pw / GH #686.
|
|
12
|
-
class Evilution::MCP::SessionShowTool < MCP::Tool
|
|
13
|
-
tool_name "evilution-session-show"
|
|
14
|
-
description "DEPRECATED: use evilution-session with action: 'show'. " \
|
|
15
|
-
"Show full details of a past mutation testing session, " \
|
|
16
|
-
"including survived mutations with diffs."
|
|
17
|
-
input_schema(
|
|
18
|
-
properties: {
|
|
19
|
-
path: {
|
|
20
|
-
type: "string",
|
|
21
|
-
description: "Path to the session JSON file (as returned by evilution-session-list)"
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
class << self
|
|
27
|
-
# rubocop:disable Lint/UnusedMethodArgument
|
|
28
|
-
def call(server_context:, path: nil)
|
|
29
|
-
unless path
|
|
30
|
-
return ::MCP::Tool::Response.new(
|
|
31
|
-
[{ type: "text", text: ::JSON.generate({ error: { type: "config_error", message: "path is required" } }) }],
|
|
32
|
-
error: true
|
|
33
|
-
)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
store = Evilution::Session::Store.new
|
|
37
|
-
data = store.load(path)
|
|
38
|
-
::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(data) }])
|
|
39
|
-
rescue Evilution::Error => e
|
|
40
|
-
::MCP::Tool::Response.new(
|
|
41
|
-
[{ type: "text", text: ::JSON.generate({ error: { type: "not_found", message: e.message } }) }],
|
|
42
|
-
error: true
|
|
43
|
-
)
|
|
44
|
-
rescue ::JSON::ParserError => e
|
|
45
|
-
::MCP::Tool::Response.new(
|
|
46
|
-
[{ type: "text", text: ::JSON.generate({ error: { type: "parse_error", message: e.message } }) }],
|
|
47
|
-
error: true
|
|
48
|
-
)
|
|
49
|
-
rescue SystemCallError => e
|
|
50
|
-
::MCP::Tool::Response.new(
|
|
51
|
-
[{ type: "text", text: ::JSON.generate({ error: { type: "runtime_error", message: e.message } }) }],
|
|
52
|
-
error: true
|
|
53
|
-
)
|
|
54
|
-
end
|
|
55
|
-
# rubocop:enable Lint/UnusedMethodArgument
|
|
56
|
-
end
|
|
57
|
-
end
|