quiet_quality 1.1.0 → 1.2.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dogfood.yml +1 -1
  3. data/.github/workflows/rspec.yml +1 -1
  4. data/.quiet_quality.ci.yml +6 -0
  5. data/.quiet_quality.yml +2 -1
  6. data/CHANGELOG.md +63 -0
  7. data/README.md +43 -13
  8. data/lib/quiet_quality/cli/arg_parser.rb +23 -7
  9. data/lib/quiet_quality/cli/entrypoint.rb +21 -30
  10. data/lib/quiet_quality/cli/presenter.rb +77 -0
  11. data/lib/quiet_quality/config/builder.rb +5 -0
  12. data/lib/quiet_quality/config/finder.rb +0 -4
  13. data/lib/quiet_quality/config/logging.rb +23 -0
  14. data/lib/quiet_quality/config/options.rb +6 -0
  15. data/lib/quiet_quality/config/parsed_options.rb +36 -0
  16. data/lib/quiet_quality/config/parser.rb +5 -8
  17. data/lib/quiet_quality/executors/base_executor.rb +9 -11
  18. data/lib/quiet_quality/logger.rb +17 -0
  19. data/lib/quiet_quality/tools/base_runner.rb +49 -0
  20. data/lib/quiet_quality/tools/brakeman/runner.rb +7 -26
  21. data/lib/quiet_quality/tools/brakeman.rb +0 -2
  22. data/lib/quiet_quality/tools/haml_lint/runner.rb +15 -50
  23. data/lib/quiet_quality/tools/haml_lint.rb +0 -2
  24. data/lib/quiet_quality/tools/markdown_lint/parser.rb +34 -0
  25. data/lib/quiet_quality/tools/markdown_lint/runner.rb +28 -0
  26. data/lib/quiet_quality/tools/markdown_lint.rb +9 -0
  27. data/lib/quiet_quality/tools/relevant_runner.rb +55 -0
  28. data/lib/quiet_quality/tools/rspec/runner.rb +9 -46
  29. data/lib/quiet_quality/tools/rspec.rb +0 -2
  30. data/lib/quiet_quality/tools/rubocop/runner.rb +9 -56
  31. data/lib/quiet_quality/tools/rubocop.rb +0 -2
  32. data/lib/quiet_quality/tools/standardrb/runner.rb +15 -3
  33. data/lib/quiet_quality/tools/standardrb.rb +0 -2
  34. data/lib/quiet_quality/tools.rb +6 -0
  35. data/lib/quiet_quality/version.rb +1 -1
  36. data/lib/quiet_quality/version_control_systems/git.rb +2 -11
  37. metadata +12 -2
@@ -0,0 +1,49 @@
1
+ module QuietQuality
2
+ module Tools
3
+ class BaseRunner
4
+ # In general, we don't want to supply a huge number of arguments to a command-line tool.
5
+ MAX_FILES = 100
6
+
7
+ def initialize(changed_files: nil, file_filter: nil)
8
+ @changed_files = changed_files
9
+ @file_filter = file_filter
10
+ end
11
+
12
+ def invoke!
13
+ @_outcome ||= performed_outcome
14
+ end
15
+
16
+ def tool_name
17
+ fail(NoMethodError, "BaseRunner subclass must implement `tool_name`")
18
+ end
19
+
20
+ def command
21
+ fail(NoMethodError, "BaseRunner subclass must implement `command`")
22
+ end
23
+
24
+ def success_status?(stat)
25
+ stat.success?
26
+ end
27
+
28
+ # distinct from _error_ status - this is asking "does this status represent failures-found?"
29
+ def failure_status?(stat)
30
+ stat.exitstatus == 1
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :changed_files, :file_filter
36
+
37
+ def performed_outcome
38
+ out, err, stat = Open3.capture3(*command)
39
+ if success_status?(stat)
40
+ Outcome.new(tool: tool_name, output: out, logging: err)
41
+ elsif failure_status?(stat)
42
+ Outcome.new(tool: tool_name, output: out, logging: err, failure: true)
43
+ else
44
+ fail(ExecutionError, "Execution of #{tool_name} failed with #{stat.exitstatus}")
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,38 +1,19 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module Brakeman
4
- class Runner
5
- # These are specified in constants at the top of brakeman.rb:
6
- # https://github.com/presidentbeef/brakeman/blob/main/lib/brakeman.rb#L6-L25
7
- KNOWN_EXIT_STATUSES = [3, 4, 5, 6, 7, 8].to_set
8
-
9
- # brakeman does not support being run against a portion of the project, so neither
10
- # changed_files nor file_filter is actually used. But they are accepted here because
11
- # that is what Runner initializers are required to accept.
12
- def initialize(changed_files: nil, file_filter: nil)
13
- @changed_files = changed_files
14
- @file_filter = file_filter
15
- end
16
-
17
- def invoke!
18
- @_outcome ||= performed_outcome
4
+ class Runner < BaseRunner
5
+ def tool_name
6
+ :brakeman
19
7
  end
