cov-loupe 3.0.0 → 4.0.0.pre

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 (281) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +230 -0
  3. data/CLAUDE.md +5 -0
  4. data/CODE_OF_CONDUCT.md +62 -0
  5. data/CONTRIBUTING.md +102 -0
  6. data/GEMINI.md +5 -0
  7. data/README.md +154 -51
  8. data/RELEASE_NOTES.md +452 -0
  9. data/dev/images/cov-loupe-icon-lores.png +0 -0
  10. data/dev/images/cov-loupe-icon-square.png +0 -0
  11. data/dev/images/cov-loupe-icon.png +0 -0
  12. data/dev/images/cov-loupe-logo.png +0 -0
  13. data/dev/prompts/README.md +74 -0
  14. data/dev/prompts/archive/architectural-review-and-actions-prompt.md +53 -0
  15. data/dev/prompts/archive/investigate-and-report-issues-prompt.md +33 -0
  16. data/dev/prompts/archive/produce-action-items-prompt.md +25 -0
  17. data/dev/prompts/guidelines/ai-code-evaluator-guidelines.md +337 -0
  18. data/dev/prompts/improve/refactor-test-suite.md +18 -0
  19. data/dev/prompts/improve/simplify-code-logic.md +133 -0
  20. data/dev/prompts/improve/update-documentation.md +21 -0
  21. data/dev/prompts/review/comprehensive-codebase-review.md +176 -0
  22. data/dev/prompts/review/identify-action-items.md +143 -0
  23. data/dev/prompts/review/verify-code-changes.md +54 -0
  24. data/dev/prompts/validate/create-screencast-outline.md +234 -0
  25. data/dev/prompts/validate/test-documentation-examples.md +180 -0
  26. data/docs/QUICKSTART.md +63 -0
  27. data/docs/assets/images/cov-loupe-logo-lores.png +0 -0
  28. data/docs/assets/images/cov-loupe-logo.png +0 -0
  29. data/docs/assets/images/favicon.png +0 -0
  30. data/docs/assets/stylesheets/branding.css +16 -0
  31. data/docs/assets/stylesheets/extra.css +15 -0
  32. data/docs/code_of_conduct.md +1 -0
  33. data/docs/contributing.md +1 -0
  34. data/docs/dev/ARCHITECTURE.md +56 -11
  35. data/docs/dev/DEVELOPMENT.md +116 -12
  36. data/docs/dev/FUTURE_ENHANCEMENTS.md +14 -0
  37. data/docs/dev/README.md +3 -2
  38. data/docs/dev/RELEASING.md +2 -0
  39. data/docs/dev/arch-decisions/README.md +10 -7
  40. data/docs/dev/arch-decisions/application-architecture.md +259 -0
  41. data/docs/dev/arch-decisions/coverage-data-quality.md +193 -0
  42. data/docs/dev/arch-decisions/output-character-mode.md +217 -0
  43. data/docs/dev/arch-decisions/path-resolution.md +90 -0
  44. data/docs/dev/arch-decisions/{004-x-arch-decision.md → policy-validation.md} +32 -28
  45. data/docs/dev/arch-decisions/{005-x-arch-decision.md → simplecov-integration.md} +47 -44
  46. data/docs/dev/presentations/cov-loupe-presentation.md +15 -13
  47. data/docs/examples/mcp-inputs.md +3 -0
  48. data/docs/examples/prompts.md +3 -0
  49. data/docs/examples/success_predicates.md +3 -0
  50. data/docs/fixtures/demo_project/.resultset.json +170 -0
  51. data/docs/fixtures/demo_project/README.md +6 -0
  52. data/docs/fixtures/demo_project/app/controllers/admin/audit_logs_controller.rb +19 -0
  53. data/docs/fixtures/demo_project/app/controllers/orders_controller.rb +26 -0
  54. data/docs/fixtures/demo_project/app/models/order.rb +20 -0
  55. data/docs/fixtures/demo_project/app/models/user.rb +19 -0
  56. data/docs/fixtures/demo_project/lib/api/client.rb +22 -0
  57. data/docs/fixtures/demo_project/lib/ops/jobs/cleanup_job.rb +16 -0
  58. data/docs/fixtures/demo_project/lib/ops/jobs/report_job.rb +17 -0
  59. data/docs/fixtures/demo_project/lib/payments/processor.rb +15 -0
  60. data/docs/fixtures/demo_project/lib/payments/refund_service.rb +15 -0
  61. data/docs/fixtures/demo_project/lib/payments/reporting/exporter.rb +16 -0
  62. data/docs/index.md +1 -0
  63. data/docs/license.md +3 -0
  64. data/docs/release_notes.md +3 -0
  65. data/docs/user/ADVANCED_USAGE.md +208 -115
  66. data/docs/user/CLI_FALLBACK_FOR_LLMS.md +2 -0
  67. data/docs/user/CLI_USAGE.md +276 -101
  68. data/docs/user/ERROR_HANDLING.md +4 -4
  69. data/docs/user/EXAMPLES.md +121 -128
  70. data/docs/user/INSTALLATION.md +9 -28
  71. data/docs/user/LIBRARY_API.md +227 -122
  72. data/docs/user/MCP_INTEGRATION.md +114 -203
  73. data/docs/user/README.md +5 -1
  74. data/docs/user/TROUBLESHOOTING.md +49 -27
  75. data/docs/user/installing-a-prelease-version-of-covloupe.md +43 -0
  76. data/docs/user/{V2-BREAKING-CHANGES.md → migrations/MIGRATING_TO_V2.md} +62 -72
  77. data/docs/user/migrations/MIGRATING_TO_V3.md +72 -0
  78. data/docs/user/migrations/MIGRATING_TO_V4.md +591 -0
  79. data/docs/user/migrations/README.md +22 -0
  80. data/docs/user/prompts/README.md +9 -0
  81. data/docs/user/prompts/non-web-coverage-analysis-prompt.md +103 -0
  82. data/docs/user/prompts/rails-coverage-analysis-prompt.md +94 -0
  83. data/docs/user/prompts/use-cli-not-mcp-prompt.md +53 -0
  84. data/examples/cli_demo.sh +77 -0
  85. data/examples/filter_and_table_demo-output.md +114 -0
  86. data/examples/filter_and_table_demo.rb +174 -0
  87. data/examples/fixtures/demo_project/coverage/.resultset.json +10 -0
  88. data/examples/mcp-inputs/README.md +66 -0
  89. data/examples/mcp-inputs/coverage_detailed.json +1 -0
  90. data/examples/mcp-inputs/coverage_raw.json +1 -0
  91. data/examples/mcp-inputs/coverage_summary.json +1 -0
  92. data/examples/mcp-inputs/list.json +1 -0
  93. data/examples/mcp-inputs/uncovered_lines.json +1 -0
  94. data/examples/prompts/README.md +27 -0
  95. data/examples/prompts/custom_resultset.txt +2 -0
  96. data/examples/prompts/detailed_with_source.txt +2 -0
  97. data/examples/prompts/list_lowest.txt +2 -0
  98. data/examples/prompts/summary.txt +2 -0
  99. data/examples/prompts/uncovered.txt +2 -0
  100. data/examples/success_predicates/README.md +198 -0
  101. data/examples/success_predicates/all_files_above_threshold_predicate.rb +21 -0
  102. data/examples/success_predicates/directory_specific_thresholds_predicate.rb +30 -0
  103. data/examples/success_predicates/project_coverage_minimum_predicate.rb +6 -0
  104. data/lib/cov_loupe/base_tool.rb +229 -20
  105. data/lib/cov_loupe/cli.rb +132 -23
  106. data/lib/cov_loupe/commands/base_command.rb +25 -6
  107. data/lib/cov_loupe/commands/command_factory.rb +0 -1
  108. data/lib/cov_loupe/commands/detailed_command.rb +10 -5
  109. data/lib/cov_loupe/commands/list_command.rb +2 -1
  110. data/lib/cov_loupe/commands/raw_command.rb +7 -5
  111. data/lib/cov_loupe/commands/summary_command.rb +12 -7
  112. data/lib/cov_loupe/commands/totals_command.rb +74 -10
  113. data/lib/cov_loupe/commands/uncovered_command.rb +7 -5
  114. data/lib/cov_loupe/commands/validate_command.rb +11 -3
  115. data/lib/cov_loupe/commands/version_command.rb +6 -4
  116. data/lib/cov_loupe/{app_config.rb → config/app_config.rb} +13 -5
  117. data/lib/cov_loupe/config/app_context.rb +43 -0
  118. data/lib/cov_loupe/config/boolean_type.rb +91 -0
  119. data/lib/cov_loupe/config/logger.rb +92 -0
  120. data/lib/cov_loupe/{option_normalizers.rb → config/option_normalizers.rb} +55 -24
  121. data/lib/cov_loupe/{option_parser_builder.rb → config/option_parser_builder.rb} +46 -24
  122. data/lib/cov_loupe/coverage/coverage_calculator.rb +53 -0
  123. data/lib/cov_loupe/coverage/coverage_reporter.rb +63 -0
  124. data/lib/cov_loupe/coverage/coverage_table_formatter.rb +133 -0
  125. data/lib/cov_loupe/{error_handler.rb → errors/error_handler.rb} +21 -33
  126. data/lib/cov_loupe/{errors.rb → errors/errors.rb} +48 -71
  127. data/lib/cov_loupe/formatters/formatters.rb +75 -0
  128. data/lib/cov_loupe/formatters/source_formatter.rb +18 -7
  129. data/lib/cov_loupe/formatters/table_formatter.rb +80 -0
  130. data/lib/cov_loupe/loaders/all.rb +15 -0
  131. data/lib/cov_loupe/loaders/all_cli.rb +10 -0
  132. data/lib/cov_loupe/loaders/all_mcp.rb +23 -0
  133. data/lib/cov_loupe/loaders/resultset_loader.rb +147 -0
  134. data/lib/cov_loupe/mcp_server.rb +3 -2
  135. data/lib/cov_loupe/model/model.rb +520 -0
  136. data/lib/cov_loupe/model/model_data.rb +13 -0
  137. data/lib/cov_loupe/model/model_data_cache.rb +116 -0
  138. data/lib/cov_loupe/option_parsers/env_options_parser.rb +17 -6
  139. data/lib/cov_loupe/option_parsers/error_helper.rb +16 -10
  140. data/lib/cov_loupe/output_chars.rb +192 -0
  141. data/lib/cov_loupe/paths/glob_utils.rb +100 -0
  142. data/lib/cov_loupe/{path_relativizer.rb → paths/path_relativizer.rb} +5 -13
  143. data/lib/cov_loupe/paths/path_utils.rb +265 -0
  144. data/lib/cov_loupe/paths/volume_case_sensitivity.rb +173 -0
  145. data/lib/cov_loupe/presenters/base_coverage_presenter.rb +9 -13
  146. data/lib/cov_loupe/presenters/coverage_payload_presenter.rb +21 -0
  147. data/lib/cov_loupe/presenters/payload_caching.rb +23 -0
  148. data/lib/cov_loupe/presenters/project_coverage_presenter.rb +73 -21
  149. data/lib/cov_loupe/presenters/project_totals_presenter.rb +16 -10
  150. data/lib/cov_loupe/repositories/coverage_repository.rb +149 -0
  151. data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +90 -76
  152. data/lib/cov_loupe/resolvers/{resolver_factory.rb → resolver_helpers.rb} +6 -5
  153. data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +40 -12
  154. data/lib/cov_loupe/scripts/command_execution.rb +113 -0
  155. data/lib/cov_loupe/scripts/latest_ci_status.rb +97 -0
  156. data/lib/cov_loupe/scripts/pre_release_check.rb +164 -0
  157. data/lib/cov_loupe/scripts/setup_doc_server.rb +23 -0
  158. data/lib/cov_loupe/scripts/start_doc_server.rb +24 -0
  159. data/lib/cov_loupe/staleness/stale_status.rb +23 -0
  160. data/lib/cov_loupe/staleness/staleness_checker.rb +328 -0
  161. data/lib/cov_loupe/staleness/staleness_message_formatter.rb +91 -0
  162. data/lib/cov_loupe/tools/coverage_detailed_tool.rb +14 -15
  163. data/lib/cov_loupe/tools/coverage_raw_tool.rb +14 -14
  164. data/lib/cov_loupe/tools/coverage_summary_tool.rb +16 -16
  165. data/lib/cov_loupe/tools/coverage_table_tool.rb +139 -21
  166. data/lib/cov_loupe/tools/coverage_totals_tool.rb +31 -13
  167. data/lib/cov_loupe/tools/help_tool.rb +16 -20
  168. data/lib/cov_loupe/tools/list_tool.rb +65 -0
  169. data/lib/cov_loupe/tools/uncovered_lines_tool.rb +14 -14
  170. data/lib/cov_loupe/tools/validate_tool.rb +18 -24
  171. data/lib/cov_loupe/tools/version_tool.rb +8 -3
  172. data/lib/cov_loupe/version.rb +1 -1
  173. data/lib/cov_loupe.rb +83 -55
  174. metadata +184 -154
  175. data/docs/dev/BRANCH_ONLY_COVERAGE.md +0 -158
  176. data/docs/dev/arch-decisions/001-x-arch-decision.md +0 -95
  177. data/docs/dev/arch-decisions/002-x-arch-decision.md +0 -159
  178. data/docs/dev/arch-decisions/003-x-arch-decision.md +0 -165
  179. data/lib/cov_loupe/app_context.rb +0 -26
  180. data/lib/cov_loupe/constants.rb +0 -22
  181. data/lib/cov_loupe/coverage_reporter.rb +0 -31
  182. data/lib/cov_loupe/formatters.rb +0 -51
  183. data/lib/cov_loupe/mode_detector.rb +0 -56
  184. data/lib/cov_loupe/model.rb +0 -339
  185. data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +0 -14
  186. data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +0 -14
  187. data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +0 -14
  188. data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +0 -14
  189. data/lib/cov_loupe/resultset_loader.rb +0 -131
  190. data/lib/cov_loupe/staleness_checker.rb +0 -247
  191. data/lib/cov_loupe/table_formatter.rb +0 -64
  192. data/lib/cov_loupe/tools/all_files_coverage_tool.rb +0 -51
  193. data/lib/cov_loupe/util.rb +0 -88
  194. data/spec/MCP_INTEGRATION_TESTS_README.md +0 -111
  195. data/spec/TIMESTAMPS.md +0 -48
  196. data/spec/all_files_coverage_tool_spec.rb +0 -53
  197. data/spec/app_config_spec.rb +0 -142
  198. data/spec/base_tool_spec.rb +0 -62
  199. data/spec/cli/show_default_report_spec.rb +0 -33
  200. data/spec/cli_enumerated_options_spec.rb +0 -90
  201. data/spec/cli_error_spec.rb +0 -184
  202. data/spec/cli_format_spec.rb +0 -123
  203. data/spec/cli_json_options_spec.rb +0 -50
  204. data/spec/cli_source_spec.rb +0 -44
  205. data/spec/cli_spec.rb +0 -192
  206. data/spec/cli_table_spec.rb +0 -28
  207. data/spec/cli_usage_spec.rb +0 -42
  208. data/spec/commands/base_command_spec.rb +0 -107
  209. data/spec/commands/command_factory_spec.rb +0 -76
  210. data/spec/commands/detailed_command_spec.rb +0 -34
  211. data/spec/commands/list_command_spec.rb +0 -28
  212. data/spec/commands/raw_command_spec.rb +0 -69
  213. data/spec/commands/summary_command_spec.rb +0 -34
  214. data/spec/commands/totals_command_spec.rb +0 -34
  215. data/spec/commands/uncovered_command_spec.rb +0 -55
  216. data/spec/commands/validate_command_spec.rb +0 -213
  217. data/spec/commands/version_command_spec.rb +0 -38
  218. data/spec/constants_spec.rb +0 -61
  219. data/spec/cov_loupe/formatters/source_formatter_spec.rb +0 -267
  220. data/spec/cov_loupe/formatters_spec.rb +0 -76
  221. data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +0 -79
  222. data/spec/cov_loupe_model_spec.rb +0 -454
  223. data/spec/cov_loupe_module_spec.rb +0 -37
  224. data/spec/cov_loupe_opts_spec.rb +0 -185
  225. data/spec/coverage_reporter_spec.rb +0 -102
  226. data/spec/coverage_table_tool_spec.rb +0 -59
  227. data/spec/coverage_totals_tool_spec.rb +0 -37
  228. data/spec/error_handler_spec.rb +0 -197
  229. data/spec/error_mode_spec.rb +0 -139
  230. data/spec/errors_edge_cases_spec.rb +0 -312
  231. data/spec/errors_stale_spec.rb +0 -83
  232. data/spec/file_based_mcp_tools_spec.rb +0 -99
  233. data/spec/help_tool_spec.rb +0 -26
  234. data/spec/integration_spec.rb +0 -789
  235. data/spec/logging_fallback_spec.rb +0 -128
  236. data/spec/mcp_logging_spec.rb +0 -44
  237. data/spec/mcp_server_integration_spec.rb +0 -23
  238. data/spec/mcp_server_spec.rb +0 -106
  239. data/spec/mode_detector_spec.rb +0 -153
  240. data/spec/model_error_handling_spec.rb +0 -269
  241. data/spec/model_staleness_spec.rb +0 -79
  242. data/spec/option_normalizers_spec.rb +0 -203
  243. data/spec/option_parsers/env_options_parser_spec.rb +0 -221
  244. data/spec/option_parsers/error_helper_spec.rb +0 -222
  245. data/spec/path_relativizer_spec.rb +0 -98
  246. data/spec/presenters/coverage_detailed_presenter_spec.rb +0 -19
  247. data/spec/presenters/coverage_raw_presenter_spec.rb +0 -15
  248. data/spec/presenters/coverage_summary_presenter_spec.rb +0 -15
  249. data/spec/presenters/coverage_uncovered_presenter_spec.rb +0 -16
  250. data/spec/presenters/project_coverage_presenter_spec.rb +0 -87
  251. data/spec/presenters/project_totals_presenter_spec.rb +0 -144
  252. data/spec/resolvers/coverage_line_resolver_spec.rb +0 -282
  253. data/spec/resolvers/resolver_factory_spec.rb +0 -61
  254. data/spec/resolvers/resultset_path_resolver_spec.rb +0 -60
  255. data/spec/resultset_loader_spec.rb +0 -167
  256. data/spec/shared_examples/README.md +0 -115
  257. data/spec/shared_examples/coverage_presenter_examples.rb +0 -66
  258. data/spec/shared_examples/file_based_mcp_tools.rb +0 -179
  259. data/spec/shared_examples/formatted_command_examples.rb +0 -64
  260. data/spec/shared_examples/mcp_tool_text_json_response.rb +0 -16
  261. data/spec/spec_helper.rb +0 -127
  262. data/spec/staleness_checker_spec.rb +0 -374
  263. data/spec/staleness_more_spec.rb +0 -42
  264. data/spec/support/cli_helpers.rb +0 -22
  265. data/spec/support/control_flow_helpers.rb +0 -20
  266. data/spec/support/fake_mcp.rb +0 -40
  267. data/spec/support/io_helpers.rb +0 -29
  268. data/spec/support/mcp_helpers.rb +0 -35
  269. data/spec/support/mcp_runner.rb +0 -66
  270. data/spec/support/mocking_helpers.rb +0 -30
  271. data/spec/table_format_spec.rb +0 -70
  272. data/spec/tools/validate_tool_spec.rb +0 -132
  273. data/spec/tools_error_handling_spec.rb +0 -130
  274. data/spec/util_spec.rb +0 -154
  275. data/spec/version_spec.rb +0 -123
  276. data/spec/version_tool_spec.rb +0 -141
  277. /data/{spec/fixtures/project1 → examples/fixtures/demo_project}/lib/bar.rb +0 -0
  278. /data/{spec/fixtures/project1 → examples/fixtures/demo_project}/lib/foo.rb +0 -0
  279. /data/lib/cov_loupe/{config_parser.rb → config/config_parser.rb} +0 -0
  280. /data/lib/cov_loupe/{predicate_evaluator.rb → config/predicate_evaluator.rb} +0 -0
  281. /data/lib/cov_loupe/{error_handler_factory.rb → errors/error_handler_factory.rb} +0 -0
