Class: Reviewer::Runner::Result

Inherits:
Struct
  • Object
show all
Defined in:
lib/reviewer/runner/result.rb

Overview

Immutable value object representing the result of running a single tool

Constant Summary collapse

STATES =
i[passed failed skipped missing not_run].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes) ⇒ Result

Freeze on initialization to maintain immutability like Data.define

Raises:

  • (ArgumentError)


49
50
51
52
53
54
55
56
57
58
# File 'lib/reviewer/runner/result.rb', line 49

def initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes)
  state ||= [[:skipped, skipped], [:missing, missing], [:passed, success == true], [:failed, success == false]]
            .find(&:last)&.first
  raise ArgumentError, "Unknown result state: #{state.inspect}" unless Result::STATES.include?(state)

  super(**attributes, state: state, success: success, skipped: skipped, missing: missing)
  validate_legacy_values
  normalize_legacy_values
  freeze
end

Instance Attribute Details

#command_stringString? (readonly)



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/reviewer/runner/result.rb', line 31

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  :state,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes)
    state ||= [[:skipped, skipped], [:missing, missing], [:passed, success == true], [:failed, success == false]]
              .find(&:last)&.first
    raise ArgumentError, "Unknown result state: #{state.inspect}" unless Result::STATES.include?(state)

    super(**attributes, state: state, success: success, skipped: skipped, missing: missing)
    validate_legacy_values
    normalize_legacy_values
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  # Builds a result for a tool that fail-fast prevented from running.
  # @param tool [Tool] the selected tool that did not run
  # @param command_type [Symbol] the requested command type
  #
  # @return [Result] an immutable not-run result
  def self.not_run(tool:, command_type:)
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: command_type,
      command_string: nil,
      state: :not_run,
      exit_status: nil,
      duration: nil,
      stdout: nil,
      stderr: nil
    )
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    tool = runner.tool
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: nil,
      state: :skipped,
      exit_status: nil, duration: nil, stdout: nil, stderr: nil
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      state: :missing,
      exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      state: runner.success? ? :passed : :failed,
      exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  def passed? = state?(:passed)
  def failed? = state?(:failed)
  def not_run? = state?(:not_run)
  def success? = state?(:passed)
  def skipped? = state?(:skipped)
  def missing? = state?(:missing)

  def state?(value) = state == value
  private :state?

  # Whether this result represents a tool that actually ran
  #
  # @return [Boolean] true if the tool was executed
  def executed? = passed? || failed?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern && summary_label

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) do
      capture = match[Regexp.last_match(1).to_i]
      return nil unless capture

      capture
    end
  rescue RegexpError
    nil
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] serialized result; skipped and not-run execution fields remain explicit nils
  def to_h
    attributes = serialized_attributes

    return attributes.compact unless skipped? || not_run?

    attributes.compact.merge(attributes.slice(:command, :exit_status, :duration, :stdout, :stderr))
  end

  private

  def validate_legacy_values
    conflicting_flags = [[self[:skipped], :skipped], [self[:missing], :missing]]
                        .select(&:first).map(&:last).any? { |value| value != state }
    conflicting_success = { true => :failed, false => :passed }.fetch(self[:success]) { nil } == state
    return unless conflicting_flags || conflicting_success

    raise ArgumentError, 'Result state conflicts with legacy values'
  end

  # Assigns unconditionally so the stored members are a pure function of
  # state. A partial assignment left the caller's argument in place, so two
  # results with identical states compared unequal through Struct#==.
  def normalize_legacy_values
    self[:success] = passed?
    self[:skipped] = (true if skipped?)
    self[:missing] = (true if missing?)
  end

  def serialized_attributes
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      state: state,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing,
      detail_summary: detail_summary
    }
  end
end

