kimera 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c0b6544c638890d575523723b8d86dcac500e247c22a989fda9fa13b9f8d68b
4
- data.tar.gz: 4b354333f7a2173a49932c9761f1731e034cd0a91df2a6534081412bd0598f92
3
+ metadata.gz: 042a730e48edfef4744060425cd10f04e34006025af3f36db3ab17f0be59e0bc
4
+ data.tar.gz: 97a3914ca3f803f01e68564af89ee07667703aadfa6ec74269bbf37470add0fb
5
5
  SHA512:
6
- metadata.gz: 84b6ffd6aac361151843796089b6a4b85e1aa34aafdc8b9a78caf407ef18e774b72527235d4596649b08c52454d2a743e1e35d032240d7eaf6c577f6ad80a3d9
7
- data.tar.gz: 0b1cd61f82643207a14f2fff230b46e05de8be433f8989a8e55df6e9e5e7af0198f55cd009d411a6beeeeedd5f4c68946d119ba1aa77326e27f12cb3406fe566
6
+ metadata.gz: 2c2d774a35384621bca26d92d5c9e77550f3b08ecf1304019b37e695eeacbbc5566622de50fbae65077f61035fa25e241817aa31e917c0455e8ad23b26c00f9b
7
+ data.tar.gz: 7ebd3017fd0edbd7a2abb3c585ee347dfe0b66a03111ffa9efb322b835b6661d29bd2554f863bc075035f242da3bf66f514f098ebaf2b64339b39b90f8dde558
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2 (2026-09-26)
4
+
5
+ - Kimera sets `KIMERA=1` in its own process before loading the suite, and in
6
+ the `kimera doctor` test commands. A coverage floor gated on
7
+ `ENV["KIMERA"]` no longer fails a passing warm run with exit 2 or blocks
8
+ doctor's dry run. (#6)
9
+ - A string interpolating a local bound by a pattern (`in [:ok, chosen]`,
10
+ `=> chosen`), a `&block` parameter, or a regexp named capture no longer
11
+ sinks its file's mutants. unparser 0.9 could not round-trip it; Kimera now
12
+ teaches unparser those bindings. (#7)
13
+ - The per-method splice fallback now claims mutants in an endless method
14
+ (`def big? = value > 10`), and reopens `Data.define` / `Struct.new`
15
+ constants instead of redefining them, so spliced guards are reached. (#7)
16
+ - Mutants Kimera could not instrument are reported as `unmutatable`, with the
17
+ reason, instead of `no_coverage`. They never gate and stay out of the score,
18
+ so `--fail-on-no-coverage` again means "no test executes this". (#7)
19
+ - `kimera run --since` no longer crashes with "invalid byte sequence in
20
+ US-ASCII" when the locale isn't UTF-8 (e.g. `LANG` unset) and the diff
21
+ contains non-ASCII text. Git's diff output is read as UTF-8, with invalid
22
+ bytes replaced. (#9)
23
+
3
24
  ## 0.1.1 (2026-09-26)
4
25
 
5
26
  - Fix a boot crash on case-sensitive filesystems (Linux): `require "English"`
data/README.md CHANGED
@@ -221,8 +221,9 @@ selector and Kimera's own harness-critical files (see below).
221
221
  An isolated child is judged on its failed tests, not its exit status. A child
222
222
  that exits non-zero with no failing test is `harness_error` (unjudged, gated
223
223
  by `--max-errors`), not a kill. The usual cause is a coverage floor such as
224
- SimpleCov's `minimum_coverage`, which a partial run always trips. Children run
225
- with `KIMERA=1` set, so skip the floor there:
224
+ SimpleCov's `minimum_coverage`, which a partial run always trips. Kimera sets
225
+ `KIMERA=1` wherever it loads your suite (the warm process, `--isolated`
226
+ children, and `kimera doctor`'s test commands), so skip the floor there:
226
227
 
227
228
  ```ruby
228
229
  SimpleCov.start { minimum_coverage(line: 100, branch: 100) unless ENV["KIMERA"] }
@@ -504,8 +505,11 @@ source ──Prism──▶ registry (mutation points, JSON) ──┬─▶ syn
504
505
  baseline, or worse, as kills that are really collisions.
505
506
  - **Score definition.** `score = killed / evaluable`.
506
507
  - Killed includes timeouts and errors (observable misbehaviour).
507
- - Evaluable excludes `no_coverage`, `ignored`, `isolated_only`, and
508
- `harness_error` mutants. Each is reported separately.
508
+ - Evaluable excludes `no_coverage`, `ignored`, `isolated_only`,
509
+ `unmutatable`, and `harness_error` mutants. Each is reported separately.
510
+ - An `unmutatable` mutant is one Kimera could not instrument for the warm
511
+ run (its file or method failed to re-emit or load); the detail says why.
512
+ It never gates, and it is not `no_coverage`: a test may well run it.
509
513
  - A `harness_error` is a mutant Kimera could not judge: its worker died, or
510
514
  its reply was unreadable. It is not a kill. It gates via `--max-errors`
511
515
  (default 0). A pool that never produces a result aborts the run.
@@ -96,8 +96,8 @@ class Kimera::CLI::Doctor
96
96
  end
97
97
 
98
98
  def floored(helper)
99
- "Coverage floor: minimum_coverage in #{helper} fails partial --isolated runs; " \
100
- "skip it when ENV[\"KIMERA\"] is set"
99
+ "Coverage floor: minimum_coverage in #{helper} fails Kimera's partial runs; " \
100
+ "skip it when ENV[\"KIMERA\"] is set (Kimera sets it)"
101
101
  end
102
102
 
103
103
  def floor?(path)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../execution/suite_env"
3
4
  require_relative "../../incremental/selection"
4
5
  require_relative "../../incremental/session"
5
6
  require_relative "digest"
@@ -71,7 +72,8 @@ class Kimera::CLI::Run::Cycle
71
72
  .transform_keys(&:to_s)
72
73
  end
73
74
 
74
- def harness(remaining, loaded)
75
+ def harness(remaining, loaded, env: ENV)
76
+ env.update(Kimera::Execution::SUITE_ENV)
75
77
  adapter = Kimera::Frameworks::Adapter.load(@options[:framework])
76
78
  Kimera::CLI::Run::Pass.new(@registry, @options, adapter).call(remaining, loaded)
77
79
  end
@@ -15,7 +15,7 @@ class Kimera::CLI::Survivors
15
15
  Kimera::Flag.build(
16
16
  "--status NAME", :status,
17
17
  "List a different status (survived, no_coverage, " \
18
- "timeout, error, killed, isolated_only)"
18
+ "timeout, error, killed, isolated_only, unmutatable)"
19
19
  )
20
20
  ]
21
21
  )
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
+ require_relative "../execution/suite_env"
4
5
 
5
6
  class Kimera::CLI::TestCommand
6
7
  LOADER = "ARGV.map { |f| File.expand_path(f) }.tap { ARGV.clear }.each { |f| require(f) }"
@@ -30,7 +31,7 @@ class Kimera::CLI::TestCommand
30
31
  end
31
32
 
32
33
  def check(command, passed)
33
- output, status = Open3.capture2e(*command, chdir: @root)
34
+ output, status = Open3.capture2e(Kimera::Execution::SUITE_ENV, *command, chdir: @root)
34
35
  status.success? ? ["✓", passed] : yield(summary(output))
35
36
  rescue Errno::ENOENT
36
37
  yield(nil)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "suite_env"
4
+
3
5
  module Kimera
4
6
  module Execution
5
7
  end
@@ -16,11 +18,9 @@ class Kimera::Execution::IsolatedPlan::ChildCommand
16
18
  @test_files = test_files
17
19
  end
18
20
 
19
- ENV_FLAG = { "KIMERA" => "1" }.freeze
20
-
21
21
  def command(mirror, locations, ledger:, paths:)
22
22
  includes = paths.flat_map { |path| ["-I", path] }
23
- bundled(mirror, ENV_FLAG.dup, ["ruby", *includes, "-I", helpers, *argv(locations, ledger)])
23
+ bundled(mirror, Kimera::Execution::SUITE_ENV.dup, ["ruby", *includes, "-I", helpers, *argv(locations, ledger)])
24
24
  end
25
25
 
26
26
  def argv(locations, ledger)
@@ -39,7 +39,12 @@ class Kimera::Execution::Schemata
39
39
  @skipped.keys.select { |path| @skipped[path].start_with?("NameError:") }.each { |path| @skipped.delete(path) }
40
40
  end
41
41
 
42
- def announce = @skipped.each { |path, reason| notice(path, reason) }
42
+ def announce
43
+ @skipped.each do |path, reason|
44
+ notice(path, reason)
45
+ @registry.at(path).select(&:safe?).each { |point| point.unmutatable!("schemata setup failed (#{reason})") }
46
+ end
47
+ end
43
48
 
44
49
  def load(path)
45
50
  file = File.join(root, path)
@@ -72,7 +77,7 @@ class Kimera::Execution::Schemata
72
77
  def notice(path, reason)
73
78
  errors.puts(
74
79
  "kimera: #{path} cannot run in warm workers — schemata setup failed " \
75
- "(#{reason}); its mutants are reported no_coverage"
80
+ "(#{reason}); its mutants are reported unmutatable"
76
81
  )
77
82
  end
78
83
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module Execution
5
+ SUITE_ENV = { "KIMERA" => "1" }.freeze
6
+ end
7
+ end
@@ -18,7 +18,7 @@ class Kimera::Execution::Verdicts
18
18
  end
19
19
 
20
20
  def reloadable?(id)
21
- !point(id).body?
21
+ point(id).reloadable?
22
22
  end
23
23
 
24
24
  def reclassify!(results)
@@ -45,7 +45,9 @@ class Kimera::Execution::Verdicts
45
45
  end
46
46
 
47
47
  def quarantine(id)
48
- Kimera::MutantResult.new(mutant_id: id, status: :isolated_only, file: path(id), detail: point(id).unsafe_reason)
48
+ found = point(id)
49
+ status = found.unmutatable? ? :unmutatable : :isolated_only
50
+ Kimera::MutantResult.new(mutant_id: id, status: status, file: found.file, detail: found.unsafe_reason)
49
51
  end
50
52
 
51
53
  def point(id)
@@ -27,12 +27,14 @@ class Kimera::Incremental::GitDiff
27
27
  def parse(text)
28
28
  files = {}
29
29
  current = nil
30
- text.each_line { |line| current = advance(files, current, line) }
30
+ decoded(text).each_line { |line| current = advance(files, current, line) }
31
31
  files
32
32
  end
33
33
 
34
34
  private
35
35
 
36
+ def decoded(text) = text.dup.force_encoding(Encoding::UTF_8).scrub
37
+
36
38
  def advance(files, current, line)
37
39
  header = line.match(FILE_HEADER)
38
40
  return named(header) if header
@@ -17,7 +17,7 @@ module Kimera
17
17
  end
18
18
 
19
19
  def within?(from, to)
20
- interior = ((from + 1)...to)
20
+ interior = ((from + 1)..to)
21
21
  interior.cover?(start_offset) && interior.cover?(finish)
22
22
  end
23
23
 
@@ -66,6 +66,7 @@ end
66
66
 
67
67
  class Kimera::MutationPoint
68
68
  CLASS_BODY_REASON = "class-body DSL (runs at load; evaluate with --isolated)"
69
+ UNMUTATABLE = "unmutatable: "
69
70
  SAFE = true
70
71
 
71
72
  FIELDS = %i[point_id file operator node_type location original_source method_name mutants unsafe_reason].freeze
@@ -98,6 +99,18 @@ class Kimera::MutationPoint
98
99
  unsafe_reason == CLASS_BODY_REASON
99
100
  end
100
101
 
102
+ def reloadable?
103
+ !body? && !unmutatable?
104
+ end
105
+
106
+ def unmutatable?
107
+ unsafe_reason.to_s.start_with?(UNMUTATABLE)
108
+ end
109
+
110
+ def unmutatable!(detail)
111
+ unsafe!("#{UNMUTATABLE}#{detail}")
112
+ end
113
+
101
114
  def safe
102
115
  @attributes.fetch(:schema_safe, SAFE)
103
116
  end
@@ -4,9 +4,10 @@ module Kimera
4
4
  module Status
5
5
  KILLING = %i[killed timeout error].freeze
6
6
 
7
- UNJUDGED = %i[no_coverage ignored isolated_only harness_error].freeze
7
+ UNJUDGED = %i[no_coverage ignored isolated_only unmutatable harness_error].freeze
8
8
 
9
- REPORTED = ([:survived] + (KILLING - [:killed]) + %i[no_coverage harness_error ignored isolated_only]).freeze
9
+ REPORTED =
10
+ ([:survived] + (KILLING - [:killed]) + %i[no_coverage harness_error ignored isolated_only unmutatable]).freeze
10
11
 
11
12
  SELF_SUSPECT = ((KILLING - [:killed]) + [:survived]).freeze
12
13
  end
@@ -10,7 +10,7 @@ end
10
10
  class Kimera::RunReport
11
11
  attr_reader :results, :leaks, :registry
12
12
 
13
- CONDITIONAL = %i[harness_error ignored isolated_only leaks].freeze
13
+ CONDITIONAL = %i[harness_error ignored isolated_only unmutatable leaks].freeze
14
14
  LABELS = { total: "mutants", harness_error: "unjudged" }.freeze
15
15
  SCHEMA_VERSION = 1
16
16
 
@@ -11,5 +11,32 @@ module Kimera
11
11
  def parse(source) = Kimera::Warnings.silence { Unparser.parse(source) }
12
12
 
13
13
  def unparse(node) = Kimera::Warnings.silence { Unparser.unparse(node) }
14
+
15
+ module Binders
16
+ private
17
+
18
+ def enter(node)
19
+ super
20
+ Binders.names(node).each { |name| define(name) }
21
+ end
22
+
23
+ class << self
24
+ def names(node)
25
+ case node.type
26
+ when :match_var, :blockarg then [node.children.first].compact
27
+ when :match_with_lvasgn then captures(node.children.first)
28
+ else []
29
+ end
30
+ end
31
+
32
+ def captures(regexp)
33
+ *parts, options = regexp.children
34
+ flags = options.children.include?(:x) ? Regexp::EXTENDED : 0
35
+ Regexp.new(parts.sum("") { |part| part.children.first }, flags).names.map(&:to_sym)
36
+ end
37
+ end
38
+ end
14
39
  end
15
40
  end
41
+
42
+ Unparser::AST::LocalVariableScopeEnumerator.prepend(Kimera::Unparse::Binders)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kimera
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
@@ -6,6 +6,8 @@ require_relative "overlay_splice"
6
6
  class Kimera::Overlay::FileWeave
7
7
  include Kimera::OverlaySplice
8
8
 
9
+ UNGUARDABLE = "unparser could not round-trip its guard"
10
+
9
11
  def initialize(file, map, safe, unsafe)
10
12
  @file = file
11
13
  @map = map
@@ -43,16 +45,15 @@ class Kimera::Overlay::FileWeave
43
45
  def finish(viable)
44
46
  dropped = @safe - viable
45
47
  notice(dropped)
46
- result = attempt(viable)
47
- result.skipped_unsafe.concat(dropped.flat_map(&:ids))
48
- result
48
+ dropped.each { |point| point.unmutatable!(UNGUARDABLE) }
49
+ attempt(viable).tap { |result| result.skipped_unsafe.concat(dropped.flat_map(&:ids)) }
49
50
  end
50
51
 
51
52
  def notice(dropped)
52
53
  io.puts(
53
54
  "kimera: #{@file}: #{dropped.size} mutation point(s) at " \
54
55
  "#{dropped.map { |p| "line #{p.location.start_line}" }.uniq.join(", ")} " \
55
- "are unguardable (unparser round-trip); their mutants are reported no_coverage"
56
+ "are unguardable (unparser round-trip); their mutants are reported unmutatable"
56
57
  )
57
58
  end
58
59
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "splice_reopening"
4
+
3
5
  module Kimera
4
6
  module OverlaySplice
5
7
  private
@@ -13,7 +15,7 @@ module Kimera
13
15
  return unless ready?
14
16
  edits, dropped = gather
15
17
  return if edits.empty?
16
- source = apply(edits)
18
+ source = apply(edits + reopenings)
17
19
  [edits, dropped, source] if Kimera::Syntax.parse(source).success?
18
20
  end
19
21
 
@@ -24,6 +26,8 @@ module Kimera
24
26
  [edits, dropped + (@safe - claimed)]
25
27
  end
26
28
 
29
+ def reopenings = Kimera::SpliceReopening.new(@map).edits
30
+
27
31
  def collect(definition, points, edits, dropped)
28
32
  rewritten = rewrite(definition, points)
29
33
  rewritten ? edits << rewritten : dropped.concat(points)
@@ -32,6 +36,7 @@ module Kimera
32
36
  def report(edits, dropped, source)
33
37
  applied = edits.flat_map(&:last)
34
38
  announce(applied, dropped)
39
+ dropped.each { |point| point.unmutatable!("unparser could not round-trip its method") }
35
40
  build(applied, dropped, source)
36
41
  end
37
42
 
@@ -46,7 +51,7 @@ module Kimera
46
51
  io.puts(
47
52
  "kimera: #{@file}: file-level round-trip failed; " \
48
53
  "#{applied.size} mutant(s) spliced per method, " \
49
- "#{dropped.flat_map(&:ids).size} reported no_coverage"
54
+ "#{dropped.flat_map(&:ids).size} reported unmutatable"
50
55
  )
51
56
  end
52
57
 
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rewrite/ast_walk"
4
+ require_relative "guardrail_value_objects"
5
+
6
+ class Kimera::SpliceReopening
7
+ FACTORIES = Kimera::GuardrailValueObjects::VALUE_OBJECT_FACTORIES
8
+ LEXICAL = "(is_a?(::Module) ? self : ::Object)"
9
+
10
+ Assignment =
11
+ Data.define(:scope, :name, :value, :location) do
12
+ def edit = [location.expression.begin_pos, value.location.begin.begin_pos, header, []]
13
+
14
+ def header
15
+ owner = base
16
+ "#{constant} = (#{owner}.const_defined?(:#{name}, false) ? #{owner}.const_get(:#{name}) : " \
17
+ "#{factory}); #{constant}.class_eval "
18
+ end
19
+
20
+ def constant = location.expression.begin.join(location.name).source
21
+
22
+ def factory = value.children.first.location.expression.source
23
+
24
+ def base
25
+ return LEXICAL unless scope
26
+ scope.type == :cbase ? "::Object" : scope.location.expression.source
27
+ end
28
+ end
29
+
30
+ def initialize(map)
31
+ @map = map
32
+ end
33
+
34
+ def edits = constants.map(&:edit)
35
+
36
+ private
37
+
38
+ def constants
39
+ found = []
40
+ Kimera::Rewrite::AstWalk.visit(@map.ast) { |node| found << assignment(node) if reopenable?(node) }
41
+ found
42
+ end
43
+
44
+ def assignment(node) = Assignment.new(*node, node.location)
45
+
46
+ def reopenable?(node)
47
+ return false unless node.type == :casgn
48
+ value = node.children.last
49
+ value.is_a?(Parser::AST::Node) && value.type == :block && factory?(value.children.first)
50
+ end
51
+
52
+ def factory?(call)
53
+ receiver, selector = call.children
54
+ call.type == :send && receiver.is_a?(Parser::AST::Node) && receiver.type == :const &&
55
+ FACTORIES[receiver.children.last] == selector
56
+ end
57
+ end
@@ -72,7 +72,8 @@ bundle exec kimera run --isolated --jobs 4 # oracle mode (see Strengthen)
72
72
  - Rails `enum` models overlay warm (the re-declaration is idempotent), so
73
73
  their method-body mutants are judged like any other. A file labeled
74
74
  `unmutatable` could not be overlaid (a distinct, reported reason); it is not
75
- an `--isolated` case.
75
+ an `--isolated` case. Its mutants report as `unmutatable` (never gating,
76
+ never scored, with the reason in the detail), not `no_coverage`.
76
77
 
77
78
  ## Triage a surviving mutant: the only four verdicts
78
79
 
@@ -118,9 +119,10 @@ bundle exec kimera run --isolated --jobs 4 # oracle mode (see Strengthen)
118
119
  suppress the warm path.
119
120
  - `--isolated` judges on failed tests, not the exit status. A child that
120
121
  exits non-zero with zero failures (typically a SimpleCov `minimum_coverage`
121
- floor tripped by the partial run) is `harness_error`, not a kill. Children
122
- run with `KIMERA=1`, so gate the floor on it (`minimum_coverage ... unless
123
- ENV["KIMERA"]`); `kimera doctor` warns when it isn't.
122
+ floor tripped by the partial run) is `harness_error`, not a kill. Kimera
123
+ sets `KIMERA=1` wherever it loads the suite (warm, `--isolated`, doctor), so
124
+ gate the floor on it (`minimum_coverage ... unless ENV["KIMERA"]`); `kimera
125
+ doctor` warns when it isn't.
124
126
  - A cluster of `error`/`timeout` verdicts in one region usually means harness
125
127
  fragility or a missing guard, not test strength. Read the cluster before
126
128
  counting the detections.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kimera
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Giacomo GK
@@ -131,6 +131,7 @@ files:
131
131
  - lib/kimera/execution/shift/coverage_channel.rb
132
132
  - lib/kimera/execution/shift/killer_memory.rb
133
133
  - lib/kimera/execution/shift/leak_guard.rb
134
+ - lib/kimera/execution/suite_env.rb
134
135
  - lib/kimera/execution/sweep.rb
135
136
  - lib/kimera/execution/transaction_isolation.rb
136
137
  - lib/kimera/execution/trial.rb
@@ -231,6 +232,7 @@ files:
231
232
  - lib/kimera/synthesis/overlay_splice.rb
232
233
  - lib/kimera/synthesis/project.rb
233
234
  - lib/kimera/synthesis/source_map.rb
235
+ - lib/kimera/synthesis/splice_reopening.rb
234
236
  - lib/kimera/synthesis/superclass_pin.rb
235
237
  - lib/kimera/version.rb
236
238
  - skills/kimera/SKILL.md