henitai 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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +19 -0
  3. data/LICENSE +21 -0
  4. data/README.md +182 -0
  5. data/assets/schema/henitai.schema.json +123 -0
  6. data/exe/henitai +6 -0
  7. data/lib/henitai/arid_node_filter.rb +97 -0
  8. data/lib/henitai/cli.rb +341 -0
  9. data/lib/henitai/configuration.rb +132 -0
  10. data/lib/henitai/configuration_validator.rb +293 -0
  11. data/lib/henitai/coverage_bootstrapper.rb +75 -0
  12. data/lib/henitai/coverage_formatter.rb +112 -0
  13. data/lib/henitai/equivalence_detector.rb +85 -0
  14. data/lib/henitai/execution_engine.rb +174 -0
  15. data/lib/henitai/git_diff_analyzer.rb +82 -0
  16. data/lib/henitai/integration.rb +417 -0
  17. data/lib/henitai/mutant/activator.rb +234 -0
  18. data/lib/henitai/mutant.rb +68 -0
  19. data/lib/henitai/mutant_generator.rb +158 -0
  20. data/lib/henitai/mutant_history_store.rb +279 -0
  21. data/lib/henitai/operator.rb +96 -0
  22. data/lib/henitai/operators/arithmetic_operator.rb +46 -0
  23. data/lib/henitai/operators/array_declaration.rb +52 -0
  24. data/lib/henitai/operators/assignment_expression.rb +78 -0
  25. data/lib/henitai/operators/block_statement.rb +31 -0
  26. data/lib/henitai/operators/boolean_literal.rb +70 -0
  27. data/lib/henitai/operators/conditional_expression.rb +184 -0
  28. data/lib/henitai/operators/equality_operator.rb +41 -0
  29. data/lib/henitai/operators/hash_literal.rb +66 -0
  30. data/lib/henitai/operators/logical_operator.rb +84 -0
  31. data/lib/henitai/operators/method_expression.rb +56 -0
  32. data/lib/henitai/operators/pattern_match.rb +66 -0
  33. data/lib/henitai/operators/range_literal.rb +40 -0
  34. data/lib/henitai/operators/return_value.rb +105 -0
  35. data/lib/henitai/operators/safe_navigation.rb +34 -0
  36. data/lib/henitai/operators/string_literal.rb +64 -0
  37. data/lib/henitai/operators.rb +25 -0
  38. data/lib/henitai/parser_current.rb +7 -0
  39. data/lib/henitai/reporter.rb +432 -0
  40. data/lib/henitai/result.rb +170 -0
  41. data/lib/henitai/runner.rb +183 -0
  42. data/lib/henitai/sampling_strategy.rb +33 -0
  43. data/lib/henitai/scenario_execution_result.rb +71 -0
  44. data/lib/henitai/source_parser.rb +41 -0
  45. data/lib/henitai/static_filter.rb +186 -0
  46. data/lib/henitai/stillborn_filter.rb +34 -0
  47. data/lib/henitai/subject.rb +71 -0
  48. data/lib/henitai/subject_resolver.rb +232 -0
  49. data/lib/henitai/syntax_validator.rb +16 -0
  50. data/lib/henitai/test_prioritizer.rb +55 -0
  51. data/lib/henitai/unparse_helper.rb +24 -0
  52. data/lib/henitai/version.rb +5 -0
  53. data/lib/henitai/warning_silencer.rb +16 -0
  54. data/lib/henitai.rb +51 -0
  55. data/sig/configuration_validator.rbs +29 -0
  56. data/sig/henitai.rbs +594 -0
  57. data/sig/unparser.rbs +3 -0
  58. metadata +153 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 586e777c1eb78b5345eedde4f010ab262fe1ced6e0ac3714e21dbd9aa7cc63d3