#command_typeSymbol (readonly)



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/reviewer/runner/result.rb', line 31

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  :state,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes)
    state ||= [[:skipped, skipped], [:missing, missing], [:passed, success == true], [:failed, success == false]]
              .find(&:last)&.first
    raise ArgumentError, "Unknown result state: #{state.inspect}" unless Result::STATES.include?(state)

    super(**attributes, state: state, success: success, skipped: skipped, missing: missing)
    validate_legacy_values
    normalize_legacy_values
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  # Builds a result for a tool that fail-fast prevented from running.
  # @param tool [Tool] the selected tool that did not run
  # @param command_type [Symbol] the requested command type
  #
  # @return [Result] an immutable not-run result
  def self.not_run(tool:, command_type:)
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: command_type,
      command_string: nil,
      state: :not_run,
      exit_status: nil,
      duration: nil,
      stdout: nil,
      stderr: nil
    )
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    tool = runner.tool
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: nil,
      state: :skipped,
      exit_status: nil, duration: nil, stdout: nil, stderr: nil
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      state: :missing,
      exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      state: runner.success? ? :passed : :failed,
      exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  def passed? = state?(:passed)
  def failed? = state?(:failed)
  def not_run? = state?(:not_run)
  def success? = state?(:passed)
  def skipped? = state?(:skipped)
  def missing? = state?(:missing)

  def state?(value) = state == value
  private :state?

  # Whether this result represents a tool that actually ran
  #
  # @return [Boolean] true if the tool was executed
  def executed? = passed? || failed?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern && summary_label

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) do
      capture = match[Regexp.last_match(1).to_i]
      return nil unless capture

      capture
    end
  rescue RegexpError
    nil
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] serialized result; skipped and not-run execution fields remain explicit nils
  def to_h
    attributes = serialized_attributes

    return attributes.compact unless skipped? || not_run?

    attributes.compact.merge(attributes.slice(:command, :exit_status, :duration, :stdout, :stderr))
  end

  private

  def validate_legacy_values
    conflicting_flags = [[self[:skipped], :skipped], [self[:missing], :missing]]
                        .select(&:first).map(&:last).any? { |value| value != state }
    conflicting_success = { true => :failed, false => :passed }.fetch(self[:success]) { nil } == state
    return unless conflicting_flags || conflicting_success

    raise ArgumentError, 'Result state conflicts with legacy values'
  end

  # Assigns unconditionally so the stored members are a pure function of
  # state. A partial assignment left the caller's argument in place, so two
  # results with identical states compared unequal through Struct#==.
  def normalize_legacy_values
    self[:success] = passed?
    self[:skipped] = (true if skipped?)
    self[:missing] = (true if missing?)
  end

  def serialized_attributes
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      state: state,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing,
      detail_summary: detail_summary
    }
  end
end

#durationFloat? (readonly)



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/reviewer/runner/result.rb', line 31

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  :state,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes)
    state ||= [[:skipped, skipped], [:missing, missing], [:passed, success == true], [:failed, success == false]]
              .find(&:last)&.first
    raise ArgumentError, "Unknown result state: #{state.inspect}" unless Result::STATES.include?(state)

    super(**attributes, state: state, success: success, skipped: skipped, missing: missing)
    validate_legacy_values
    normalize_legacy_values
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  # Builds a result for a tool that fail-fast prevented from running.
  # @param tool [Tool] the selected tool that did not run
  # @param command_type [Symbol] the requested command type
  #
  # @return [Result] an immutable not-run result
  def self.not_run(tool:, command_type:)
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: command_type,
      command_string: nil,
      state: :not_run,
      exit_status: nil,
      duration: nil,
      stdout: nil,
      stderr: nil
    )
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    tool = runner.tool
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: nil,
      state: :skipped,
      exit_status: nil, duration: nil, stdout: nil, stderr: nil
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      state: :missing,
      exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      state: runner.success? ? :passed : :failed,
      exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  def passed? = state?(:passed)
  def failed? = state?(:failed)
  def not_run? = state?(:not_run)
  def success? = state?(:passed)
  def skipped? = state?(:skipped)
  def missing? = state?(:missing)

  def state?(value) = state == value
  private :state?

  # Whether this result represents a tool that actually ran
  #
  # @return [Boolean] true if the tool was executed
  def executed? = passed? || failed?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern && summary_label

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) do
      capture = match[Regexp.last_match(1).to_i]
      return nil unless capture

      capture
    end
  rescue RegexpError
    nil
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] serialized result; skipped and not-run execution fields remain explicit nils
  def to_h
    attributes = serialized_attributes

    return attributes.compact unless skipped? || not_run?

    attributes.compact.merge(attributes.slice(:command, :exit_status, :duration, :stdout, :stderr))
  end

  private

  def validate_legacy_values
    conflicting_flags = [[self[:skipped], :skipped], [self[:missing], :missing]]
                        .select(&:first).map(&:last).any? { |value| value != state }
    conflicting_success = { true => :failed, false => :passed }.fetch(self[:success]) { nil } == state
    return unless conflicting_flags || conflicting_success

    raise ArgumentError, 'Result state conflicts with legacy values'
  end

  # Assigns unconditionally so the stored members are a pure function of
  # state. A partial assignment left the caller's argument in place, so two
  # results with identical states compared unequal through Struct#==.
  def normalize_legacy_values
    self[:success] = passed?
    self[:skipped] = (true if skipped?)
    self[:missing] = (true if missing?)
  end

  def serialized_attributes
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      state: state,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing,
      detail_summary: detail_summary
    }
  end
end