20
8
 
21
- private
22
-
23
9
  def command
24
10
  ["brakeman", "-f", "json"]
25
11
  end
26
12
 
27
- def performed_outcome
28
- out, err, stat = Open3.capture3(*command)
29
- if stat.success?
30
- Outcome.new(tool: :brakeman, output: out, logging: err)
31
- elsif KNOWN_EXIT_STATUSES.include?(stat.exitstatus)
32
- Outcome.new(tool: :brakeman, output: out, logging: err, failure: true)
33
- else
34
- fail(ExecutionError, "Execution of brakeman failed with #{stat.exitstatus}")
35
- end
13
+ # These are specified in constants at the top of brakeman.rb:
14
+ # https://github.com/presidentbeef/brakeman/blob/main/lib/brakeman.rb#L6-L25
15
+ def failure_status?(stat)
16
+ [3, 4, 5, 6, 7, 8].include?(stat.exitstatus)
36
17
  end
37
18
  end
38
19
  end
@@ -3,8 +3,6 @@ require_relative "./rubocop"
3
3
  module QuietQuality
4
4
  module Tools
5
5
  module Brakeman
6
- ExecutionError = Class.new(Tools::Error)
7
- ParsingError = Class.new(Tools::Error)
8
6
  end
9
7
  end
10
8
  end
@@ -1,64 +1,29 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module HamlLint
4
- class Runner
5
- MAX_FILES = 100
6
- NO_FILES_OUTPUT = %({"files": []})
7
-
8
- # haml-lint uses the `sysexits` gem, and exits with Sysexits::EX_DATAERR for the
9
- # failures case here in lib/haml_lint/cli.rb. That's mapped to status 65 - other
10
- # statuses have other failure meanings, which we don't want to interpret as "problems
11
- # encountered"
12
- FAILURE_STATUS = 65
13
-
14
- def initialize(changed_files: nil, file_filter: nil)
15
- @changed_files = changed_files
16
- @file_filter = file_filter
17
- end
18
-
19
- def invoke!
20
- @_outcome ||= skip_execution? ? skipped_outcome : performed_outcome
21
- end
22
-
23
- private
24
-
25
- attr_reader :changed_files, :file_filter
26
-
27
- def skip_execution?
28
- changed_files && relevant_files.empty?
29
- end
30
-
31
- def relevant_files
32
- return nil if changed_files.nil?
33
- changed_files.paths
34
- .select { |path| path.end_with?(".haml") }
35
- .select { |path| file_filter.nil? || file_filter.match?(path) }
4
+ class Runner < RelevantRunner
5
+ def tool_name
6
+ :haml_lint
36
7
  end
37
8
 
38
- def target_files
39
- return [] if changed_files.nil?
40
- return [] if relevant_files.length > MAX_FILES
41
- relevant_files
9
+ def no_files_output
10
+ %({"files": []})
42
11
  end
43
12
 
44
- def command
45
- return nil if skip_execution?
46
- ["haml-lint", "--reporter", "json"] + target_files.sort
13
+ def base_command
14
+ ["haml-lint", "--reporter", "json"]
47
15
  end
48
16
 
49
- def skipped_outcome
50
- Outcome.new(tool: :haml_lint, output: NO_FILES_OUTPUT)
17
+ def relevant_path?(path)
18
+ path.end_with?(".haml")
51
19
  end
52
20
 
53
- def performed_outcome
54
- out, err, stat = Open3.capture3(*command)
55
- if stat.success?
56
- Outcome.new(tool: :haml_lint, output: out, logging: err)
57
- elsif stat.exitstatus == FAILURE_STATUS
58
- Outcome.new(tool: :haml_lint, output: out, logging: err, failure: true)
59
- else
60
- fail(ExecutionError, "Execution of haml-lint failed with #{stat.exitstatus}")
61
- end
21
+ # haml-lint uses the `sysexits` gem, and exits with Sysexits::EX_DATAERR for the
22
+ # failures case here in lib/haml_lint/cli.rb. That's mapped to status 65 - other
23
+ # statuses have other failure meanings, which we don't want to interpret as "problems
24
+ # encountered"
25
+ def failure_status?(stat)
26
+ stat.exitstatus == 65
62
27
  end
63
28
  end
64
29
  end
@@ -1,8 +1,6 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module HamlLint
4
- ExecutionError = Class.new(Tools::Error)
5
- ParsingError = Class.new(Tools::Error)
6
4
  end
7
5
  end
8
6
  end
