henitai 0.3.1 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 425e1946a343ce94a9172939a8ad5c5d49e01fe86fbf2b01326b2f0be068df85
4
- data.tar.gz: 85867b98b26cb3710d0c1e56bd4b37b5ba78f08f47ad8378dc76ae07ce1dfc9a
3
+ metadata.gz: 458245aa72b025224bd6c95c2bcca98ea095dd86a41e93ab8f0671ba56644369
4
+ data.tar.gz: 94a822238b18f41803753a85adae77675f422d2a901cdd89723ea68b14a8842a
5
5
  SHA512:
6
- metadata.gz: f0de6ea6a0a5a830a917f1097ac243ce43c6b8bf3bcc44ac1967133288770746d9c4df8fdee6609081613bfa174be5169e13143a22f1d3d22e8f74fd0cc008d7
7
- data.tar.gz: 37eedd8f351dbd8a9094c668ce7c56560186936f0953b098f740b6cee1654971d1b5402efe2fd0d5650442b701af7fedaea257ec9f7827575f3f04c052c2593a
6
+ metadata.gz: ed70e5425023aa08cd3ecd4ff6cb20dbd6c78b14f1811caaf9b43034e7649cdfeed617398a4fbf200730c1f4e8a36ab1fdfbe727b27379f4ed3b4eeef53be781
7
+ data.tar.gz: 0f2b4a04643f9e1bcd3f188e06d851f34eafbbf65dcafd42b9698678cb0ee540fca70e31207003d81cb4713ee1ca2c78901780e5f474128e20331c150949052a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-07-14
11
+
12
+ ### Added
13
+ - `hard` operator set (`mutation.operators: hard` / `--operators hard`), a
14
+ strict superset of `full` for usually-unkillable mutations (ADR-12):
15
+ currently `EqualityIdentityOperator` and the new `HashKeyType`
16
+ (`{ a: 1 }` -> `{ "a" => 1 }`) — framework key normalization (e.g.
17
+ ActiveRecord `order`/`where`) makes key-type mutants frequently equivalent
18
+ - `HashLiteral` gains per-pair removal (`{ a: 1, b: 2 }` -> `{ b: 2 }`);
19
+ single-pair hashes and double-splat entries are skipped
20
+
21
+ ### Changed
22
+ - `full` now means "usually killable": the symbol-key -> string-key mutation
23
+ moved from `HashLiteral` into the new hard-set `HashKeyType`, and
24
+ `EqualityIdentityOperator` moved from `full` to `hard` — `full` runs emit
25
+ fewer, higher-signal mutants
26
+
10
27
  ## [0.3.1] - 2026-07-13
11
28
 
12
29
  ### Fixed
@@ -405,7 +422,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
405
422
  - CLI critical path: `henitai run` now executes the full pipeline, supports `--since`, returns CI-friendly exit codes, and `henitai version` prints `Henitai::VERSION`
406
423
  - RSpec per-test coverage output: `henitai/coverage_formatter` now writes `coverage/henitai_per_test.json`
407
424
 
408
- [Unreleased]: https://github.com/martinotten/henitai/compare/v0.3.1...HEAD
425
+ [Unreleased]: https://github.com/martinotten/henitai/compare/v0.4.0...HEAD
426
+ [0.4.0]: https://github.com/martinotten/henitai/compare/v0.3.1...v0.4.0
409
427
  [0.3.1]: https://github.com/martinotten/henitai/compare/v0.3.0...v0.3.1
410
428
  [0.3.0]: https://github.com/martinotten/henitai/compare/v0.2.1...v0.3.0
411
429
  [0.2.1]: https://github.com/martinotten/henitai/compare/v0.2.0...v0.2.1