@@ -2,8 +2,12 @@
2
2
 
3
3
  require 'mcp'
4
4
  require 'json'
5
- require_relative 'errors'
6
- require_relative 'error_handler'
5
+ require_relative 'errors/errors'
6
+ require_relative 'errors/error_handler'
7
+ require_relative 'model/model'
8
+ require_relative 'presenters/coverage_payload_presenter'
9
+ require_relative 'output_chars'
10
+ require_relative 'config/option_normalizers'
7
11
 
8
12
  module CovLoupe
9
13
  class BaseTool < ::MCP::Tool
@@ -18,19 +22,26 @@ module CovLoupe
18
22
  type: 'string',
19
23
  description: 'Path to the SimpleCov .resultset.json file (absolute or relative to root).'
20
24
  },
21
- staleness: {
22
- type: 'string',
23
- description: 'How to handle missing/outdated coverage data. ' \
24
- "'off' skips checks; 'error' raises.",
25
- enum: [:off, :error],
26
- default: :off
25
+ raise_on_stale: {
26
+ type: 'boolean',
27
+ description: 'If true, raise error if coverage data is stale (missing files, ' \
28
+ 'timestamp mismatch). Defaults to false.',
29
+ default: false
27
30
  },
28
31
  error_mode: {
29
32
  type: 'string',
30
33
  description: "Error handling mode: 'off' (silent), 'log' (log errors), " \
31
- "'debug' (verbose with backtraces).",
34
+ "'debug' (verbose with backtraces).",
32
35
  enum: %w[off log debug],
33
36
  default: 'log'
37
+ },
38
+ output_chars: {
39
+ type: 'string',
40
+ description: "Output character mode: 'default' (UTF-8 encoding uses fancy, else ascii), " \
41
+ "'fancy' (Unicode box-drawing and symbols), 'ascii' (ASCII-only 0x00-0x7F). " \
42
+ 'Accepts: d[efault], f[ancy], a[scii].',
43
+ enum: %w[default fancy ascii d f a],
44
+ default: 'default'
34
45
  }
35
46
  }.freeze