#exit_statusInteger? (readonly)



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/reviewer/runner/result.rb', line 31

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  :state,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes)
    state ||= [[:skipped, skipped], [:missing, missing], [:passed, success == true], [:failed, success == false]]
              .find(&:last)&.first
    raise ArgumentError, "Unknown result state: #{state.inspect}" unless Result::STATES.include?(state)

    super(**attributes, state: state, success: success, skipped: skipped, missing: missing)
    validate_legacy_values
    normalize_legacy_values
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  # Builds a result for a tool that fail-fast prevented from running.
  # @param tool [Tool] the selected tool that did not run
  # @param command_type [Symbol] the requested command type
  #
  # @return [Result] an immutable not-run result
  def self.not_run(tool:, command_type:)
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: command_type,
      command_string: nil,
      state: :not_run,
      exit_status: nil,
      duration: nil,
      stdout: nil,
      stderr: nil
    )
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    tool = runner.tool
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: nil,
      state: :skipped,
      exit_status: nil, duration: nil, stdout: nil, stderr: nil
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      state: :missing,
      exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      state: runner.success? ? :passed : :failed,
      exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  def passed? = state?(:passed)
  def failed? = state?(:failed)
  def not_run? = state?(:not_run)
  def success? = state?(:passed)
  def skipped? = state?(:skipped)
  def missing? = state?(:missing)

  def state?(value) = state == value
  private :state?

  # Whether this result represents a tool that actually ran
  #
  # @return [Boolean] true if the tool was executed
  def executed? = passed? || failed?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern && summary_label

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) do
      capture = match[Regexp.last_match(1).to_i]
      return nil unless capture

      capture
    end
  rescue RegexpError
    nil
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] serialized result; skipped and not-run execution fields remain explicit nils
  def to_h
    attributes = serialized_attributes

    return attributes.compact unless skipped? || not_run?

    attributes.compact.merge(attributes.slice(:command, :exit_status, :duration, :stdout, :stderr))
  end

  private

  def validate_legacy_values
    conflicting_flags = [[self[:skipped], :skipped], [self[:missing], :missing]]
                        .select(&:first).map(&:last).any? { |value| value != state }
    conflicting_success = { true => :failed, false => :passed }.fetch(self[:success]) { nil } == state
    return unless conflicting_flags || conflicting_success

    raise ArgumentError, 'Result state conflicts with legacy values'
  end

  # Assigns unconditionally so the stored members are a pure function of
  # state. A partial assignment left the caller's argument in place, so two
  # results with identical states compared unequal through Struct#==.
  def normalize_legacy_values
    self[:success] = passed?
    self[:skipped] = (true if skipped?)
    self[:missing] = (true if missing?)
  end

  def serialized_attributes
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      state: state,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing,
      detail_summary: detail_summary
    }
  end
end

#missingBoolean? (readonly)



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/reviewer/runner/result.rb', line 31

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  :state,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes)
    state ||= [[:skipped, skipped], [:missing, missing], [:passed, success == true], [:failed, success == false]]
              .find(&:last)&.first
    raise ArgumentError, "Unknown result state: #{state.inspect}" unless Result::STATES.include?(state)

    super(**attributes, state: state, success: success, skipped: skipped, missing: missing)
    validate_legacy_values
    normalize_legacy_values
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  # Builds a result for a tool that fail-fast prevented from running.
  # @param tool [Tool] the selected tool that did not run
  # @param command_type [Symbol] the requested command type
  #
  # @return [Result] an immutable not-run result
  def self.not_run(tool:, command_type:)
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: command_type,
      command_string: nil,
      state: :not_run,
      exit_status: nil,
      duration: nil,
      stdout: nil,
      stderr: nil
    )
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    tool = runner.tool
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: nil,
      state: :skipped,
      exit_status: nil, duration: nil, stdout: nil, stderr: nil
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      state: :missing,
      exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      state: runner.success? ? :passed : :failed,
      exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  def passed? = state?(:passed)
  def failed? = state?(:failed)
  def not_run? = state?(:not_run)
  def success? = state?(:passed)
  def skipped? = state?(:skipped)
  def missing? = state?(:missing)

  def state?(value) = state == value
  private :state?

  # Whether this result represents a tool that actually ran
  #
  # @return [Boolean] true if the tool was executed
  def executed? = passed? || failed?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern && summary_label

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) do
      capture = match[Regexp.last_match(1).to_i]
      return nil unless capture

      capture
    end
  rescue RegexpError
    nil
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] serialized result; skipped and not-run execution fields remain explicit nils
  def to_h
    attributes = serialized_attributes

    return attributes.compact unless skipped? || not_run?

    attributes.compact.merge(attributes.slice(:command, :exit_status, :duration, :stdout, :stderr))
  end

  private

  def validate_legacy_values
    conflicting_flags = [[self[:skipped], :skipped], [self[:missing], :missing]]
                        .select(&:first).map(&:last).any? { |value| value != state }
    conflicting_success = { true => :failed, false => :passed }.fetch(self[:success]) { nil } == state
    return unless conflicting_flags || conflicting_success

    raise ArgumentError, 'Result state conflicts with legacy values'
  end

  # Assigns unconditionally so the stored members are a pure function of
  # state. A partial assignment left the caller's argument in place, so two
  # results with identical states compared unequal through Struct#==.
  def normalize_legacy_values
    self[:success] = passed?
    self[:skipped] = (true if skipped?)
    self[:missing] = (true if missing?)
  end

  def serialized_attributes
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      state: state,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing,
      detail_summary: detail_summary
    }
  end
end

