reviewer 1.1.0 → 1.1.1

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: 8d918dfc0f53f9d2283d846207fc760f0aa0cafaa2d9db0f2225396935cdbd38
4
- data.tar.gz: 2f8635ce31fee7646608c5dd591c277190e918b3c940b773d04731254c3ea707
3
+ metadata.gz: f8754795211c60e4aff15d716db73abc4ecbbc55c99afda217ab56180dc74b35
4
+ data.tar.gz: 9d7172360cecb1439d2ae513053211027f064786d72ad59f3a4f8029fbbdb557
5
5
  SHA512:
6
- metadata.gz: fc770176795e88d1c88e56903eb1ce3d756f0f46176edc1d1a280bff14069b0739a27a4aefa6ca04313c08f1324e3f9d45da86860c9563c97f7a82472005f7ca
7
- data.tar.gz: dbfdb47cbc0c321bc2338251b4a16242d976d4b6fb893019e7dc5578d875bfc85ef4124297864fa50cbe52cf7189471e6fc2ece940e114cf92fe8ab0e429e369
6
+ metadata.gz: ebbf843d62775a5aa4c7ead5da403f9a368680a503782433e2674df6643a5b9911802e9663200fb39e184d26cffb25c166af6ceef340c5f8926efb5f7cbbec93
7
+ data.tar.gz: e48861c8fe35cf51877298cd97e177705022720f0576e4aa4dc1bd73dd9c4221ca27a676837e888eeff7bf4529e3c5418656ce8b78aba643025cee3a0ad6879e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.1] - 2026-09-02
4
+
5
+ ### Fixed
6
+
7
+ - Keep explicit and Git-derived file-scoped runs focused by skipping tools that cannot accept files,
8
+ while preserving bare runs and failed retries; reject empty file options instead of running broadly.
9
+
3
10
  ## [1.1.0] - 2026-08-28
4
11
 
5
12
  Reviewer 1.1 makes review outcomes more precise and dependable for people and automated consumers.
@@ -106,8 +106,11 @@ rubocop:
106
106
  | `files.pattern` | Keeps matching paths before the command runs |
107
107
  | `files.map_to_tests` | Maps Ruby source paths to existing `minitest` or `rspec` test paths |
108
108
 