36
47
 
@@ -43,6 +54,17 @@ module CovLoupe
43
54
  items: { type: 'string' }
44
55
  }.freeze
45
56
 
57
+ DEFAULT_SORT_ORDER = CoverageModel::DEFAULT_SORT_ORDER.to_s
58
+
59
+ SORT_ORDER_PROPERTY = {
60
+ type: 'string',
61
+ description: 'Sort order for coverage percentages. ' \
62
+ "'#{DEFAULT_SORT_ORDER}' (default) lists highest coverage first. " \
63
+ 'Accepts: a[scending], d[escending].',
64
+ default: DEFAULT_SORT_ORDER,
65
+ enum: %w[ascending descending a d]
66
+ }.freeze
67
+
46
68
  PATH_PROPERTY = {
47
69
  type: 'string',
48
70
  description: 'Repo-relative or absolute path to the file whose coverage data you need.',
@@ -50,12 +72,13 @@ module CovLoupe
50
72
  }.freeze
51
73
 
52
74
  def self.coverage_schema(additional_properties: {}, required: [])
53
- {
75
+ schema = {
54
76
  type: 'object',
55
77
  additionalProperties: false,
56
- properties: COMMON_PROPERTIES.merge(additional_properties),
57
- required: required
58
- }.freeze
78
+ properties: COMMON_PROPERTIES.merge(additional_properties)
79
+ }
80
+ schema[:required] = required unless required.empty?
81
+ schema.freeze
59
82
  end