#skippedBoolean? (readonly)



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/reviewer/runner/result.rb', line 31

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  :state,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes)
    state ||= [[:skipped, skipped], [:missing, missing], [:passed, success == true], [:failed, success == false]]
              .find(&:last)&.first
    raise ArgumentError, "Unknown result state: #{state.inspect}" unless Result::STATES.include?(state)

    super(**attributes, state: state, success: success, skipped: skipped, missing: missing)
    validate_legacy_values
    normalize_legacy_values
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  # Builds a result for a tool that fail-fast prevented from running.
  # @param tool [Tool] the selected tool that did not run
  # @param command_type [Symbol] the requested command type
  #
  # @return [Result] an immutable not-run result
  def self.not_run(tool:, command_type:)
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: command_type,
      command_string: nil,
      state: :not_run,
      exit_status: nil,
      duration: nil,
      stdout: nil,
      stderr: nil
    )
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    tool = runner.tool
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: nil,
      state: :skipped,
      exit_status: nil, duration: nil, stdout: nil, stderr: nil
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      state: :missing,
      exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      state: runner.success? ? :passed : :failed,
      exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  def passed? = state?(:passed)
  def failed? = state?(:failed)
  def not_run? = state?(:not_run)
  def success? = state?(:passed)
  def skipped? = state?(:skipped)
  def missing? = state?(:missing)

  def state?(value) = state == value
  private :state?

  # Whether this result represents a tool that actually ran
  #
  # @return [Boolean] true if the tool was executed
  def executed? = passed? || failed?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern && summary_label

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) do
      capture = match[Regexp.last_match(1).to_i]
      return nil unless capture

      capture
    end
  rescue RegexpError
    nil
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] serialized result; skipped and not-run execution fields remain explicit nils
  def to_h
    attributes = serialized_attributes

    return attributes.compact unless skipped? || not_run?

    attributes.compact.merge(attributes.slice(:command, :exit_status, :duration, :stdout, :stderr))
  end

  private

  def validate_legacy_values
    conflicting_flags = [[self[:skipped], :skipped], [self[:missing], :missing]]
                        .select(&:first).map(&:last).any? { |value| value != state }
    conflicting_success = { true => :failed, false => :passed }.fetch(self[:success]) { nil } == state
    return unless conflicting_flags || conflicting_success

    raise ArgumentError, 'Result state conflicts with legacy values'
  end

  # Assigns unconditionally so the stored members are a pure function of
  # state. A partial assignment left the caller's argument in place, so two
  # results with identical states compared unequal through Struct#==.
  def normalize_legacy_values
    self[:success] = passed?
    self[:skipped] = (true if skipped?)
    self[:missing] = (true if missing?)
  end

  def serialized_attributes
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      state: state,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing,
      detail_summary: detail_summary
    }
  end
end

#stateSymbol (readonly)



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/reviewer/runner/result.rb', line 31

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  :state,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes)
    state ||= [[:skipped, skipped], [:missing, missing], [:passed, success == true], [:failed, success == false]]
              .find(&:last)&.first
    raise ArgumentError, "Unknown result state: #{state.inspect}" unless Result::STATES.include?(state)

    super(**attributes, state: state, success: success, skipped: skipped, missing: missing)
    validate_legacy_values
    normalize_legacy_values
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  # Builds a result for a tool that fail-fast prevented from running.
  # @param tool [Tool] the selected tool that did not run
  # @param command_type [Symbol] the requested command type
  #
  # @return [Result] an immutable not-run result
  def self.not_run(tool:, command_type:)
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: command_type,
      command_string: nil,
      state: :not_run,
      exit_status: nil,
      duration: nil,
      stdout: nil,
      stderr: nil
    )
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    tool = runner.tool
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: nil,
      state: :skipped,
      exit_status: nil, duration: nil, stdout: nil, stderr: nil
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      state: :missing,
      exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      state: runner.success? ? :passed : :failed,
      exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  def passed? = state?(:passed)
  def failed? = state?(:failed)
  def not_run? = state?(:not_run)
  def success? = state?(:passed)
  def skipped? = state?(:skipped)
  def missing? = state?(:missing)

  def state?(value) = state == value
  private :state?

  # Whether this result represents a tool that actually ran
  #
  # @return [Boolean] true if the tool was executed
  def executed? = passed? || failed?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern && summary_label

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) do
      capture = match[Regexp.last_match(1).to_i]
      return nil unless capture

      capture
    end
  rescue RegexpError
    nil
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] serialized result; skipped and not-run execution fields remain explicit nils
  def to_h
    attributes = serialized_attributes

    return attributes.compact unless skipped? || not_run?

    attributes.compact.merge(attributes.slice(:command, :exit_status, :duration, :stdout, :stderr))
  end

  private

  def validate_legacy_values
    conflicting_flags = [[self[:skipped], :skipped], [self[:missing], :missing]]
                        .select(&:first).map(&:last).any? { |value| value != state }
    conflicting_success = { true => :failed, false => :passed }.fetch(self[:success]) { nil } == state
    return unless conflicting_flags || conflicting_success

    raise ArgumentError, 'Result state conflicts with legacy values'
  end

  # Assigns unconditionally so the stored members are a pure function of
  # state. A partial assignment left the caller's argument in place, so two
  # results with identical states compared unequal through Struct#==.
  def normalize_legacy_values
    self[:success] = passed?
    self[:skipped] = (true if skipped?)
    self[:missing] = (true if missing?)
  end

  def serialized_attributes
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      state: state,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing,
      detail_summary: detail_summary
    }
  end