data/README.md CHANGED
@@ -271,7 +271,17 @@ The repository ships a JSON Schema at [`assets/schema/henitai.schema.json`](/wor
271
271
  - `AssignmentExpression` — mutate compound assignment
272
272
  - `UnaryOperator` — remove unary `-` and `~`
273
273
  - `UpdateOperator` — swap compound assignments (`+=`↔`-=`, `*=`↔`/=`, `||=`↔`&&=`)
274
+
275
+ **Hard** — adds usually-unkillable mutations on top of full, for hunting the
276
+ last survivors (see [ADR-12](docs/architecture/adr/ADR-12-hard-operator-set.md)):
277
+
274
278
  - `EqualityIdentityOperator` — `==` ↔ `eql?`/`equal?` (hardest equality pairing to kill; see [ADR-10](docs/architecture/adr/ADR-10-split-equality-identity-mutations.md))
279
+ - `HashKeyType` — `{ a: 1 }` → `{ "a" => 1 }` (frameworks that normalize key
280
+ types, e.g. ActiveRecord `order`/`where`, make these mutants equivalent at
281
+ many call sites; disable per site with `# henitai:disable HashKeyType`)
282
+
283
+ `HashLiteral` (full set) empties the hash and removes one pair at a time
284
+ (`{ a: 1, b: 2 }` → `{}` / `{ b: 2 }`).
275
285
 
276
286
  ## Stryker Dashboard integration
277
287
 
@@ -44,7 +44,7 @@
44
44
  "additionalProperties": false,
45
45
  "properties": {
46
46
  "operators": {
47
- "enum": ["light", "full"]
47
+ "enum": ["light", "full", "hard"]
48
48
  },
49
49
  "timeout": {
50
50
  "type": "number"
@@ -14,7 +14,8 @@ module Henitai
14
14
  "StringLiteral" => ["String literals", '"foo" -> ""'],
15
15
  "ReturnValue" => ["Return expressions", "return x -> return nil"],
16
16
  "ArrayDeclaration" => ["Array literals", "[1, 2] -> []"],
17
- "HashLiteral" => ["Hash literals", "{ a: 1 } -> {}"],
17
+ "HashLiteral" => ["Hash literals", "{ a: 1, b: 2 } -> {} / { b: 2 }"],
18
+ "HashKeyType" => ["Hash key types", '{ a: 1 } -> { "a" => 1 }'],
18
19
  "RangeLiteral" => ["Range literals", "1..5 -> 1...5"],
19
20
  "SafeNavigation" => ["Safe navigation", "user&.name -> user.name"],
20
21
  "PatternMatch" => ["Pattern matching", "in { x: Integer } -> in { x: String }"],
@@ -21,7 +21,7 @@ module Henitai
21
21
  end
22
22
 
23
23
  def add_operator_option(opts, options)
24
- opts.on("--operators SET", "Operator set: light | full") do |set|
24
+ opts.on("--operators SET", "Operator set: light | full | hard") do |set|
25
25
  options[:operators] = set
26
26
  end
27
27
  end
@@ -33,7 +33,7 @@ module Henitai
33
33
  VALID_THRESHOLDS_KEYS = %i[high low].freeze
34
34
  VALID_DASHBOARD_KEYS = %i[project base_url].freeze
35
35
  VALID_INTEGRATION_KEYS = %i[name].freeze
36
- VALID_OPERATORS = %i[light full].freeze
36
+ VALID_OPERATORS = %i[light full hard].freeze
37
37
  VALIDATION_STEPS = %i[
38
38
  validate_top_level_keys
39
39
  validate_integration
@@ -44,13 +44,23 @@ module Henitai
44
44
  AssignmentExpression
45
45
  UnaryOperator
46
46
  UpdateOperator
47
+ ]).freeze
48
+
49
+ # Usually-unkillable operators on top of full: mutations whose survival
50
+ # rarely indicates a test gap (framework key normalization, the
51
+ # ==/eql?/equal? pairing). Opt in when hunting the last survivors
52
+ # (ADR-12).
53
+ HARD_SET = (FULL_SET + %w[
47
54
  EqualityIdentityOperator
55
+ HashKeyType
48
56
  ]).freeze
49
57
 
50
- # @param set [Symbol] :light or :full
58
+ SETS = { light: LIGHT_SET, full: FULL_SET, hard: HARD_SET }.freeze
59
+
60
+ # @param set [Symbol] :light, :full or :hard
51
61
  # @return [Array<Operator>] operator instances for the given set
52
62
  def self.for_set(set)
53
- names = set.to_sym == :full ? FULL_SET : LIGHT_SET
63
+ names = SETS.fetch(set.to_sym, LIGHT_SET)
54
64
  names.map { |name| Henitai::Operators.const_get(name).new }
55
65
  end
56
66
 
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Henitai
4
+ module Operators
5
+ # Mutates symbol hash keys into string keys, one pair at a time
6
+ # (`{ a: 1 }` -> `{ "a" => 1 }`). Symbol/string key confusion is a real
7
+ # defect class, but frameworks that normalize keys (e.g. ActiveRecord's
8
+ # `order`/`where`) make these mutants frequently unkillable — hence the
9
+ # hard set, not full (ADR-12).
10
+ class HashKeyType < Henitai::Operator
11
+ NODE_TYPES = [:hash].freeze
12
+
13
+ def self.node_types
14
+ NODE_TYPES
15
+ end
16
+
17
+ def mutate(node, subject:)
18
+ node.children.each_with_index.filter_map do |pair, index|
19
+ next unless symbol_key_pair?(pair)
20
+
21
+ build_mutant(
22
+ subject:,
23
+ original_node: node,
24
+ mutated_node: mutated_hash(node, index),
25
+ description: "replaced symbol key with string key"
26
+ )
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def symbol_key_pair?(node)
33
+ node.type == :pair && node.children.first&.type == :sym
34
+ end
35
+
36
+ def mutated_hash(node, index)
37
+ pairs = node.children.each_with_index.map do |pair, pair_index|
38
+ pair_index == index ? stringified_pair(pair) : pair
39
+ end
40
+ Parser::AST::Node.new(:hash, pairs)
41
+ end
42
+
43
+ def stringified_pair(pair)
44
+ key, value = pair.children
45
+ string_key = Parser::AST::Node.new(:str, [key.children.first.to_s])
46
+ Parser::AST::Node.new(:pair, [string_key, value])
47
+ end
48
+ end
49
+ end
50
+ end
@@ -4,7 +4,10 @@ require_relative "../parser_current"
4
4
 
5
5
  module Henitai
6
6
  module Operators
7
- # Reduces hash literals by emptying them or mutating symbol keys.
7
+ # Reduces hash literals: empties the whole hash and removes one pair at a
8
+ # time. Symbol-key -> string-key mutation lives in {HashKeyType} (hard
9
+ # set) because framework key normalization makes it frequently unkillable
10
+ # (ADR-12).
8
11
  class HashLiteral < Henitai::Operator
9
12
  NODE_TYPES = [:hash].freeze
10
13
 
@@ -16,7 +19,7 @@ module Henitai
16
19
  return [] if node.children.empty?
17
20
 
18
21
  mutants = [empty_hash_mutant(node, subject:)]
19
- mutants.concat(symbol_key_mutants(node, subject:))
22
+ mutants.concat(pair_removal_mutants(node, subject:))
20
23
  mutants
21
24
  end
22
25
 
@@ -31,35 +34,31 @@ module Henitai
31
34
  )
32
35
  end
33
36
 
34
- def symbol_key_mutants(node, subject:)
37
+ # Removing the only entry would duplicate the empty-hash mutant.
38
+ def pair_removal_mutants(node, subject:)
39
+ return [] if node.children.size < 2
40
+
35
41
  node.children.each_with_index.filter_map do |pair, index|
36
- next unless symbol_key_pair?(pair)
42
+ next unless pair.type == :pair
37
43
 
38
44
  build_mutant(
39
45
  subject:,
40
46
  original_node: node,
41
- mutated_node: mutated_hash(node, index),
42
- description: "replaced symbol key with string key"
47
+ mutated_node: hash_without_pair(node, index),
48
+ description: "removed hash pair #{pair_key_label(pair)}"
43
49
  )
44
50
  end
45
51
  end
46
52
 
47
- def symbol_key_pair?(node)
48
- node.type == :pair && node.children.first&.type == :sym
49
- end
50
-
51
- def mutated_hash(node, pair_index)
52
- mutated_pairs = node.children.each_with_index.map do |pair, index|
53
- index == pair_index ? mutated_pair(pair) : pair
54
- end
55
-
56
- Parser::AST::Node.new(:hash, mutated_pairs)
53
+ def hash_without_pair(node, pair_index)
54
+ remaining = node.children.reject.with_index { |_pair, index| index == pair_index }
55
+ Parser::AST::Node.new(:hash, remaining)
57
56
  end
58
57
 
59
- def mutated_pair(pair)
60
- key, value = pair.children
61
- mutated_key = Parser::AST::Node.new(:str, [key.children.first.to_s])
62
- Parser::AST::Node.new(:pair, [mutated_key, value])
58
+ # Pair keys are always AST nodes (sym/str/…); their first child is the
59
+ # literal value used purely as a human-readable label.
60
+ def pair_key_label(pair)
61
+ pair.children.first.children.first
63
62
  end
64
63
  end
65
64
  end
@@ -16,6 +16,7 @@ module Henitai
16
16
  autoload :ReturnValue, "henitai/operators/return_value"
17
17
  autoload :ArrayDeclaration, "henitai/operators/array_declaration"
18
18
  autoload :HashLiteral, "henitai/operators/hash_literal"
19
+ autoload :HashKeyType, "henitai/operators/hash_key_type"
19
20
  autoload :RangeLiteral, "henitai/operators/range_literal"
20
21
  autoload :SafeNavigation, "henitai/operators/safe_navigation"
21
22
  autoload :PatternMatch, "henitai/operators/pattern_match"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Henitai
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
data/sig/henitai.rbs CHANGED
@@ -353,6 +353,8 @@ module Henitai
353
353
  class Operator
354
354
  LIGHT_SET: Array[String]
355
355
  FULL_SET: Array[String]
356
+ HARD_SET: Array[String]
357
+ SETS: Hash[Symbol, Array[String]]
356
358
 
357
359
  def self.for_set: (Symbol) -> Array[untyped]
358
360
  def self.node_types: () -> Array[Symbol]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: henitai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Otten
@@ -166,6 +166,7 @@ files:
166
166
  - lib/henitai/operators/conditional_expression.rb
167
167
  - lib/henitai/operators/equality_identity_operator.rb
168
168
  - lib/henitai/operators/equality_operator.rb
169
+ - lib/henitai/operators/hash_key_type.rb
169
170
  - lib/henitai/operators/hash_literal.rb
170
171
  - lib/henitai/operators/logical_operator.rb
171
172
  - lib/henitai/operators/method_chain_unwrap.rb
@@ -223,7 +224,7 @@ metadata:
223
224
  changelog_uri: https://github.com/martinotten/henitai/blob/main/CHANGELOG.md
224
225
  documentation_uri: https://github.com/martinotten/henitai/blob/main/README.md
225
226
  homepage_uri: https://github.com/martinotten/henitai
226
- source_code_uri: https://github.com/martinotten/henitai/tree/v0.3.1
227
+ source_code_uri: https://github.com/martinotten/henitai/tree/v0.4.0
227
228
  rubygems_mfa_required: 'true'
228
229
  rdoc_options: []
229
230
  require_paths: