kimera 0.1.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.
Files changed (171) hide show
  1. checksums.yaml +7 -0
  2. data/.kimera.yml.example +59 -0
  3. data/CHANGELOG.md +36 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +528 -0
  6. data/exe/kimera +22 -0
  7. data/lib/kimera/audit/operator_audit.rb +71 -0
  8. data/lib/kimera/cli/argv.rb +15 -0
  9. data/lib/kimera/cli/baseline.rb +87 -0
  10. data/lib/kimera/cli/changed.rb +27 -0
  11. data/lib/kimera/cli/ci.rb +27 -0
  12. data/lib/kimera/cli/completion.rb +37 -0
  13. data/lib/kimera/cli/doctor.rb +115 -0
  14. data/lib/kimera/cli/flag.rb +55 -0
  15. data/lib/kimera/cli/help_text.rb +45 -0
  16. data/lib/kimera/cli/init.rb +105 -0
  17. data/lib/kimera/cli/mutant.rb +82 -0
  18. data/lib/kimera/cli/report_file.rb +20 -0
  19. data/lib/kimera/cli/run/arguments.rb +58 -0
  20. data/lib/kimera/cli/run/cycle.rb +78 -0
  21. data/lib/kimera/cli/run/digest.rb +49 -0
  22. data/lib/kimera/cli/run/emission.rb +45 -0
  23. data/lib/kimera/cli/run/gate.rb +43 -0
  24. data/lib/kimera/cli/run/options.rb +85 -0
  25. data/lib/kimera/cli/run/pass.rb +100 -0
  26. data/lib/kimera/cli/run/sources.rb +56 -0
  27. data/lib/kimera/cli/run.rb +52 -0
  28. data/lib/kimera/cli/skill.rb +23 -0
  29. data/lib/kimera/cli/suggestion.rb +36 -0
  30. data/lib/kimera/cli/survivors/panel.rb +81 -0
  31. data/lib/kimera/cli/survivors.rb +89 -0
  32. data/lib/kimera/cli/synthesize.rb +66 -0
  33. data/lib/kimera/cli/workflows.rb +8 -0
  34. data/lib/kimera/cli.rb +132 -0
  35. data/lib/kimera/error.rb +3 -0
  36. data/lib/kimera/execution/baseline_failure.rb +47 -0
  37. data/lib/kimera/execution/baseline_pass.rb +123 -0
  38. data/lib/kimera/execution/boot.rb +50 -0
  39. data/lib/kimera/execution/callback.rb +33 -0
  40. data/lib/kimera/execution/child_process.rb +48 -0
  41. data/lib/kimera/execution/composite_isolation.rb +24 -0
  42. data/lib/kimera/execution/harness.rb +150 -0
  43. data/lib/kimera/execution/isolated.rb +64 -0
  44. data/lib/kimera/execution/isolated_child.rb +15 -0
  45. data/lib/kimera/execution/isolated_child_command.rb +44 -0
  46. data/lib/kimera/execution/isolated_child_minitest.rb +34 -0
  47. data/lib/kimera/execution/isolated_plan.rb +54 -0
  48. data/lib/kimera/execution/isolated_scheduling.rb +54 -0
  49. data/lib/kimera/execution/isolated_test_selection.rb +22 -0
  50. data/lib/kimera/execution/isolated_verdict.rb +72 -0
  51. data/lib/kimera/execution/isolated_watchdog.rb +59 -0
  52. data/lib/kimera/execution/isolation.rb +17 -0
  53. data/lib/kimera/execution/null_progress.rb +17 -0
  54. data/lib/kimera/execution/overlay_guard_modules.rb +95 -0
  55. data/lib/kimera/execution/overlay_guards.rb +56 -0
  56. data/lib/kimera/execution/parallel_test_databases.rb +64 -0
  57. data/lib/kimera/execution/pool_driver.rb +101 -0
  58. data/lib/kimera/execution/priority.rb +23 -0
  59. data/lib/kimera/execution/reload.rb +104 -0
  60. data/lib/kimera/execution/rig.rb +41 -0
  61. data/lib/kimera/execution/schedule.rb +105 -0
  62. data/lib/kimera/execution/schemata.rb +88 -0
  63. data/lib/kimera/execution/shift/attempt.rb +34 -0
  64. data/lib/kimera/execution/shift/coverage_channel.rb +43 -0
  65. data/lib/kimera/execution/shift/killer_memory.rb +20 -0
  66. data/lib/kimera/execution/shift/leak_guard.rb +34 -0
  67. data/lib/kimera/execution/shift.rb +137 -0
  68. data/lib/kimera/execution/sweep.rb +30 -0
  69. data/lib/kimera/execution/transaction_isolation.rb +32 -0
  70. data/lib/kimera/execution/trial.rb +16 -0
  71. data/lib/kimera/execution/verdicts.rb +78 -0
  72. data/lib/kimera/execution/worker_pool/fleet.rb +104 -0
  73. data/lib/kimera/execution/worker_pool/stillborn_guard.rb +40 -0
  74. data/lib/kimera/execution/worker_pool/worker.rb +32 -0
  75. data/lib/kimera/execution/worker_pool.rb +101 -0
  76. data/lib/kimera/frameworks/adapter.rb +82 -0
  77. data/lib/kimera/frameworks/minitest_adapter.rb +102 -0
  78. data/lib/kimera/frameworks/rspec_adapter.rb +123 -0
  79. data/lib/kimera/frameworks/rspec_group_index.rb +45 -0
  80. data/lib/kimera/incremental/git_diff.rb +98 -0
  81. data/lib/kimera/incremental/selection.rb +27 -0
  82. data/lib/kimera/incremental/session.rb +109 -0
  83. data/lib/kimera/memoization.rb +104 -0
  84. data/lib/kimera/operators/argument_drop.rb +19 -0
  85. data/lib/kimera/operators/arithmetic.rb +12 -0
  86. data/lib/kimera/operators/audit.rb +3 -0
  87. data/lib/kimera/operators/base.rb +40 -0
  88. data/lib/kimera/operators/binary_swap.rb +23 -0
  89. data/lib/kimera/operators/boolean.rb +26 -0
  90. data/lib/kimera/operators/chain_link_deletion.rb +25 -0
  91. data/lib/kimera/operators/collection_literal.rb +18 -0
  92. data/lib/kimera/operators/comparison.rb +13 -0
  93. data/lib/kimera/operators/conditional.rb +36 -0
  94. data/lib/kimera/operators/default_argument.rb +17 -0
  95. data/lib/kimera/operators/element_drop.rb +31 -0
  96. data/lib/kimera/operators/index_fetch.rb +15 -0
  97. data/lib/kimera/operators/kernel_coercion.rb +17 -0
  98. data/lib/kimera/operators/method_unwrap.rb +20 -0
  99. data/lib/kimera/operators/negation.rb +15 -0
  100. data/lib/kimera/operators/node_swap.rb +20 -0
  101. data/lib/kimera/operators/numeric_literal.rb +39 -0
  102. data/lib/kimera/operators/op_assign.rb +18 -0
  103. data/lib/kimera/operators/plugins.rb +27 -0
  104. data/lib/kimera/operators/rails_association.rb +24 -0
  105. data/lib/kimera/operators/rails_callback.rb +49 -0
  106. data/lib/kimera/operators/rails_declaration.rb +16 -0
  107. data/lib/kimera/operators/rails_permit.rb +34 -0
  108. data/lib/kimera/operators/rails_validation.rb +17 -0
  109. data/lib/kimera/operators/range.rb +15 -0
  110. data/lib/kimera/operators/regexp_literal.rb +16 -0
  111. data/lib/kimera/operators/respond_to_guard.rb +24 -0
  112. data/lib/kimera/operators/return_value.rb +36 -0
  113. data/lib/kimera/operators/safe_navigation.rb +15 -0
  114. data/lib/kimera/operators/selector_swap.rb +24 -0
  115. data/lib/kimera/operators/statement_deletion.rb +26 -0
  116. data/lib/kimera/operators/string_literal.rb +22 -0
  117. data/lib/kimera/operators/symbol_literal.rb +26 -0
  118. data/lib/kimera/operators/vocabulary.rb +82 -0
  119. data/lib/kimera/operators.rb +141 -0
  120. data/lib/kimera/plugins.rb +3 -0
  121. data/lib/kimera/registry/builder.rb +59 -0
  122. data/lib/kimera/registry/mutation_point.rb +146 -0
  123. data/lib/kimera/registry/numbering.rb +15 -0
  124. data/lib/kimera/registry/registry.rb +120 -0
  125. data/lib/kimera/registry/source_file.rb +71 -0
  126. data/lib/kimera/registry/tally.rb +11 -0
  127. data/lib/kimera/registry/walking.rb +55 -0
  128. data/lib/kimera/report/actions.rb +38 -0
  129. data/lib/kimera/report/coloring.rb +25 -0
  130. data/lib/kimera/report/formats.rb +78 -0
  131. data/lib/kimera/report/live.rb +56 -0
  132. data/lib/kimera/report/log.rb +46 -0
  133. data/lib/kimera/report/progress.rb +55 -0
  134. data/lib/kimera/report/screen.rb +44 -0
  135. data/lib/kimera/report/sections.rb +50 -0
  136. data/lib/kimera/report/tally.rb +84 -0
  137. data/lib/kimera/report/text.rb +104 -0
  138. data/lib/kimera/results/result.rb +72 -0
  139. data/lib/kimera/results/run_report.rb +114 -0
  140. data/lib/kimera/rewrite/ast_walk.rb +20 -0
  141. data/lib/kimera/rewrite/directive.rb +61 -0
  142. data/lib/kimera/rewrite/directive_extra_handlers.rb +52 -0
  143. data/lib/kimera/rewrite/directive_handlers.rb +138 -0
  144. data/lib/kimera/rewrite/directive_kwarg_handlers.rb +44 -0
  145. data/lib/kimera/rewrite/numbered_params.rb +50 -0
  146. data/lib/kimera/runtime.rb +51 -0
  147. data/lib/kimera/scope/config.rb +90 -0
  148. data/lib/kimera/scope/file_set.rb +51 -0
  149. data/lib/kimera/scope/ignore_list.rb +55 -0
  150. data/lib/kimera/self_protection.rb +56 -0
  151. data/lib/kimera/support/duration.rb +13 -0
  152. data/lib/kimera/support/errors.rb +8 -0
  153. data/lib/kimera/support/operator_protocol.rb +10 -0
  154. data/lib/kimera/support/syntax.rb +68 -0
  155. data/lib/kimera/support/syntax_types.rb +20 -0
  156. data/lib/kimera/support/unparse.rb +15 -0
  157. data/lib/kimera/support/version.rb +5 -0
  158. data/lib/kimera/support/warnings.rb +15 -0
  159. data/lib/kimera/synthesis/file_weave.rb +78 -0
  160. data/lib/kimera/synthesis/guard_weaver.rb +77 -0
  161. data/lib/kimera/synthesis/guardrail_dispatch.rb +68 -0
  162. data/lib/kimera/synthesis/guardrail_interpolation.rb +59 -0
  163. data/lib/kimera/synthesis/guardrail_value_objects.rb +46 -0
  164. data/lib/kimera/synthesis/overlay.rb +52 -0
  165. data/lib/kimera/synthesis/overlay_splice.rb +101 -0
  166. data/lib/kimera/synthesis/project.rb +58 -0
  167. data/lib/kimera/synthesis/source_map.rb +42 -0
  168. data/lib/kimera/version.rb +3 -0
  169. data/lib/kimera.rb +13 -0
  170. data/skills/kimera/SKILL.md +118 -0
  171. metadata +259 -0
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../operators"
4
+ require_relative "../../report/formats"
5
+ require_relative "../../scope/config"
6
+ require_relative "../../scope/file_set"
7
+ require_relative "options"
8
+
9
+ class Kimera::CLI::Run::Arguments
10
+ def parse(argv)
11
+ options = defaults.merge(Kimera::Config.parse(argv))
12
+ finish(options, Kimera::CLI::RunOptions::OPTIONS.parse(argv, options))
13
+ end
14
+
15
+ private
16
+
17
+ def finish(options, rest)
18
+ validate(options)
19
+ options.except(:cli_tests).merge(scoped(options, rest))
20
+ end
21
+
22
+ def validate(options) = Kimera::Report::Formats.new.normalize(options[:format])
23
+
24
+ def scoped(options, rest)
25
+ {
26
+ tests: Kimera::Config.prefer(options[:cli_tests], options[:tests], Kimera::CLI::RunOptions::DEFAULT_TESTS),
27
+ paths: Kimera::Config.prefer(rest, options[:paths], Kimera::FileSet::DEFAULT_GLOBS)
28
+ }.merge(arrays(options))
29
+ end
30
+
31
+ def arrays(options)
32
+ { exclude: Array(options[:exclude]), ignore: Array(options[:ignore]) }
33
+ .merge(isolate_when_covered_by: Array(options[:isolate_when_covered_by]), focus: Array(options[:focus]))
34
+ end
35
+
36
+ def defaults
37
+ core.merge(gates).merge(scope)
38
+ end
39
+
40
+ def core
41
+ { framework: "rspec", source_root: ".", tests: [] }
42
+ .merge(paths: nil, operators: Kimera::Operators::DEFAULT_KEYS, soft_timeout: 5.0)
43
+ .merge(hard_timeout: nil, leak_every: 10, registry: nil)
44
+ .merge(report: nil, format: "text", focus: [], gate: true, coverage: true, require: [])
45
+ end
46
+
47
+ def gates
48
+ { since: nil, session: nil, max_survivors: nil }.merge(max_ignored: nil, max_errors: 0, jobs: 1)
49
+ end
50
+
51
+ def scope
52
+ { exclude: [], exclude_tests: [], config: nil }
53
+ .merge(ignore: [], isolate_db: false, isolated: false)
54
+ .merge(fail_on_no_coverage: false, progress: nil, color: nil, quiet: false, verbose: false, log: nil)
55
+ .merge(isolate_when_covered_by: [])
56
+ .merge(cli_tests: [])
57
+ end
58
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../incremental/selection"
4
+ require_relative "../../incremental/session"
5
+ require_relative "digest"
6
+ require_relative "pass"
7
+ require_relative "sources"
8
+
9
+ class Kimera::CLI::Run::Cycle
10
+ def initialize(options, registry, changed, digest:)
11
+ @options = options
12
+ @registry = registry
13
+ @changed = changed
14
+ @digest = digest
15
+ end
16
+
17
+ def call
18
+ @digest.verbose(@options) if @options[:verbose]
19
+ scope(@changed && Kimera::Incremental::Selection.select(@registry, @changed))
20
+ end
21
+
22
+ private
23
+
24
+ def scope(selected)
25
+ eligible = focus(selected || @registry.each.map { |mutant, _| mutant.id })
26
+ ignored = Kimera::IgnoreList.ids(@registry, @options[:ignore]) & eligible
27
+ notify(selected, ignored)
28
+ session(eligible - ignored, ignored)
29
+ end
30
+
31
+ def notify(selected, ignored)
32
+ @digest.stale(@registry, @options[:ignore])
33
+ @digest.announce(selected, ignored, files: @registry.files.size, since: @options[:since])
34
+ end
35
+
36
+ def session(todo, ignored)
37
+ loaded = Kimera::Incremental::Session.load(@options[:session], registry: @registry)
38
+ perform(todo, loaded)
39
+ conclude(todo, ignored, loaded)
40
+ end
41
+
42
+ def perform(todo, loaded)
43
+ remaining = todo.reject { |id| loaded.done?(id) }
44
+ harness(remaining, loaded) unless remaining.empty?
45
+ path = @options[:session]
46
+ loaded.save(path, registry: @registry, meta: { "since" => @options[:since] }) if path
47
+ end
48
+
49
+ def conclude(todo, ignored, loaded)
50
+ report = loaded.report(todo, ignored, @registry)
51
+ @digest.emit(report, @registry, **emission)
52
+ @options[:gate] == false ? 0 : @digest.gate(report, @options)
53
+ end
54
+
55
+ def emission
56
+ { path: @options[:report], format: @options.fetch(:format, "text"), metadata: provenance }
57
+ .merge(coverage: coverage, log: @options[:log])
58
+ end
59
+
60
+ def coverage = @options[:fail_on_no_coverage] ? :list : :hint
61
+
62
+ def focus(eligible)
63
+ ids = Array(@options[:focus])
64
+ return eligible if ids.empty?
65
+ missing = ids - eligible
66
+ missing.empty? ? ids : raise(Kimera::UsageError, "focused mutant(s) not in scope: #{missing.join(", ")}")
67
+ end
68
+
69
+ def provenance
70
+ @options.slice(:framework, :source_root, :tests, :exclude_tests, :operators, :coverage, :isolated, :jobs)
71
+ .transform_keys(&:to_s)
72
+ end
73
+
74
+ def harness(remaining, loaded)
75
+ adapter = Kimera::Frameworks::Adapter.load(@options[:framework])
76
+ Kimera::CLI::Run::Pass.new(@registry, @options, adapter).call(remaining, loaded)
77
+ end
78
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../results/run_report"
4
+ require_relative "../../scope/ignore_list"
5
+ require_relative "emission"
6
+ require_relative "gate"
7
+
8
+ class Kimera::CLI::Run::Digest
9
+ def initialize(emission:, io: $stdout, errors: $stderr)
10
+ @io = io
11
+ @errors = errors
12
+ @emission = emission
13
+ end
14
+
15
+ def emit(...) = @emission.emit(...)
16
+
17
+ def gate(report, options) = Kimera::CLI::Run::Gate.new(report, options).status(@errors)
18
+
19
+ def announce(selected, ignored, files:, since:)
20
+ incremental(selected, files, since) if selected
21
+ @io.puts("Ignoring #{ignored.size} mutant(s) marked equivalent.") unless ignored.empty?
22
+ end
23
+
24
+ def stale(registry, rules)
25
+ Kimera::IgnoreList.stale(registry, rules).each { |rule| @errors.puts(anchor(rule)) }
26
+ end
27
+
28
+ def verbose(options)
29
+ @io.puts(
30
+ "Kimera: framework=#{options[:framework]} jobs=#{options[:jobs]} " \
31
+ "sources=#{Array(options[:paths]).join(",")} tests=#{Array(options[:tests]).join(",")} " \
32
+ "format=#{options.fetch(:format, "text")}"
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ def incremental(selected, files, since)
39
+ @io.puts("Incremental: #{selected.size} mutants on lines changed since #{since} (#{files} changed files).")
40
+ end
41
+
42
+ def anchor(rule)
43
+ "kimera: warning: ignore entry matches no mutant (stale anchor?): #{spot(rule)} #{rule[:label]}".rstrip
44
+ end
45
+
46
+ def spot(rule)
47
+ [rule[:file], rule[:line]].compact.join(":")
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "stringio"
6
+ require_relative "../../report/coloring"
7
+ require_relative "../../report/formats"
8
+ require_relative "../../report/text"
9
+
10
+ class Kimera::CLI::Run::Emission
11
+ include Kimera::Report::Coloring
12
+
13
+ def initialize(io: $stdout, output: io, color: nil, quiet: false)
14
+ @io = io
15
+ @output = output
16
+ @color = color
17
+ @quiet = quiet
18
+ end
19
+
20
+ def emit(report, registry, path: nil, format: "text", metadata: nil, coverage: :hint, log: nil)
21
+ text = rendered(report, registry, coverage, path)
22
+ document = report.document(metadata)
23
+ present(text, format, document)
24
+ save(log, text) if log
25
+ save(path, JSON.pretty_generate(document)) if path
26
+ end
27
+
28
+ private
29
+
30
+ def rendered(report, registry, coverage, path)
31
+ io = StringIO.new
32
+ Kimera::Report::Text.new(registry, io: io, color: color?).report(report, coverage: coverage, path: path)
33
+ io.string
34
+ end
35
+
36
+ def present(text, format, document)
37
+ return Kimera::Report::Formats.new(io: @output).render(format, document) unless format == "text"
38
+ @io.write(text) unless @quiet
39
+ end
40
+
41
+ def save(file, text)
42
+ FileUtils.mkdir_p(File.dirname(file))
43
+ File.write(file, text)
44
+ end
45
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kimera::CLI::Run::Gate
4
+ def initialize(report, options)
5
+ @report = report
6
+ @options = options
7
+ end
8
+
9
+ def status(stream)
10
+ return 0 if reasons.empty?
11
+ reasons.each { |reason| stream.puts("kimera: gate failed: #{reason}") }
12
+ 2
13
+ end
14
+
15
+ private
16
+
17
+ def reasons = @_reasons ||= [survivors, coverage, ignored, unjudged].compact
18
+
19
+ def survivors
20
+ max = @options[:max_survivors]
21
+ count = @report.survived.size
22
+ return unless max ? count > max : count.positive?
23
+ "survivors=#{count} > max_survivors=#{max || 0}"
24
+ end
25
+
26
+ def coverage
27
+ uncovered = @report.uncovered.size
28
+ return unless @options[:fail_on_no_coverage] && uncovered.positive?
29
+ "#{uncovered} mutant(s) with no covering test (--fail-on-no-coverage)"
30
+ end
31
+
32
+ def ignored
33
+ cap = @options[:max_ignored]
34
+ count = @report.statuses(:ignored).size
35
+ "ignored=#{count} > max_ignored=#{cap}" if cap && count > cap
36
+ end
37
+
38
+ def unjudged
39
+ count = @report.errors.size
40
+ cap = Integer(@options[:max_errors] || 0)
41
+ "unjudged=#{count} > max_errors=#{cap}" if count > cap
42
+ end
43
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../flag"
4
+
5
+ module Kimera
6
+ end
7
+
8
+ class Kimera::CLI
9
+ end
10
+
11
+ module Kimera::CLI::RunOptions
12
+ DEFAULT_TESTS = ["spec/**/*_spec.rb"].freeze
13
+
14
+ OPTIONS = Kimera::FlagTable.new(
15
+ banner: "Usage: kimera run [paths...] [options]\n" \
16
+ "Exit codes: 0 gates passed, 1 usage or setup error, 2 gate failed",
17
+ flags: [
18
+ Kimera::Flag.build("--framework NAME", :framework, "Test framework (rspec)"),
19
+ Kimera::Flag.build(
20
+ "--tests GLOB", :cli_tests,
21
+ "Test file glob (repeatable; replaces config tests:)", collect: true
22
+ ),
23
+ Kimera::Flag.build("--source-root DIR", :source_root, "Project root for synthesis"),
24
+ Kimera::REGISTRY_FLAG,
25
+ Kimera::OPERATORS_FLAG,
26
+ Kimera::REQUIRE_FLAG,
27
+ Kimera::Flag.build("--soft-timeout SEC", :soft_timeout, "Per-test soft timeout", type: Float),
28
+ Kimera::Flag.build("--hard-timeout SEC", :hard_timeout, "Per-mutant hard watchdog timeout", type: Float),
29
+ Kimera::Flag.build("--leak-every N", :leak_every, "Re-check a killed mutant every N", type: Integer),
30
+ Kimera::Flag.build("--[no-]coverage", :coverage, "Run each mutant only against covering tests (default: on)"),
31
+ Kimera::Flag.build("--since REF", :since, "Incremental: only mutate lines changed vs REF (git diff)"),
32
+ Kimera::Flag.build("--session FILE", :session, "Persist/resume per-mutant results in FILE"),
33
+ Kimera::Flag.build(
34
+ "--focus ID", :focus, "Evaluate only one or more mutant IDs (repeatable)",
35
+ type: Integer, collect: true
36
+ ),
37
+ Kimera::Flag.build("--[no-]gate", :gate, "Return gate status (default: on; use --no-gate for exploration)"),
38
+ Kimera::Flag.build("--max-survivors N", :max_survivors, "Gate: fail only if survivors exceed N", type: Integer),
39
+ Kimera::Flag.build(
40
+ "--max-ignored N", :max_ignored,
41
+ "Gate: fail if more than N mutants are ignore-listed", type: Integer
42
+ ),
43
+ Kimera::Flag.build(
44
+ "--max-errors N", :max_errors,
45
+ "Gate: fail if more than N mutants could not be judged (default 0)", type: Integer
46
+ ),
47
+ Kimera::Flag.build(
48
+ "--[no-]fail-on-no-coverage", :fail_on_no_coverage,
49
+ "Gate: also fail when any mutant in scope has no covering test"
50
+ ),
51
+ Kimera::Flag.build("--jobs N", :jobs, "Concurrent workers (warm pool / isolated mirrors)", type: Integer),
52
+ Kimera::Flag.build(
53
+ "--[no-]progress", :progress, "Live progress bar on stderr (default: on when stderr is a tty)"
54
+ ),
55
+ Kimera::Flag.build(
56
+ "--[no-]color", :color,
57
+ "Color text output (default: on when stdout is a tty; respects NO_COLOR)"
58
+ ),
59
+ Kimera::Flag.build(["-q", "--quiet"], :quiet, "Suppress the final human-readable report"),
60
+ Kimera::Flag.build(["-v", "--verbose"], :verbose, "Print resolved scope and execution settings"),
61
+ Kimera::Flag.build("--log FILE", :log, "Write the final human-readable report to FILE"),
62
+ Kimera::Flag.build(
63
+ "--[no-]isolate-db", :isolate_db, "Wrap each mutant in a rolled-back ActiveRecord transaction"
64
+ ),
65
+ Kimera::Flag.build(
66
+ "--isolated", :isolated,
67
+ "Evaluate every mutant in a fresh subprocess (slow, but trustworthy for " \
68
+ "the runtime selector and harness-critical code)"
69
+ ),
70
+ Kimera::Flag.build(
71
+ "--isolate-when-covered-by SUBSTR", :isolate_when_covered_by,
72
+ "Isolate only mutants whose covering set includes a test id matching SUBSTR " \
73
+ "(repeatable) — a fast, targeted alternative to --isolated for pool-unsafe specs",
74
+ collect: true
75
+ ),
76
+ Kimera::Flag.build("--exclude GLOB", :exclude, "Exclude matching files (repeatable)", collect: true),
77
+ Kimera::Flag.build(
78
+ "--exclude-test GLOB", :exclude_tests, "Exclude matching test files (repeatable)", collect: true
79
+ ),
80
+ Kimera::CONFIG_FLAG,
81
+ Kimera::Flag.build("--report FILE", :report, "Write the canonical JSON report to FILE"),
82
+ Kimera::Flag.build("--format NAME", :format, "Output: text, json, ndjson, github, or sarif (default: text)")
83
+ ]
84
+ )
85
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../execution/harness"
4
+ require_relative "../../execution/isolated"
5
+ require_relative "../../frameworks/adapter"
6
+ require_relative "../../report/progress"
7
+ require_relative "../../results/run_report"
8
+ require_relative "../../self_protection"
9
+ require_relative "sources"
10
+
11
+ class Kimera::CLI::Run::Pass
12
+ def initialize(registry, options, adapter)
13
+ @registry = registry
14
+ @options = options
15
+ @adapter = adapter
16
+ end
17
+
18
+ def call(remaining, session)
19
+ report = with_adapter { perform(remaining) }
20
+ session.merge!(report)
21
+ report
22
+ end
23
+
24
+ private
25
+
26
+ def progress = @_progress ||= Kimera::Report::Progress.new(enabled: @options[:progress])
27
+
28
+ def with_adapter
29
+ yield
30
+ ensure
31
+ @adapter&.finish
32
+ end
33
+
34
+ def perform(remaining)
35
+ warm
36
+ run(remaining)
37
+ end
38
+
39
+ def harness = @_harness ||= Kimera::Execution::Harness.new(**settings)
40
+
41
+ def files
42
+ @_files ||= Kimera::CLI::Run::Sources.new.expand(
43
+ @options[:tests], @options[:exclude_tests], "test", "check --tests"
44
+ )
45
+ end
46
+
47
+ def settings
48
+ base.merge({ hard_timeout: @options[:hard_timeout] }.compact)
49
+ end
50
+
51
+ def base
52
+ { registry: @registry, adapter: @adapter, source_root: @options[:source_root] }
53
+ .merge(soft_timeout: @options[:soft_timeout], leak_every: @options[:leak_every], jobs: @options[:jobs])
54
+ .merge(isolate_db: @options[:isolate_db], progress: progress)
55
+ end
56
+
57
+ def warm
58
+ if @options[:isolated] || @options[:coverage]
59
+ harness.warm!(files)
60
+ else
61
+ harness.without_coverage!(files)
62
+ end
63
+ end
64
+
65
+ def run(remaining)
66
+ isolated = ->(ids) { isolation.run(ids: ids, label: "mutants (isolated)") }
67
+ return isolated.call(remaining) if @options[:isolated]
68
+ pooled(remaining, isolated)
69
+ end
70
+
71
+ def pooled(remaining, isolated)
72
+ apart, together = split(remaining)
73
+ return harness.run(ids: together, label: "mutants") if apart.empty?
74
+ harness.run(ids: together, label: "mutants (warm)").merge(isolated.call(apart))
75
+ end
76
+
77
+ def isolation
78
+ Kimera::Execution::IsolatedExecution.new(**core, **{ hard_timeout: @options[:hard_timeout] }.compact)
79
+ end
80
+
81
+ def core
82
+ { registry: @registry, root: @options[:source_root], tests: @adapter.test_ids }
83
+ .merge(coverage: harness.coverage, progress: progress, jobs: @options[:jobs])
84
+ .merge(framework: @options[:framework], test_files: files)
85
+ end
86
+
87
+ def split(ids)
88
+ patterns = Array(@options[:isolate_when_covered_by])
89
+ ids.partition do |id|
90
+ critical?(id) || patterns.any? { |pattern| matches?(harness.coverage, id, pattern) }
91
+ end
92
+ end
93
+
94
+ def critical?(id) = Kimera::SelfProtection.critical?(@registry.index.fetch(id).file)
95
+
96
+ def matches?(coverage, id, pattern)
97
+ tests = coverage.fetch(id) { coverage.fetch(id.to_s, []) }
98
+ tests.any? { |test| test.include?(pattern) }
99
+ end
100
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../error"
4
+ require_relative "../../incremental/git_diff"
5
+ require_relative "../../operators"
6
+ require_relative "../../registry/builder"
7
+ require_relative "../../registry/registry"
8
+ require_relative "../../scope/file_set"
9
+
10
+ class Kimera::CLI::Run::Sources
11
+ class << self
12
+ def changed(options)
13
+ Kimera::Incremental::GitDiff.lines(since: options[:since], root: options[:source_root])
14
+ end
15
+ end
16
+
17
+ def initialize(errors: $stderr)
18
+ @errors = errors
19
+ end
20
+
21
+ def load(options, changed = nil)
22
+ stored = options[:registry]
23
+ return Kimera::Registry.load(stored) if stored
24
+ build(options, files(options, changed))
25
+ end
26
+
27
+ def files(options, changed)
28
+ found = expand(options[:paths], options[:exclude], "source", "check paths and --exclude")
29
+ return found unless changed
30
+ root = File.expand_path(options[:source_root])
31
+ found.select { |file| changed.key?(Kimera::FileSet.relative(file, root)) }
32
+ end
33
+
34
+ def expand(patterns, exclude, kind, hint)
35
+ notice(patterns, exclude, kind)
36
+ found = Kimera::FileSet.expand(patterns, exclude: exclude)
37
+ raise(Kimera::UsageError, "no #{kind} files matched: #{Array(patterns).join(", ")} (#{hint})") if found.empty?
38
+ found
39
+ end
40
+
41
+ def notice(patterns, exclude, kind)
42
+ Kimera::FileSet.unused(patterns, exclude).each do |pattern|
43
+ @errors.puts("kimera: warning: exclude pattern matched no #{kind} files: #{pattern}")
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def build(options, files)
50
+ Kimera::RegistryScan.new(
51
+ operators: Kimera::Operators.build(keys: options[:operators]),
52
+ root: options[:source_root],
53
+ shielded: !options[:isolated]
54
+ ).build(files)
55
+ end
56
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../error"
4
+ require_relative "../plugins"
5
+
6
+ module Kimera
7
+ end
8
+
9
+ class Kimera::CLI
10
+ end
11
+
12
+ class Kimera::CLI::Run
13
+ end
14
+
15
+ require_relative "run/arguments"
16
+ require_relative "run/cycle"
17
+ require_relative "run/digest"
18
+ require_relative "run/options"
19
+ require_relative "run/sources"
20
+
21
+ class Kimera::CLI::Run
22
+ include Kimera::CLI::RunOptions
23
+
24
+ def initialize(io: $stdout, errors: $stderr)
25
+ @io = io
26
+ @errors = errors
27
+ end
28
+
29
+ def run(argv)
30
+ cycle(parse(argv)).call
31
+ rescue Kimera::Error => error
32
+ @errors.puts("kimera: #{error.message}")
33
+ 1
34
+ end
35
+
36
+ private
37
+
38
+ def parse(argv) = Kimera::CLI::Run::Arguments.new.parse(argv)
39
+
40
+ def cycle(options)
41
+ Kimera::Plugins.load!(options[:require], root: options[:source_root])
42
+ changed = options[:since] ? Kimera::CLI::Run::Sources.changed(options) : nil
43
+ registry = Kimera::CLI::Run::Sources.new(errors: @errors).load(options, changed)
44
+ Kimera::CLI::Run::Cycle.new(options, registry, changed, digest: digest(options))
45
+ end
46
+
47
+ def digest(options = {})
48
+ narration = options.fetch(:format, "text") == "text" ? @io : @errors
49
+ emission = Kimera::CLI::Run::Emission.new(io: narration, output: @io, **options.slice(:color, :quiet))
50
+ Kimera::CLI::Run::Digest.new(io: narration, errors: @errors, emission: emission)
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kimera::CLI::Skill
4
+ PATH = File.expand_path("../../../skills/kimera/SKILL.md", __dir__)
5
+
6
+ def initialize(io: $stdout, errors: $stderr)
7
+ @io = io
8
+ @errors = errors
9
+ end
10
+
11
+ def run(argv)
12
+ return usage unless argv.empty?
13
+ @io.write(File.read(PATH))
14
+ 0
15
+ end
16
+
17
+ private
18
+
19
+ def usage
20
+ @errors.puts("kimera: usage: kimera skill")
21
+ 1
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ end
5
+
6
+ class Kimera::CLI
7
+ end
8
+
9
+ class Kimera::CLI::Suggestion
10
+ MAX_EDITS = 3
11
+
12
+ def initialize(known)
13
+ @known = known
14
+ end
15
+
16
+ def hint(word)
17
+ nearest = @known.min_by { |candidate| distance(word, candidate) }
18
+ nearest && distance(word, nearest) <= MAX_EDITS ? "; did you mean #{nearest.inspect}?" : ""
19
+ end
20
+
21
+ private
22
+
23
+ def distance(left, right)
24
+ left.each_char.with_index(1).reduce(origin(right)) do |above, (char, index)|
25
+ below(above, right.each_char.map { |other| other == char ? 0 : 1 }, index)
26
+ end.last
27
+ end
28
+
29
+ def origin(word) = (0..word.length).to_a
30
+
31
+ def below(above, costs, index)
32
+ costs.each_with_index.reduce([index]) do |row, (cost, column)|
33
+ row << [above[column + 1] + 1, row[column] + 1, above[column] + cost].min
34
+ end
35
+ end
36
+ end