60
83
 
61
84
  FILE_INPUT_SCHEMA = coverage_schema(
@@ -67,36 +90,222 @@ module CovLoupe
67
90
  # Wrap tool execution with consistent error handling.
68
91
  # Yields to the block and rescues any error, delegating to handle_mcp_error.
69
92
  # This eliminates duplicate rescue blocks across all tools.
70
- def self.with_error_handling(tool_name, error_mode:)
93
+ #
94
+ # @param tool_name [String] Name of the tool for error reporting
95
+ # @param error_mode [Symbol, String] Error handling mode (:off, :log, :debug)
96
+ # @param output_chars [Symbol, String, nil] Output character mode for error messages
97
+ def self.with_error_handling(tool_name, error_mode:, output_chars: :default)
71
98
  yield
72
99
  rescue => e
73
- handle_mcp_error(e, tool_name, error_mode: error_mode)
100
+ handle_mcp_error(e, tool_name, error_mode: error_mode, output_chars: output_chars)
74
101
  end
75
102
 
76
103
  # Handle errors consistently across all MCP tools
77
104
  # Returns an MCP::Tool::Response with appropriate error message
78
- def self.handle_mcp_error(error, tool_name, error_mode: :log)
105
+ #
106
+ # @param error [Exception] The error to handle
107
+ # @param tool_name [String] Name of the tool for error reporting
108
+ # @param error_mode [Symbol, String] Error handling mode
109
+ # @param output_chars [Symbol, String, nil] Output character mode for error messages
110
+ # @return [MCP::Tool::Response] Error response
111
+ def self.handle_mcp_error(error, tool_name, error_mode: :log, output_chars: :default)
112
+ # Safely normalize error_mode to a symbol, defaulting to :log for invalid inputs
113
+ # This prevents crashes when MCP clients send invalid types (null, numbers, objects, etc.)
114
+ safe_mode = case error_mode
115
+ when Symbol then error_mode
116
+ when String then error_mode.to_sym
117
+ else :log
118
+ end
119
+
120
+ # Validate against VALID_ERROR_MODES and fallback to :log if invalid
121
+ # This prevents ArgumentError when handling errors with invalid error_mode values
122
+ safe_mode = :log unless ErrorHandler::VALID_ERROR_MODES.include?(safe_mode)
123
+
79
124
  # Create error handler with the specified mode
80
- error_handler = ErrorHandlerFactory.for_mcp_server(error_mode: error_mode.to_sym)
125
+ error_handler = ErrorHandlerFactory.for_mcp_server(error_mode: safe_mode)
81
126
 
82
127
  # Normalize to a CovLoupe::Error so we can handle/log uniformly
83
128
  normalized = error.is_a?(CovLoupe::Error) \
84
129
  ? error : error_handler.convert_standard_error(error)
85
130
  log_mcp_error(normalized, tool_name, error_handler)
86
- ::MCP::Tool::Response.new([{ 'type' => 'text', 'text' => "Error: #{normalized.user_friendly_message}" }])
131
+
132
+ # Convert error message to ASCII if needed
133
+ error_message = normalized.user_friendly_message
134
+ error_message = OutputChars.convert(error_message, output_chars || :default)
135
+ ::MCP::Tool::Response.new([{ 'type' => 'text', 'text' => "Error: #{error_message}" }])
87
136
  end
88
137
 
89
138
  # Respond with JSON as a resource to avoid clients mutating content types.
90
139
  # The resource embeds the JSON string with a clear MIME type.
91
- def self.respond_json(payload, name: 'data.json', pretty: false)
92
- json = pretty ? JSON.pretty_generate(payload) : JSON.generate(payload)
140
+ #
141
+ # @param payload [Object] The data to serialize as JSON
142
+ # @param name [String] Logical name for the JSON resource (informational)
143
+ # @param pretty [Boolean] Use pretty formatting with indentation
144
+ # @param output_chars [Symbol, String, nil] Output character mode (:default, :fancy, :ascii)
145
+ # @return [MCP::Tool::Response] Response containing the JSON string
146
+ def self.respond_json(payload, name: 'data.json', pretty: false, output_chars: :default)
147
+ ascii_only = ascii_only?(output_chars)
148
+ json = if pretty
149
+ ascii_only ? JSON.pretty_generate(payload, ascii_only: true) : JSON.pretty_generate(payload)
150
+ else
151
+ ascii_only ? JSON.generate(payload, ascii_only: true) : JSON.generate(payload)
152
+ end
93
153
  ::MCP::Tool::Response.new([{ 'type' => 'text', 'text' => json }])
94
154
  end
95
155
 
156
+ # Determines if ASCII-only output is required based on the character mode setting.
157
+ # Normalizes string inputs to symbols (MCP JSON provides strings, internal code uses symbols).
158
+ # Uses strict validation to raise errors for invalid values.
159
+ #
160
+ # @param char_mode [Symbol, String, nil] The character mode (:default, :fancy, :ascii)
161
+ # @return [Boolean] true if ASCII-only output is required
162
+ # @raise [CovLoupe::UsageError] if char_mode is invalid
163
+ def self.ascii_only?(char_mode)
164
+ return false if char_mode.nil?
165
+
166
+ normalized_mode_name = normalize_output_chars_strict(char_mode)
167
+ OutputChars.ascii_mode?(normalized_mode_name)
168
+ end
169
+ private_class_method :ascii_only?
170
+
96
171
  def self.log_mcp_error(error, tool_name, error_handler)
97
172
  # Use the provided error handler for logging
98
173
  error_handler.send(:log_error, error, tool_name)
99
174
  end
100
175
  private_class_method :log_mcp_error
176
+
177
+ # Merges configuration from server context (CLI flags) with explicit tool parameters (JSON).
178
+ # Explicit parameters take precedence over context config, which takes precedence over defaults.
179
+ # @param server_context [AppContext] The server context containing app_config from CLI
180
+ # @param model_option_overrides [Hash] Tool call parameters that override model defaults
181
+ # @return [Hash] Merged configuration for CoverageModel initialization
182
+ def self.model_config_for(server_context:, **model_option_overrides)
183
+ # Start with config from context (CLI flags) or hardcoded defaults
184
+ base = server_context.app_config&.model_options || default_model_options
185
+
186
+ # Merge explicit params from JSON, removing nils
187
+ # (nil means "not provided", so use base config)
188
+ base.merge(model_option_overrides.compact)
189
+ end
190
+
191
+ # Creates and configures a CoverageModel instance.
192
+ # Encapsulates the common pattern of merging config and initializing the model.
193
+ # @param server_context [AppContext] The server context
194
+ # @param model_option_overrides [Hash] Tool call parameters that override model defaults
195
+ # @return [CoverageModel] The configured model
196
+ def self.create_model(server_context:, **model_option_overrides)
197
+ model, _config = create_configured_model(server_context: server_context,
198
+ **model_option_overrides)
199
+ model
200
+ end
201
+
202
+ # Creates and configures a CoverageModel instance, returning both the model and the configuration.
203
+ # Useful when the tool needs access to the resolved configuration (e.g., root, raise_on_stale).
204
+ #
205
+ # Models are now lightweight (data is loaded lazily via ModelDataCache), so we create
206
+ # a fresh instance on each call rather than caching at the model level.
207
+ #
208
+ # @param server_context [AppContext] The server context
209
+ # @param model_option_overrides [Hash] Tool call parameters that override model defaults
210
+ # @return [Array<CoverageModel, Hash>] The configured model and the configuration hash
211
+ def self.create_configured_model(server_context:, **model_option_overrides)
212
+ config = model_config_for(server_context: server_context, **model_option_overrides)
213
+ model = CoverageModel.new(**config)
214
+ [model, config]
215
+ end
216
+
217
+ # Default configuration when no context or explicit params are provided
218
+ def self.default_model_options
219
+ { root: '.', resultset: nil, raise_on_stale: false, tracked_globs: [] }
220
+ end
221
+
222
+ # Resolves output_chars from tool parameter or server context.
223
+ # Tool parameter takes precedence over server context config.
224
+ # Uses strict validation for tool parameters to catch invalid values.
225
+ #
226
+ # @param output_chars [String, Symbol, nil] Tool parameter value
227
+ # @param server_context [AppContext] Server context with app_config
228
+ # @return [Symbol] Normalized output_chars mode (:default, :fancy, or :ascii)
229
+ # @raise [CovLoupe::UsageError] if output_chars parameter is invalid
230
+ def self.resolve_output_chars(output_chars, server_context)
231
+ # Use explicit parameter if provided
232
+ return normalize_output_chars_strict(output_chars) if output_chars
233
+
234
+ # Fall back to server context config
235
+ server_context.app_config&.output_chars || :default
236
+ end
237
+
238
+ # Normalizes output_chars value with strict validation.
239
+ # Converts string inputs to symbols and validates against allowed values.
240
+ #
241
+ # @param value [String, Symbol, nil] The output_chars value to normalize
242
+ # @return [Symbol] Normalized output_chars mode (:default, :fancy, or :ascii)
243
+ # @raise [CovLoupe::UsageError] if value is invalid
244
+ def self.normalize_output_chars_strict(value)
245
+ case value
246
+ when Symbol then value
247
+ when String
248
+ begin
249
+ OptionNormalizers.normalize_output_chars(value, strict: true)
250
+ rescue OptionParser::InvalidArgument
251
+ raise CovLoupe::UsageError, "Invalid output_chars value: #{value.inspect}. " \
252
+ 'Must be one of: default, fancy, ascii (or abbreviations: d, f, a)'
253
+ end
254
+ else
255
+ raise CovLoupe::UsageError, "Invalid output_chars type: #{value.class.name}. " \
256
+ 'Must be a string (one of: default, fancy, ascii, or abbreviations: d, f, a)'
257
+ end
258
+ end
259
+ private_class_method :normalize_output_chars_strict
260
+
261
+ # Runs a file-based tool request by deriving payload method and JSON name from the tool class.
262
+ # @param path [String] File path to analyze
263
+ # @param error_mode [String] Error handling mode
264
+ # @param output_chars [String, Symbol, nil] Output character mode
265
+ # @param server_context [AppContext] Server context
266
+ # @param model_option_overrides [Hash] Tool call parameters that override model defaults
267
+ # @return [MCP::Tool::Response] JSON response
268
+ def self.call_with_file_payload(path:, error_mode:, server_context:, output_chars: nil,
269
+ **model_option_overrides)
270
+ tool_name = name.split('::').last
271
+ output_chars_sym = resolve_output_chars(output_chars, server_context)
272
+
273
+ with_error_handling(tool_name, error_mode: error_mode, output_chars: output_chars_sym) do
274
+ model, config = create_configured_model(server_context: server_context,
275
+ **model_option_overrides)
276
+ presenter = Presenters::CoveragePayloadPresenter.new(
277
+ model: model,
278
+ path: path,
279
+ payload_method: payload_method_for(tool_name),
280
+ raise_on_stale: config[:raise_on_stale]
281
+ )
282
+ respond_json(presenter.relativized_payload, name: json_name_for(tool_name), pretty: true,
283
+ output_chars: output_chars_sym)
284
+ end
285
+ end
286
+
287
+ # Infer CoverageModel method name from a tool class name.
288
+ # CoverageSummaryTool -> :summary_for, CoverageRawTool -> :raw_for,
289
+ # CoverageDetailedTool -> :detailed_for, UncoveredLinesTool -> :uncovered_for.
290
+ def self.payload_method_for(tool_name)
291
+ base = tool_name.sub(/Tool\z/, '')
292
+ underscored = underscore(base).sub(/\Acoverage_/, '').sub(/_lines\z/, '')
293
+ :"#{underscored}_for"
294
+ end
295
+
296
+ # Infer the MCP JSON resource name from a tool class name.
297
+ # CoverageSummaryTool -> coverage_summary.json, UncoveredLinesTool -> uncovered_lines.json.
298
+ def self.json_name_for(tool_name)
299
+ "#{underscore(tool_name.sub(/Tool\z/, ''))}.json"
300
+ end
301
+
302
+ # Minimal underscore helper to avoid pulling in ActiveSupport.
303
+ def self.underscore(value)
304
+ value
305
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\\1_\\2')
306
+ .gsub(/([a-z\\d])([A-Z])/, '\\1_\\2')
307
+ .downcase
308
+ end
309
+ private_class_method :payload_method_for, :json_name_for, :underscore
101
310
  end
102
311
  end
data/lib/cov_loupe/cli.rb CHANGED
@@ -1,21 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
- require_relative 'app_config'
5
- require_relative 'option_parser_builder'
4
+ require_relative 'config/app_config'
5
+ require_relative 'config/option_parser_builder'
6
6
  require_relative 'commands/command_factory'
7
7
  require_relative 'option_parsers/error_helper'
8
8
  require_relative 'option_parsers/env_options_parser'
9
- require_relative 'constants'
10
9
  require_relative 'presenters/project_coverage_presenter'
10
+ require_relative 'output_chars'
11
11
 
12
12
  module CovLoupe
13
13
  class CoverageCLI
14
- SUBCOMMANDS = %w[list summary raw uncovered detailed totals validate version].freeze
15
14
  HORIZONTAL_RULE = '-' * 79
16
15
 
17
- # Reference shared constant to avoid duplication with ModeDetector
18
- OPTIONS_EXPECTING_ARGUMENT = Constants::OPTIONS_EXPECTING_ARGUMENT
16
+ # Valid CLI subcommands.
17
+ SUBCOMMANDS = %w[list summary raw uncovered detailed totals validate version].freeze
19
18
 
20
19
  attr_reader :config
21
20
 
@@ -44,6 +43,7 @@ module CovLoupe
44
43
  )