@@ -0,0 +1,34 @@
1
+ module QuietQuality
2
+ module Tools
3
+ module MarkdownLint
4
+ class Parser
5
+ def initialize(text)
6
+ @text = text
7
+ end
8
+
9
+ def messages
10
+ return @_messages if defined?(@_messages)
11
+ messages = content.map { |entry| message_for_entry(entry) }
12
+ @_messages = Messages.new(messages)
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :text
18
+
19
+ def content
20
+ @_content ||= JSON.parse(text, symbolize_names: true)
21
+ end
22
+
23
+ def message_for_entry(entry)
24
+ Message.new(
25
+ path: entry.fetch(:filename),
26
+ start_line: entry.fetch(:line),
27
+ rule: entry.fetch(:description),
28
+ body: entry.fetch(:docs)
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ module QuietQuality
2
+ module Tools
3
+ module MarkdownLint
4
+ class Runner < RelevantRunner
5
+ def tool_name
6
+ :markdown_lint
7
+ end
8
+
9
+ def no_files_output
10
+ "[]"
11
+ end
12
+
13
+ def command
14
+ return nil if skip_execution?
15
+ if target_files.any?
16
+ ["mdl", "--json"] + target_files.sort
17
+ else
18
+ ["mdl", "--json", "."]
19
+ end
20
+ end
21
+
22
+ def relevant_path?(path)
23
+ path.end_with?(".md")
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ module QuietQuality
2
+ module Tools
3
+ module MarkdownLint
4
+ end
5
+ end
6
+ end
7
+
8
+ glob = File.expand_path("../markdown_lint/*.rb", __FILE__)
9
+ Dir.glob(glob).sort.each { |f| require f }
@@ -0,0 +1,55 @@
1
+ require_relative "./base_runner"
2
+
3
+ module QuietQuality
4
+ module Tools
5
+ class RelevantRunner < BaseRunner
6
+ # In general, we don't want to supply a huge number of arguments to a command-line tool.
7
+ # This will probably become configurable later.
8
+ MAX_FILES = 100
9
+
10
+ def invoke!
11
+ @_outcome ||= skip_execution? ? skipped_outcome : performed_outcome
12
+ end
13
+
14
+ def command
15
+ return nil if skip_execution?
16
+ base_command + target_files.sort
17
+ end
18
+
19
+ def relevant_path?(path)
20
+ fail(NoMethodError, "RelevantRunner subclass must implement `relevant_path?`")
21
+ end
22
+
23
+ def base_command
24
+ fail(NoMethodError, "RelevantRunner subclass must implement either `command` or `base_command`")
25
+ end
26
+
27
+ def no_files_output
28
+ fail(NoMethodError, "RelevantRunner subclass must implement `no_files_output`")
29
+ end
30
+
31
+ private
32
+
33
+ def skip_execution?
34
+ changed_files && relevant_files.empty?
35
+ end
36
+
37
+ def relevant_files
38
+ return nil if changed_files.nil?
39
+ changed_files.paths
40
+ .select { |path| relevant_path?(path) }
41
+ .select { |path| file_filter.nil? || file_filter.match?(path) }
42
+ end
43
+
44
+ def target_files
45
+ return [] if changed_files.nil?
46
+ return [] if relevant_files.length > MAX_FILES
47
+ relevant_files
48
+ end
49
+
50
+ def skipped_outcome
51
+ Outcome.new(tool: tool_name, output: no_files_output)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,58 +1,21 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module Rspec
4
- class Runner
5
- MAX_FILES = 100
6
- NO_FILES_OUTPUT = '{"examples": [], "summary": {"failure_count": 0}}'
7
-
8
- def initialize(changed_files: nil, file_filter: nil)
9
- @changed_files = changed_files
10
- @file_filter = file_filter
11
- end
12
-
13
- def invoke!
14
- @_outcome ||= skip_execution? ? skipped_outcome : performed_outcome
15
- end
16
-
17
- private
18
-
19
- attr_reader :changed_files, :file_filter
20
-
21
- def skip_execution?
22
- changed_files && relevant_files.empty?
23
- end
24
-
25
- def relevant_files
26
- return nil if changed_files.nil?
27
- changed_files.paths
28
- .select { |path| path.end_with?("_spec.rb") }
29
- .select { |path| file_filter.nil? || file_filter.match?(path) }
30
- end
31
-
32
- def target_files
33
- return [] if changed_files.nil?
34
- return [] if relevant_files.length > MAX_FILES
35
- relevant_files
4
+ class Runner < RelevantRunner
5
+ def tool_name
6
+ :rspec
36
7
  end
37
8
 
38
- def command
39
- return nil if skip_execution?
40
- ["rspec", "-f", "json"] + target_files.sort
9
+ def no_files_output
10
+ '{"examples": [], "summary": {"failure_count": 0}}'
41
11
  end
42
12
 
43
- def skipped_outcome
44
- Outcome.new(tool: :rspec, output: NO_FILES_OUTPUT)
13
+ def base_command
14
+ ["rspec", "-f", "json"]
45
15
  end
46
16
 
47
- def performed_outcome
48
- out, err, stat = Open3.capture3(*command)
49
- if stat.success?
50
- Outcome.new(tool: :rspec, output: out, logging: err)
51
- elsif stat.exitstatus == 1
52
- Outcome.new(tool: :rspec, output: out, logging: err, failure: true)
53
- else
54
- fail(ExecutionError, "Execution of rspec failed with #{stat.exitstatus}")
55
- end
17
+ def relevant_path?(path)
18
+ path.end_with?("_spec.rb")
56
19
  end
57
20
  end
58
21
  end
@@ -1,8 +1,6 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module Rspec
4
- ExecutionError = Class.new(Tools::Error)
5
- ParsingError = Class.new(Tools::Error)
6
4
  end
7
5
  end
8
6
  end
@@ -1,68 +1,21 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module Rubocop
4
- class Runner
5
- MAX_FILES = 100
6
- NO_FILES_OUTPUT = '{"files": [], "summary": {"offense_count": 0}}'
7
-
8
- def command_name
9
- "rubocop"
10
- end
11
-
12
- # Supplying changed_files: nil means "run against all files".
13
- def initialize(changed_files: nil, file_filter: nil)
14
- @changed_files = changed_files
15
- @file_filter = file_filter
16
- end
17
-
18
- def invoke!
19
- @_outcome ||= skip_execution? ? skipped_outcome : performed_outcome
20
- end
21
-
22
- private
23
-
24
- attr_reader :changed_files, :file_filter
25
-
26
- # If we were told that _no files changed_ (which is distinct from not being told that
27
- # any files changed - a [] instead of a nil), then we shouldn't run rubocop at all.
28
- def skip_execution?
29
- changed_files && relevant_files.empty?
30
- end
31
-
32
- # Note: if target_files goes over MAX_FILES, it's _empty_ instead - that means that
33
- # we run against the full repository instead of the specific files (rubocop's behavior
34
- # when no target files are specified)
35
- def command
36
- return nil if skip_execution?
37
- [command_name, "-f", "json"] + target_files.sort
38
- end
39
-
40
- def relevant_files
41
- return nil if changed_files.nil?
42
- changed_files.paths
43
- .select { |path| path.end_with?(".rb") }
44
- .select { |path| file_filter.nil? || file_filter.match?(path) }
4
+ class Runner < RelevantRunner
5
+ def tool_name
6
+ :rubocop
45
7
  end
46
8
 
47
- def target_files
48
- return [] if changed_files.nil?
49
- return [] if relevant_files.length > MAX_FILES
50
- relevant_files
9
+ def no_files_output
10
+ '{"files": [], "summary": {"offense_count": 0}}'
51
11
  end
52
12
 
53
- def skipped_outcome
54
- Outcome.new(tool: command_name.to_sym, output: NO_FILES_OUTPUT)
13
+ def base_command
14
+ ["rubocop", "-f", "json"]
55
15
  end
56
16
 
57
- def performed_outcome
58
- out, err, stat = Open3.capture3(*command)
59
- if stat.success?
60
- Outcome.new(tool: command_name.to_sym, output: out, logging: err)
61
- elsif stat.exitstatus == 1
62
- Outcome.new(tool: command_name.to_sym, output: out, logging: err, failure: true)
63
- else
64
- fail(ExecutionError, "Execution of #{command_name} failed with #{stat.exitstatus}")
65
- end
17
+ def relevant_path?(path)
18
+ path.end_with?(".rb")
66
19
  end
67
20
  end
68
21
  end
@@ -1,8 +1,6 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module Rubocop
4
- ExecutionError = Class.new(Tools::Error)
5
- ParsingError = Class.new(Tools::Error)
6
4
  end
7
5
  end
8
6
  end
@@ -1,9 +1,21 @@
1
1
  module QuietQuality
2
2
  module Tools
3
3
  module Standardrb
4
- class Runner < Rubocop::Runner
5
- def command_name
6
- "standardrb"
4
+ class Runner < RelevantRunner
5
+ def tool_name
6
+ :standardrb
7
+ end
8
+
9
+ def no_files_output
10
+ '{"files": [], "summary": {"offense_count": 0}}'
11
+ end
12
+
13
+ def base_command
14
+ ["standardrb", "-f", "json"]
15
+ end
16
+
17
+ def relevant_path?(path)
18
+ path.end_with?(".rb")
7
19
  end
8
20
  end
9
21
  end
@@ -3,8 +3,6 @@ require_relative "./rubocop"
3
3
  module QuietQuality
4
4
  module Tools
5
5
  module Standardrb
6
- ExecutionError = Class.new(Tools::Error)
7
- ParsingError = Class.new(Tools::Error)
8
6
  end
9
7
  end
10
8
  end
@@ -3,9 +3,14 @@ require "open3"
3
3
  module QuietQuality
4
4
  module Tools
5
5
  Error = Class.new(::QuietQuality::Error)
6
+ ExecutionError = Class.new(Error)
7
+ ParsingError = Class.new(Error)
6
8
  end
7
9
  end
8
10
 
11
+ require_relative "./tools/base_runner"
12
+ require_relative "./tools/relevant_runner"
13
+
9
14
  glob = File.expand_path("../tools/*.rb", __FILE__)
10
15
  Dir.glob(glob).sort.each { |f| require f }
11
16
 
@@ -15,6 +20,7 @@ module QuietQuality
15
20
  AVAILABLE = {
16
21
  brakeman: Brakeman,
17
22
  haml_lint: HamlLint,
23
+ markdown_lint: MarkdownLint,
18
24
  rspec: Rspec,
19
25
  rubocop: Rubocop,
20
26
  standardrb: Standardrb
@@ -1,3 +1,3 @@
1
1
  module QuietQuality
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.1"
3
3
  end
@@ -66,12 +66,6 @@ module QuietQuality
66
66
 
67
67
  private
68
68
 
69
- def changed_lines_for(diff)
70
- GitDiffParser.parse(diff).flat_map do |parsed_diff|
71
- parsed_diff.changed_line_numbers.to_set
72
- end
73
- end
74
-
75
69
  def committed_changed_files(base, sha)
76
70
  ChangedFiles.new(committed_changes(base, sha))
77
71
  end
@@ -105,11 +99,8 @@ module QuietQuality
105
99
  end
106
100
 
107
101
  def untracked_paths
108
- out, err, stat = Open3.capture3("git", "-C", path, "ls-files", "--others", "--exclude-standard")
109
- unless stat.success?
110
- warn err
111
- fail(Error, "git ls-files failed")
112
- end
102
+ out, _err, stat = Open3.capture3("git", "-C", path, "ls-files", "--others", "--exclude-standard")
103
+ fail(Error, "git ls-files failed") unless stat.success?
113
104
  out.split
114
105
  end
115
106
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quiet_quality
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Mueller
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-27 00:00:00.000000000 Z
11
+ date: 2023-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: git
@@ -166,9 +166,11 @@ files:
166
166
  - ".gitignore"
167
167
  - ".mdl_rules.rb"
168
168
  - ".mdlrc"
169
+ - ".quiet_quality.ci.yml"
169
170
  - ".quiet_quality.yml"
170
171
  - ".rspec"
171
172
  - ".rubocop.yml"
173
+ - CHANGELOG.md
172
174
  - Gemfile
173
175
  - LICENSE
174
176
  - README.md
@@ -183,9 +185,11 @@ files:
183
185
  - lib/quiet_quality/cli.rb
184
186
  - lib/quiet_quality/cli/arg_parser.rb
185
187
  - lib/quiet_quality/cli/entrypoint.rb
188
+ - lib/quiet_quality/cli/presenter.rb
186
189
  - lib/quiet_quality/config.rb
187
190
  - lib/quiet_quality/config/builder.rb
188
191
  - lib/quiet_quality/config/finder.rb
192
+ - lib/quiet_quality/config/logging.rb
189
193
  - lib/quiet_quality/config/options.rb
190
194
  - lib/quiet_quality/config/parsed_options.rb
191
195
  - lib/quiet_quality/config/parser.rb
@@ -195,17 +199,23 @@ files:
195
199
  - lib/quiet_quality/executors/concurrent_executor.rb
196
200
  - lib/quiet_quality/executors/pipeline.rb
197
201
  - lib/quiet_quality/executors/serial_executor.rb
202
+ - lib/quiet_quality/logger.rb
198
203
  - lib/quiet_quality/message.rb
199
204
  - lib/quiet_quality/message_filter.rb
200
205
  - lib/quiet_quality/messages.rb
201
206
  - lib/quiet_quality/tools.rb
207
+ - lib/quiet_quality/tools/base_runner.rb
202
208
  - lib/quiet_quality/tools/brakeman.rb
203
209
  - lib/quiet_quality/tools/brakeman/parser.rb
204
210
  - lib/quiet_quality/tools/brakeman/runner.rb
205
211
  - lib/quiet_quality/tools/haml_lint.rb
206
212
  - lib/quiet_quality/tools/haml_lint/parser.rb
207
213
  - lib/quiet_quality/tools/haml_lint/runner.rb
214
+ - lib/quiet_quality/tools/markdown_lint.rb
215
+ - lib/quiet_quality/tools/markdown_lint/parser.rb
216
+ - lib/quiet_quality/tools/markdown_lint/runner.rb
208
217
  - lib/quiet_quality/tools/outcome.rb
218
+ - lib/quiet_quality/tools/relevant_runner.rb
209
219
  - lib/quiet_quality/tools/rspec.rb
210
220
  - lib/quiet_quality/tools/rspec/parser.rb
211
221
  - lib/quiet_quality/tools/rspec/runner.rb