end

#stderrString? (readonly)



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/reviewer/runner/result.rb', line 31

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  :state,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes)
    state ||= [[:skipped, skipped], [:missing, missing], [:passed, success == true], [:failed, success == false]]
              .find(&:last)&.first
    raise ArgumentError, "Unknown result state: #{state.inspect}" unless Result::STATES.include?(state)

    super(**attributes, state: state, success: success, skipped: skipped, missing: missing)
    validate_legacy_values
    normalize_legacy_values
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  # Builds a result for a tool that fail-fast prevented from running.
  # @param tool [Tool] the selected tool that did not run
  # @param command_type [Symbol] the requested command type
  #
  # @return [Result] an immutable not-run result
  def self.not_run(tool:, command_type:)
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: command_type,
      command_string: nil,
      state: :not_run,
      exit_status: nil,
      duration: nil,
      stdout: nil,
      stderr: nil
    )
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    tool = runner.tool
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: nil,
      state: :skipped,
      exit_status: nil, duration: nil, stdout: nil, stderr: nil
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      state: :missing,
      exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      state: runner.success? ? :passed : :failed,
      exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  def passed? = state?(:passed)
  def failed? = state?(:failed)
  def not_run? = state?(:not_run)
  def success? = state?(:passed)
  def skipped? = state?(:skipped)
  def missing? = state?(:missing)

  def state?(value) = state == value
  private :state?

  # Whether this result represents a tool that actually ran
  #
  # @return [Boolean] true if the tool was executed
  def executed? = passed? || failed?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern && summary_label

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) do
      capture = match[Regexp.last_match(1).to_i]
      return nil unless capture

      capture
    end
  rescue RegexpError
    nil
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] serialized result; skipped and not-run execution fields remain explicit nils
  def to_h
    attributes = serialized_attributes

    return attributes.compact unless skipped? || not_run?

    attributes.compact.merge(attributes.slice(:command, :exit_status, :duration, :stdout, :stderr))
  end

  private

  def validate_legacy_values
    conflicting_flags = [[self[:skipped], :skipped], [self[:missing], :missing]]
                        .select(&:first).map(&:last).any? { |value| value != state }
    conflicting_success = { true => :failed, false => :passed }.fetch(self[:success]) { nil } == state
    return unless conflicting_flags || conflicting_success

    raise ArgumentError, 'Result state conflicts with legacy values'
  end

  # Assigns unconditionally so the stored members are a pure function of
  # state. A partial assignment left the caller's argument in place, so two
  # results with identical states compared unequal through Struct#==.
  def normalize_legacy_values
    self[:success] = passed?
    self[:skipped] = (true if skipped?)
    self[:missing] = (true if missing?)
  end

  def serialized_attributes
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      state: state,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing,
      detail_summary: detail_summary
    }
  end
end

#stdoutString? (readonly)



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/reviewer/runner/result.rb', line 31

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  :state,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes)
    state ||= [[:skipped, skipped], [:missing, missing], [:passed, success == true], [:failed, success == false]]
              .find(&:last)&.first
    raise ArgumentError, "Unknown result state: #{state.inspect}" unless Result::STATES.include?(state)

    super(**attributes, state: state, success: success, skipped: skipped, missing: missing)
    validate_legacy_values
    normalize_legacy_values
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  # Builds a result for a tool that fail-fast prevented from running.
  # @param tool [Tool] the selected tool that did not run
  # @param command_type [Symbol] the requested command type
  #
  # @return [Result] an immutable not-run result
  def self.not_run(tool:, command_type:)
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: command_type,
      command_string: nil,
      state: :not_run,
      exit_status: nil,
      duration: nil,
      stdout: nil,
      stderr: nil
    )
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    tool = runner.tool
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: nil,
      state: :skipped,
      exit_status: nil, duration: nil, stdout: nil, stderr: nil
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      state: :missing,
      exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      state: runner.success? ? :passed : :failed,
      exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  def passed? = state?(:passed)
  def failed? = state?(:failed)
  def not_run? = state?(:not_run)
  def success? = state?(:passed)
  def skipped? = state?(:skipped)
  def missing? = state?(:missing)

  def state?(value) = state == value
  private :state?

  # Whether this result represents a tool that actually ran
  #
  # @return [Boolean] true if the tool was executed
  def executed? = passed? || failed?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern && summary_label

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) do
      capture = match[Regexp.last_match(1).to_i]
      return nil unless capture

      capture
    end
  rescue RegexpError
    nil
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] serialized result; skipped and not-run execution fields remain explicit nils
  def to_h
    attributes = serialized_attributes

    return attributes.compact unless skipped? || not_run?

    attributes.compact.merge(attributes.slice(:command, :exit_status, :duration, :stdout, :stderr))
  end

  private

  def validate_legacy_values
    conflicting_flags = [[self[:skipped], :skipped], [self[:missing], :missing]]
                        .select(&:first).map(&:last).any? { |value| value != state }
    conflicting_success = { true => :failed, false => :passed }.fetch(self[:success]) { nil } == state
    return unless conflicting_flags || conflicting_success

    raise ArgumentError, 'Result state conflicts with legacy values'
  end

  # Assigns unconditionally so the stored members are a pure function of
  # state. A partial assignment left the caller's argument in place, so two
  # results with identical states compared unequal through Struct#==.
  def normalize_legacy_values
    self[:success] = passed?
    self[:skipped] = (true if skipped?)
    self[:missing] = (true if missing?)
  end

  def serialized_attributes
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      state: state,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing,
      detail_summary: detail_summary
    }
  end