45
44
 
46
45
  CovLoupe.with_context(context) do
46
+ log_cli_params
47
47
  if @cmd
48
48
  run_subcommand(@cmd, @cmd_args)
49
49
  else
@@ -58,28 +58,49 @@ module CovLoupe
58
58
  with_context_if_available(context) { handle_user_facing_error(e) }
59
59
  end
60
60
 
61
+ private def log_cli_params
62
+ # Log CLI parameters for transparency
63
+ if CovLoupe.logger
64
+ params = { mode: :cli, subcommand: @cmd || 'default' }
65
+ params[:root] = config.root if config.root
66
+ params[:resultset] = config.resultset if config.resultset
67
+ params[:format] = config.format if config.format
68
+ params[:sort_order] = config.sort_order if config.sort_order
69
+ params[:raise_on_stale] = config.raise_on_stale if config.raise_on_stale
70
+ params[:tracked_globs] = config.tracked_globs if config.tracked_globs&.any?
71
+ params[:error_mode] = config.error_mode if config.error_mode
72
+ CovLoupe.logger.info("CLI parameters: #{params.inspect}")
73
+ end
74
+ end
75
+
61
76
  def show_default_report(sort_order: :descending, output: $stdout)
62
77
  model = CoverageModel.new(**config.model_options)
