simplecov-mcp 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +98 -50
  3. data/docs/{ARCHITECTURE.md → dev/ARCHITECTURE.md} +11 -10
  4. data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
  5. data/docs/{DEVELOPMENT.md → dev/DEVELOPMENT.md} +2 -1
  6. data/docs/dev/README.md +10 -0
  7. data/docs/dev/RELEASING.md +146 -0
  8. data/docs/{arch-decisions → dev/arch-decisions}/001-x-arch-decision.md +3 -1
  9. data/docs/{arch-decisions → dev/arch-decisions}/002-x-arch-decision.md +7 -5
  10. data/docs/{arch-decisions → dev/arch-decisions}/003-x-arch-decision.md +2 -0
  11. data/docs/{arch-decisions → dev/arch-decisions}/004-x-arch-decision.md +6 -2
  12. data/docs/{arch-decisions → dev/arch-decisions}/005-x-arch-decision.md +4 -2
  13. data/docs/{arch-decisions → dev/arch-decisions}/README.md +3 -3
  14. data/docs/{presentations → dev/presentations}/simplecov-mcp-presentation.md +28 -22
  15. data/docs/fixtures/demo_project/README.md +9 -0
  16. data/docs/{ADVANCED_USAGE.md → user/ADVANCED_USAGE.md} +129 -319
  17. data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
  18. data/docs/user/CLI_USAGE.md +750 -0
  19. data/docs/{ERROR_HANDLING.md → user/ERROR_HANDLING.md} +12 -12
  20. data/docs/user/EXAMPLES.md +588 -0
  21. data/docs/user/INSTALLATION.md +130 -0
  22. data/docs/{LIBRARY_API.md → user/LIBRARY_API.md} +90 -32
  23. data/docs/{MCP_INTEGRATION.md → user/MCP_INTEGRATION.md} +36 -34
  24. data/docs/user/README.md +14 -0
  25. data/docs/{TROUBLESHOOTING.md → user/TROUBLESHOOTING.md} +21 -100
  26. data/docs/user/V2-BREAKING-CHANGES.md +472 -0
  27. data/exe/simplecov-mcp +1 -1
  28. data/lib/simplecov_mcp/{cli_config.rb → app_config.rb} +12 -12
  29. data/lib/simplecov_mcp/app_context.rb +1 -1
  30. data/lib/simplecov_mcp/base_tool.rb +66 -38
  31. data/lib/simplecov_mcp/cli.rb +67 -123
  32. data/lib/simplecov_mcp/commands/base_command.rb +16 -27
  33. data/lib/simplecov_mcp/commands/command_factory.rb +8 -2
  34. data/lib/simplecov_mcp/commands/detailed_command.rb +16 -2
  35. data/lib/simplecov_mcp/commands/list_command.rb +1 -1
  36. data/lib/simplecov_mcp/commands/raw_command.rb +18 -2
  37. data/lib/simplecov_mcp/commands/summary_command.rb +20 -3
  38. data/lib/simplecov_mcp/commands/totals_command.rb +53 -0
  39. data/lib/simplecov_mcp/commands/uncovered_command.rb +24 -5
  40. data/lib/simplecov_mcp/commands/validate_command.rb +60 -0
  41. data/lib/simplecov_mcp/commands/version_command.rb +19 -4
  42. data/lib/simplecov_mcp/config_parser.rb +32 -0
  43. data/lib/simplecov_mcp/constants.rb +3 -3
  44. data/lib/simplecov_mcp/coverage_reporter.rb +31 -0
  45. data/lib/simplecov_mcp/error_handler.rb +81 -40
  46. data/lib/simplecov_mcp/error_handler_factory.rb +2 -2
  47. data/lib/simplecov_mcp/errors.rb +32 -20
  48. data/lib/simplecov_mcp/formatters/source_formatter.rb +23 -19
  49. data/lib/simplecov_mcp/formatters.rb +51 -0
  50. data/lib/simplecov_mcp/mcp_server.rb +9 -7
  51. data/lib/simplecov_mcp/mode_detector.rb +6 -5
  52. data/lib/simplecov_mcp/model.rb +122 -83
  53. data/lib/simplecov_mcp/option_normalizers.rb +39 -18
  54. data/lib/simplecov_mcp/option_parser_builder.rb +82 -65
  55. data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +3 -5
  56. data/lib/simplecov_mcp/option_parsers/error_helper.rb +18 -17
  57. data/lib/simplecov_mcp/path_relativizer.rb +17 -14
  58. data/lib/simplecov_mcp/predicate_evaluator.rb +72 -0
  59. data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +1 -3
  60. data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +1 -3
  61. data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +1 -3
  62. data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +1 -3
  63. data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +1 -3
  64. data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +1 -3
  65. data/lib/simplecov_mcp/presenters/project_totals_presenter.rb +27 -0
  66. data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +14 -18
  67. data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +7 -9
  68. data/lib/simplecov_mcp/resultset_loader.rb +20 -25
  69. data/lib/simplecov_mcp/staleness_checker.rb +50 -46
  70. data/lib/simplecov_mcp/table_formatter.rb +64 -0
  71. data/lib/simplecov_mcp/tools/all_files_coverage_tool.rb +20 -50
  72. data/lib/simplecov_mcp/tools/coverage_detailed_tool.rb +13 -7
  73. data/lib/simplecov_mcp/tools/coverage_raw_tool.rb +12 -7
  74. data/lib/simplecov_mcp/tools/coverage_summary_tool.rb +13 -8
  75. data/lib/simplecov_mcp/tools/coverage_table_tool.rb +20 -60
  76. data/lib/simplecov_mcp/tools/coverage_totals_tool.rb +44 -0
  77. data/lib/simplecov_mcp/tools/help_tool.rb +38 -66
  78. data/lib/simplecov_mcp/tools/uncovered_lines_tool.rb +13 -8
  79. data/lib/simplecov_mcp/tools/validate_tool.rb +72 -0
  80. data/lib/simplecov_mcp/tools/version_tool.rb +7 -14
  81. data/lib/simplecov_mcp/util.rb +18 -12
  82. data/lib/simplecov_mcp/version.rb +1 -1
  83. data/lib/simplecov_mcp.rb +23 -29
  84. data/spec/all_files_coverage_tool_spec.rb +4 -3
  85. data/spec/{cli_config_spec.rb → app_config_spec.rb} +31 -26
  86. data/spec/base_tool_spec.rb +17 -14
  87. data/spec/cli/show_default_report_spec.rb +2 -2
  88. data/spec/cli_enumerated_options_spec.rb +31 -9
  89. data/spec/cli_error_spec.rb +46 -23
  90. data/spec/cli_format_spec.rb +123 -0
  91. data/spec/cli_json_options_spec.rb +50 -0
  92. data/spec/cli_source_spec.rb +11 -63
  93. data/spec/cli_spec.rb +82 -97
  94. data/spec/cli_usage_spec.rb +15 -15
  95. data/spec/commands/base_command_spec.rb +12 -92
  96. data/spec/commands/command_factory_spec.rb +7 -3
  97. data/spec/commands/detailed_command_spec.rb +10 -24
  98. data/spec/commands/list_command_spec.rb +28 -0
  99. data/spec/commands/raw_command_spec.rb +43 -20
  100. data/spec/commands/summary_command_spec.rb +10 -23
  101. data/spec/commands/totals_command_spec.rb +34 -0
  102. data/spec/commands/uncovered_command_spec.rb +29 -23
  103. data/spec/commands/validate_command_spec.rb +213 -0
  104. data/spec/commands/version_command_spec.rb +38 -0
  105. data/spec/constants_spec.rb +3 -3
  106. data/spec/coverage_reporter_spec.rb +102 -0
  107. data/spec/coverage_table_tool_spec.rb +21 -10
  108. data/spec/coverage_totals_tool_spec.rb +37 -0
  109. data/spec/error_handler_spec.rb +120 -4
  110. data/spec/error_mode_spec.rb +18 -22
  111. data/spec/errors_edge_cases_spec.rb +101 -28
  112. data/spec/errors_stale_spec.rb +34 -0
  113. data/spec/file_based_mcp_tools_spec.rb +6 -6
  114. data/spec/fixtures/project1/lib/bar.rb +2 -0
  115. data/spec/fixtures/project1/lib/foo.rb +2 -0
  116. data/spec/help_tool_spec.rb +2 -18
  117. data/spec/integration_spec.rb +114 -170
  118. data/spec/logging_fallback_spec.rb +3 -3
  119. data/spec/mcp_server_integration_spec.rb +1 -1
  120. data/spec/mcp_server_spec.rb +70 -53
  121. data/spec/mode_detector_spec.rb +46 -41
  122. data/spec/model_error_handling_spec.rb +141 -82
  123. data/spec/model_staleness_spec.rb +13 -13
  124. data/spec/option_normalizers_spec.rb +111 -112
  125. data/spec/option_parsers/env_options_parser_spec.rb +25 -37
  126. data/spec/option_parsers/error_helper_spec.rb +56 -56
  127. data/spec/path_relativizer_spec.rb +15 -0
  128. data/spec/presenters/coverage_detailed_presenter_spec.rb +1 -1
  129. data/spec/presenters/coverage_summary_presenter_spec.rb +1 -1
  130. data/spec/presenters/coverage_uncovered_presenter_spec.rb +1 -1
  131. data/spec/presenters/project_coverage_presenter_spec.rb +9 -8
  132. data/spec/presenters/project_totals_presenter_spec.rb +144 -0
  133. data/spec/resolvers/coverage_line_resolver_spec.rb +261 -36
  134. data/spec/resolvers/resultset_path_resolver_spec.rb +13 -8
  135. data/spec/shared_examples/file_based_mcp_tools.rb +23 -18
  136. data/spec/shared_examples/formatted_command_examples.rb +64 -0
  137. data/spec/simple_cov_mcp_module_spec.rb +24 -3
  138. data/spec/simplecov_mcp/formatters/source_formatter_spec.rb +267 -0
  139. data/spec/simplecov_mcp/formatters_spec.rb +76 -0
  140. data/spec/simplecov_mcp/presenters/base_coverage_presenter_spec.rb +79 -0
  141. data/spec/simplecov_mcp_model_spec.rb +99 -49
  142. data/spec/simplecov_mcp_opts_spec.rb +42 -39
  143. data/spec/spec_helper.rb +27 -92
  144. data/spec/staleness_checker_spec.rb +10 -9
  145. data/spec/staleness_more_spec.rb +4 -4
  146. data/spec/support/cli_helpers.rb +22 -0
  147. data/spec/support/control_flow_helpers.rb +20 -0
  148. data/spec/support/fake_mcp.rb +40 -0
  149. data/spec/support/io_helpers.rb +29 -0
  150. data/spec/support/mcp_helpers.rb +35 -0
  151. data/spec/support/mcp_runner.rb +10 -8
  152. data/spec/support/mocking_helpers.rb +30 -0
  153. data/spec/table_format_spec.rb +70 -0
  154. data/spec/tools/validate_tool_spec.rb +132 -0
  155. data/spec/tools_error_handling_spec.rb +34 -48
  156. data/spec/util_spec.rb +5 -4
  157. data/spec/version_spec.rb +7 -7
  158. data/spec/version_tool_spec.rb +20 -22
  159. metadata +90 -23
  160. data/docs/BRANCH_ONLY_COVERAGE.md +0 -81
  161. data/docs/CLI_USAGE.md +0 -637
  162. data/docs/EXAMPLES.md +0 -430
  163. data/docs/INSTALLATION.md +0 -352
  164. data/spec/cli_success_predicate_spec.rb +0 -141
