kimera 0.1.3 → 0.1.4

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: 558ffe918ec2624918506fb0616bd8426d8bb1a4c569abb4dd31e4ef9e74217d
4
- data.tar.gz: 8ab25fa8b1f4c06085fb63606e20b1fb26691017d1bffc06c2d5dfa59b9e743c
3
+ metadata.gz: 05af564bc168e88e752fd28c2c623d8fe03b3d02402472819930015ceaff5492
4
+ data.tar.gz: 1708453bd3e4fb16b5eb054be04e94ff4ab8dc06a2c6297adc257c8e23d5fb9e
5
5
  SHA512:
6
- metadata.gz: 889e8498b185c70c2e933954c2343729bfa8292a9651bd3b9f583080e85f9e7c212c4590c9d23388c6415798e81eff89620a598701ce2de1854956f806873434
7
- data.tar.gz: bdd9bfab7f3409e107e52921bf6f210f0c25cf56a58b3afa6ebd01485ef1ea806fc5165b7fa732abd19bc1ec3eb59545d6055960f38ba6062c934891bfc9fa72
6
+ metadata.gz: 98620d29d7d69bdca3fc0bd51b51ee040e7a033627841e41e42860623cfd006d617805b817d428a2d688ca5df4c24ffde65aca0337985d7898ffae03b2f816c0
7
+ data.tar.gz: e509c0ff5416eb1cb41d8d933190d8e9d63249e010791e07ba5b0c0b0678d7e7d00aef6d2bee28dac103cfa4d8ffe6aea22b8515549b79a0bfe299f58df30d58
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4 (2026-09-27)
4
+
5
+ - Guard-style memoization is no longer mutated, just as `@x ||=` never was.
6
+ In a method that opens with `return @x if @x` (and assigns `@x` later) or
7
+ `return if @x` then `@x = true`, the guard gets no `condition` mutant and
8
+ the flag no `true => false`. Removing a memo only recomputes an equal value,
9
+ so those mutants always survived. A re-entrancy guard of the same shape is
10
+ skipped too.
11
+ - `--isolated` first runs the unmutated suite once in a fresh mirror. If it
12
+ isn't green there (for example, a test helper that won't load), the run
13
+ stops with exit 1 and the error, as a red warm baseline does. Before, every
14
+ mutant it covered was scored `killed` with no failing test named.
15
+ - An isolated mirror symlinks `vendor/`, `node_modules/` and `.bundle/`
16
+ instead of leaving them out. It creates `tmp/` and `log/` empty, and still
17
+ leaves out `.git/` and `coverage/`. A boot that needs frontend tooling or a
18
+ vendored bundle now loads in the mirror.
19
+ - An isolated kill caused by a test file failing to load now puts the load
20
+ error (Minitest: message and first backtrace lines; RSpec: its load-error
21
+ report) in the result's `detail`.
22
+ - Parallel warm workers follow Rails' fork contract more closely. Each sets
23
+ `ActiveSupport::TestCase.parallel_worker_id` to its slot, so per-worker
24
+ resources keyed on it (a Redis db, lease or key namespace, a port) no
25
+ longer collide between workers. `parallelize_before_fork` hooks run once per
26
+ fleet, not again before every spawn (including replacement workers forked
27
+ while other workers are mid-test). `parallelize_teardown` hooks get the
28
+ worker number.
29
+ - A red parallel baseline lists, for each worker that ran a failing test,
30
+ where each failure fell in that worker's run and which tests ran just
31
+ before it. A test lost when its worker hit the hard timeout or crashed now
32
+ says so, instead of showing up as an unexplained failure.
33
+
3
34
  ## 0.1.3 (2026-09-27)
4
35
 
5
36
  - `--isolated` no longer scores a child that crashes before reporting results
