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,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../support/unparse"
4
+ require_relative "../registry/builder"
5
+ require_relative "../rewrite/directive"
6
+
7
+ class Kimera::Operators::Audit
8
+ OK = :ok
9
+ UNRENDERABLE = :unrenderable
10
+ IDENTICAL = :identical
11
+
12
+ Finding =
13
+ Data.define(:operator, :original, :label, :rendered, :verdict) do
14
+ def ok? = verdict == OK
15
+
16
+ def to_s = "#{operator}: #{label}\n #{original}\n -> #{rendered.inspect} (#{verdict})"
17
+ end
18
+
19
+ Rendering =
20
+ Data.define(:original, :rendered) do
21
+ def verdict
22
+ return UNRENDERABLE unless rendered
23
+ rendered == reprinted ? IDENTICAL : OK
24
+ end
25
+ private
26
+ def reprinted
27
+ Kimera::Unparse.unparse(Kimera::Unparse.parse(original))
28
+ rescue StandardError
29
+ original
30
+ end
31
+ end
32
+
33
+ class << self
34
+ def audit(operators, source, file: "audit.rb")
35
+ new(operators).audit(source, file: file)
36
+ end
37
+
38
+ def faults(operators, source, file: "audit.rb")
39
+ audit(operators, source, file: file).reject(&:ok?)
40
+ end
41
+ end
42
+
43
+ def initialize(operators)
44
+ @operators = operators
45
+ end
46
+
47
+ def audit(source, file: "audit.rb")
48
+ scan(source, file).points.flat_map { |point| findings(point) }
49
+ end
50
+
51
+ private
52
+
53
+ def operators = Array(@operators)
54
+
55
+ def scan(source, file)
56
+ Kimera::RegistryScan.new(operators: operators).source(source, file: file)
57
+ end
58
+
59
+ def findings(point)
60
+ key, original, mutants = point.project(:operator, :original_source, :mutants)
61
+ mutants.map { |mutant| finding(key, original, mutant) }
62
+ end
63
+
64
+ def finding(key, original, mutant)
65
+ rendered = Kimera::Rewrite::Directive.render(original, mutant.directive)
66
+ Finding.new(
67
+ operator: key, original: original, label: mutant.label,
68
+ rendered: rendered, verdict: Rendering.new(original, rendered).verdict
69
+ )
70
+ end
71
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ end
5
+
6
+ class Kimera::CLI
7
+ end
8
+
9
+ module Kimera::CLI::Argv
10
+ module_function
11
+
12
+ def option?(args, name) = args.any? { |arg| arg == name || arg.start_with?("#{name}=") }
13
+
14
+ def any?(args, names) = names.any? { |name| option?(args, name) }
15
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "../error"
5
+ require_relative "flag"
6
+ require_relative "report_file"
7
+
8
+ class Kimera::CLI::Baseline
9
+ COMMANDS = { "create" => :create, "review" => :review }.freeze
10
+ ENTRY_KEYS = %w[file line label].freeze
11
+
12
+ CREATE = Kimera::FlagTable.new(
13
+ banner: "Usage: kimera baseline create REPORT.json --reason TEXT [--output FILE]",
14
+ flags: [
15
+ Kimera::Flag.build("--reason TEXT", :reason, "Why this existing mutation debt is being accepted"),
16
+ Kimera::Flag.build("--output FILE", :output, "Baseline file to write (default: .kimera-baseline.yml)"),
17
+ Kimera::FORCE_FLAG.with(help: "Replace an existing baseline file")
18
+ ]
19
+ )
20
+
21
+ def initialize(io: $stdout, errors: $stderr)
22
+ @io = io
23
+ @errors = errors
24
+ end
25
+
26
+ def run(argv)
27
+ __send__(command(argv.shift), argv)
28
+ rescue Kimera::UsageError => error
29
+ @errors.puts("kimera: #{error.message}")
30
+ 1
31
+ end
32
+
33
+ private
34
+
35
+ def command(name) = COMMANDS.fetch(name) { raise(Kimera::UsageError, "usage: kimera baseline <create|review> ...") }
36
+
37
+ def create(argv)
38
+ options = { reason: nil, output: ".kimera-baseline.yml", force: false }
39
+ report, = CREATE.parse(argv, options)
40
+ check(report, options)
41
+ write(report, options[:output], options[:reason])
42
+ end
43
+
44
+ def check(report, options)
45
+ raise(Kimera::UsageError, "baseline create needs a report file") unless report
46
+ raise(Kimera::UsageError, "baseline create requires --reason TEXT") if options[:reason].to_s.strip.empty?
47
+ raise(Kimera::UsageError, "no such report: #{report}") unless File.file?(report)
48
+ output = options[:output]
49
+ overwrite(output) if File.exist?(output) && !options[:force]
50
+ end
51
+
52
+ def overwrite(output) = raise(Kimera::UsageError, "#{output} already exists (use --force to replace it)")
53
+
54
+ def write(report, output, reason)
55
+ ignored = survivors(report).map { |row| entry(row, reason) }
56
+ File.write(output, YAML.dump("format_version" => 1, "ignore" => ignored))
57
+ @io.puts("Created #{output} with #{ignored.size} accepted survivor(s).")
58
+ @io.puts("Add `baseline: #{output}` to .kimera.yml to apply it.")
59
+ 0
60
+ end
61
+
62
+ def review(argv)
63
+ file = argv.shift
64
+ raise(Kimera::UsageError, "usage: kimera baseline review BASELINE.yml") unless file && argv.empty?
65
+ raise(Kimera::UsageError, "no such baseline: #{file}") unless File.file?(file)
66
+ list(file, Array((YAML.safe_load_file(file) || {})["ignore"]))
67
+ end
68
+
69
+ def list(file, entries)
70
+ @io.puts("#{entries.size} accepted mutant(s) in #{file}:")
71
+ entries.sort_by { |entry| [entry["file"], entry["line"], entry["label"]] }.each { |entry| @io.puts(line(entry)) }
72
+ 0
73
+ end
74
+
75
+ def line(entry) = " #{entry["file"]}:#{entry["line"]} [#{entry["label"]}] — #{entry["reason"]}"
76
+
77
+ def survivors(path)
78
+ Kimera::CLI::ReportFile.parse(path).fetch("results", []).select { |row| row["status"] == "survived" }
79
+ end
80
+
81
+ def entry(row, reason)
82
+ id, *fields = row.values_at("mutant_id", *ENTRY_KEYS)
83
+ ENTRY_KEYS.zip(fields).to_h { |key, value| [key, value || missing(key, id)] }.merge("reason" => reason)
84
+ end
85
+
86
+ def missing(key, id) = raise(Kimera::UsageError, "report is missing #{key} for mutant ##{id}")
87
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../error"
4
+ require_relative "argv"
5
+ require_relative "run"
6
+
7
+ class Kimera::CLI::Changed
8
+ def initialize(io: $stdout, errors: $stderr)
9
+ @io = io
10
+ @errors = errors
11
+ end
12
+
13
+ def run(argv)
14
+ args = argv.dup
15
+ args.unshift("--since", reference) unless Kimera::CLI::Argv.option?(args, "--since")
16
+ Kimera::CLI::Run.new(io: @io, errors: @errors).run(args)
17
+ end
18
+
19
+ private
20
+
21
+ def reference
22
+ %w[origin/main main].find { |name| branch?(name) } ||
23
+ raise(Kimera::UsageError, "cannot find origin/main or main; pass --since REF")
24
+ end
25
+
26
+ def branch?(name) = system("git", "rev-parse", "--verify", "--quiet", name, out: File::NULL, err: File::NULL)
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "argv"
4
+ require_relative "run"
5
+
6
+ class Kimera::CLI::CI
7
+ DEFAULTS = [
8
+ [%w[--max-survivors], %w[--max-survivors 0]],
9
+ [%w[--fail-on-no-coverage --no-fail-on-no-coverage], %w[--fail-on-no-coverage]],
10
+ [%w[--report], %w[--report tmp/kimera/report.json]],
11
+ [%w[--format], %w[--format github]]
12
+ ].freeze
13
+
14
+ def initialize(io: $stdout, errors: $stderr)
15
+ @io = io
16
+ @errors = errors
17
+ end
18
+
19
+ def run(argv)
20
+ args = DEFAULTS.reduce(argv.dup) { |given, (names, extra)| defaulted(given, names, extra) }
21
+ Kimera::CLI::Run.new(io: @io, errors: @errors).run(args)
22
+ end
23
+
24
+ private
25
+
26
+ def defaulted(args, names, extra) = Kimera::CLI::Argv.any?(args, names) ? args : args + extra
27
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ end
5
+
6
+ class Kimera::CLI
7
+ end
8
+
9
+ class Kimera::CLI::Completion
10
+ SCRIPTS = {
11
+ "bash" => "_kimera() { COMPREPLY=( $(compgen -W '%s' -- \"${COMP_WORDS[1]}\") ); }\ncomplete -F _kimera kimera",
12
+ "zsh" => "#compdef kimera\n_arguments '1:command:(%s)'",
13
+ "fish" => "complete -c kimera -f -a '%s'"
14
+ }.freeze
15
+ SHELLS = SCRIPTS.keys.freeze
16
+
17
+ def initialize(io: $stdout, errors: $stderr)
18
+ @io = io
19
+ @errors = errors
20
+ end
21
+
22
+ def run(argv)
23
+ shell = argv.first
24
+ return usage unless SHELLS.include?(shell) && argv.size == 1
25
+ @io.puts(script(shell))
26
+ 0
27
+ end
28
+
29
+ private
30
+
31
+ def usage
32
+ @errors.puts("kimera: usage: kimera completion <#{SHELLS.join("|")}>")
33
+ 1
34
+ end
35
+
36
+ def script(shell) = format(SCRIPTS.fetch(shell), Kimera::CLI::COMMAND_NAMES.join(" "))
37
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require_relative "../error"
5
+ require_relative "../scope/config"
6
+ require_relative "../scope/file_set"
7
+ require_relative "flag"
8
+
9
+ class Kimera::CLI::Doctor
10
+ OPTIONS = Kimera::FlagTable.new(
11
+ banner: "Usage: kimera doctor [--check-baseline]",
12
+ flags: [
13
+ Kimera::Flag.build("--check-baseline", :check_baseline, "Run the configured test suite and verify it is green")
14
+ ]
15
+ )
16
+
17
+ FRAMEWORK_FEATURES = { "rspec" => "rspec/core", "minitest" => "minitest" }.freeze
18
+
19
+ def initialize(io: $stdout, errors: $stderr, root: ".")
20
+ @io = io
21
+ @errors = errors
22
+ @root = root
23
+ end
24
+
25
+ def run(argv)
26
+ options = { check_baseline: false }
27
+ OPTIONS.parse(argv, options)
28
+ verdict(checks(Kimera::Config.root(root: @root), options))
29
+ rescue Kimera::UsageError => error
30
+ usage(error)
31
+ end
32
+
33
+ private
34
+
35
+ def usage(error)
36
+ @errors.puts("kimera: #{error.message}")
37
+ 1
38
+ end
39
+
40
+ def checks(config, options)
41
+ all = [configuration(config), framework(config), sources(config), tests(config), git, rails]
42
+ options[:check_baseline] ? all << baseline(config) : all
43
+ end
44
+
45
+ def verdict(checks)
46
+ checks.each { |status, text| @io.puts("#{status} #{text}") }
47
+ failed = checks.count { |status,| status == "✗" }
48
+ return 0 if failed.zero?
49
+ @errors.puts("kimera: doctor found #{failed} blocking issue(s)")
50
+ 1
51
+ end
52
+
53
+ def configuration(config)
54
+ path = File.join(@root, Kimera::Config::DEFAULT_PATH)
55
+ return ["!", "Configuration: none found; run `kimera init`"] unless File.file?(path)
56
+ ["✓", "Configuration: #{Kimera::Config::DEFAULT_PATH} (#{config.fetch(:framework, "rspec")})"]
57
+ end
58
+
59
+ def framework(config)
60
+ name = config.fetch(:framework, "rspec").to_s
61
+ feature = FRAMEWORK_FEATURES.fetch(name) { return ["✗", "Framework: unknown #{name}; use rspec or minitest"] }
62
+ return ["✓", "Framework: #{name} is installed"] if Gem.find_files(feature).any?
63
+ ["✗", "Framework: #{name} is not in this bundle; fix framework: in .kimera.yml or add it to the Gemfile"]
64
+ end
65
+
66
+ def sources(config)
67
+ count = glob(config.fetch(:paths, Kimera::FileSet::DEFAULT_GLOBS)).size
68
+ found(count, "Source scope: #{count} Ruby files", "Source scope: no Ruby files found; fix paths: in .kimera.yml")
69
+ end
70
+
71
+ def tests(config)
72
+ count = test_files(config).size
73
+ found(count, "Test discovery: #{count} files", "Test discovery: no test files found; fix tests: in .kimera.yml")
74
+ end
75
+
76
+ def found(count, present, absent) = count.zero? ? ["✗", absent] : ["✓", present]
77
+
78
+ def git
79
+ return ["✓", "Git: incremental runs are available"] if File.directory?(File.join(@root, ".git"))
80
+ ["!", "Git: not a repository; `kimera changed` is unavailable"]
81
+ end
82
+
83
+ def rails
84
+ return ["!", "Rails detected: verify parallel database isolation before using --jobs > 1"] if rails?
85
+ ["✓", "Runtime: no Rails-specific parallel setup required"]
86
+ end
87
+
88
+ def rails? = File.file?(File.join(@root, "config", "application.rb"))
89
+
90
+ def baseline(config)
91
+ output, status = Open3.capture2e(*command(config), chdir: @root)
92
+ return ["✓", "Baseline: configured test suite is green"] if status.success?
93
+ ["✗", "Baseline: #{summary(output)} (fix it, then rerun `kimera doctor --check-baseline`)"]
94
+ rescue Errno::ENOENT
95
+ ["✗", "Baseline: Bundler is unavailable; run your test suite, then retry"]
96
+ end
97
+
98
+ def command(config)
99
+ files = test_files(config)
100
+ minitest?(config) ? ["bundle", "exec", "ruby", "-Itest", *files] : ["bundle", "exec", "rspec", *files]
101
+ end
102
+
103
+ def summary(output)
104
+ output.lines.reverse.find { |line| line.match?(/\d+ failures?|\d+ examples?|failed/i) }&.strip ||
105
+ "test command failed"
106
+ end
107
+
108
+ def minitest?(config) = config.fetch(:framework, "rspec") == "minitest"
109
+
110
+ def test_files(config) = glob(config.fetch(:tests, tests_for(config)))
111
+
112
+ def tests_for(config) = minitest?(config) ? ["test/**/*_test.rb"] : ["spec/**/*_spec.rb"]
113
+
114
+ def glob(patterns) = Array(patterns).flat_map { |pattern| Dir.glob(File.join(@root, pattern)) }.uniq
115
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "../error"
5
+
6
+ module Kimera
7
+ Flag =
8
+ Data.define(:switch, :key, :help, :type, :collect) do
9
+ def self.build(switch, key, help, type: nil, collect: false)
10
+ new(switch: switch, key: key, help: help, type: type, collect: collect)
11
+ end
12
+
13
+ def define(parser, options)
14
+ parser.on(*signature) { |value| store(options, value) }
15
+ end
16
+ private
17
+ def signature = [*switch, type, help].compact
18
+
19
+ def store(options, value)
20
+ collect ? options[key] << value : options[key] = value
21
+ end
22
+ end
23
+
24
+ FlagTable =
25
+ Data.define(:banner, :flags) do
26
+ def parse(argv, options)
27
+ build(options).parse(argv)
28
+ rescue OptionParser::ParseError => error
29
+ raise(UsageError, "#{error.message}\n#{banner}")
30
+ end
31
+
32
+ def build(options)
33
+ OptionParser.new do |parser|
34
+ parser.banner = banner
35
+ flags.each { |flag| flag.define(parser, options) }
36
+ end
37
+ end
38
+
39
+ def keys = flags.map(&:key)
40
+ end
41
+
42
+ FORCE_FLAG = Flag.build("--force", :force, "Replace an existing file")
43
+ CONFIG_FLAG = Flag.build("--config FILE", :config, "Load config (default: .kimera.yml)")
44
+ REGISTRY_FLAG = Flag.build("--registry FILE", :registry, "Load registry JSON instead of building")
45
+ REQUIRE_FLAG = Flag.build(
46
+ "--require FILE", :require,
47
+ "Load a Ruby file before scanning, for custom operators (repeatable)",
48
+ collect: true
49
+ )
50
+ OPERATORS_FLAG = Flag.build(
51
+ "--operators a,b,c", :operators,
52
+ "Operator keys, or 'all'/'rails' (default: conservative core)",
53
+ type: Array
54
+ )
55
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../scope/file_set"
4
+ require_relative "../version"
5
+
6
+ module Kimera
7
+ module HelpText
8
+ HELP = <<~HELP.freeze
9
+ kimera #{Kimera::VERSION} -- schemata-based mutation testing for Ruby
10
+ (every mutant is compiled into one guarded source tree, so the suite
11
+ loads once, not once per mutant)
12
+
13
+ Usage: kimera <command> [options]
14
+
15
+ Start here:
16
+ init Detect this project and create .kimera.yml
17
+ doctor Validate source/test discovery and runtime safety
18
+ run [paths...] Run mutation tests locally
19
+ changed [paths...] Mutate only code changed since origin/main (or main)
20
+ ci [paths...] Strict CI gate; writes a report artifact
21
+
22
+ Investigate:
23
+ report REPORT.json Filter a saved report (survivors, uncovered, errors)
24
+ mutant ID --report R Show one mutant in full detail
25
+ registry [paths...] Inspect what Kimera would mutate
26
+ synthesize [paths...] Write schemata sources for inspection
27
+
28
+ Setup:
29
+ baseline ... Create or review a baseline of accepted survivors
30
+ completion SHELL Print Bash, Zsh, or Fish completion setup
31
+ skill Print the guide for AI agents running Kimera
32
+ version Print version
33
+ help Show this help
34
+
35
+ With no paths, defaults to: #{Kimera::FileSet::DEFAULT_GLOBS.join(", ")}
36
+
37
+ Exit codes: 0 ok, 1 usage or setup error, 2 gate failed (run)
38
+
39
+ Examples:
40
+ kimera init && kimera doctor
41
+ kimera changed --report tmp/kimera/report.json
42
+ kimera report tmp/kimera/report.json --status no_coverage
43
+ HELP
44
+ end
45
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "etc"
4
+ require "yaml"
5
+ require_relative "../error"
6
+ require_relative "../scope/config"
7
+ require_relative "../scope/file_set"
8
+ require_relative "flag"
9
+
10
+ class Kimera::CLI::Init
11
+ OPTIONS = Kimera::FlagTable.new(
12
+ banner: "Usage: kimera init [options]",
13
+ flags: [
14
+ Kimera::FORCE_FLAG.with(help: "Replace an existing .kimera.yml"),
15
+ Kimera::Flag.build("--dry-run", :dry_run, "Print the detected configuration without writing it")
16
+ ]
17
+ )
18
+
19
+ HELPERS = %w[spec/spec_helper.rb spec/rails_helper.rb spec/helper.rb test/test_helper.rb test/helper.rb].freeze
20
+ AGENT_POINTER = "Mutation testing: before running kimera or triaging its results, read `bundle exec kimera skill`.\n"
21
+
22
+ def initialize(io: $stdout, errors: $stderr, root: ".")
23
+ @io = io
24
+ @errors = errors
25
+ @root = root
26
+ end
27
+
28
+ def run(argv)
29
+ options = { force: false, dry_run: false }
30
+ OPTIONS.parse(argv, options)
31
+ options[:dry_run] ? preview : write(options)
32
+ rescue Kimera::UsageError => error
33
+ usage(error)
34
+ end
35
+
36
+ private
37
+
38
+ def usage(error)
39
+ @errors.puts("kimera: #{error.message}")
40
+ 1
41
+ end
42
+
43
+ def preview
44
+ @io.write(YAML.dump(detected))
45
+ 0
46
+ end
47
+
48
+ def write(options)
49
+ path = File.join(@root, Kimera::Config::DEFAULT_PATH)
50
+ overwrite(path) if File.exist?(path) && !options[:force]
51
+ File.write(path, YAML.dump(detected))
52
+ announce(*signpost)
53
+ end
54
+
55
+ def overwrite(path) = raise(Kimera::UsageError, "#{path} already exists (use --force to replace it)")
56
+
57
+ def signpost
58
+ path = File.join(@root, "AGENTS.md")
59
+ existing = File.file?(path) ? File.read(path) : ""
60
+ return [] if existing.include?("kimera skill")
61
+ File.write(path, pointed(existing))
62
+ ["Pointed AI agents to `kimera skill` in AGENTS.md."]
63
+ end
64
+
65
+ def pointed(text) = [text.rstrip, AGENT_POINTER].reject(&:empty?).join("\n\n")
66
+
67
+ def announce(*notes)
68
+ @io.puts("Created #{Kimera::Config::DEFAULT_PATH} for #{framework}.", *notes, "Next: kimera doctor")
69
+ 0
70
+ end
71
+
72
+ def detected
73
+ Kimera::Config.document(
74
+ framework: framework, paths: sources, tests: tests,
75
+ jobs: [Etc.nprocessors, 8].min, max_survivors: 0, max_errors: 0, fail_on_no_coverage: false
76
+ )
77
+ end
78
+
79
+ def framework = @_framework ||= declared || layout
80
+
81
+ def declared
82
+ return "rspec" if File.file?(File.join(@root, ".rspec"))
83
+ helper = HELPERS.map { |path| File.join(@root, path) }.find { |path| File.file?(path) }
84
+ helper && framework_named_in(File.read(helper))
85
+ end
86
+
87
+ def framework_named_in(text)
88
+ return "rspec" if text.match?(/\bRSpec\b|require\s*\(?\s*["']rspec/)
89
+ "minitest" if text.match?(/minitest/i)
90
+ end
91
+
92
+ def layout
93
+ File.directory?(File.join(@root, "test")) && !File.directory?(File.join(@root, "spec")) ? "minitest" : "rspec"
94
+ end
95
+
96
+ def sources
97
+ paths = %w[app/**/*.rb lib/**/*.rb].select { |glob| Dir.glob(File.join(@root, glob)).any? }
98
+ paths.empty? ? Kimera::FileSet::DEFAULT_GLOBS : paths
99
+ end
100
+
101
+ def tests
102
+ globs = framework == "minitest" ? %w[test/**/*_test.rb spec/**/*_spec.rb] : %w[spec/**/*_spec.rb]
103
+ [globs.find { |glob| Dir.glob(File.join(@root, glob)).any? } || globs.first]
104
+ end
105
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../error"
4
+ require_relative "flag"
5
+ require_relative "report_file"
6
+ require_relative "run"
7
+ require_relative "survivors"
8
+
9
+ class Kimera::CLI::Mutant
10
+ OPTIONS = Kimera::FlagTable.new(
11
+ banner: "Usage: kimera mutant ID --report REPORT.json",
12
+ flags: [
13
+ Kimera::Flag.build("--report FILE", :report, "Report generated by kimera run"),
14
+ Kimera::Flag.build("--rerun", :rerun, "Re-evaluate only this mutant in the current project")
15
+ ]
16
+ )
17
+
18
+ def initialize(io: $stdout, errors: $stderr)
19
+ @io = io
20
+ @errors = errors
21
+ end
22
+
23
+ def run(argv)
24
+ options = { report: nil, rerun: false }
25
+ id, = OPTIONS.parse(argv, options)
26
+ show(identify(id, options), options)
27
+ rescue Kimera::UsageError => error
28
+ usage(error)
29
+ end
30
+
31
+ private
32
+
33
+ def usage(error)
34
+ @errors.puts("kimera: #{error.message}")
35
+ 1
36
+ end
37
+
38
+ def identify(id, options)
39
+ raise(Kimera::UsageError, "mutant ID is required") unless id
40
+ raise(Kimera::UsageError, "--report REPORT.json is required") unless options[:report]
41
+ Integer(id)
42
+ rescue ArgumentError
43
+ raise(Kimera::UsageError, "mutant ID must be an integer")
44
+ end
45
+
46
+ def show(id, options)
47
+ report = options[:report]
48
+ return rerun(id, report) if options[:rerun]
49
+ Kimera::CLI::Survivors.new(io: @io, errors: @errors).run([report, "--id", id.to_s])
50
+ end
51
+
52
+ def rerun(id, report)
53
+ document = Kimera::CLI::ReportFile.parse(report)
54
+ row = locate(document, id)
55
+ @io.puts("Re-running mutant ##{id} in #{row["file"]}.")
56
+ Kimera::CLI::Run.new(io: @io, errors: @errors).run(arguments_for(row, id, provenance(document)))
57
+ end
58
+
59
+ def locate(document, id)
60
+ row = document.fetch("results", []).find { |result| result["mutant_id"] == id }
61
+ raise(Kimera::UsageError, "no mutant ##{id} in this report") unless row
62
+ raise(Kimera::UsageError, "mutant ##{id} has no source file") unless row["file"]
63
+ row
64
+ end
65
+
66
+ def provenance(document)
67
+ document["run"] ||
68
+ raise(Kimera::UsageError, "report has no run provenance; rerun with a report created by this Kimera version")
69
+ end
70
+
71
+ def arguments_for(row, id, settings)
72
+ [row.fetch("file"), "--focus", id.to_s, "--framework", settings.fetch("framework", "rspec")]
73
+ .push("--source-root", settings["source_root"])
74
+ .concat(Array(settings["tests"]).flat_map { |glob| ["--tests", glob] })
75
+ .push("--operators", Array(settings["operators"]).join(","))
76
+ .concat(modes(settings))
77
+ end
78
+
79
+ def modes(settings)
80
+ [settings["coverage"] == false ? "--no-coverage" : nil, settings["isolated"] ? "--isolated" : nil].compact
81
+ end
82
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../error"
5
+
6
+ module Kimera
7
+ end
8
+
9
+ class Kimera::CLI
10
+ end
11
+
12
+ module Kimera::CLI::ReportFile
13
+ module_function
14
+
15
+ def parse(path)
16
+ JSON.parse(File.read(path))
17
+ rescue JSON::ParserError => error
18
+ raise(Kimera::UsageError, "unreadable report #{path}: #{error.message}")
19
+ end
20
+ end