109
- Only tools with a `files:` block receive resolved file arguments; tools without one run their
110
- configured command unchanged.
109
+ Only tools with a `files:` block receive resolved file arguments. File-scoped requests skip tools
110
+ without one so `rvw staged`, `rvw modified`, and `rvw -f ...` cannot expand into full-project checks.
111
+ Bare `rvw` still runs every tool in the default batch. See the
112
+ [file-scoped behavior matrix](usage.md#file-scoped-behavior) for explicit scopes, composed Git
113
+ selectors, and failed-run precedence.
111
114
 
112
115
  Slashless patterns match each path's basename. Patterns containing `/` match normalized
113
116
  repository-relative paths with pathname semantics. In path patterns, `**/` crosses directory
data/docs/usage.md CHANGED
@@ -63,9 +63,10 @@ File selectors resolve paths from Git:
63
63
  | `untracked` | Untracked, non-ignored files |
64
64
 
65
65
  `failed` selects tools whose last executed review failed and, when available, reuses each tool's
66
- stored failed file paths. A tool remains selected until an executed review records a pass. Skipped,
67
- missing, formatted, and not-run tools leave that review history unchanged, so a passing scoped run
68
- can coexist with tools still selected by `rvw failed`.
66
+ stored failed file paths. File-aware tools retry those paths; tools without file support retry their
67
+ full command. A tool remains selected until an executed review records a pass. Skipped, missing,
68
+ formatted, and not-run tools leave that review history unchanged, so a passing scoped run can coexist
69
+ with tools still selected by `rvw failed`.
69
70
 
70
71
  Selections compose:
71
72
 
@@ -75,6 +76,10 @@ rvw -t ruby modified
75
76
  rvw tests -f test/reviewer_test.rb
76
77
  ```
77
78
 
79
+ Explicit `-f`, `staged`, `unstaged`, `modified`, and `untracked` requests skip tools without a
80
+ `files:` configuration instead of running their full-project command. Bare `rvw` remains the full
81
+ configured review.
82
+
78
83
  The [configuration reference](configuration.md#file-targeting) describes filtering, file-scoped
79
84
  commands, and Minitest/RSpec source-to-test mapping.
80
85
 
@@ -117,7 +122,7 @@ State describes the terminal disposition Reviewer acts on. For executed failures
117
122
  |---|---|---:|---|
118
123
  | `passed` | The command executed within its configured threshold | `true` | None |
119
124
  | `failed` | The command executed outside its configured threshold | `false` | Exit `1` |
120
- | `skipped` | Requested files did not match the tool | `false` | None |
125
+ | `skipped` | The tool cannot accept the file scope, or no requested files matched | `false` | None |
121
126
  | `missing` | The executable was unavailable | `false` | None in 1.1 |
122
127
  | `not_run` | An earlier tool failed and fail-fast stopped the batch | `false` | None |
123
128
 
@@ -161,7 +166,10 @@ Every summary contains `total`, all five state counts, and `duration`; the state
161
166
  |---|---|---|
162
167
  | Nonempty report | `schema_version`, `success`, `summary`, `tools` | `state`, `message`, `error` |
163
168
  | Empty report | `schema_version`, `state: "empty"`, `success: true`, `message`, zero summary, `tools: []` | `error` |
164
- | Selector error | `schema_version`, `state: "error"`, `error`, zero summary, `tools: []` | `success`, `message` |
169
+ | Usage error | `schema_version`, `state: "error"`, `error`, zero summary, `tools: []` | `success`, `message` |
170
+
171
+ Usage errors include `error.code: "unrecognized_selector"` for an unknown tool, tag, or keyword and
172
+ `error.code: "missing_files"` for an empty or missing `-f`/`--files` value.
165
173
 
166
174
  | Tool state | Execution fields | Compatibility flags |
167
175
  |---|---|---|
@@ -175,13 +183,31 @@ tool now has `success: false`, and its unavailable execution fields are null ins
175
183
  omitted. Valid 1.x `Runner::Result.new` keyword calls and the legacy true-valued payload flags remain
176
184
  supported through the 1.x line.
177
185
 
186
+ ## File-scoped behavior
187
+
188
+ | Request | Tools with `files:` | Tools without `files:` |
189
+ |---|---|---|
190
+ | `rvw` | Run without a file filter | Run without a file filter |
191
+ | `rvw -f app/a.rb` | Run with `app/a.rb` | Skip |
192
+ | `rvw staged untracked` | Run with the deduplicated union | Skip |
193
+ | `rvw staged` with no staged files | Do not run | Do not run |
194
+ | `rvw failed` | Use stored failed paths when available; otherwise retry without a file filter | Retry without a file filter |
195
+ | `rvw failed staged` | Use the newly resolved staged paths instead of stored paths | Skip |
196
+ | `rvw failed -f app/a.rb` | Use `app/a.rb` instead of stored paths | Skip |
197
+
198
+ Repeated `-f` options and Git selectors compose. For review and format execution, every `-f` or
199
+ `--files` occurrence requires at least one path; an empty or missing value exits 2 without running
200
+ any tool. Informational `--help` and `--version` requests, plus the `init` and `doctor` control
201
+ commands, exit before file-scope validation. A valid Git selector that finds no files is instead a
202
+ successful no-op because the selector itself was complete.
203
+
178
204
  ## Exit statuses
179
205
 
180
206
  | Status | Meaning |
181
207
  |---|---|
182
208
  | `0` | Every executed tool passed; skipped, missing, and not-run tools do not fail the run |
183
209
  | `1` | At least one executed tool failed |
184
- | `2` | The request used an unrecognized tool, tag, or keyword |
210
+ | `2` | The request used an unrecognized selector or an empty `-f`/`--files` option |
185
211
 
186
212
  A tool may pass with a nonzero process status when its `commands.max_exit_status` allows it. Reviewer
187
213
  returns its own status above instead of forwarding the tool's process status.
@@ -15,9 +15,10 @@ module Reviewer
15
15
  #
16
16
  # @return [void]
17
17
  # :reek:TooManyStatements -- a flag declaration list, not branching logic
18
- def configure(opts)
18
+ def configure(opts, files_callback: nil)
19
19
  # Narrowing what gets reviewed
20
- opts.array '-f', '--files', 'a list of comma-separated files or paths', delimiter: ',', default: []
20
+ opts.array '-f', '--files', 'a list of comma-separated files or paths',
21
+ delimiter: ',', default: [], &files_callback
21
22
  opts.array '-t', '--tags', 'a list of comma-separated tags', delimiter: ',', default: []
22
23
 
23
24
  # Selecting how results are displayed
@@ -42,7 +42,11 @@ module Reviewer
42
42
  # @return [self]
43
43
  def initialize(options = ARGV, output: Output.new)
44
44
  @output = output
45
- @options = Slop.parse(options) { |opts| Options.configure(opts) }
45
+ @raw_options = options.to_a.dup
46
+ @invalid_files_option = false
47
+ @parser = Slop::Options.new
48
+ Options.configure(@parser, files_callback: files_validation_callback)
49
+ @options = @parser.parse(normalized_options)
46
50
  end
47
51
 
48
52
  private
@@ -105,6 +109,16 @@ module Reviewer
105
109
  # @return [Boolean] true if JSON output mode is requested
106
110
  def json? = options[:json]
107
111
 
112
+ # Whether the --capabilities flag was passed
113
+ #
114
+ # @return [Boolean] true if capabilities were requested
115
+ def capabilities? = options[:capabilities]
116
+
117
+ # Whether a files option was supplied without a usable value
118
+ #
119
+ # @return [Boolean] true when any -f/--files occurrence is empty
120
+ def invalid_files_option? = @invalid_files_option
121
+
108
122
  # The output format for results
109
123
  #
110
124
  # @return [Symbol] the output format (:streaming, :summary, or :json)
@@ -114,7 +128,7 @@ module Reviewer
114
128
  value = options[:format].to_sym
115
129
  return value if KNOWN_FORMATS.include?(value)
116
130
 
117
- session_formatter.invalid_format(options[:format], KNOWN_FORMATS)
131
+ session_formatter.invalid_format(options[:format], KNOWN_FORMATS) unless invalid_files_option?
118
132
  :streaming
119
133
  end
120
134
 
@@ -133,5 +147,53 @@ module Reviewer
133
147
 
134
148
  multiple_tools ? Runner::Strategies::Captured : Runner::Strategies::Passthrough
135
149
  end
150
+
151
+ private
152
+
153
+ def files_validation_callback
154
+ previous_file_count = 0
155
+ lambda do |files|
156
+ current_files = Array(files)
157
+ file_count = current_files.length
158
+ new_files = current_files.drop(previous_file_count)
159
+ @invalid_files_option = true if file_count <= previous_file_count ||
160
+ new_files.all? { |file| file.strip.empty? }
161
+ previous_file_count = file_count
162
+ end
163
+ end
164
+
165
+ def normalized_options
166
+ parsing_options = true
167
+ @raw_options.each_with_index.flat_map do |token, index|
168
+ parsing_options = false if token == '--'
169
+ missing_value = parsing_options && files_option_awaiting_value?(token) &&
170
+ known_option_token?(@raw_options[index + 1])
171
+ normalized_token = parsing_options ? normalize_compact_files_option(token) : token
172
+
173
+ missing_value ? [normalized_token, ''] : [normalized_token]
174
+ end
175
+ end
176
+
177
+ def normalize_compact_files_option(token)
178
+ return token unless token.start_with?('-f') && token.length > 2 && !token.start_with?('-f=')
179
+
180
+ "-f=#{token.delete_prefix('-f')}"
181
+ end
182
+
183
+ def known_option_token?(token)
184
+ return false unless token&.start_with?('-') && token != '--'
185
+
186
+ Slop.parse([token]) { |opts| Options.configure(opts) }
187
+ true
188
+ rescue Slop::UnknownOption
189
+ false
190
+ end
191
+
192
+ def files_option_awaiting_value?(token)
193
+ return true if %w[-f --files].include?(token)
194
+ return false if token.start_with?('-f')
195
+
196
+ token.match?(/\A-[^-]+f\z/) && known_option_token?(token)
197
+ end
136
198
  end
137
199
  end
@@ -34,7 +34,7 @@ module Reviewer
34
34
  runnable_tools = matching_tools
35
35
  elapsed_time = Benchmark.realtime do
36
36
  failed_index = runnable_tools.find_index { |tool| run_tool(tool).failed? }
37
- record_not_run(runnable_tools.drop(failed_index + 1)) if failed_index
37
+ record_remaining(runnable_tools.drop(failed_index + 1)) if failed_index
38
38
  end
39
39
 
40
40
  @report.record_duration(elapsed_time)
@@ -43,8 +43,13 @@ module Reviewer
43
43
 
44
44
  private
45
45
 
46
- def record_not_run(tools)
46
+ def record_remaining(tools)
47
47
  tools.each do |tool|
48
+ if Command.new(tool, command_type, context: context).skip?
49
+ run_tool(tool)
50
+ next
51
+ end
52
+
48
53
  result = Runner::Result.not_run(tool: tool, command_type: command_type)
49
54
  @report.add(result)
50
55
  end
@@ -73,10 +73,12 @@ module Reviewer
73
73
  @target_files ||= tool.resolve_files(requested_files)
74
74
  end
75
75
 
76
- # Determines if this command should be skipped because files were requested but none match
76
+ # Determines if this command should be skipped for the requested file scope
77
77
  #
78
- # @return [Boolean] true if files were requested but resolution left none for this tool
78
+ # @return [Boolean] true if the tool cannot accept an explicit file scope or no files match it
79
79
  def skip?
80
+ return true if explicit_files.any? && !tool.supports_files?
81
+
80
82
  tool.skip_files?(requested_files)
81
83
  end
82
84
 
@@ -97,16 +99,15 @@ module Reviewer
97
99
  #
98
100
  # @return [Array<String>] files from -f flag, keywords like 'staged', or stored failed files
99
101
  def requested_files
100
- @requested_files ||= begin
101
- explicit = arguments.files.to_a
102
- if explicit.empty? && arguments.keywords.failed?
103
- stored_failed_files
104
- else
105
- explicit
106
- end
107
- end
102
+ @requested_files ||= if explicit_files.empty? && arguments.keywords.failed?
103
+ stored_failed_files
104
+ else
105
+ explicit_files
106
+ end
108
107
  end
109
108
 
109
+ def explicit_files = @explicit_files ||= arguments.files.to_a
110
+
110
111
  # Retrieves failed files stored from the last executed review for this tool
111
112
  #
112
113
  # @return [Array<String>] stored failed file paths, or empty array
@@ -8,6 +8,8 @@ module Reviewer
8
8
  class Formatter
9
9
  include Output::Formatting
10
10
 
11
+ MISSING_FILES_MESSAGE = 'The --files option requires at least one file or path.'
12
+
11
13
  attr_reader :output, :printer
12
14
  private :output, :printer
13
15
 
@@ -55,6 +57,29 @@ module Reviewer
55
57
  printer.write_raw("#{JSON.pretty_generate(payload)}\n")
56
58
  end
57
59
 
60
+ # Displays a usage error when -f/--files has no value
61
+ #
62
+ # @return [void]
63
+ def missing_files_option
64
+ printer.puts(:warning, MISSING_FILES_MESSAGE)
65
+ output.newline
66
+ end
67
+
68
+ # Renders the machine-readable form of the missing files usage error
69
+ #
70
+ # @return [void]
71
+ def missing_files_option_json
72
+ payload = {
73
+ schema_version: Report::SCHEMA_VERSION,
74
+ state: 'error',
75
+ error: { code: 'missing_files', message: MISSING_FILES_MESSAGE },
76
+ summary: Report.empty_summary,
77
+ tools: []
78
+ }
79
+
80
+ printer.write_raw("#{JSON.pretty_generate(payload)}\n")
81
+ end
82
+
58
83
  # Displays a warning when an unrecognized output format is requested
59
84
  # @param value [String] the invalid format name
60
85
  # @param known [Array<Symbol>] the valid format options
@@ -45,11 +45,17 @@ module Reviewer
45
45
  def history = context.history
46
46
 
47
47
  def run_tools(command_type)
48
+ return reject_invalid_files_option if arguments.invalid_files_option?
48
49
  return reject_unrecognized_selectors if unrecognized_selectors.any?
49
50
 
50
51
  json_output? ? run_json(command_type) : run_text(command_type)
51
52
  end
52
53
 
54
+ def reject_invalid_files_option
55
+ json_output? ? formatter.missing_files_option_json : formatter.missing_files_option
56
+ USAGE_ERROR
57
+ end
58
+
53
59
  def run_json(command_type)
54
60
  message = json_early_exit_message
55
61
  return emit_json_early_exit(message) if message
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Reviewer
4
- VERSION = '1.1.0'
4
+ VERSION = '1.1.1'
5
5
  end
data/lib/reviewer.rb CHANGED
@@ -100,13 +100,18 @@ module Reviewer
100
100
  return Setup.run(configuration: configuration) if subcommand?(:init)
101
101
  return run_doctor if subcommand?(:doctor)
102
102
  return run_capabilities if capabilities_flag?
103
- return run_first_time_setup unless configuration.file.exist?
103
+ return run_first_time_setup if first_time_setup?
104
104
 
105
105
  yield
106
106
  end
107
107
 
108
+ def first_time_setup? = !arguments.invalid_files_option? && !configuration.file.exist?
109
+
108
110
  def subcommand?(name) = ARGV.first == name.to_s
109
- def capabilities_flag? = ARGV.include?('--capabilities') || ARGV.include?('-c')
111
+
112
+ def capabilities_flag?
113
+ !arguments.invalid_files_option? && arguments.capabilities?
114
+ end
110
115
 
111
116
  def show_help
112
117
  output.help(help_text)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Garrett Dimon