@@ -18,7 +18,7 @@ module SimpleCovMcp
18
18
  'uncovered' => :uncovered
19
19
  }.freeze
20
20
 
21
- STALE_MODE_MAP = {
21
+ STALENESS_MAP = {
22
22
  'o' => :off,
23
23
  'off' => :off,
24
24
  'e' => :error,
@@ -27,19 +27,29 @@ module SimpleCovMcp
27
27
 
28
28
  ERROR_MODE_MAP = {
29
29
  'off' => :off,
30
- 'on' => :on,
31
- 't' => :trace,
32
- 'trace' => :trace
30
+ 'o' => :off,
31
+ 'log' => :log,
32
+ 'l' => :log,
33
+ 'debug' => :debug,
34
+ 'd' => :debug
33
35
  }.freeze
34
36
 
35
- module_function
37
+ FORMAT_MAP = {
38
+ 't' => :table,
39
+ 'table' => :table,
40
+ 'j' => :json,
41
+ 'json' => :json,
42
+ 'J' => :pretty_json,
43
+ 'pretty_json' => :pretty_json,
44
+ 'pretty-json' => :pretty_json,
45
+ 'y' => :yaml,
46
+ 'yaml' => :yaml,
47
+ 'a' => :awesome_print,
48
+ 'awesome_print' => :awesome_print,
49
+ 'ap' => :awesome_print
50
+ }.freeze
36
51
 
37
- # Normalize sort order value.
38
- # @param value [String, Symbol] The value to normalize
39
- # @param strict [Boolean] If true, raises on invalid value; if false, returns nil
40
- # @return [Symbol, nil] The normalized symbol or nil if invalid and not strict
41
- # @raise [OptionParser::InvalidArgument] If strict and value is invalid
42
- def normalize_sort_order(value, strict: true)
52
+ module_function def normalize_sort_order(value, strict: true)
43
53
  normalized = SORT_ORDER_MAP[value.to_s.downcase]
44
54
  return normalized if normalized
45
55
  raise OptionParser::InvalidArgument, "invalid argument: #{value}" if strict
@@ -52,9 +62,7 @@ module SimpleCovMcp
52
62
  # @param strict [Boolean] If true, raises on invalid value; if false, returns nil
53
63
  # @return [Symbol, nil] The normalized symbol or nil if invalid and not strict
54
64
  # @raise [OptionParser::InvalidArgument] If strict and value is invalid
55
- def normalize_source_mode(value, strict: true)
56
- return :full if value.nil? || value == ''
57
-
65
+ module_function def normalize_source_mode(value, strict: true)
58
66
  normalized = SOURCE_MODE_MAP[value.to_s.downcase]
59
67
  return normalized if normalized
60
68
  raise OptionParser::InvalidArgument, "invalid argument: #{value}" if strict
@@ -67,8 +75,8 @@ module SimpleCovMcp
67
75
  # @param strict [Boolean] If true, raises on invalid value; if false, returns nil
68
76
  # @return [Symbol, nil] The normalized symbol or nil if invalid and not strict
69
77
  # @raise [OptionParser::InvalidArgument] If strict and value is invalid
70
- def normalize_stale_mode(value, strict: true)
71
- normalized = STALE_MODE_MAP[value.to_s.downcase]
78
+ module_function def normalize_staleness(value, strict: true)
79
+ normalized = STALENESS_MAP[value.to_s.downcase]
72
80
  return normalized if normalized
73
81
  raise OptionParser::InvalidArgument, "invalid argument: #{value}" if strict
74
82
 
@@ -81,12 +89,25 @@ module SimpleCovMcp
81
89
  # @param default [Symbol] The default value to return if invalid and not strict
82
90
  # @return [Symbol] The normalized symbol or default if invalid and not strict
83
91
  # @raise [OptionParser::InvalidArgument] If strict and value is invalid
84
- def normalize_error_mode(value, strict: true, default: :on)
85
- normalized = ERROR_MODE_MAP[value&.downcase]
92
+ module_function def normalize_error_mode(value, strict: true, default: :log)
93
+ normalized = ERROR_MODE_MAP[value.to_s.downcase]
86
94
  return normalized if normalized
87
95
  raise OptionParser::InvalidArgument, "invalid argument: #{value}" if strict
88
96
 
89
97
  default
90
98
  end
99
+
100
+ # Normalize format value.
101
+ # @param value [String, Symbol] The value to normalize
102
+ # @param strict [Boolean] If true, raises on invalid value; if false, returns nil
103
+ # @return [Symbol, nil] The normalized symbol or nil if invalid and not strict
104
+ # @raise [OptionParser::InvalidArgument] If strict and value is invalid
105
+ module_function def normalize_format(value, strict: true)
106
+ normalized = FORMAT_MAP[value.to_s.downcase]
107
+ return normalized if normalized
108
+ raise OptionParser::InvalidArgument, "invalid argument: #{value}" if strict
109
+
110
+ nil
111
+ end
91
112
  end
92
113
  end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'option_normalizers'
4
+ require_relative 'version'
4
5
 
5
6
  module SimpleCovMcp
6
7
  class OptionParserBuilder
7
8
  HORIZONTAL_RULE = '-' * 79
8
- SUBCOMMANDS = %w[list summary raw uncovered detailed version].freeze
9
+ SUBCOMMANDS = %w[list summary raw uncovered detailed totals validate version].freeze
9
10
 
10
11
  attr_reader :config
11
12
 
@@ -15,21 +16,19 @@ module SimpleCovMcp
15
16
 
16
17
  def build_option_parser
17
18
  require 'optparse'
18
- OptionParser.new do |o|
19
- configure_banner(o)
20
- define_subcommands_help(o)
21
- define_options(o)
22
- define_examples(o)
23
- add_help_handler(o)
19
+ OptionParser.new do |parser|
20
+ configure_banner(parser)
21
+ define_subcommands_help(parser)
22
+ define_options(parser)
23
+ define_examples(parser)
24
+ add_help_handler(parser)
24
25
  end
25
26
  end
26
27
 
27
- private
28
-
29
- def configure_banner(o)
30
- o.banner = <<~BANNER
28
+ private def configure_banner(parser)
29
+ parser.banner = <<~BANNER
31
30
  #{HORIZONTAL_RULE}
32
- Usage: simplecov-mcp [subcommand] [options] [args]
31
+ Usage: simplecov-mcp [options] [subcommand] [args]
33
32
  Repository: https://github.com/keithrbennett/simplecov-mcp
34
33
  Version: #{SimpleCovMcp::VERSION}
35
34
  #{HORIZONTAL_RULE}
@@ -37,98 +36,116 @@ module SimpleCovMcp
37
36
  BANNER
38
37
  end
39
38
 
40
- def define_subcommands_help(o)
41
- o.separator <<~SUBCOMMANDS
39
+ private def define_subcommands_help(parser)
40
+ parser.separator <<~SUBCOMMANDS
42
41
  Subcommands:
43
- list Show files coverage (table or --json)
42
+ list Show files coverage (default: table, or use --format)
44
43
  summary <path> Show covered/total/% for a file
45
44
  raw <path> Show the SimpleCov 'lines' array
46
45
  uncovered <path> Show uncovered lines and a summary
47
46
  detailed <path> Show per-line rows with hits/covered
47
+ totals Show aggregated line totals and average %
48
+ validate <file> Evaluate coverage policy from file (exit 0=pass, 1=fail, 2=error)
49
+ validate -e <code> Evaluate coverage policy from code string
48
50
  version Show version information
49
51
 
50
52
  SUBCOMMANDS
51
53
  end
52
54
 
53
- def define_options(o)
54
- o.separator 'Options:'
55
- o.on('-r', '--resultset PATH', String,
55
+ private def define_options(parser)
56
+ parser.separator 'Options:'
57
+ parser.on('-r', '--resultset PATH', String,
56
58
  'Path or directory that contains .resultset.json (default: coverage/.resultset.json)') \
57
- do |v|
58
- config.resultset = v
59
+ do |value|
60
+ config.resultset = value
61
+ end
62
+ parser.on('-R', '--root PATH', String, 'Project root (default: .)') do |value|
63
+ config.root = value
64
+ end
65
+ parser.on(
66
+ '-f', '--format FORMAT', String,
67
+ 'Output format: t[able]|j[son]|pretty-json|y[aml]|a[wesome-print] (default: table)'
68
+ ) do |value|
69
+ config.format = normalize_format(value)
59
70
  end
60
- o.on('-R', '--root PATH', String, 'Project root (default: .)') { |v| config.root = v }
61
- o.on('-j', '--json', 'Output JSON for machine consumption') { config.json = true }
62
- o.on('-o', '--sort-order ORDER', String,
63
- 'Sort order for list: a[scending]|d[escending] (default ascending)') do |v|
64
- config.sort_order = normalize_sort_order(v)
71
+ parser.on('-o', '--sort-order ORDER', String,
72
+ 'Sort order for list: a[scending]|d[escending] (default descending)') do |value|
73
+ config.sort_order = normalize_sort_order(value)
65
74
  end
66
- o.on('-s', '--source[=MODE]', String,
67
- 'Include source (MODE: f[ull]|u[ncovered]; default full)') do |v|
68
- config.source_mode = normalize_source_mode(v)
75
+ parser.on('-s', '--source MODE', String,
76
+ 'Source display: f[ull]|u[ncovered]') do |value|
77
+ config.source_mode = normalize_source_mode(value)
69
78
  end
70
- o.on('-c', '--source-context N', Integer,
71
- 'For --source=uncovered, show N context lines (default: 2)') do |v|
72
- config.source_context = v
79
+ parser.on('-c', '--context-lines N', Integer,
80
+ 'Context lines around uncovered lines (non-negative, default: 2)') do |value|
81
+ config.source_context = value
73
82
  end
74
- o.on('--color', 'Enable ANSI colors for source output') { config.color = true }
75
- o.on('--no-color', 'Disable ANSI colors') { config.color = false }
76
- o.on('-S', '--stale MODE', String,
77
- 'Staleness mode: o[ff]|e[rror] (default off)') do |v|
78
- config.stale_mode = normalize_stale_mode(v)
83
+ parser.on('--color', 'Enable ANSI colors for source output') { config.color = true }
84
+ parser.on('--no-color', 'Disable ANSI colors') { config.color = false }
85
+ parser.on('-S', '--staleness MODE', String,
86
+ 'Staleness detection: o[ff]|e[rror] (default off)') do |value|
87
+ config.staleness = normalize_staleness(value)
79
88
  end
80
- o.on('-g', '--tracked-globs x,y,z', Array,
81
- 'Globs for filtering files (list subcommand)') do |v|
82
- config.tracked_globs = v
89
+ parser.on('-g', '--tracked-globs x,y,z', Array,
90
+ 'Globs for filtering files (list/totals subcommands)') do |value|
91
+ config.tracked_globs = value
83
92
  end
84
- o.on('-l', '--log-file PATH', String,
85
- 'Log file path (default ./simplecov_mcp.log, use stdout/stderr for streams)') do |v|
86
- config.log_file = v
93
+ parser.on('-l', '--log-file PATH', String,
94
+ 'Log file path (default ./simplecov_mcp.log, use stdout/stderr for streams)') do |value|
95
+ config.log_file = value
87
96
  end
88
- o.on('--error-mode MODE', String,
89
- 'Error handling mode: off|on|t[trace] (default on)') do |v|
90
- config.error_mode = normalize_error_mode(v)
97
+ parser.on('--error-mode MODE', String,
98
+ 'Error handling mode: o[ff]|l[og]|d[ebug] (default log). ' \
99
+ 'off (silent), log (log errors to file), debug (verbose with backtraces)') do |value|
100
+ config.error_mode = normalize_error_mode(value)
91
101
  end
92
- o.on('--force-cli', 'Force CLI mode (useful in scripts where auto-detection fails)') do
102
+ parser.on('--force-cli', 'Force CLI mode (useful in scripts where auto-detection fails)') do
93
103
  # This flag is mainly for mode detection - no action needed here
94
104
  end
95
- o.on('--success-predicate FILE', String,
96
- 'Ruby file returning callable; exits 0 if truthy, 1 if falsy') do |v|
97
- config.success_predicate = v
105
+ parser.on('-v', '--version', 'Show version information and exit') do
106
+ config.show_version = true
98
107
  end
99
108
  end
100
109
 
101
- def define_examples(o)
102
- o.separator <<~EXAMPLES
110
+ private def define_examples(parser)
111
+ parser.separator <<~EXAMPLES
103
112
 
104
113
  Examples:
105
- simplecov-mcp list --resultset coverage
106
- simplecov-mcp summary lib/foo.rb --json --resultset coverage
107
- simplecov-mcp uncovered lib/foo.rb --source=uncovered --source-context 2
114
+ simplecov-mcp --resultset coverage list
115
+ simplecov-mcp --format json --resultset coverage summary lib/foo.rb
116
+ simplecov-mcp --source uncovered --context-lines 2 uncovered lib/foo.rb
117
+ simplecov-mcp totals --format json
108
118
  EXAMPLES
109
119
  end
110
120
 
111
- def add_help_handler(o)
112
- o.on('-h', '--help', 'Show help') do
113
- puts o
121
+ private def add_help_handler(parser)
122
+ parser.on('-h', '--help', 'Show help') do
123
+ puts parser
124
+ gem_root = File.expand_path('../..', __dir__)
125
+ puts "\nFor more detailed help, consult README.md and docs/user/**/*.md"
126
+ puts "in the installed gem at: #{gem_root}"
114
127
  exit 0
115
128
  end
116
129
  end
117
130
 
118
- def normalize_sort_order(v)
119
- OptionNormalizers.normalize_sort_order(v, strict: true)
131
+ private def normalize_sort_order(value)
132
+ OptionNormalizers.normalize_sort_order(value, strict: true)
133
+ end
134
+
135
+ private def normalize_source_mode(value)
136
+ OptionNormalizers.normalize_source_mode(value, strict: true)
120
137
  end
121
138
 
122
- def normalize_source_mode(v)
123
- OptionNormalizers.normalize_source_mode(v, strict: true)
139
+ private def normalize_staleness(value)
140
+ OptionNormalizers.normalize_staleness(value, strict: true)
124
141
  end
125
142
 
126
- def normalize_stale_mode(v)
127
- OptionNormalizers.normalize_stale_mode(v, strict: true)
143
+ private def normalize_error_mode(value)
144
+ OptionNormalizers.normalize_error_mode(value, strict: true)
128
145
  end
129
146
 
130
- def normalize_error_mode(v)
131
- OptionNormalizers.normalize_error_mode(v, strict: true)
147
+ private def normalize_format(value)
148
+ OptionNormalizers.normalize_format(value, strict: true)
132
149
  end
133
150
  end
134
151
  end
@@ -35,15 +35,13 @@ module SimpleCovMcp
35
35
  end
36
36
  end
37
37
  nil
38
- rescue StandardError
38
+ rescue
39
39
  # Ignore errors during pre-scan; they'll be caught during actual parsing
40
40
  nil
41
41
  end
42
42
 
43
- private
44
-
45
- def normalize_error_mode(value)
46
- OptionNormalizers.normalize_error_mode(value, strict: false, default: :on)
43
+ private def normalize_error_mode(value)
44
+ OptionNormalizers.normalize_error_mode(value, strict: false, default: :log)
47
45
  end
48
46
  end
49
47
  end
@@ -3,7 +3,7 @@
3
3
  module SimpleCovMcp
4
4
  module OptionParsers
5
5
  class ErrorHelper
6
- SUBCOMMANDS = %w[list summary raw uncovered detailed version].freeze
6
+ SUBCOMMANDS = %w[list summary raw uncovered detailed totals version].freeze
7
7
 
8
8
  def initialize(subcommands = SUBCOMMANDS)
9
9
  @subcommands = subcommands
@@ -14,7 +14,7 @@ module SimpleCovMcp
14
14
  # Suggest a subcommand when an invalid option matches a known subcommand
15
15
  option = extract_invalid_option(message)
16
16
 
17
- if option && option.start_with?('--') && @subcommands.include?(option[2..-1])
17
+ if option&.start_with?('--') && @subcommands.include?(option[2..])
18
18
  suggest_subcommand(option)
19
19
  else
20
20
  # Generic message from OptionParser
@@ -28,19 +28,19 @@ module SimpleCovMcp
28
28
  exit 1
29
29
  end
30
30
 
31
- private
32
-
33
- def extract_invalid_option(message)
34
- message.match(/invalid option: (.+)/)[1] rescue nil
31
+ private def extract_invalid_option(message)
32
+ message.match(/invalid option: (.+)/)[1]
33
+ rescue
34
+ nil
35
35
  end
36
36
 
37
- def suggest_subcommand(option)
38
- subcommand = option[2..-1]
37
+ private def suggest_subcommand(option)
38
+ subcommand = option[2..]
39
39
  warn "Error: '#{option}' is not a valid option. Did you mean the '#{subcommand}' subcommand?"
40
40
  warn "Try: #{program_name} #{subcommand} [args]"
41
41
  end
42
42
 
43
- def build_enum_value_hint(argv)
43
+ private def build_enum_value_hint(argv)
44
44
  rules = enumerated_option_rules
45
45
  tokens = Array(argv)
46
46
  rules.each do |rule|
@@ -50,7 +50,7 @@ module SimpleCovMcp
50
50
  nil
51
51
  end
52
52
 
53
- def build_hint_for_rule(rule, tokens)
53
+ private def build_hint_for_rule(rule, tokens)
54
54
  switches = rule[:switches]
55
55
  allowed = rule[:values]
56
56
  display = rule[:display] || allowed.join(', ')
@@ -72,17 +72,17 @@ module SimpleCovMcp
72
72
  nil
73
73
  end
74
74
 
75
- def equal_form_match?(token, switches, preferred)
75
+ private def equal_form_match?(token, switches, preferred)
76
76
  token.start_with?(preferred + '=') || switches.any? { |s| token.start_with?(s + '=') }
77
77
  end
78
78
 
79
- def handle_equal_form(token, switches, preferred, display, allowed)
79
+ private def handle_equal_form(token, switches, preferred, display, allowed)
80
80
  sw = switches.find { |s| token.start_with?(s + '=') } || preferred
81
81
  val = token.split('=', 2)[1]
82
82
  "Valid values for #{sw}: #{display}" if val && !allowed.include?(val)
83
83
  end
84
84
 
85
- def handle_space_form(tokens, index, preferred, display, allowed)
85
+ private def handle_space_form(tokens, index, preferred, display, allowed)
86
86
  val = tokens[index + 1]
87
87
  # If missing value, provide hint; if present and invalid, also hint
88
88
  if val.nil? || val.start_with?('-') || !allowed.include?(val)
@@ -90,18 +90,19 @@ module SimpleCovMcp
90
90
  end
91
91
  end
92
92
 
93
- def enumerated_option_rules
93
+ private def enumerated_option_rules
94
94
  [
95
- { switches: ['-S', '--stale'], values: %w[off o error e], display: 'o[ff]|e[rror]' },
95
+ { switches: ['-S', '--staleness'], values: %w[off o error e], display: 'o[ff]|e[rror]' },
96
96
  { switches: ['-s', '--source'], values: %w[full f uncovered u],
97
97
  display: 'f[ull]|u[ncovered]' },
98
- { switches: ['--error-mode'], values: %w[off on trace t], display: 'off|on|t[race]' },
98
+ { switches: ['--error-mode'], values: %w[off o log l debug d],
99
+ display: 'o[ff]|l[og]|d[ebug]' },
99
100
  { switches: ['-o', '--sort-order'], values: %w[a d ascending descending],
100
101
  display: 'a[scending]|d[escending]' }
101
102
  ]
102
103
  end
103
104
 
104
- def program_name
105
+ private def program_name
105
106
  'simplecov-mcp'
106
107
  end
107
108
  end
@@ -16,9 +16,22 @@ module SimpleCovMcp
16
16
  deep_copy_and_relativize(obj)
17
17
  end
18
18
 
19
- private
19
+ # Converts an absolute path to a path relative to the root.
20
+ # Falls back to the original path if conversion fails (e.g., different drive on Windows).
21
+ #
22
+ # @param path [String] file path (absolute or relative)
23
+ # @return [String] relative path or original path on failure
24
+ def relativize_path(path)
25
+ root_str = @root.to_s
26
+ abs = File.absolute_path(path, root_str)
27
+ return path unless abs.start_with?(root_prefix(root_str)) || abs == root_str
20
28
 
21
- def deep_copy_and_relativize(obj, key_context = nil)
29
+ Pathname.new(abs).relative_path_from(@root).to_s
30
+ rescue ArgumentError
31
+ path
32
+ end
33
+
34
+ private def deep_copy_and_relativize(obj)
22
35
  case obj
23
36
  when Hash
24
37
  obj.each_with_object({}) do |(k, v), acc|
@@ -31,7 +44,7 @@ module SimpleCovMcp
31
44
  end
32
45
  end
33
46
 
34
- def relativize_value(key, value)
47
+ private def relativize_value(key, value)
35
48
  key_str = key.to_s
36
49
  if @scalar_keys.include?(key_str) && value.is_a?(String)
37
50
  relativize_path(value)
@@ -44,17 +57,7 @@ module SimpleCovMcp
44
57
  end
45
58
  end
46
59
 
47
- def relativize_path(path)
48
- abs = File.absolute_path(path, @root.to_s)
49
- root_str = @root.to_s
50
- return path unless abs.start_with?(root_prefix(root_str)) || abs == root_str
51
-
52
- Pathname.new(abs).relative_path_from(@root).to_s
53
- rescue ArgumentError
54
- path
55
- end
56
-
57
- def root_prefix(root_str)
60
+ private def root_prefix(root_str)
58
61
  root_str.end_with?(File::SEPARATOR) ? root_str : root_str + File::SEPARATOR
59
62
  end
60
63
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCovMcp
4
+ # Evaluates coverage predicates from either Ruby code strings or files.
5
+ # Used by the validate subcommand, validate MCP tool, and library API.
6
+ #
7
+ # Security Warning:
8
+ # Predicates execute as arbitrary Ruby code with full system privileges.
9
+ # Only use predicates from trusted sources.
10
+ class PredicateEvaluator
11
+ # Evaluate a predicate from a code string
12
+ #
13
+ # @param code [String] Ruby code that returns a callable (lambda, proc, or object with #call)
14
+ # @param model [CoverageModel] The coverage model to pass to the predicate
15
+ # @return [Boolean] The result of calling the predicate with the model
16
+ # @raise [RuntimeError] If the code doesn't return a callable or has syntax errors
17
+ def self.evaluate_code(code, model)
18
+ # WARNING: The predicate code executes with full Ruby privileges.
19
+ # It has unrestricted access to the file system, network, and system commands.
20
+ # Only use predicate code from trusted sources.
21
+ #
22
+ # We evaluate in a fresh Object context to prevent accidental access to
23
+ # internals, but this provides NO security isolation.
24
+ evaluation_context = Object.new
25
+ predicate = evaluation_context.instance_eval(code, '<predicate>', 1)
26
+
27
+ validate_callable(predicate)
28
+ predicate.call(model)
29
+ rescue SyntaxError => e
30
+ raise "Syntax error in predicate code: #{e.message}"
31
+ end
32
+
33
+ # Evaluate a predicate from a file
34
+ #
35
+ # @param path [String] Path to Ruby file containing predicate code
36
+ # @param model [CoverageModel] The coverage model to pass to the predicate
37
+ # @return [Boolean] The result of calling the predicate with the model
38
+ # @raise [RuntimeError] If the file doesn't exist, doesn't return a callable, or has syntax errors
39
+ def self.evaluate_file(path, model)
40
+ unless File.exist?(path)
41
+ raise "Predicate file not found: #{path}"
42
+ end
43
+
44
+ content = File.read(path)
45
+
46
+ # WARNING: The predicate code executes with full Ruby privileges.
47
+ # It has unrestricted access to the file system, network, and system commands.
48
+ # Only use predicate files from trusted sources.
49
+ #
50
+ # We evaluate in a fresh Object context to prevent accidental access to
51
+ # internals, but this provides NO security isolation.
52
+ evaluation_context = Object.new
53
+ predicate = evaluation_context.instance_eval(content, path, 1)
54
+
55
+ validate_callable(predicate)
56
+ predicate.call(model)
57
+ rescue SyntaxError => e
58
+ raise "Syntax error in predicate file: #{e.message}"
59
+ end
60
+
61
+ # Validate that an object is callable
62
+ #
63
+ # @param predicate [Object] The object to check
64
+ # @raise [RuntimeError] If the object doesn't respond to #call
65
+ def self.validate_callable(predicate)
66
+ unless predicate.respond_to?(:call)
67
+ raise 'Predicate must be callable (lambda, proc, or object with #call method)'
68
+ end
69
+ end
70
+ private_class_method :validate_callable
71
+ end
72
+ end
@@ -34,9 +34,7 @@ module SimpleCovMcp
34
34
  relativized_payload['file']
35
35
  end
36
36
 
37
- private
38
-
39
- def build_payload
37
+ private def build_payload
40
38
  raise NotImplementedError, "#{self.class} must implement #build_payload"
41
39
  end
42
40
  end
@@ -6,9 +6,7 @@ module SimpleCovMcp
6
6
  module Presenters
7
7
  # Provides shared detailed coverage payloads for CLI and MCP callers.
8
8
  class CoverageDetailedPresenter < BaseCoveragePresenter
9
- private
10
-
11
- def build_payload
9
+ private def build_payload
12
10
  model.detailed_for(path)
13
11
  end
14
12
  end
@@ -6,9 +6,7 @@ module SimpleCovMcp
6
6
  module Presenters
7
7
  # Provides shared raw coverage payloads for CLI and MCP callers.
8
8
  class CoverageRawPresenter < BaseCoveragePresenter
9
- private
10
-
11
- def build_payload
9
+ private def build_payload
12
10
  model.raw_for(path)
13
11
  end
14
12
  end
@@ -6,9 +6,7 @@ module SimpleCovMcp
6
6
  module Presenters
7
7
  # Builds a consistent summary payload that both the CLI and MCP surfaces can use.
8
8
  class CoverageSummaryPresenter < BaseCoveragePresenter
9
- private
10
-
11
- def build_payload
9
+ private def build_payload
12
10
  model.summary_for(path)
13
11
  end
14
12
  end
@@ -6,9 +6,7 @@ module SimpleCovMcp
6
6
  module Presenters
7
7
  # Provides shared uncovered coverage payloads for CLI and MCP callers.
8
8
  class CoverageUncoveredPresenter < BaseCoveragePresenter
9
- private
10
-
11
- def build_payload
9
+ private def build_payload
12
10
  model.uncovered_for(path)
13
11
  end
14
12
  end
@@ -40,9 +40,7 @@ module SimpleCovMcp
40
40
  relativized_payload['counts']
41
41
  end
42
42
 
43
- private
44
-
45
- def build_counts(files)
43
+ private def build_counts(files)
46
44
  total = files.length
47
45
  stale = files.count { |f| f['stale'] }
48
46
  { 'total' => total, 'ok' => total - stale, 'stale' => stale }
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCovMcp
4
+ module Presenters
5
+ # Provides aggregated line totals and average coverage across the project.
6
+ class ProjectTotalsPresenter
7
+ attr_reader :model, :check_stale, :tracked_globs
8
+
9
+ def initialize(model:, check_stale:, tracked_globs:)
10
+ @model = model
11
+ @check_stale = check_stale
12
+ @tracked_globs = tracked_globs
13
+ end
14
+
15
+ def absolute_payload
16
+ @absolute_payload ||= model.project_totals(
17
+ tracked_globs: tracked_globs,
18
+ check_stale: check_stale
19
+ )
20
+ end
21
+
22
+ def relativized_payload
23
+ @relativized_payload ||= model.relativize(absolute_payload)
24
+ end
25
+ end
26
+ end
27
+ end