4
+ data.tar.gz: 7d6d5d3939777b28ab127133dc4f2d9663c29c812f6f4a77af4705a3f36ef973
5
+ SHA512:
6
+ metadata.gz: 0fd445f2506b0dec762413e3e992af6b93797728c4253a86de2e1ce0f60fcd189ac2d2f342033ac8d16653064ce3fc3cd885c69ad34026e9781c60ca88d85de4
7
+ data.tar.gz: 9d00b7d9f3237b70460306aae62729d38e0142fbf7b954c2b4528feda3a4e596f08bd373c9ed448fd0ff12de7c5c1fb867c3ea9f8cf8582792a9c3a454d2bfd5
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - Initial gem scaffold with Ruby 4.0.2 support
12
+ - Dev Container configuration (official `ruby:4.0.2-alpine` base image, Codex CLI preinstalled)
13
+ - CI pipeline (RuboCop + RSpec + incremental mutation testing on PRs)
14
+ - `.henitai.yml` configuration schema
15
+ - Module structure: `Configuration`, `Subject`, `Mutant`, `Operator`, `Runner`, `Reporter`, `Integration`, `Result`
16
+ - CLI critical path: `henitai run` now executes the full pipeline, supports `--since`, returns CI-friendly exit codes, and `henitai version` prints `Henitai::VERSION`
17
+ - RSpec per-test coverage output: `henitai/coverage_formatter` now writes `coverage/henitai_per_test.json`
18
+
19
+ [Unreleased]: https://github.com/martinotten/henitai/commits/main
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Martin Otten
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,182 @@
1
+ # hen’itai
2
+
3
+ 変異体 (へんいたい, hen’itai)
4
+ Pronunciation: he-n-i-ta-i (4 morae; no stress accent)
5
+ Meaning: mutant, mutated organism, or variant entity
6
+
7
+
8
+ A Ruby mutation testing framework
9
+
10
+ [![CI](https://github.com/martinotten/henitai/actions/workflows/ci.yml/badge.svg)](https://github.com/martinotten/henitai/actions/workflows/ci.yml)
11
+ [![Gem Version](https://badge.fury.io/rb/henitai.svg)](https://badge.fury.io/rb/henitai)
12
+
13
+ ---
14
+
15
+ ## Maturaty
16
+
17
+ - This is alpha software, there will be bugs
18
+ - Henitai tests itself
19
+ - Stryker Dashboard support is untested
20
+ - Minitest support is untested
21
+ - Lots of code has been carefully crafted by AI agents, not everything has been reviewed by humans (yet)
22
+
23
+ ## What is mutation testing?
24
+
25
+ Mutation testing answers the question that code coverage cannot: **does your test suite actually verify the behaviour of your code?**
26
+
27
+ A mutation testing tool makes small, systematic changes — *mutants* — to your source code (e.g. replacing `>` with `>=`, removing a `return` statement, flipping a boolean) and then runs your tests. A mutant that causes at least one test to fail is *killed*. A mutant that passes all tests is *survived* — evidence that your tests are not covering that behaviour.
28
+
29
+ The ratio of killed mutants to total mutants is the **Mutation Score** (MS). A high mutation score is a strong quality signal.
30
+
31
+ ## Installation
32
+
33
+ Add to your `Gemfile`:
34
+
35
+ ```ruby
36
+ gem "henitai", group: :development
37
+ ```
38
+
39
+ Or install globally:
40
+
41
+ ```sh
42
+ gem install henitai
43
+ ```
44
+
45
+ **Requires Ruby 4.0.2+**
46
+
47
+ ## Quick start
48
+
49
+ ```sh
50
+ # Run mutation testing on the entire project
51
+ bundle exec henitai run
52
+
53
+ # Run only on subjects changed since main (CI-friendly)
54
+ bundle exec henitai run --since origin/main
55
+
56
+ # Run on a specific subject pattern
57
+ bundle exec henitai run 'MyClass#my_method'
58
+ bundle exec henitai run 'MyNamespace*'
59
+ ```
60
+
61
+ Configuration lives in `.henitai.yml`:
62
+
63
+ ```yaml
64
+ # yaml-language-server: $schema=./assets/schema/henitai.schema.json
65
+ integration:
66
+ name: rspec
67
+
68
+ includes:
69
+ - lib
70
+
71
+ mutation:
72
+ operators: light # light | full
73
+ timeout: 10.0
74
+ max_mutants_per_line: 1
75
+ max_flaky_retries: 3
76
+ sampling:
77
+ ratio: 0.05
78
+ strategy: stratified
79
+ reports_dir: reports
80
+
81
+ thresholds:
82
+ high: 80
83
+ low: 60
84
+ ```
85
+
86
+ Henitai warns on unknown config keys and aborts with `Henitai::ConfigurationError`
87
+ when a value is invalid.
88
+
89
+ CLI flags override the corresponding values from `.henitai.yml`.
90
+
91
+ Before mutation testing starts, Henitai checks whether the current coverage data
92
+ covers the configured source files. If not, Henitai runs the configured test
93
+ suite once to bootstrap a usable coverage baseline. If coverage is still
94
+ unavailable for the current sources, `henitai run` aborts with
95
+ `Henitai::CoverageError`.
96
+
97
+ Surviving mutants are retried up to `mutation.max_flaky_retries` times before
98
+ they are classified as survivors. The default retry budget is 3.
99
+
100
+ Per-test coverage reporting is currently wired through the RSpec child runner.
101
+ Minitest integration reuses the same selection and execution flow, but does not
102
+ yet enable the per-test coverage formatter.
103
+
104
+ By default, Henitai keeps child test output out of the live terminal. Each
105
+ baseline or mutant run writes captured stdout/stderr to `reports/mutation-logs/`
106
+ and the terminal only shows progress plus a concise summary. Pass
107
+ `--all-logs` (or `--verbose`) to print every captured child log.
108
+
109
+ `henitai version` prints the installed version. `henitai run` exits with `0`
110
+ when the mutation score meets the low threshold, `1` when it does not, and `2`
111
+ for framework errors.
112
+
113
+ The repository ships a JSON Schema at [`assets/schema/henitai.schema.json`](/workspaces/henitai/assets/schema/henitai.schema.json) for editor autocompletion.
114
+
115
+ ## Operator sets
116
+
117
+ **Light** (default) — high-signal, low-noise operators covering the majority of real-world defects:
118
+
119
+ - `ArithmeticOperator` — `+` ↔ `-`, `*` ↔ `/`
120
+ - `EqualityOperator` — `==` ↔ `!=`, `>` ↔ `<`, etc.
121
+ - `LogicalOperator` — `&&` ↔ `||`
122
+ - `BooleanLiteral` — `true` ↔ `false`, `!expr`
123
+ - `ConditionalExpression` — remove branch bodies
124
+ - `StringLiteral` — empty string replacement
125
+ - `ReturnValue` — mutate return expressions
126
+
127
+ **Full** — adds lower-signal operators:
128
+
129
+ - `ArrayDeclaration`, `HashLiteral`, `RangeLiteral`
130
+ - `SafeNavigation` — `&.` → `.`
131
+ - `PatternMatch` — case/in arm removal
132
+ - `BlockStatement` — remove blocks
133
+ - `MethodExpression` — remove calls
134
+ - `AssignmentExpression` — mutate compound assignment
135
+
136
+ ## Stryker Dashboard integration (untested)
137
+
138
+ ```yaml
139
+ # .henitai.yml
140
+ reporters:
141
+ - terminal
142
+ - html
143
+ - json
144
+ - dashboard
145
+
146
+ dashboard:
147
+ project: "github.com/your-org/your-repo"
148
+ base_url: "https://dashboard.stryker-mutator.io"
149
+ ```
150
+
151
+ Set `STRYKER_DASHBOARD_API_KEY` in your CI environment to publish reports.
152
+
153
+ JSON reports are written to `reports/mutation-report.json` by default. Set
154
+ `reports_dir` to change the output directory.
155
+
156
+ ## Development
157
+
158
+ ```sh
159
+ git clone https://github.com/martinotten/henitai
160
+ cd henitai
161
+ bundle install
162
+ bundle exec rspec # run tests
163
+ bundle exec rubocop # lint
164
+ bundle exec henitai run # dogfood
165
+ ```
166
+
167
+ A Dev Container configuration is included (`.devcontainer/`) for VS Code with the official `ruby:4.0.2-alpine` image and the Codex CLI preinstalled.
168
+
169
+ ## Architecture
170
+
171
+ See [`docs/architecture/architecture.md`](docs/architecture/architecture.md) for the full design document, including:
172
+
173
+ - Phase-Gate pipeline (5 gates)
174
+ - AST-based operator implementation
175
+ - Fork isolation model
176
+ - Stryker JSON schema integration
177
+ - Architecture decisions in [`docs/architecture/adr/`](docs/architecture/adr/)
178
+ - Three-phase roadmap
179
+
180
+ ## License
181
+
182
+ [MIT License](LICENSE) — © 2026 Martin Otten
@@ -0,0 +1,123 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/martinotten/henitai/assets/schema/henitai.schema.json",
4
+ "title": "Henitai configuration",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "integration": {
9
+ "oneOf": [
10
+ {
11
+ "type": "string"
12
+ },
13
+ {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "properties": {
17
+ "name": {
18
+ "type": "string"
19
+ }
20
+ }
21
+ }
22
+ ]
23
+ },
24
+ "includes": {
25
+ "type": "array",
26
+ "items": {
27
+ "type": "string"
28
+ }
29
+ },
30
+ "mutation": {
31
+ "type": "object",
32
+ "additionalProperties": false,
33
+ "properties": {
34
+ "operators": {
35
+ "enum": ["light", "full"]
36
+ },
37
+ "timeout": {
38
+ "type": "number"
39
+ },
40
+ "max_mutants_per_line": {
41
+ "type": "integer",
42
+ "minimum": 1
43
+ },
44
+ "max_flaky_retries": {
45
+ "type": "integer",
46
+ "minimum": 0
47
+ },
48
+ "ignore_patterns": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "string"
52
+ }
53
+ },
54
+ "sampling": {
55
+ "type": "object",
56
+ "additionalProperties": false,
57
+ "required": ["ratio", "strategy"],
58
+ "properties": {
59
+ "ratio": {
60
+ "type": "number",
61
+ "minimum": 0,
62
+ "maximum": 1
63
+ },
64
+ "strategy": {
65
+ "enum": ["stratified"]
66
+ }
67
+ }
68
+ }
69
+ }
70
+ },
71
+ "coverage_criteria": {
72
+ "type": "object",
73
+ "additionalProperties": false,
74
+ "properties": {
75
+ "test_result": {
76
+ "type": "boolean"
77
+ },
78
+ "timeout": {
79
+ "type": "boolean"
80
+ },
81
+ "process_abort": {
82
+ "type": "boolean"
83
+ }
84
+ }
85
+ },
86
+ "thresholds": {
87
+ "type": "object",
88
+ "additionalProperties": false,
89
+ "properties": {
90
+ "high": {
91
+ "type": "integer"
92
+ },
93
+ "low": {
94
+ "type": "integer"
95
+ }
96
+ }
97
+ },
98
+ "reporters": {
99
+ "type": "array",
100
+ "items": {
101
+ "type": "string"
102
+ }
103
+ },
104
+ "reports_dir": {
105
+ "type": "string"
106
+ },
107
+ "dashboard": {
108
+ "type": "object",
109
+ "additionalProperties": false,
110
+ "properties": {
111
+ "project": {
112
+ "type": "string"
113
+ },
114
+ "base_url": {
115
+ "type": "string"
116
+ }
117
+ }
118
+ },
119
+ "jobs": {
120
+ "type": ["integer", "null"]
121
+ }
122
+ }
123
+ }
data/exe/henitai ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "henitai"
5
+
6
+ Henitai::CLI.start(ARGV)
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Henitai
4
+ # Suppresses AST nodes that are unlikely to produce useful mutants.
5
+ class AridNodeFilter
6
+ DIRECT_OUTPUT_METHODS = %i[puts p pp warn].freeze
7
+ DIRECT_DEBUG_METHODS = %i[byebug debugger].freeze
8
+ BINDING_DEBUG_METHODS = %i[pry].freeze
9
+ LOGGER_METHODS = %i[debug info warn error fatal].freeze
10
+ INVARIANT_METHODS = %i[is_a? respond_to? kind_of?].freeze
11
+ DSL_METHODS = %i[let subject before after].freeze
12
+
13
+ def suppressed?(node, config)
14
+ custom_pattern_match?(node, config) || catalog_match?(node)
15
+ end
16
+
17
+ private
18
+
19
+ def custom_pattern_match?(node, config)
20
+ source = node.location&.expression&.source
21
+ return false unless source
22
+
23
+ compiled_ignore_patterns(config).any? do |pattern|
24
+ pattern.match?(source)
25
+ end
26
+ end
27
+
28
+ def compiled_ignore_patterns(config)
29
+ patterns = Array(config&.ignore_patterns).dup.freeze
30
+ @compiled_ignore_patterns ||= {}
31
+ @compiled_ignore_patterns[patterns] ||= patterns.map { |pattern| Regexp.new(pattern) }
32
+ end
33
+
34
+ def catalog_match?(node)
35
+ case node.type
36
+ when :send
37
+ send_match?(node)
38
+ when :block
39
+ block_match?(node)
40
+ when :or_asgn
41
+ true
42
+ else
43
+ false
44
+ end
45
+ end
46
+
47
+ def send_match?(node)
48
+ receiver, method_name, = node.children
49
+ method_name = method_name&.to_sym
50
+
51
+ return true if direct_output_call?(receiver, method_name)
52
+ return true if direct_debug_call?(receiver, method_name)
53
+ return true if binding_debug_call?(receiver, method_name)
54
+ return true if rails_logger_call?(receiver, method_name)
55
+
56
+ invariant_call?(method_name)
57
+ end
58
+
59
+ def block_match?(node)
60
+ send_node = node.children.first
61
+ return false unless send_node&.type == :send
62
+
63
+ receiver, method_name, = send_node.children
64
+ receiver.nil? && DSL_METHODS.include?(method_name.to_sym)
65
+ end
66
+
67
+ def direct_output_call?(receiver, method_name)
68
+ receiver.nil? && DIRECT_OUTPUT_METHODS.include?(method_name)
69
+ end
70
+
71
+ def direct_debug_call?(receiver, method_name)
72
+ receiver.nil? && DIRECT_DEBUG_METHODS.include?(method_name)
73
+ end
74
+
75
+ def binding_debug_call?(receiver, method_name)
76
+ send_call?(receiver, :binding) && BINDING_DEBUG_METHODS.include?(method_name)
77
+ end
78
+
79
+ def rails_logger_call?(receiver, method_name)
80
+ logger_receiver?(receiver) && LOGGER_METHODS.include?(method_name)
81
+ end
82
+
83
+ def invariant_call?(method_name)
84
+ INVARIANT_METHODS.include?(method_name)
85
+ end
86
+
87
+ def logger_receiver?(node)
88
+ send_call?(node, :logger) &&
89
+ node.children.first&.type == :const &&
90
+ node.children.first.children.last == :Rails
91
+ end
92
+
93
+ def send_call?(node, method_name)
94
+ node&.type == :send && node.children[1] == method_name
95
+ end
96
+ end
97
+ end