63
78
  presenter = Presenters::ProjectCoveragePresenter.new(
64
79
  model: model,
65
80
  sort_order: sort_order,
66
- check_stale: (config.staleness == :error),
81
+ raise_on_stale: config.raise_on_stale,
67
82
  tracked_globs: config.tracked_globs
68
83
  )
69
84
 
70
- if config.format != :table
71
- require_relative 'formatters'
72
- output.puts Formatters.format(presenter.relativized_payload, config.format)
73
- return
85
+ if config.format == :table
86
+ file_summaries = presenter.relative_files
87
+ output.puts model.format_table(
88
+ file_summaries,
89
+ sort_order: sort_order,
90
+ raise_on_stale: config.raise_on_stale,
91
+ tracked_globs: nil,
92
+ output_chars: config.output_chars
93
+ )
94
+ show_exclusions_summary(presenter, $stderr)
95
+ warn_missing_timestamps(presenter, $stderr)
96
+ else
97
+ require_relative 'formatters/formatters'
98
+ output.puts Formatters.format(presenter.relativized_payload, config.format,
99
+ output_chars: config.output_chars)
74
100
  end
75
101
 
76
- file_summaries = presenter.relative_files
77
- output.puts model.format_table(
78
- file_summaries,
79
- sort_order: sort_order,
80
- check_stale: (config.staleness == :error),
81
- tracked_globs: nil
82
- )
102
+ warn_skipped_rows(presenter)
103
+ warn_missing_timestamps(presenter)
83
104
  end