end

#successBoolean (readonly)



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/reviewer/runner/result.rb', line 31

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  :state,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes)
    state ||= [[:skipped, skipped], [:missing, missing], [:passed, success == true], [:failed, success == false]]
              .find(&:last)&.first
    raise ArgumentError, "Unknown result state: #{state.inspect}" unless Result::STATES.include?(state)

    super(**attributes, state: state, success: success, skipped: skipped, missing: missing)
    validate_legacy_values
    normalize_legacy_values
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  # Builds a result for a tool that fail-fast prevented from running.
  # @param tool [Tool] the selected tool that did not run
  # @param command_type [Symbol] the requested command type
  #
  # @return [Result] an immutable not-run result
  def self.not_run(tool:, command_type:)
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: command_type,
      command_string: nil,
      state: :not_run,
      exit_status: nil,
      duration: nil,
      stdout: nil,
      stderr: nil
    )
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    tool = runner.tool
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: nil,
      state: :skipped,
      exit_status: nil, duration: nil, stdout: nil, stderr: nil
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      state: :missing,
      exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      state: runner.success? ? :passed : :failed,
      exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  def passed? = state?(:passed)
  def failed? = state?(:failed)
  def not_run? = state?(:not_run)
  def success? = state?(:passed)
  def skipped? = state?(:skipped)
  def missing? = state?(:missing)

  def state?(value) = state == value
  private :state?

  # Whether this result represents a tool that actually ran
  #
  # @return [Boolean] true if the tool was executed
  def executed? = passed? || failed?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern && summary_label

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) do
      capture = match[Regexp.last_match(1).to_i]
      return nil unless capture

      capture
    end
  rescue RegexpError
    nil
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] serialized result; skipped and not-run execution fields remain explicit nils
  def to_h
    attributes = serialized_attributes

    return attributes.compact unless skipped? || not_run?

    attributes.compact.merge(attributes.slice(:command, :exit_status, :duration, :stdout, :stderr))
  end

  private

  def validate_legacy_values
    conflicting_flags = [[self[:skipped], :skipped], [self[:missing], :missing]]
                        .select(&:first).map(&:last).any? { |value| value != state }
    conflicting_success = { true => :failed, false => :passed }.fetch(self[:success]) { nil } == state
    return unless conflicting_flags || conflicting_success

    raise ArgumentError, 'Result state conflicts with legacy values'
  end

  # Assigns unconditionally so the stored members are a pure function of
  # state. A partial assignment left the caller's argument in place, so two
  # results with identical states compared unequal through Struct#==.
  def normalize_legacy_values
    self[:success] = passed?
    self[:skipped] = (true if skipped?)
    self[:missing] = (true if missing?)
  end

  def serialized_attributes
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      state: state,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing,
      detail_summary: detail_summary
    }
  end
end

#summary_labelObject

Returns the value of attribute summary_label



31
32
33
# File 'lib/reviewer/runner/result.rb', line 31

def summary_label
  @summary_label
end

#summary_patternObject

Returns the value of attribute summary_pattern



31
32
33
# File 'lib/reviewer/runner/result.rb', line 31

def summary_pattern
  @summary_pattern
end