data/README.md CHANGED
@@ -234,6 +234,13 @@ SimpleCov.start { minimum_coverage(line: 100, branch: 100) unless ENV["KIMERA"]
234
234
 
235
235
  `kimera doctor` warns when it finds an ungated `minimum_coverage`.
236
236
 
237
+ Each isolated mirror is a copy of the project root with some exceptions:
238
+ `.git/` and `coverage/` are left out, `tmp/` and `log/` start empty, and
239
+ `vendor/`, `node_modules/` and `.bundle/` are symlinked to the originals. The
240
+ exception is a directory that holds a mutated file, which is always copied.
241
+ Before judging any mutant, `--isolated` runs the unmutated suite once in a
242
+ mirror. If it isn't green there, the run stops with exit 1 and the error.
243
+
237
244
  ### Progress output
238
245
 
239
246
  During a run, a progress bar tracks each phase on stderr. It shows only when
@@ -306,6 +313,13 @@ The *default* set is small and low-noise:
306
313
  - `!x => x`
307
314
  - forced `if` conditions
308
315
 
316
+ Memoization is not a mutation target. Dropping a memo only recomputes an equal
317
+ value, so that mutant would always survive. `@x ||= …` has no such mutant, and
318
+ neither does a guard that opens a method: `return @x if defined?(@x)`,
319
+ `return @x if @x` (with `@x` assigned later in the method), or `return if @x`
320
+ followed by `@x = true`. Everything the method computes is still mutated. A
321
+ re-entrancy guard with the run-once shape is skipped too.
322
+
309
323
  The extended families are one flag away. Use `--operators all`, or pick from:
310
324
 
311
325
  | family | mutation |
@@ -63,7 +63,7 @@ class Kimera::CLI::Run::Pass
63
63
  end
64
64
 
65
65
  def run(remaining)
66
- isolated = ->(ids) { isolation.run(ids: ids, label: "mutants (isolated)") }
66
+ isolated = ->(ids) { judge(ids) }
67
67
  return isolated.call(remaining) if @options[:isolated]
68
68
  pooled(remaining, isolated)
69
69
  end
@@ -74,8 +74,13 @@ class Kimera::CLI::Run::Pass
74
74
  harness.run(ids: together, label: "mutants (warm)").merge(isolated.call(apart))
75
75
  end
76
76
 
77
+ def judge(ids)
78
+ isolation.verify!
79
+ isolation.run(ids: ids, label: "mutants (isolated)")
80
+ end
81
+
77
82
  def isolation
78
- Kimera::Execution::IsolatedExecution.new(**core, **{ hard_timeout: @options[:hard_timeout] }.compact)
83
+ @_isolation ||= Kimera::Execution::IsolatedExecution.new(**core, **limits)
79
84
  end
80
85
 
81
86
  def core
@@ -84,6 +89,8 @@ class Kimera::CLI::Run::Pass
84
89
  .merge(framework: @options[:framework], test_files: files)
85
90
  end
86
91
 
92
+ def limits = { hard_timeout: @options[:hard_timeout] }.compact
93
+
87
94
  def split(ids)
88
95
  patterns = Array(@options[:isolate_when_covered_by])
89
96
  ids.partition do |id|
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kimera::Execution::BaselineFailure::Breakdown
4
+ MAX_FAILURES = 5
5
+ PRECEDING = 3
6
+
7
+ def initialize(failed, workers)
8
+ @failed = failed
9
+ @workers = workers
10
+ end
11
+
12
+ def to_s
13
+ lines = @workers.sort_by(&:first).filter_map { |slot, ids| worker(slot, ids) }
14
+ lines.empty? ? "" : "\n per worker (tests in the order it ran them):\n#{lines.join("\n")}"
15
+ end
16
+
17
+ private
18
+
19
+ def worker(slot, ids)
20
+ hits = ids.each_index.select { |index| @failed.include?(ids[index]) }
21
+ return if hits.empty?
22
+ " worker #{slot}: ran #{ids.size}; failed #{failures(ids, hits)}#{preceding(ids, hits.first)}"
23
+ end
24
+
25
+ def failures(ids, hits)
26
+ shown = hits.first(MAX_FAILURES).map { |index| "##{index + 1} #{ids[index]}" }
27
+ extra = hits.size - shown.size
28
+ shown.join(", ") + (extra.positive? ? " (+#{extra} more)" : "")
29
+ end
30
+
31
+ def preceding(ids, first)
32
+ before = ids[[first - PRECEDING, 0].max...first]
33
+ before.empty? ? " (first test on this worker)" : "; just before: #{before.join(", ")}"
34
+ end
35
+ end
@@ -10,15 +10,27 @@ end
10
10
  class Kimera::Execution::BaselineFailure < Kimera::Error
11
11
  MAX_DETAILS = 3
12
12
  MAX_MESSAGE = 300
13
+ LOSSES = { timeout: "its worker was killed at the hard timeout (--hard-timeout) before reporting a result" }.freeze
14
+ CRASH = "its worker died before reporting a result"
13
15
 
14
16
  class << self
15
- def build(failed, messages, command)
16
- new(summary(failed, messages, command))
17
+ def build(failed, messages, command, workers: {})
18
+ new(summary(failed, messages, command, workers: workers))
17
19
  end
18
20
 
19
- def summary(failed, messages, command)
21
+ def lost(reason) = LOSSES.fetch(reason, CRASH)
22
+
23
+ def mirrored(reason, hint)
24
+ new(
25
+ "isolated baseline is not green: the unmutated suite fails in a mirror of the project\n " \
26
+ "#{reason.gsub("\n", "\n ")}\n#{hint}"
27
+ )
28
+ end
29
+
30
+ def summary(failed, messages, command, workers: {})
20
31
  text = "baseline suite is not green: #{failed.join(", ")}"
21
32
  text += appendix(failed, messages)
33
+ text += Kimera::Execution::BaselineFailure::Breakdown.new(failed, workers).to_s
22
34
  "#{text}\n reproduce without kimera: #{command}"
23
35
  end
24
36
 
@@ -41,3 +53,5 @@ class Kimera::Execution::BaselineFailure < Kimera::Error
41
53
  end
42
54
  end
43
55
  end
56
+
57
+ require_relative "baseline_failure/breakdown"
@@ -71,7 +71,13 @@ class Kimera::Execution::BaselinePass
71
71
  end
72
72
 
73
73
  def dispatch
74
- yield(resolve: resolve, lost: loss)
74
+ yield(resolve: resolve, lost: loss, trace: tracer)
75
+ end
76
+
77
+ def journal = @_journal ||= Hash.new { |workers, slot| workers[slot] = [] }
78
+
79
+ def tracer
80
+ ->(slot, test_id) { journal[slot] << test_id }
75
81
  end
76
82
 
77
83
  def resolve
@@ -90,7 +96,8 @@ class Kimera::Execution::BaselinePass
90
96
  method(:lost)
91
97
  end
92
98
 
93
- def lost(test_id, _reason)
99
+ def lost(test_id, reason)
100
+ @messages[test_id] = Kimera::Execution::BaselineFailure.lost(reason)
94
101
  tally.failures << test_id
95
102
  @progress.tick
96
103
  end
@@ -119,6 +126,6 @@ class Kimera::Execution::BaselinePass
119
126
 
120
127
  def failure!(failed)
121
128
  command = @adapter.reproduce(failed.first(Kimera::Execution::BaselineFailure::MAX_DETAILS))
122
- raise(Kimera::Execution::BaselineFailure.build(failed, @messages, command))
129
+ raise(Kimera::Execution::BaselineFailure.build(failed, @messages, command, workers: journal))
123
130
  end
124
131
  end
@@ -126,8 +126,8 @@ class Kimera::Execution::Harness
126
126
  end
127
127
 
128
128
  def parallel(pass)
129
- pass.parallel! do |resolve:, lost:|
130
- driver.drive(adapter.test_ids, driver.method(:channel), resolve: resolve, lost: lost)
129
+ pass.parallel! do |resolve:, lost:, trace:|
130
+ driver.drive(adapter.test_ids, driver.method(:channel), resolve: resolve, lost: lost, trace: trace)
131
131
  end
132
132
  end
133
133
 
@@ -5,6 +5,7 @@ require "tmpdir"
5
5
  require_relative "../results/result"
6
6
  require_relative "../results/run_report"
7
7
  require_relative "../synthesis/overlay"
8
+ require_relative "baseline_failure"
8
9
  require_relative "isolated_plan"
9
10
  require_relative "isolated_scheduling"
10
11
  require_relative "isolated_verdict"
@@ -30,6 +31,13 @@ class Kimera::Execution::IsolatedExecution
30
31
  @options = options
31
32
  end
32
33
 
34
+ def verify!
35
+ errors.puts("kimera: isolated baseline (unmutated mirror)") unless progress.enabled?
36
+ outcome = with_mirror { |mirror| verdict(mirror, plan.suite) }
37
+ return if outcome.status == :survived
38
+ raise(Kimera::Execution::BaselineFailure.mirrored(outcome.explain(limit), Kimera::Execution::IsolatedPlan::MIRROR_HINT))
39
+ end
40
+
33
41
  def run(ids: self.ids, label: "mutants")
34
42
  progress.start(ids.size, label)
35
43
  report(ids)
@@ -9,7 +9,8 @@ begin
9
9
  rescue StandardError, ScriptError
10
10
  nil
11
11
  end
12
- RSpec.configuration.output_stream = StringIO.new
12
+ output = StringIO.new
13
+ RSpec.configuration.output_stream = output
13
14
  RSpec.configuration.error_stream = StringIO.new
14
15
  RSpec.configuration.deprecation_stream = StringIO.new
15
16
 
@@ -19,6 +20,8 @@ code = RSpec::Core::Runner.run(locations)
19
20
 
20
21
  failed = RSpec.world.all_examples.select { |example| example.execution_result.status == :failed }
21
22
  outside = RSpec.world.non_example_failure ? 1 : 0
22
- File.write(ledger, JSON.generate(failures: failed.size + outside, failing: failed.map(&:id)))
23
+ report = { failures: failed.size + outside, failing: failed.map(&:id) }
24
+ report[:load_error] = output.string.strip[0, 4000] if outside.positive?
25
+ File.write(ledger, JSON.generate(report))
23
26
 
24
27
  exit code
@@ -21,7 +21,8 @@ ARGV.clear
21
21
  begin
22
22
  files.each { |f| require File.expand_path(f) }
23
23
  rescue StandardError, ScriptError => error
24
- File.write(ledger, JSON.generate(failures: 1, failing: [], load_error: "#{error.class}: #{error.message}"))
24
+ cause = ["#{error.class}: #{error.message}", *Array(error.backtrace).first(5).map { |line| " #{line}" }]
25
+ File.write(ledger, JSON.generate(failures: 1, failing: [], load_error: cause.join("\n")))
25
26
  exit(1)
26
27
  end
27
28
 
@@ -19,6 +19,13 @@ class Kimera::Execution::IsolatedOutcome
19
19
  @detail = detail
20
20
  end
21
21
 
22
+ def explain(limit)
23
+ return "timed out after #{limit}s" if status == :timeout
24
+ named = Array(failing)
25
+ return "failing: #{named.join(", ")}" if named.any?
26
+ detail || "the test child failed without naming a test"
27
+ end
28
+
22
29
  class << self
23
30
  def judge(exit, path, stderr = "")
24
31
  return new(:timeout) if exit == :timeout
@@ -50,7 +57,7 @@ class Kimera::Execution::IsolatedOutcome
50
57
 
51
58
  def tally(exit, ledger)
52
59
  failing = Array(ledger["failing"])
53
- return new(:killed, failing) if ledger["failures"].positive?
60
+ return new(:killed, failing, ledger["load_error"]) if ledger["failures"].positive?
54
61
  return new(:survived) if exit.success?
55
62
  new(:harness_error, nil, unexplained(exit))
56
63
  end
@@ -10,7 +10,12 @@ module Kimera
10
10
  end
11
11
 
12
12
  class Kimera::Execution::IsolatedPlan
13
- MIRROR_SKIP = %w[.git tmp vendor node_modules coverage log .bundle].freeze
13
+ MIRROR_SKIP = %w[.git coverage].freeze
14
+ MIRROR_EMPTY = %w[tmp log].freeze
15
+ MIRROR_LINK = %w[vendor node_modules .bundle].freeze
16
+ MIRROR_HINT =
17
+ " the mirror copies the project except #{MIRROR_SKIP.join(", ")} " \
18
+ "(#{MIRROR_EMPTY.join(", ")} start empty; #{MIRROR_LINK.join(", ")} are symlinked)".freeze
14
19
 
15
20
  attr_reader :registry
16
21
 
@@ -34,14 +39,17 @@ class Kimera::Execution::IsolatedPlan
34
39
  child.command(mirror, locations, ledger: ledger, paths: mutables)
35
40
  end
36
41
 
42
+ def suite = selection.all
43
+
37
44
  def mutables
38
45
  @registry.files.map { |f| f.split(File::SEPARATOR).first }.uniq
39
46
  end
40
47
 
41
- class << self
42
- def mirrors(entries)
43
- entries.reject { |entry| MIRROR_SKIP.include?(entry) }
44
- end
48
+ def placement(entry)
49
+ return :copy if mutables.include?(entry)
50
+ return :skip if MIRROR_SKIP.include?(entry)
51
+ return :empty if MIRROR_EMPTY.include?(entry)
52
+ MIRROR_LINK.include?(entry) ? :link : :copy
45
53
  end
46
54
 
47
55
  private
@@ -59,8 +59,14 @@ module Kimera
59
59
 
60
60
  def populate(mirror)
61
61
  root = plan.root
62
- IsolatedPlan.mirrors(Dir.children(root)).each do |entry|
63
- FileUtils.cp_r(File.join(root, entry), File.join(mirror, entry))
62
+ Dir.children(root).each { |entry| place(File.join(root, entry), File.join(mirror, entry), entry) }
63
+ end
64
+
65
+ def place(source, target, entry)
66
+ case plan.placement(entry)
67
+ when :copy then FileUtils.cp_r(source, target)
68
+ when :link then File.symlink(source, target)
69
+ when :empty then FileUtils.mkdir_p(target) if File.directory?(source)
64
70
  end
65
71
  end
66
72
 
@@ -19,23 +19,31 @@ class Kimera::Execution::ParallelTestDatabases
19
19
  end
20
20
 
21
21
  def after_fork(index)
22
+ identify(index)
22
23
  return unless index && active?
23
24
  ActiveSupport::Testing::Parallelization.after_fork_hooks.each { |hook| hook.call(index) }
24
25
  @adapter.start
25
26
  end
26
27
 
27
28
  def before_exit(index)
28
- Array(index).each { cleanup }
29
+ Array(index).each { |worker| cleanup(worker) }
29
30
  rescue StandardError => error
30
31
  errors.puts("kimera: parallelize_teardown failed (#{error.class}: #{error.message})")
31
32
  end
32
33
 
33
34
  private
34
35
 
35
- def cleanup
36
+ def cleanup(index)
36
37
  return unless active?
37
38
  scrub
38
- ActiveSupport::Testing::Parallelization.run_cleanup_hooks.each(&:call)
39
+ ActiveSupport::Testing::Parallelization.run_cleanup_hooks.each { |hook| hook.call(index) }
40
+ end
41
+
42
+ def identify(index)
43
+ return unless index && @jobs > 1 && defined?(ActiveSupport::TestCase)
44
+ ActiveSupport::TestCase.parallel_worker_id = index
45
+ rescue NoMethodError
46
+ nil
39
47
  end
40
48
 
41
49
  def scrub
@@ -46,10 +46,11 @@ class Kimera::Execution::Pool
46
46
  @options[:coverage] = measured
47
47
  end
48
48
 
49
- def drive(queue, spawner, resolve:, lost:)
49
+ def drive(queue, spawner, resolve:, lost:, trace: nil)
50
+ database.before_fork
50
51
  Kimera::Execution::WorkerPool.new(
51
52
  queue: queue, spawner: spawner, jobs: @options.fetch(:jobs),
52
- hard_timeout: @options.fetch(:hard), resolve: resolve, lost: lost
53
+ hard_timeout: @options.fetch(:hard), resolve: resolve, lost: lost, trace: trace
53
54
  ).run
54
55
  end
55
56
 
@@ -66,7 +67,6 @@ class Kimera::Execution::Pool
66
67
  def database = @options.fetch(:paralleldb)
67
68
 
68
69
  def launch(entry, index)
69
- database.before_fork
70
70
  duty = Duty.new(entry, index, channels)
71
71
  [spawn(duty), *duty.pipes.parent!]
72
72
  end
@@ -10,6 +10,11 @@ Kimera::Execution::WorkerPool::Worker =
10
10
  self.inflight = id
11
11
  end
12
12
 
13
+ def claim(id, limit)
14
+ busy!(id)
15
+ renew(limit)
16
+ end
17
+
13
18
  def renew(limit)
14
19
  self.deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + limit
15
20
  end
@@ -29,8 +29,8 @@ class Kimera::Execution::WorkerPool
29
29
  def assign(worker)
30
30
  id = fleet.take
31
31
  return close(worker) unless id
32
- worker.busy!(id)
33
- worker.renew(limit)
32
+ worker.claim(id, limit)
33
+ trace(worker.slot, id)
34
34
  worker.offer(id)
35
35
  end
36
36
 
@@ -83,6 +83,8 @@ class Kimera::Execution::WorkerPool
83
83
  worker.renew(limit)
84
84
  end
85
85
 
86
+ def trace(slot, id) = context.options[:trace]&.call(slot, id)
87
+
86
88
  def close(worker) = shut(worker.retire)
87
89
 
88
90
  def watch
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/syntax"
4
+ require_relative "support/syntax_types"
5
+
6
+ class Kimera::MemoGuard
7
+ CONDITIONAL = "conditional"
8
+ BOOLEAN_LITERAL = "boolean_literal"
9
+ READ = Kimera::SyntaxTypes::InstanceVariableReadNode
10
+ WRITE = Kimera::SyntaxTypes::InstanceVariableWriteNode
11
+
12
+ Exemption = Data.define(:location, :operator)
13
+
14
+ class << self
15
+ def exemptions(source)
16
+ result = Kimera::Syntax.parse(source)
17
+ return [] if result.failure?
18
+ defs(result.value).flat_map { new(statements(it.body)).exemptions }
19
+ end
20
+
21
+ def statements(body) = body.is_a?(Kimera::SyntaxTypes::StatementsNode) ? body.body : []
22
+
23
+ def defs(node, found = [])
24
+ found << node if node.is_a?(Kimera::SyntaxTypes::DefNode)
25
+ node.compact_child_nodes.each { defs(it, found) }
26
+ found
27
+ end
28
+ end
29
+
30
+ def initialize(statements)
31
+ @guard, @flag, *@rest = statements
32
+ end
33
+
34
+ def exemptions
35
+ return [] unless name && (value? || flag?)
36
+ [Exemption.new(@guard.location, CONDITIONAL), *raised]
37
+ end
38
+
39
+ private
40
+
41
+ def name
42
+ predicate = @guard.predicate if guard?
43
+ predicate.name if predicate.is_a?(READ)
44
+ end
45
+
46
+ def guard? = @guard.is_a?(Kimera::SyntaxTypes::IfNode) && @guard.compact_child_nodes.size == 2 && returns?
47
+
48
+ def returns? = @guard.statements.body.size == 1 && returned.is_a?(Kimera::SyntaxTypes::ReturnNode)
49
+
50
+ def returned = @guard.statements.body.first
51
+
52
+ def value? = caches? && [@flag, *@rest].any? { named?(it, WRITE) }
53
+
54
+ def caches?
55
+ values = returned.arguments&.arguments
56
+ values&.size == 1 && named?(values.first, READ)
57
+ end
58
+
59
+ def flag? = returned.compact_child_nodes.empty? && raises?
60
+
61
+ def raises? = named?(@flag, WRITE) && @flag.value.is_a?(Kimera::SyntaxTypes::TrueNode)
62
+
63
+ def raised = flag? ? [Exemption.new(@flag.value.location, BOOLEAN_LITERAL)] : []
64
+
65
+ def named?(node, kind) = node.is_a?(kind) && node.name == name
66
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prism"
4
+ require_relative "../memo_guard"
4
5
  require_relative "../memoization"
5
6
  require_relative "mutation_point"
6
7
  require_relative "walking"
@@ -25,6 +26,8 @@ class Kimera::RegistryScan::SourceFile
25
26
 
26
27
  def tainted = @_tainted ||= Kimera::Memoization.ranges(@source)
27
28
 
29
+ def guards = @_guards ||= Kimera::MemoGuard.exemptions(@source)
30
+
28
31
  def mined(root)
29
32
  found = []
30
33
  walk(root, origin) { |node, cursor| bank(found, node, cursor) }
@@ -36,7 +39,7 @@ class Kimera::RegistryScan::SourceFile
36
39
  end
37
40
 
38
41
  def bank(found, node, cursor)
39
- pairs = harvest(applicable(cursor), node, cursor.position)
42
+ pairs = harvest(applicable(cursor), node, cursor.position).reject { |key, _| guarded?(node, key) }
40
43
  return if pairs.empty?
41
44
  found << assemble(node, pairs, cursor).taint!(tainted)
42
45
  end
@@ -46,6 +49,8 @@ class Kimera::RegistryScan::SourceFile
46
49
  cursor.position ? chosen : chosen.reject(&:statement?)
47
50
  end
48
51
 
52
+ def guarded?(node, key) = guards.include?(Kimera::MemoGuard::Exemption.new(node.location, key))
53
+
49
54
  def harvest(operators, node, position)
50
55
  operators.flat_map { |operator| operator.pairs(node, position: position) }
51
56
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kimera
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.4"
5
5
  end
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.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Giacomo GK
@@ -99,6 +99,7 @@ files:
99
99
  - lib/kimera/cli/workflows.rb
100
100
  - lib/kimera/error.rb
101
101
  - lib/kimera/execution/baseline_failure.rb
102
+ - lib/kimera/execution/baseline_failure/breakdown.rb
102
103
  - lib/kimera/execution/baseline_pass.rb
103
104
  - lib/kimera/execution/boot.rb
104
105
  - lib/kimera/execution/callback.rb
@@ -150,6 +151,7 @@ files:
150
151
  - lib/kimera/incremental/git_diff.rb
151
152
  - lib/kimera/incremental/selection.rb
152
153
  - lib/kimera/incremental/session.rb
154
+ - lib/kimera/memo_guard.rb
153
155
  - lib/kimera/memoization.rb
154
156
  - lib/kimera/operators.rb
155
157
  - lib/kimera/operators/argument_drop.rb