84
105
 
85
106
  private def parse_options!(argv)
@@ -149,16 +170,21 @@ module CovLoupe
149
170
 
150
171
  private def handle_option_parser_error(error, argv: [])
151
172
  @error_helper ||= OptionParsers::ErrorHelper.new(SUBCOMMANDS)
152
- @error_helper.handle_option_parser_error(error, argv: argv)
173
+ @error_helper.handle_option_parser_error(error, argv: argv, output_chars: config.output_chars)
153
174
  end
154
175
 
155
176
  private def check_for_misplaced_global_options(cmd, args)
156
177
  # Global options that users commonly place after subcommands by mistake
157
178
  global_options = %w[-r --resultset -R --root -f --format -o --sort-order -s --source
158
- -c --context-lines -S --staleness -g --tracked-globs
159
- -l --log-file --error-mode --color --no-color]
179
+ -c --context-lines -S --raise-on-stale -g --tracked-globs
180
+ -l --log-file --error-mode --color -m --mode -v --version
181
+ -O --output-chars]
160
182
 
161
- misplaced = args.select { |arg| global_options.include?(arg) }
183
+ misplaced = args.select do |arg|
184
+ # Extract base option (e.g., --format from --format=json)
185
+ base = arg.split('=', 2).first
186
+ global_options.include?(base)
187
+ end
162
188
  return if misplaced.empty?
163
189
 
164
190
  option_list = misplaced.join(', ')
@@ -170,9 +196,92 @@ module CovLoupe
170
196
 
171
197
  private def handle_user_facing_error(error)
172
198
  error_handler.handle_error(error, context: 'CLI', reraise: false)
173
- warn error.user_friendly_message
199
+ # Convert error message to ASCII if in ascii mode
200
+ message = OutputChars.convert(error.user_friendly_message, config.output_chars)
201
+ warn message
174
202
  warn error.backtrace.first(5).join("\n") if config.error_mode == :debug && error.backtrace
175
203
  exit 1
176
204
  end
205
+
206
+ private def warn_skipped_rows(presenter)
207
+ skipped = presenter.relative_skipped_files
208
+ return if skipped.nil? || skipped.empty?
209
+
210
+ count = skipped.length
211
+ warn ''
212
+ warn "WARNING: #{count} coverage row#{count == 1 ? '' : 's'} skipped due to errors:"
213
+ skipped.each do |row|
214
+ # Paths are already relativized by presenter
215
+ file_path = OutputChars.convert(row['file'], config.output_chars)
216
+ error_msg = OutputChars.convert(row['error'], config.output_chars)
217
+ warn " - #{file_path}: #{error_msg}"
218
+ end
219
+ warn 'Run again with --raise-on-stale to exit when rows are skipped.'
220
+ end
221
+
222
+ private def warn_missing_timestamps(presenter, output = $stderr)
223
+ return unless presenter.timestamp_status == 'missing'
224
+
225
+ output.puts <<~WARNING
226
+
227
+ WARNING: Coverage timestamps are missing. Time-based staleness checks were skipped.
228
+ Files may appear "ok" even if source code is newer than the coverage data.
229
+ Check your coverage tool configuration to ensure timestamps are recorded.
230
+ WARNING
231
+ end
232
+
233
+ private def show_exclusions_summary(presenter, output)
234
+ missing = presenter.relative_missing_tracked_files
235
+ newer = presenter.relative_newer_files
236
+ deleted = presenter.relative_deleted_files
237
+ length_mismatch = presenter.relative_length_mismatch_files
238
+ unreadable = presenter.relative_unreadable_files
239
+ skipped = presenter.relative_skipped_files
240
+
241
+ # Only show if there are any exclusions
242
+ return if missing.empty? && newer.empty? && deleted.empty? &&
243
+ length_mismatch.empty? && unreadable.empty? && skipped.empty?
244
+
245
+ output.puts "\nFiles excluded from coverage:"
246
+
247
+ # Helper to convert paths to ASCII if needed
248
+ convert_path = ->(path) { OutputChars.convert(path, config.output_chars) }
249
+
250
+ unless missing.empty?
251
+ output.puts "\nMissing tracked files (#{missing.length}):"
252
+ missing.each { |file| output.puts " - #{convert_path.call(file)}" }
253
+ end
254
+
255
+ unless newer.empty?
256
+ output.puts "\nFiles newer than coverage (#{newer.length}):"
257
+ newer.each { |file| output.puts " - #{convert_path.call(file)}" }
258
+ end
259
+
260
+ unless deleted.empty?
261
+ output.puts "\nDeleted files with coverage (#{deleted.length}):"
262
+ deleted.each { |file| output.puts " - #{convert_path.call(file)}" }
263
+ end
264
+
265
+ unless length_mismatch.empty?
266
+ output.puts "\nLine count mismatches (#{length_mismatch.length}):"
267
+ length_mismatch.each { |file| output.puts " - #{convert_path.call(file)}" }
268
+ end
269
+
270
+ unless unreadable.empty?
271
+ output.puts "\nUnreadable files (#{unreadable.length}):"
272
+ unreadable.each { |file| output.puts " - #{convert_path.call(file)}" }
273
+ end
274
+
275
+ unless skipped.empty?
276
+ output.puts "\nFiles skipped due to errors (#{skipped.length}):"
277
+ skipped.each do |row|
278
+ file_path = OutputChars.convert(row['file'], config.output_chars)
279
+ error_msg = OutputChars.convert(row['error'], config.output_chars)
280
+ output.puts " - #{file_path}: #{error_msg}"
281
+ end
282
+ end
283
+
284
+ output.puts "\nRun with --raise-on-stale to exit when files are excluded."
285
+ end
177
286
  end