#tool_keySymbol (readonly)



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/reviewer/runner/result.rb', line 31

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  :state,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes)
    state ||= [[:skipped, skipped], [:missing, missing], [:passed, success == true], [:failed, success == false]]
              .find(&:last)&.first
    raise ArgumentError, "Unknown result state: #{state.inspect}" unless Result::STATES.include?(state)

    super(**attributes, state: state, success: success, skipped: skipped, missing: missing)
    validate_legacy_values
    normalize_legacy_values
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  # Builds a result for a tool that fail-fast prevented from running.
  # @param tool [Tool] the selected tool that did not run
  # @param command_type [Symbol] the requested command type
  #
  # @return [Result] an immutable not-run result
  def self.not_run(tool:, command_type:)
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: command_type,
      command_string: nil,
      state: :not_run,
      exit_status: nil,
      duration: nil,
      stdout: nil,
      stderr: nil
    )
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    tool = runner.tool
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: nil,
      state: :skipped,
      exit_status: nil, duration: nil, stdout: nil, stderr: nil
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      state: :missing,
      exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      state: runner.success? ? :passed : :failed,
      exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  def passed? = state?(:passed)
  def failed? = state?(:failed)
  def not_run? = state?(:not_run)
  def success? = state?(:passed)
  def skipped? = state?(:skipped)
  def missing? = state?(:missing)

  def state?(value) = state == value
  private :state?

  # Whether this result represents a tool that actually ran
  #
  # @return [Boolean] true if the tool was executed
  def executed? = passed? || failed?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern && summary_label

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) do
      capture = match[Regexp.last_match(1).to_i]
      return nil unless capture

      capture
    end
  rescue RegexpError
    nil
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] serialized result; skipped and not-run execution fields remain explicit nils
  def to_h
    attributes = serialized_attributes

    return attributes.compact unless skipped? || not_run?

    attributes.compact.merge(attributes.slice(:command, :exit_status, :duration, :stdout, :stderr))
  end

  private

  def validate_legacy_values
    conflicting_flags = [[self[:skipped], :skipped], [self[:missing], :missing]]
                        .select(&:first).map(&:last).any? { |value| value != state }
    conflicting_success = { true => :failed, false => :passed }.fetch(self[:success]) { nil } == state
    return unless conflicting_flags || conflicting_success

    raise ArgumentError, 'Result state conflicts with legacy values'
  end

  # Assigns unconditionally so the stored members are a pure function of
  # state. A partial assignment left the caller's argument in place, so two
  # results with identical states compared unequal through Struct#==.
  def normalize_legacy_values
    self[:success] = passed?
    self[:skipped] = (true if skipped?)
    self[:missing] = (true if missing?)
  end

  def serialized_attributes
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      state: state,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing,
      detail_summary: detail_summary
    }
  end
end

#tool_nameString (readonly)



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/reviewer/runner/result.rb', line 31

