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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1a60ca56321e6bfd3e82903b80f1ad75fc16e0647fff5f88be9042424643e3f0
4
+ data.tar.gz: d204852e218e3707f920e9ab5b1efb422395c971b2fa69aecb5a571ac28b7d73
5
+ SHA512:
6
+ metadata.gz: 63e971e4626da90289ad7b6f8a9483190820b6defa09f12d3e070639d3606fdde01cccaf73da24774de75b212764623747be187f3435b9e8cb7e4e50223e0d1f
7
+ data.tar.gz: be9614ecc3c6d9a89add30e5893a6ccbb5c934b0b06c5df518f19cd0483a9ade6eded8cfd15409c762cb147bf3f6c6803bf75607190e755977bdac0c2b2753a6
@@ -0,0 +1,59 @@
1
+ # Example Kimera configuration. Copy to .kimera.yml and adjust.
2
+ # Every key is optional; command-line flags override these values.
3
+
4
+ # Test framework: rspec (default) or minitest
5
+ framework: rspec
6
+
7
+ # Files to mutate (globs, dirs, or paths)
8
+ paths:
9
+ - "app/**/*.rb"
10
+ - "lib/**/*.rb"
11
+
12
+ # Test files to run
13
+ tests:
14
+ - "spec/**/*_spec.rb"
15
+
16
+ # Files to skip
17
+ exclude:
18
+ - "app/legacy/**/*.rb"
19
+
20
+ # Mutation operators to enable (default: this low-noise set; see the README
21
+ # for the extended and rails families, or use "all")
22
+ operators:
23
+ - comparison
24
+ - boolean_connective
25
+ - boolean_literal
26
+ - statement_deletion
27
+ - negation
28
+ - conditional
29
+
30
+ # Concurrency: number of warm pool workers pulling from one shared queue
31
+ jobs: 4
32
+
33
+ # CI gate: fail only if survivors exceed this count (omit for "any survivor fails")
34
+ max_survivors: 0
35
+
36
+ # Budget for the ignore list below: the gate fails when more than this many
37
+ # mutants are ignore-listed, so growing the list is a deliberate, reviewable act.
38
+ max_ignored: 1
39
+
40
+ # Also fail when any mutant in scope has no covering test. Best on incremental
41
+ # runs (--since), where "no coverage" means the diff adds untested logic.
42
+ fail_on_no_coverage: false
43
+
44
+ # Per-test soft timeout / per-mutant hard watchdog timeout, in seconds
45
+ soft_timeout: 5.0
46
+ # hard_timeout: 16.0
47
+
48
+ # Equivalent-mutant suppression. Equivalence is undecidable, so mark known
49
+ # equivalents by hand; matched mutants are excluded from the score and gate.
50
+ # A rule matches when every field it lists matches (file is a glob). Every
51
+ # entry must carry a reason: — entries without one are rejected at startup.
52
+ ignore:
53
+ - file: "app/models/discount.rb"
54
+ line: 33
55
+ label: "> => >="
56
+ reason: known equivalent at the 50 boundary (spend > 50 vs >= 50 both hit the tier)
57
+ # - file: "app/services/*.rb"
58
+ # original: "a == b"
59
+ # reason: why this mutant is equivalent
data/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-09-26)
4
+
5
+ Initial public release.
6
+
7
+ - Schemata-based mutation testing for Ruby and Rails. All mutants for a file
8
+ compile into one guarded-dispatch program and are selected at runtime, so the
9
+ suite loads once and mutants flip in a warm process.
10
+ - Warm worker pool (`--jobs N`) pulling from one shared, heaviest-first queue,
11
+ with opt-in per-mutant transaction rollback (`--isolate-db`), cache resets,
12
+ per-worker test databases, and a hard-killable watchdog.
13
+ - Covering tests run most-recent-killer first, cutting test executions without
14
+ narrowing the measured covering set.
15
+ - Diff-driven incremental mode (`--since`) with resumable sessions and CI
16
+ gating (`--max-survivors`, `--max-errors`, `--fail-on-no-coverage`).
17
+ - Isolated mode (`--isolated`): each mutant baked into a fresh tree and judged
18
+ in a clean subprocess.
19
+ - RSpec and Minitest adapters, including `before(:suite)` / `after(:suite)`
20
+ hooks. `--exclude-test GLOB` drops unrunnable test files. A red baseline
21
+ prints how to reproduce it without Kimera.
22
+ - Low-noise default operators, plus extended and Rails-aware families
23
+ (`--operators all`). These include contract operators (`index_fetch`,
24
+ `kernel_coercion`, `respond_to_guard`, `symbol_literal`, `default_argument`)
25
+ and `chain_link_deletion` (`a.where(x).recent.first` => `a.where(x).first`).
26
+ - Harness failures get their own `harness_error` status and never count as
27
+ kills.
28
+ - Equivalent-mutant ignore list: a reason is required, `max_ignored` budgets
29
+ it, `column:` pins ambiguous lines, and a warning fires when an entry no
30
+ longer matches any mutant.
31
+ - `kimera skill` prints a bundled guide for AI coding agents: how to run,
32
+ triage, and gate with Kimera, and the rules against gaming the score.
33
+ `kimera init` points agents to it from `AGENTS.md`.
34
+ - Kimera holds itself to zero surviving mutants: the full warm self-host run
35
+ gates at `max_survivors: 0`, with harness-critical files judged in the
36
+ isolated tier.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kimera contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,528 @@
1
+ <h1 align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
4
+ <img src="assets/logo-light.svg" alt="Kimera" width="464">
5
+ </picture>
6
+ </h1>
7
+
8
+ Mutation testing for Ruby and Rails, fast enough to gate a pull request.
9
+
10
+ Kimera plants small bugs ("mutants") in your code and checks that a test fails
11
+ for each one. A mutant no test catches is a **survivor**: a hole in your suite
12
+ that line coverage can't see. Kimera compiles all mutants into your code *once*
13
+ and flips them at runtime, so your app boots once, not once per mutant.
14
+
15
+ - **Fast.** [Stryker](https://stryker-mutator.io)-style mutant schemata
16
+ ([Untch, Offutt & Harrold, 1993](https://doi.org/10.1145/154183.154265)): no
17
+ reparse or reload per mutant. On an 8-core Apple M3, dry-inflector's `lib`
18
+ (742 mutants, `--operators all`) runs in 17s with `--jobs 4`. Kimera's own
19
+ codebase (2,552 mutants) takes about 6 minutes with `--jobs 8`, mostly in the
20
+ fresh-process tier for harness-critical files.
21
+ - **Parallel.** Warm workers, forked once, pull mutants from one shared
22
+ heaviest-first queue. A single large file spreads across every core.
23
+ - **Rails-ready.** Opt-in transaction rollback (`--isolate-db`), per-worker
24
+ test databases via Rails' parallel-testing hooks, and 4 Rails operator
25
+ families (strong params, validations, callbacks, associations).
26
+ - **Built for CI.** `kimera changed` mutates only the lines a PR touched.
27
+ `kimera ci` fails the build on any survivor or untested change.
28
+ - **Low noise.** A small default operator set. 19 extended families are one
29
+ flag away, and you can write your own.
30
+ - **Honest verdicts.** Code the warm path can't judge safely is reported
31
+ separately, never scored. `--isolated` re-checks any mutant in a fresh
32
+ process.
33
+ - **Small.** Ruby >= 3.4 (CI covers 3.4 and 4.0), RSpec or Minitest, and two
34
+ dependencies: [Prism](https://github.com/ruby/prism) and
35
+ [unparser](https://github.com/mbj/unparser).
36
+
37
+ ```sh
38
+ $ bundle exec kimera run app --since origin/main --tests 'spec/**/*_spec.rb'
39
+ mutants=28 killed=17 survived=5 timeout=0 error=0 no_coverage=6 score=77.3%
40
+
41
+ survived #1 app/models/discount.rb:18 [<= => <]
42
+ - order_total <= 0
43
+ + order_total < 0
44
+ covered by 7 test(s): ./spec/discount_spec.rb[1:1:1], …
45
+ ```
46
+
47
+ This survivor says no test checks an order total of exactly `0`. Add that test
48
+ and the mutant dies.
49
+
50
+ **Contents:** [Getting started](#getting-started) ·
51
+ [Commands](#commands) · [Configuration](#configuration) ·
52
+ [Operators](#operators) ([Rails](#rails), [Custom](#custom-operators)) ·
53
+ [How it works](#how-it-works) · [Design notes](#design-notes--known-limits) ·
54
+ [Development](#development)
55
+
56
+ ---
57
+
58
+ ## Getting started
59
+
60
+ **1. Add Kimera to your Gemfile.** It runs inside your app's bundle so
61
+ dependency versions resolve consistently.
62
+
63
+ ```ruby
64
+ gem "kimera", group: :test
65
+ ```
66
+
67
+ ```sh
68
+ bundle install
69
+ ```
70
+
71
+ **2. Generate a config.** `init` detects RSpec or Minitest, your source roots
72
+ (`app/`, `lib/`), and your test glob. It writes `.kimera.yml`, and adds a line
73
+ to `AGENTS.md` pointing AI coding agents to `kimera skill`: a guide to running
74
+ Kimera and triaging survivors without gaming the score. It matches your
75
+ installed version and works with any agent that can run a shell command.
76
+
77
+ ```sh
78
+ bundle exec kimera init
79
+ ```
80
+
81
+ **3. Check the setup.** `doctor` confirms source and test discovery, Git, and
82
+ Rails parallel-safety. `--check-baseline` also runs your suite once to confirm
83
+ it is green.
84
+
85
+ ```sh
86
+ bundle exec kimera doctor --check-baseline
87
+ ```
88
+
89
+ **4. Run it.** Start with the code your branch changed, then widen.
90
+
91
+ ```sh
92
+ bundle exec kimera changed # lines changed since origin/main (or main)
93
+ bundle exec kimera run app/models # one directory
94
+ bundle exec kimera run # every path in .kimera.yml
95
+ ```
96
+
97
+ **5. Triage survivors.** Save a report so you can dig in without rerunning the
98
+ suite.
99
+
100
+ ```sh
101
+ bundle exec kimera run --report tmp/kimera/report.json
102
+ bundle exec kimera report tmp/kimera/report.json --status survived
103
+ bundle exec kimera mutant 42 --report tmp/kimera/report.json
104
+ ```
105
+
106
+ For each survivor, write the test that kills it. If a mutant is truly
107
+ equivalent, add it to `ignore:` in `.kimera.yml` with a reason (see
108
+ [Configuration](#configuration)).
109
+
110
+ **6. Gate pull requests.** `kimera ci` fails on any survivor or uncovered
111
+ mutant. It writes `tmp/kimera/report.json` and prints GitHub annotations. Pass
112
+ `--since` to mutate only the PR's lines (this needs full Git history):
113
+
114
+ ```yaml
115
+ # .github/workflows/mutation.yml
116
+ name: Mutation
117
+ on: pull_request
118
+ jobs:
119
+ kimera:
120
+ runs-on: ubuntu-latest
121
+ steps:
122
+ - uses: actions/checkout@v4
123
+ with:
124
+ fetch-depth: 0
125
+ - uses: ruby/setup-ruby@v1
126
+ with:
127
+ bundler-cache: true
128
+ - run: bundle exec kimera ci --since origin/${{ github.base_ref }}
129
+ ```
130
+
131
+ > **Adopting on an existing suite?** Snapshot today's survivors as reviewed
132
+ > debt, so the gate blocks only *new* holes:
133
+ > `bundle exec kimera baseline create tmp/kimera/report.json --reason "adopting Kimera"`.
134
+ > Then add the printed `baseline:` line to `.kimera.yml`.
135
+
136
+ ---
137
+
138
+ ## Commands
139
+
140
+ ```sh
141
+ # Summarize what would be mutated
142
+ bundle exec kimera registry app
143
+
144
+ # Write the synthesized schemata mirror (for inspection)
145
+ bundle exec kimera synthesize app --out tmp/schemata
146
+
147
+ # Run the full mutation suite
148
+ bundle exec kimera run app --tests 'spec/**/*_spec.rb'
149
+
150
+ # Minitest
151
+ bundle exec kimera run app --framework minitest --tests 'test/**/*_test.rb'
152
+
153
+ # Incremental: mutate only lines changed since main, gate CI, resume on retry
154
+ bundle exec kimera run app --since origin/main \
155
+ --session tmp/kimera.json --max-survivors 0 --report tmp/report.json
156
+
157
+ # Also fail when a changed line has no covering test
158
+ # (a no_coverage mutant is untested new logic, not a pass)
159
+ bundle exec kimera run app --since origin/main --fail-on-no-coverage
160
+
161
+ # N warm workers pull from one shared queue, so this scales with cores
162
+ # even on a single big file
163
+ bundle exec kimera run app --jobs 4
164
+
165
+ # Drop spec files that can't run this way (order-dependent, need a browser)
166
+ # without rewriting the whole --tests glob
167
+ bundle exec kimera run app --exclude-test 'spec/system/**/*_spec.rb'
168
+
169
+ # Judge every mutant in a fresh subprocess: bake it into a mirror of the tree
170
+ # and run its covering tests under plain rspec. Slower, but trustworthy even
171
+ # for code that can't be instrumented in-process.
172
+ bundle exec kimera run app --isolated
173
+ ```
174
+
175
+ ### Daily and CI workflows
176
+
177
+ ```sh
178
+ # Mutate only the PR's changed lines (origin/main, falling back to main)
179
+ bundle exec kimera changed --report tmp/kimera/report.json
180
+
181
+ # Strict gate: zero survivors, no uncovered mutants, a JSON artifact, and
182
+ # GitHub annotations. Override with --format sarif/json.
183
+ bundle exec kimera ci
184
+
185
+ # Continue triage without rerunning the suite
186
+ bundle exec kimera report tmp/kimera/report.json --status no_coverage
187
+ bundle exec kimera mutant 42 --report tmp/kimera/report.json
188
+
189
+ # Snapshot pre-existing survivors while adopting Kimera
190
+ bundle exec kimera baseline create tmp/kimera/report.json --reason "adopting Kimera"
191
+ # Add the printed `baseline:` entry to .kimera.yml, then review it in code review
192
+ bundle exec kimera baseline review .kimera-baseline.yml
193
+ ```
194
+
195
+ ### Output formats and exit codes
196
+
197
+ - `--format json`, `ndjson`, `github`, and `sarif` write *only* that format to
198
+ stdout. Progress and diagnostics go to stderr.
199
+ - `--report FILE` always writes the canonical JSON report for later triage.
200
+ - Text output honors `NO_COLOR`. `--no-color` forces it off.
201
+ - `--quiet` suits scripts that only need an artifact. `--verbose` prints the
202
+ resolved scope. `--log FILE` keeps the final text report.
203
+
204
+ Exit codes:
205
+
206
+ - **`0`**: pass.
207
+ - **`2`**: the gate failed. Survivors exceed the threshold (default: any
208
+ survivor), or a mutant in scope has no covering test under
209
+ `--fail-on-no-coverage`, ignored mutants exceed `max_ignored`, or more
210
+ mutants than `--max-errors` (default 0) could not be judged.
211
+ - **`1`**: the baseline is not green, the pool cannot keep a worker alive, or
212
+ the invocation is invalid. Paths that match no source files, `--tests` that
213
+ matches no test files, and an unknown `--since` ref all count. An empty or
214
+ broken scope fails loudly instead of scoring perfectly over nothing.
215
+
216
+ `--isolated` trades speed for trust. It never overlays schemata or shares a
217
+ process with the code under test. On ordinary code it reaches the same verdicts
218
+ as the warm path. It can also test what the warm path can't: the runtime
219
+ selector and Kimera's own harness-critical files (see below).
220
+
221
+ ### Progress output
222
+
223
+ During a run, a progress bar tracks each phase on stderr. It shows only when
224
+ stderr is a tty; `--[no-]progress` overrides. Completed phases stay visible:
225
+
226
+ - `baseline` records coverage.
227
+ - `mutants (warm)` uses the shared worker pool.
228
+ - `mutants (isolated)` evaluates the subset routed to fresh mirrors.
229
+
230
+ The last two labels appear only when a run uses both strategies. Output the
231
+ suite prints during mutant evaluation is swallowed, since mutated code warns,
232
+ raises, and logs freely. Verdicts travel back as structured results.
233
+
234
+ ```
235
+ baseline [========================] 895/895 100% 0:03
236
+ mutants (warm) [========================] 2104/2104 100% killed=1867 survived=137 no_coverage=100 0:59
237
+ mutants (isolated) [===========> ] 224/445 50% killed=196 survived=0 no_coverage=19 timeout=9 2:01
238
+ ```
239
+
240
+ ---
241
+
242
+ ## Configuration
243
+
244
+ `.kimera.yml` in the project root sets defaults. Command-line flags override
245
+ them.
246
+
247
+ ```yaml
248
+ framework: rspec
249
+ paths: ["app/**/*.rb"]
250
+ tests: ["spec/**/*_spec.rb"]
251
+ exclude: ["app/legacy/**/*.rb"]
252
+ operators: [comparison, boolean_connective, boolean_literal, statement_deletion, negation, conditional]
253
+ # Ruby files loaded before the scan, for custom operators.
254
+ require: []
255
+ jobs: 4
256
+ max_survivors: 0
257
+ # Fail when any mutant in scope lacks a covering test. Best on incremental
258
+ # runs, where "no coverage" means the PR adds logic no test executes.
259
+ fail_on_no_coverage: false
260
+
261
+ # The gate fails when more than this many mutants are ignore-listed. Marking
262
+ # one more mutant equivalent means raising this number in the same diff, so
263
+ # the ignore list stays a reviewed decision, not a shortcut past a test.
264
+ max_ignored: 1
265
+
266
+ # Accepted pre-adoption survivors, merged with the ignore rules below.
267
+ # Uncomment only after creating the file with `kimera baseline create`.
268
+ # baseline: .kimera-baseline.yml
269
+
270
+ # Known-equivalent mutants. Equivalence is undecidable, so Kimera doesn't
271
+ # guess: you mark a mutant and it stops being a survivor. An entry without
272
+ # a reason: is rejected at startup.
273
+ ignore:
274
+ - file: app/models/discount.rb
275
+ line: 33
276
+ label: "> => >="
277
+ reason: known equivalent at the 50 boundary (spend > 50 vs >= 50 both hit the tier)
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Operators
283
+
284
+ The *default* set is small and low-noise:
285
+
286
+ - comparison operators (boundary and negation)
287
+ - boolean connectives and boolean literals
288
+ - deletion of side-effecting statements
289
+ - `!x => x`
290
+ - forced `if` conditions
291
+
292
+ The extended families are one flag away. Use `--operators all`, or pick from:
293
+
294
+ | family | mutation |
295
+ |---|---|
296
+ | `arithmetic` | `+ => -`, `* => /`, `% => *`, `** => *`, ... |
297
+ | `safe_navigation` | `a&.b => a.b` |
298
+ | `numeric_literal` | `n => n-1, n+1, 0`; floats `f => f+1.0` |
299
+ | `string_literal` | `"text" => ""` |
300
+ | `collection_literal` | `[a, b] => []`, `{k: v} => {}` |
301
+ | `range` | `a..b <=> a...b` |
302
+ | `method_unwrap` | `x.strip => x` (curated argless transformations) |
303
+ | `return_value` | `return x => return nil`; def-body tail `=> nil` |
304
+ | `selector_swap` | `select <=> reject`, `all? <=> any?`, `min <=> max`, `first <=> last`, `detect => first` |
305
+ | `regexp` | `/pat/ => //` (match all) and `=> /(?!)/` (match none) |
306
+ | `element_drop` | `[a, b, c]` / `{k: v, ...}` => drop one element/pair |
307
+ | `argument_drop` | `pay(amount, currency)` => drop one argument |
308
+ | `op_assign` | `total += x => total -= x`, `*= <=> /=` |
309
+ | `index_fetch` | `h[k] => h.fetch(k)` (a tolerated miss becomes a `KeyError`) |
310
+ | `kernel_coercion` | `Array(x) => x`, likewise `String`/`Integer`/`Float` |
311
+ | `respond_to_guard` | `x.respond_to?(:m) => x` |
312
+ | `symbol_literal` | `:total => :total__kimera__` |
313
+ | `default_argument` | `def f(a = 1)` / `f(b: 1)` => the parameter becomes required |
314
+ | `chain_link_deletion` | `a.where(x).recent.first => a.where(x).first`: drop one link, keep both ends |
315
+
316
+ Turning them all on typically triples the mutant count. Adopt them through the
317
+ baseline burn-down workflow, not by flipping them on in a mature gate.
318
+
319
+ **Contract operators.** The five from `index_fetch` through `default_argument`
320
+ each remove an affordance the code grants its callers. A survivor means no test
321
+ exercises the case the affordance exists for. `index_fetch` and
322
+ `kernel_coercion` are equivalent wherever the key is always present or the
323
+ value already has the target shape. That is the normal case for an options hash
324
+ built from a defaults literal. Scope them to data that crosses a boundary
325
+ (params, parsed JSON, ENV); on internal bookkeeping they are noise.
326
+
327
+ **`chain_link_deletion`** casts the widest net and matters most for Rails. Most
328
+ Rails business rules live mid-chain: the scope, the filter, the decorator.
329
+ Deleting a link asks whether any test depends on it narrowing anything. It
330
+ generates roughly one mutant per chained call, so scope it to the code you are
331
+ burning down, not a whole app.
332
+
333
+ ### Rails
334
+
335
+ Four Rails-aware families ship as the `rails` group. `--operators
336
+ comparison,rails` composes, and `all` includes them.
337
+
338
+ | family | mutation | what a survivor means |
339
+ |---|---|---|
340
+ | `rails_permit` | `permit(:name, :admin)` => drop one key | over-permissive strong params no test notices |
341
+ | `rails_validation` | delete `validates ...` | a validation no test ever violates |
342
+ | `rails_callback` | delete `before_action :authenticate_user!` etc.; drop one action from an `only:`/`except:` list | an unguarded path (or a guarded action) no test exercises |
343
+ | `rails_association` | `has_many :posts, dependent: :destroy` => drop `dependent:` | orphaned-record cleanup no test observes |
344
+
345
+ - **Scope lambdas** are mutated on the *warm* path. A `-> { ... }` body at class
346
+ level re-executes per call, so ordinary operators mutate it safely
347
+ (`scope :active, -> { where(active: true) }`). Run-once `do ... end` DSL
348
+ blocks (`included do`, `class_eval`) stay excluded.
349
+ - **`rails_permit`** runs on the normal warm path.
350
+ - **Validations and callbacks** are class-body DSL. They execute once at load,
351
+ so no in-process scheme can judge them. The warm run reports them as
352
+ `isolated_only` (excluded from the score, never gating). `--isolated` gives
353
+ the verdict by baking each deletion into a fresh tree.
354
+ - **Zeitwerk.** Kimera calls `Rails.application.eager_load!` before overlaying,
355
+ so lazily autoloaded constants can't escape mutation.
356
+
357
+ ### Custom operators
358
+
359
+ The built-in operators cover what every Ruby app shares. The mutants that find
360
+ the most in *your* app are often ones only you can write: the call whose
361
+ deletion should break a test, the keyword whose absence should be caught.
362
+ Register them the way you add a RuboCop cop: a Ruby file named in `require:`,
363
+ loaded before the scan.
364
+
365
+ ```ruby
366
+ # lib/kimera/operators/authorization.rb
367
+ require "kimera/operators"
368
+
369
+ class Authorization < Kimera::Operators::Base
370
+ NAMES = %i[authorize authorize! policy_scope].freeze
371
+
372
+ class << self
373
+ def key = "authorization"
374
+
375
+ def statement? = true
376
+ end
377
+
378
+ def variants(node, **)
379
+ return unless matches?(node, NAMES)
380
+ deletion(node)
381
+ end
382
+ end
383
+
384
+ Kimera::Operators.register(Authorization)
385
+ ```
386
+
387
+ ```yaml
388
+ # .kimera.yml
389
+ require: [lib/kimera/operators/authorization.rb]
390
+ operators: [comparison, custom] # `custom` is every operator you registered
391
+ ```
392
+
393
+ `variants` receives a [Prism](https://github.com/ruby/prism) node and returns
394
+ `Variant`s. Each has a label for the report and a *directive* naming a rewrite
395
+ Kimera already knows: `selector_swap`, `drop_argument`, `kwarg_pair_drop`,
396
+ `statement_deletion`, and the rest of the built-in vocabulary. A directive type
397
+ with no handler is an error, not a silent pass. Custom rewrite handlers are not
398
+ supported yet.
399
+
400
+ Operators load once, in the CLI parent. The warm pool forks its workers and the
401
+ isolated tier bakes each mutant before spawning. So a registered operator
402
+ reaches every process that builds a mutant, with no per-worker setup.
403
+
404
+ Seven worked examples ship in [`examples/operators`](examples/operators), each
405
+ with a note on what a survivor means:
406
+
407
+ - authorization deletion
408
+ - bang-call stripping
409
+ - `perform_later => perform_now`
410
+ - HTTP status dropping
411
+ - cache-expiry dropping
412
+ - money rounding
413
+ - encrypted attribute deletion
414
+
415
+ **Audit your operator before it guards anything.** A run can't show two
416
+ failures: a directive that won't render, and one that rebuilds the original
417
+ source. The second is an equivalent mutant that survives every run and ends up
418
+ hand-added to the ignore list. Check it against a representative snippet:
419
+
420
+ ```ruby
421
+ require "kimera/operators/audit"
422
+
423
+ faults = Kimera::Operators::Audit.faults([Authorization.new], File.read("app/controllers/orders_controller.rb"))
424
+ expect(faults).to(be_empty)
425
+ ```
426
+
427
+ ---
428
+
429
+ ## How it works
430
+
431
+ ```
432
+ source ──Prism──▶ registry (mutation points, JSON) ──┬─▶ synthesis (unparser) ─▶ schemata
433
+ └─▶ reporting
434
+ │
435
+ suite ─load once─▶ overlay schemata ─▶ baseline+coverage ─▶ warm worker-pool kill loop
436
+ ```
437
+
438
+ 1. **Registry.** Walk the Prism AST and record every mutation point: operator,
439
+ byte/line span, original node, and variant directives. Each gets a globally
440
+ unique integer id. The JSON registry is the single source of truth.
441
+ 2. **Synthesis.** Replace each schema-safe point with a nested guarded dispatch
442
+ (`if Kimera::Runtime.active?(102) … else …end`) and unparse. Memoized
443
+ expressions (`@x ||= …`) and load-time-only code go to a reload fallback, so
444
+ they can never read as a false "survived".
445
+ 3. **Execution.** Load the suite once and overlay the schemata. Check the
446
+ baseline is green while recording per-test coverage. With `--jobs N`, the
447
+ coverage pass also fans out across the warm pool (each worker runs a slice
448
+ of the examples). Then fork the warm pool and stream mutants to it from one
449
+ shared, heaviest-first queue. For each mutant, flip `Runtime.active` and run
450
+ only its covering tests. The queue is shared, not split by file, so
451
+ `--jobs N` scales with cores even when mutants sit in one large file.
452
+
453
+ ---
454
+
455
+ ## Design notes & known limits
456
+
457
+ - **Runs under your bundle.** `unparser` pulls in a `diff-lcs` version that can
458
+ conflict with `rspec-expectations` outside a resolved bundle. Use
459
+ `bundle exec`. A source checkout's `exe/kimera` activates the current
460
+ directory's Gemfile before booting Kimera.
461
+ - **Schema-unsafe mutants.** Code that runs once at load (class/module bodies,
462
+ constants) or is memoized can't be toggled in a warm process. Kimera mutates
463
+ only inside method bodies and routes memoized points to a fork-per-mutant
464
+ reload fallback.
465
+ - **An un-emittable node costs one method, not a file.** `unparser` refuses to
466
+ re-emit some valid Ruby (an interpolated `%r{}x` pattern, for example). When
467
+ a whole file won't round-trip, Kimera guards and re-emits each method that
468
+ carries points and splices those back into the original text. The offending
469
+ constant stays verbatim and the rest of the file stays mutatable.
470
+ - **Standing risks and mitigations:**
471
+ - Global-state leakage: opt-in transaction rollback (`--isolate-db`), cache
472
+ resets, periodic kill-confirmation, one mutant per warm worker at a time.
473
+ - Selector-induced hangs: a soft per-test timeout, plus a watchdog that
474
+ SIGKILLs a wedged worker so a replacement pulls from the shared queue.
475
+ - Equivalent mutants: a small operator set plus human suppression hooks, not
476
+ a solver.
477
+ - **`.rspec` and suite hooks are honored.** The RSpec adapter applies the
478
+ project's `.rspec` (`--require spec_helper`/`rails_helper`, the `spec` load
479
+ path) as the `rspec` executable does. It fires `config.before(:suite)` once
480
+ before the baseline and `after(:suite)` at the end. Libraries install global
481
+ switches there: `webmock/rspec` calls `WebMock.enable!` only in that hook.
482
+ Without it, every stubbed HTTP request would reach the real network.
483
+ - **Parallel runs need parallel state.** With `--jobs N` on a Rails app, Kimera
484
+ runs Rails' parallel-testing fork hooks. Each worker gets its own database
485
+ (`app_test_0`, `app_test_1`, …), as with `rails test -j`, and the suite's
486
+ `before(:suite)` setup replays against it. Anything *else* your suite shares
487
+ (Redis, Elasticsearch, a temp directory) needs the same treatment. Register
488
+ a hook and Kimera runs it:
489
+ `ActiveSupport::Testing::Parallelization.after_fork_hook { |i| ... }`.
490
+ Without one, concurrent examples fight over one keyspace. That reads as a red
491
+ baseline, or worse, as kills that are really collisions.
492
+ - **Score definition.** `score = killed / evaluable`.
493
+ - Killed includes timeouts and errors (observable misbehaviour).
494
+ - Evaluable excludes `no_coverage`, `ignored`, `isolated_only`, and
495
+ `harness_error` mutants. Each is reported separately.
496
+ - A `harness_error` is a mutant Kimera could not judge: its worker died, or
497
+ its reply was unreadable. It is not a kill. It gates via `--max-errors`
498
+ (default 0). A pool that never produces a result aborts the run.
499
+ - Uncovered mutants don't lower the score, unlike some tools.
500
+ `--fail-on-no-coverage` closes that gap.
501
+ - **One top-level constant.** `require "kimera"` defines `::MutantRuntime`,
502
+ aliased to `Kimera::Runtime` unless already bound. Every synthesized guard
503
+ dispatches through it. It stays top-level so an outer measurer can pre-bind
504
+ it. It is the only constant Kimera adds outside its namespace.
505
+
506
+ ## Mutation-testing Kimera with Kimera
507
+
508
+ Kimera gates itself at zero survivors (`bundle exec kimera run`, configured by
509
+ the repo's `.kimera.yml`). The warm path can't judge its own harness: the
510
+ runtime selector would recurse, and a mutant in the RSpec adapter can disable
511
+ the runner judging it. Those files are excluded or reported as
512
+ `isolated_only`. `bundle exec kimera run --isolated` judges them in fresh
513
+ subprocesses that share nothing with Kimera's runtime.
514
+
515
+ ## Development
516
+
517
+ ```sh
518
+ bundle install
519
+ bin/spec # run the test suite
520
+ COVERAGE=1 bin/spec # run with line+branch coverage (SimpleCov)
521
+ ```
522
+
523
+ `examples/` holds the runnable fixtures the integration tests drive
524
+ (`sample_app` for RSpec, `minitest_app` for Minitest).
525
+
526
+ ## License
527
+
528
+ MIT
data/exe/kimera ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ if File.file?(File.join(Dir.pwd, "Gemfile")) && !defined?(Bundler)
6
+ require "bundler/setup"
7
+ raise(LoadError, "kimera is not in this project's Gemfile") if Gem.find_files("unparser.rb").empty?
8
+ end
9
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
10
+ require "kimera"
11
+ rescue LoadError, StandardError => error
12
+ raise unless error.is_a?(LoadError) || (defined?(Bundler::BundlerError) && error.is_a?(Bundler::BundlerError))
13
+ warn(
14
+ "kimera: could not load its dependencies (#{error.message.lines.first.strip})",
15
+ "Kimera runs inside your project's bundle: add `gem \"kimera\", group: :test` to the Gemfile, " \
16
+ "run `bundle install`, then `bundle exec kimera`."
17
+ )
18
+ exit(1)
19
+ end
20
+
21
+ status = Kimera::CLI.start(ARGV)
22
+ exit(status.is_a?(Integer) ? status : 0)