178
287
  end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
- require_relative '../formatters'
4
+ require_relative '../formatters/formatters'
5
5
  require_relative '../formatters/source_formatter'
6
- require_relative '../model'
7
- require_relative '../errors'
6
+ require_relative '../model/model'
7
+ require_relative '../errors/errors'
8
+ require_relative '../output_chars'
8
9
 
9
10
  module CovLoupe
10
11
  module Commands
@@ -23,8 +24,14 @@ module CovLoupe
23
24
  @model ||= CoverageModel.new(**config.model_options)
24
25
  end
25
26
 
27
+ # Convert text to ASCII if output_chars is :ascii
28
+ protected def convert_text(text)
29
+ OutputChars.convert(text, config.output_chars)
30
+ end
31
+
26
32
  protected def handle_with_path(args, name)
27
33
  path = args.shift or raise UsageError.for_subcommand("#{name} <path>")
34
+ reject_extra_args(args, name)
28
35
  yield(path)
29
36
  rescue Errno::ENOENT
30
37
  raise FileNotFoundError, "File not found: #{path}"
@@ -32,10 +39,20 @@ module CovLoupe
32
39
  raise FilePermissionError, "Permission denied: #{path}"
33
40
  end
34
41
 
42
+ # Validates that no unexpected arguments remain after parsing.
43
+ # Raises UsageError if extra args are present.
44
+ protected def reject_extra_args(args, command_name)
45
+ return if args.empty?
46
+
47
+ extra = args.join(' ')
48
+ raise UsageError, "Unexpected argument(s) for '#{command_name}': #{extra}"
49
+ end
50
+
35
51
  protected def maybe_output_structured_format?(obj, model)
36
52
  return false if config.format == :table
37
53
 
38
- puts CovLoupe::Formatters.format(model.relativize(obj), config.format)
54
+ puts CovLoupe::Formatters.format(model.relativize(obj), config.format,
55
+ output_chars: config.output_chars)
39
56
  true
40
57
  end
41
58
 
@@ -45,9 +62,11 @@ module CovLoupe
45
62
  relativized = model.relativize(data)
46
63
  if config.source_mode
47
64
  payload = relativized.merge('source' => build_source_payload(model, path))
48
- puts CovLoupe::Formatters.format(payload, config.format)
65
+ puts CovLoupe::Formatters.format(payload, config.format,
66
+ output_chars: config.output_chars)
49
67
  else
50
- puts CovLoupe::Formatters.format(relativized, config.format)
68
+ puts CovLoupe::Formatters.format(relativized, config.format,
69
+ output_chars: config.output_chars)
51
70
  end
52
71
  true
53
72
  end
@@ -21,7 +21,6 @@ module CovLoupe
21
21
  'uncovered' => UncoveredCommand,
22
22
  'detailed' => DetailedCommand,
23
23
  'totals' => TotalsCommand,
24
- 'total' => TotalsCommand, # Alias for backward compatibility
25
24
  'validate' => ValidateCommand
26
25
  }.freeze
27
26
 
@@ -2,20 +2,24 @@
2
2
 
3
3
  require_relative 'base_command'
4
4
  require_relative '../formatters/source_formatter'
5
- require_relative '../presenters/coverage_detailed_presenter'
6
- require_relative '../table_formatter'
5
+ require_relative '../presenters/coverage_payload_presenter'
6
+ require_relative '../staleness/stale_status'
7
7
 
8
8
  module CovLoupe
9
9
  module Commands
10
10
  class DetailedCommand < BaseCommand
11
11
  def execute(args)
12
12
  handle_with_path(args, 'detailed') do |path|
13
- presenter = Presenters::CoverageDetailedPresenter.new(model: model, path: path)
13
+ presenter = Presenters::CoveragePayloadPresenter.new(model: model, path: path,
14
+ payload_method: :detailed_for)
14
15
  data = presenter.absolute_payload
15
16
  break if emit_structured_format_with_optional_source?(data, model, path)
16
17
 
17
- relative_path = presenter.relative_path
18
+ relative_path = convert_text(presenter.relative_path)
19
+ summary = data['summary']
18
20
  puts "File: #{relative_path}"
21
+ puts "Coverage: #{summary['covered']}/#{summary['total']} lines (#{format('%.2f%%', summary['percentage'])})"
22
+ puts "Stale: #{data['stale']}" if StaleStatus.stale?(data['stale'])
19
23
  puts
20
24
 
21
25
  # Table format with box-drawing
@@ -27,7 +31,8 @@ module CovLoupe
27
31
  puts TableFormatter.format(
28
32
  headers: headers,
29
33
  rows: rows,
30
- alignments: [:right, :right, :center]
34
+ alignments: [:right, :right, :center],
35
+ output_chars: config.output_chars
31
36
  )
32
37
 
33
38
  print_source_for(model, path) if config.source_mode
@@ -5,7 +5,8 @@ require_relative 'base_command'
5
5
  module CovLoupe
6
6
  module Commands
7
7
  class ListCommand < BaseCommand
8
- def execute(_args)
8
+ def execute(args)
9
+ reject_extra_args(args, 'list')
9
10
  cli.send(:show_default_report, sort_order: config.sort_order)
10
11
  end
11
12
  end