Result = Struct.new(
  :tool_key,
  :tool_name,
  :command_type,
  :command_string,
  :success,
  :exit_status,
  :duration,
  :stdout,
  :stderr,
  :skipped,
  :missing,
  :summary_pattern,
  :summary_label,
  :state,
  keyword_init: true
) do
  # Freeze on initialization to maintain immutability like Data.define
  def initialize(state: nil, success: nil, skipped: nil, missing: nil, **attributes)
    state ||= [[:skipped, skipped], [:missing, missing], [:passed, success == true], [:failed, success == false]]
              .find(&:last)&.first
    raise ArgumentError, "Unknown result state: #{state.inspect}" unless Result::STATES.include?(state)

    super(**attributes, state: state, success: success, skipped: skipped, missing: missing)
    validate_legacy_values
    normalize_legacy_values
    freeze
  end

  # Builds an immutable Result from a runner's current state.
  # @param runner [Runner] the runner after command execution
  #
  # @return [Result] an immutable result for reporting
  def self.from_runner(runner)
    if runner.skipped?
      build_skipped(runner)
    elsif runner.missing?
      build_missing(runner)
    else
      build_executed(runner)
    end
  end

  # Builds a result for a tool that fail-fast prevented from running.
  # @param tool [Tool] the selected tool that did not run
  # @param command_type [Symbol] the requested command type
  #
  # @return [Result] an immutable not-run result
  def self.not_run(tool:, command_type:)
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: command_type,
      command_string: nil,
      state: :not_run,
      exit_status: nil,
      duration: nil,
      stdout: nil,
      stderr: nil
    )
  end

  def self.base_attributes(runner)
    tool = runner.tool
    {
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: runner.command.string
    }
  end

  def self.build_skipped(runner)
    tool = runner.tool
    new(
      tool_key: tool.key,
      tool_name: tool.name,
      command_type: runner.command.type,
      command_string: nil,
      state: :skipped,
      exit_status: nil, duration: nil, stdout: nil, stderr: nil
    )
  end

  def self.build_missing(runner)
    new(
      **base_attributes(runner),
      state: :missing,
      exit_status: runner.shell.result.exit_status, duration: 0,
      stdout: nil, stderr: nil
    )
  end

  def self.build_executed(runner)
    shell = runner.shell
    shell_result = shell.result
    settings = runner.tool.settings
    new(
      **base_attributes(runner),
      state: runner.success? ? :passed : :failed,
      exit_status: shell_result.exit_status,
      duration: shell.timer.total_seconds,
      stdout: shell_result.stdout, stderr: shell_result.stderr,
      summary_pattern: settings.summary_pattern,
      summary_label: settings.summary_label
    )
  end

  private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed

  def passed? = state?(:passed)
  def failed? = state?(:failed)
  def not_run? = state?(:not_run)
  def success? = state?(:passed)
  def skipped? = state?(:skipped)
  def missing? = state?(:missing)

  def state?(value) = state == value
  private :state?

  # Whether this result represents a tool that actually ran
  #
  # @return [Boolean] true if the tool was executed
  def executed? = passed? || failed?

  # Extracts a short summary detail from stdout for display purposes.
  # Each tool type may have its own summary format (test count, offense count, etc.)
  #
  # @return [String, nil] a brief summary or nil if no detail can be extracted
  def detail_summary
    return nil unless summary_pattern && summary_label

    match = stdout&.match(/#{summary_pattern}/i)
    return nil unless match

    summary_label.gsub(/\\(\d+)/) do
      capture = match[Regexp.last_match(1).to_i]
      return nil unless capture

      capture
    end
  rescue RegexpError
    nil
  end

  # Converts the result to a hash suitable for serialization
  #
  # @return [Hash] serialized result; skipped and not-run execution fields remain explicit nils
  def to_h
    attributes = serialized_attributes

    return attributes.compact unless skipped? || not_run?

    attributes.compact.merge(attributes.slice(:command, :exit_status, :duration, :stdout, :stderr))
  end

  private

  def validate_legacy_values
    conflicting_flags = [[self[:skipped], :skipped], [self[:missing], :missing]]
                        .select(&:first).map(&:last).any? { |value| value != state }
    conflicting_success = { true => :failed, false => :passed }.fetch(self[:success]) { nil } == state
    return unless conflicting_flags || conflicting_success

    raise ArgumentError, 'Result state conflicts with legacy values'
  end

  # Assigns unconditionally so the stored members are a pure function of
  # state. A partial assignment left the caller's argument in place, so two
  # results with identical states compared unequal through Struct#==.
  def normalize_legacy_values
    self[:success] = passed?
    self[:skipped] = (true if skipped?)
    self[:missing] = (true if missing?)
  end

  def serialized_attributes
    {
      tool: tool_key,
      name: tool_name,
      command_type: command_type,
      command: command_string,
      state: state,
      success: success,
      exit_status: exit_status,
      duration: duration,
      stdout: stdout,
      stderr: stderr,
      skipped: skipped,
      missing: missing,
      detail_summary: detail_summary
    }
  end
end

Class Method Details

.from_runner(runner) ⇒ Result

Builds an immutable Result from a runner's current state.



64
65
66
67
68
69
70
71
72
# File 'lib/reviewer/runner/result.rb', line 64

def self.from_runner(runner)
  if runner.skipped?
    build_skipped(runner)
  elsif runner.missing?
    build_missing(runner)
  else
    build_executed(runner)
  end
end

.not_run(tool:, command_type:) ⇒ Result

Builds a result for a tool that fail-fast prevented from running.



79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/reviewer/runner/result.rb', line 79

def self.not_run(tool:, command_type:)
  new(
    tool_key: tool.key,
    tool_name: tool.name,
    command_type: command_type,
    command_string: nil,
    state: :not_run,
    exit_status: nil,
    duration: nil,
    stdout: nil,
    stderr: nil
  )
end

Instance Method Details

#detail_summaryString?

Extracts a short summary detail from stdout for display purposes. Each tool type may have its own summary format (test count, offense count, etc.)



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/reviewer/runner/result.rb', line 160

def detail_summary
  return nil unless summary_pattern && summary_label

  match = stdout&.match(/#{summary_pattern}/i)
  return nil unless match

  summary_label.gsub(/\\(\d+)/) do
    capture = match[Regexp.last_match(1).to_i]
    return nil unless capture

    capture
  end
rescue RegexpError
  nil
end

#executed?Boolean

Whether this result represents a tool that actually ran



154
# File 'lib/reviewer/runner/result.rb', line 154

def executed? = passed? || failed?

#failed?Boolean



142
# File 'lib/reviewer/runner/result.rb', line 142

def failed? = state?(:failed)

#missing?Boolean



146
# File 'lib/reviewer/runner/result.rb', line 146

def missing? = state?(:missing)

#not_run?Boolean



143
# File 'lib/reviewer/runner/result.rb', line 143

def not_run? = state?(:not_run)

#passed?Boolean



141
# File 'lib/reviewer/runner/result.rb', line 141

def passed? = state?(:passed)

#skipped?Boolean



145
# File 'lib/reviewer/runner/result.rb', line 145

def skipped? = state?(:skipped)

#success?Boolean



144
# File 'lib/reviewer/runner/result.rb', line 144

def success? = state?(:passed)

#to_hHash

Converts the result to a hash suitable for serialization



179
180
181
182
183
184
185
# File 'lib/reviewer/runner/result.rb', line 179

def to_h
  attributes = serialized_attributes

  return attributes.compact unless skipped? || not_run?

  attributes.compact.merge(attributes.slice(:command, :exit_status, :duration, :stdout, :stderr))
end