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
@@ -1,19 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'base_command'
4
- require_relative '../presenters/coverage_raw_presenter'
5
- require_relative '../table_formatter'
4
+ require_relative '../presenters/coverage_payload_presenter'
5
+ require_relative '../formatters/table_formatter'
6
6
 
7
7
  module CovLoupe
8
8
  module Commands
9
9
  class RawCommand < BaseCommand
10
10
  def execute(args)
11
11
  handle_with_path(args, 'raw') do |path|
12
- presenter = Presenters::CoverageRawPresenter.new(model: model, path: path)
12
+ presenter = Presenters::CoveragePayloadPresenter.new(model: model, path: path,
13
+ payload_method: :raw_for)
13
14
  data = presenter.absolute_payload
14
15
  break if maybe_output_structured_format?(data, model)
15
16
 
16
- relative_path = presenter.relative_path
17
+ relative_path = convert_text(presenter.relative_path)
17
18
  puts "File: #{relative_path}"
18
19
  puts
19
20
 
@@ -29,7 +30,8 @@ module CovLoupe
29
30
  puts TableFormatter.format(
30
31
  headers: headers,
31
32
  rows: rows,
32
- alignments: [:right, :right]
33
+ alignments: [:right, :right],
34
+ output_chars: config.output_chars
33
35
  )
34
36
  end
35
37
  end
@@ -1,27 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'base_command'
4
- require_relative '../presenters/coverage_summary_presenter'
5
- require_relative '../table_formatter'
4
+ require_relative '../presenters/coverage_payload_presenter'
5
+ require_relative '../formatters/table_formatter'
6
+ require_relative '../staleness/stale_status'
6
7
 
7
8
  module CovLoupe
8
9
  module Commands
9
10
  class SummaryCommand < BaseCommand
10
11
  def execute(args)
11
12
  handle_with_path(args, 'summary') do |path|
12
- presenter = Presenters::CoverageSummaryPresenter.new(model: model, path: path)
13
+ presenter = Presenters::CoveragePayloadPresenter.new(model: model, path: path,
14
+ payload_method: :summary_for)
13
15
  data = presenter.absolute_payload
14
16
  break if emit_structured_format_with_optional_source?(data, model, path)
15
17
 
16
- relative_path = presenter.relative_path
18
+ relative_path = convert_text(presenter.relative_path)
17
19
  summary = data['summary']
18
20
 
19
21
  # Table format with box-drawing
20
22
  headers = ['File', '%', 'Covered', 'Total', 'Stale']
21
- stale_marker = data['stale'] ? 'Yes' : ''
23
+ stale_marker = StaleStatus.stale?(data['stale']) ? 'Yes' : ''
24
+ percent_display = summary['percentage'] ? format('%.2f%%', summary['percentage']) : 'n/a'.rjust(6)
25
+
22
26
  rows = [[
23
27
  relative_path,
24
- format('%.2f%%', summary['percentage']),
28
+ percent_display,
25
29
  summary['covered'].to_s,
26
30
  summary['total'].to_s,
27
31
  stale_marker
@@ -30,7 +34,8 @@ module CovLoupe
30
34
  puts TableFormatter.format(
31
35
  headers: headers,
32
36
  rows: rows,
33
- alignments: [:left, :right, :right, :right, :center]
37
+ alignments: [:left, :right, :right, :right, :center],
38
+ output_chars: config.output_chars
34
39
  )
35
40
  puts
36
41
  print_source_for(model, path) if config.source_mode
@@ -2,19 +2,17 @@
2
2
 
3
3
  require_relative 'base_command'
4
4
  require_relative '../presenters/project_totals_presenter'
5
- require_relative '../table_formatter'
5
+ require_relative '../formatters/table_formatter'
6
6
 
7
7
  module CovLoupe
8
8
  module Commands
9
9
  class TotalsCommand < BaseCommand
10
10
  def execute(args)
11
- unless args.empty?
12
- raise UsageError.for_subcommand('totals')
13
- end
11
+ reject_extra_args(args, 'totals')
14
12
 
15
13
  presenter = Presenters::ProjectTotalsPresenter.new(
16
14
  model: model,
17
- check_stale: (config.staleness == :error),
15
+ raise_on_stale: config.raise_on_stale,
18
16
  tracked_globs: config.tracked_globs
19
17
  )
20
18
  payload = presenter.absolute_payload
@@ -22,22 +20,36 @@ module CovLoupe
22
20
 
23
21
  lines = payload['lines']
24
22
  files = payload['files']
23
+ tracking = payload['tracking']
24
+ with_coverage = files['with_coverage']
25
+ without_coverage = files['without_coverage']
26
+
27
+ if tracking && tracking['enabled']
28
+ puts 'Tracked globs:'
29
+ tracking['globs'].each { |glob| puts " - #{convert_text(glob)}" }
30
+ else
31
+ puts 'Tracked globs: (tracking disabled)'
32
+ end
33
+ puts
25
34
 
26
- # Table format
35
+ puts 'Totals'
27
36
  headers = ['Metric', 'Total', 'Covered', 'Uncovered', '%']
37
+ file_ok = with_coverage['ok']
38
+ file_uncovered = files['total'] - file_ok
39
+ percent_display = lines['percent_covered'].nil? ? 'n/a' : format('%.2f%%', lines['percent_covered'])
28
40
  rows = [
29
41
  [
30
42
  'Lines',
31
43
  lines['total'].to_s,
32
44
  lines['covered'].to_s,
33
45
  lines['uncovered'].to_s,
34
- format('%.2f%%', payload['percentage'])
46
+ percent_display
35
47
  ],
36
48
  [
37
49
  'Files',
38
50
  files['total'].to_s,
39
- files['ok'].to_s,
40
- files['stale'].to_s,
51
+ file_ok.to_s,
52
+ file_uncovered.to_s,
41
53
  ''
42
54
  ]
43
55
  ]
@@ -45,8 +57,60 @@ module CovLoupe
45
57
  puts TableFormatter.format(
46
58
  headers: headers,
47
59
  rows: rows,
48
- alignments: [:left, :right, :right, :right, :right]
60
+ alignments: [:left, :right, :right, :right, :right],
61
+ output_chars: config.output_chars
49
62
  )
63
+ with_coverage_line = format_with_coverage_line(with_coverage)
64
+ stale_line = format_stale_breakdown(with_coverage['stale']['by_type'])
65
+ without_coverage_line, without_breakdown_line =
66
+ format_without_coverage_lines(without_coverage)
67
+
68
+ puts <<~BREAKDOWN
69
+
70
+ File breakdown:
71
+ #{with_coverage_line}
72
+ #{stale_line}
73
+ #{without_coverage_line}
74
+ #{without_breakdown_line}
75
+ BREAKDOWN
76
+
77
+ warn_missing_timestamps(presenter)
78
+ end
79
+
80
+ private def format_with_coverage_line(with_coverage)
81
+ stale = with_coverage['stale']
82
+ " With coverage: #{with_coverage['total']} total, #{with_coverage['ok']} ok, #{stale['total']} stale"
83
+ end
84
+
85
+ private def format_stale_breakdown(stale_by_type)
86
+ ' Stale: missing on disk = ' \
87
+ "#{stale_by_type['missing_from_disk']}, " \
88
+ "newer than coverage = #{stale_by_type['newer']}, " \
89
+ "line mismatch = #{stale_by_type['length_mismatch']}, " \
90
+ "unreadable = #{stale_by_type['unreadable']}"
91
+ end
92
+
93
+ private def format_without_coverage_lines(without_coverage)
94
+ return [nil, nil] unless without_coverage
95
+
96
+ without_by_type = without_coverage['by_type']
97
+ without_coverage_line = " Without coverage: #{without_coverage['total']} total"
98
+ without_breakdown_line = ' Missing from coverage = ' \
99
+ "#{without_by_type['missing_from_coverage']}, " \
100
+ "unreadable = #{without_by_type['unreadable']}, " \
101
+ "skipped (errors) = #{without_by_type['skipped']}"
102
+ [without_coverage_line, without_breakdown_line]
103
+ end
104
+
105
+ private def warn_missing_timestamps(presenter)
106
+ return unless presenter.timestamp_status == 'missing'
107
+
108
+ warn <<~WARNING
109
+
110
+ WARNING: Coverage timestamps are missing. Time-based staleness checks were skipped.
111
+ Files may appear "ok" even if source code is newer than the coverage data.
112
+ Check your coverage tool configuration to ensure timestamps are recorded.
113
+ WARNING
50
114
  end
51
115
  end
52
116
  end
@@ -1,19 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'base_command'
4
- require_relative '../presenters/coverage_uncovered_presenter'
5
- require_relative '../table_formatter'
4
+ require_relative '../presenters/coverage_payload_presenter'
5
+ require_relative '../formatters/table_formatter'
6
6
 
7
7
  module CovLoupe
8
8
  module Commands
9
9
  class UncoveredCommand < BaseCommand
10
10
  def execute(args)
11
11
  handle_with_path(args, 'uncovered') do |path|
12
- presenter = Presenters::CoverageUncoveredPresenter.new(model: model, path: path)
12
+ presenter = Presenters::CoveragePayloadPresenter.new(model: model, path: path,
13
+ payload_method: :uncovered_for)
13
14
  data = presenter.absolute_payload
14
15
  break if emit_structured_format_with_optional_source?(data, model, path)
15
16
 
16
- relative_path = presenter.relative_path
17
+ relative_path = convert_text(presenter.relative_path)
17
18
  summary = data['summary']
18
19
 
19
20
  puts "File: #{relative_path}"
@@ -32,7 +33,8 @@ module CovLoupe
32
33
  puts TableFormatter.format(
33
34
  headers: headers,
34
35
  rows: rows,
35
- alignments: [:right]
36
+ alignments: [:right],
37
+ output_chars: config.output_chars
36
38
  )
37
39
  end
38
40
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'base_command'
4
- require_relative '../predicate_evaluator'
4
+ require_relative '../config/predicate_evaluator'
5
5
 
6
6
  module CovLoupe
7
7
  module Commands
@@ -35,6 +35,9 @@ module CovLoupe
35
35
  code = file_path
36
36
  end
37
37
 
38
+ # Ensure no extra arguments remain
39
+ reject_extra_args(args, 'validate')
40
+
38
41
  # Evaluate the predicate
39
42
  result = if inline_mode
40
43
  PredicateEvaluator.evaluate_code(code, model)
@@ -51,8 +54,13 @@ module CovLoupe
51
54
  end
52
55
 
53
56
  private def handle_predicate_error(error)
54
- warn "Predicate error: #{error.message}"
55
- warn error.backtrace.first(5).join("\n") if config.error_mode == :debug
57
+ # Convert error message to ASCII if in ascii mode
58
+ message = convert_text(error.message)
59
+ warn "Predicate error: #{message}"
60
+ if config.error_mode == :debug
61
+ backtrace = error.backtrace.first(5).map { |line| convert_text(line) }
62
+ warn backtrace.join("\n")
63
+ end
56
64
  exit 2
57
65
  end
58
66
  end
@@ -2,12 +2,13 @@
2
2
 
3
3
  require 'json'
4
4
  require_relative 'base_command'
5
- require_relative '../table_formatter'
5
+ require_relative '../formatters/table_formatter'
6
6
 
7
7
  module CovLoupe
8
8
  module Commands
9
9
  class VersionCommand < BaseCommand
10
- def execute(_args)
10
+ def execute(args)
11
+ reject_extra_args(args, 'version')
11
12
  @gem_root = File.expand_path('../../..', __dir__)
12
13
 
13
14
  if config.format == :table
@@ -16,9 +17,10 @@ module CovLoupe
16
17
  'Gem Root' => @gem_root,
17
18
  'Documentation' => 'README.md and docs/user/**/*.md in gem root'
18
19
  }
19
- puts TableFormatter.format_vertical(data)
20
+ puts TableFormatter.format_vertical(data, output_chars: config.output_chars)
20
21
  else
21
- puts CovLoupe::Formatters.format(version_info, config.format)
22
+ puts CovLoupe::Formatters.format(version_info, config.format,
23
+ output_chars: config.output_chars)
22
24
  end
23
25
  end
24
26
 
@@ -12,10 +12,12 @@ module CovLoupe
12
12
  :source_context,
13
13
  :color,
14
14
  :error_mode,
15
- :staleness,
15
+ :raise_on_stale,
16
16
  :tracked_globs,
17
17
  :log_file,
18
18
  :show_version,
19
+ :mode,
20
+ :output_chars,
19
21
  keyword_init: true
20
22
  ) do
21
23
  # Set sensible defaults - ALL SYMBOLS FOR ENUMS
@@ -28,11 +30,16 @@ module CovLoupe
28
30
  source_context: 2,
29
31
  color: $stdout.tty?,
30
32
  error_mode: :log,
31
- staleness: :off,
33
+ raise_on_stale: false,
32
34
  tracked_globs: nil,
33
35
  log_file: nil,
34
- show_version: false
36
+ show_version: false,
37
+ mode: :cli,
38
+ output_chars: :default
35
39
  )
40
+ # Default to empty array (show all files in resultset and don't look for files lacking coverage data)
41
+ # Users should set COV_LOUPE_OPTS to match SimpleCov track_files patterns
42
+ tracked_globs = [] if tracked_globs.nil?
36
43
  super
37
44
  end
38
45
 
@@ -41,7 +48,7 @@ module CovLoupe
41
48
  {
42
49
  root: root,
43
50
  resultset: resultset,
44
- staleness: staleness,
51
+ raise_on_stale: raise_on_stale,
45
52
  tracked_globs: tracked_globs
46
53
  }
47
54
  end
@@ -49,7 +56,8 @@ module CovLoupe
49
56
  # Convenience method for SourceFormatter initialization
50
57
  def formatter_options
51
58
  {
52
- color_enabled: color
59
+ color_enabled: color,
60
+ output_chars: output_chars
53
61
  }
54
62
  end
55
63
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'logger'
4
+
5
+ module CovLoupe
6
+ # Encapsulates per-request configuration such as error handling and logging.
7
+ AppContext = Data.define(:error_handler, :log_target, :mode, :app_config, :logger) do
8
+ def initialize(error_handler:, log_target: nil, mode: :library, app_config: nil, logger: nil)
9
+ logger ||= Logger.new(target: log_target, mode: mode)
10
+ super
11
+ end
12
+
13
+ # Overrides Data#with to handle derived state.
14
+ #
15
+ # Since the `logger` depends on `log_target` and `mode`, we must ensure
16
+ # it is regenerated if either of those fields are changed. Otherwise,
17
+ # the new instance would point to the old logger (e.g. logging to the
18
+ # wrong file).
19
+ def with(**kwargs)
20
+ target_changed = kwargs.key?(:log_target) && kwargs[:log_target] != log_target
21
+ mode_changed = kwargs.key?(:mode) && kwargs[:mode] != mode
22
+
23
+ if target_changed || mode_changed
24
+ target = kwargs.fetch(:log_target, log_target)
25
+ new_mode = kwargs.fetch(:mode, mode)
26
+ kwargs[:logger] = Logger.new(target: target, mode: new_mode)
27
+ end
28
+ super
29
+ end
30
+
31
+ def mcp_mode?
32
+ mode == :mcp
33
+ end
34
+
35
+ def cli_mode?
36
+ mode == :cli
37
+ end
38
+
39
+ def library_mode?
40
+ mode == :library
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CovLoupe
4
+ # A pluggable boolean type converter for OptionParser.
5
+ # Accepts various string representations of true/false and converts them to boolean values.
6
+ #
7
+ # Usage with OptionParser:
8
+ # parser.accept(BooleanType) { |v| BooleanType.parse(v) }
9
+ # parser.on('--flag [BOOLEAN]', BooleanType) { |v| config.flag = v }
10
+ #
11
+ # Supported values (case-insensitive):
12
+ # true: yes, y, true, t, on, +, 1
13
+ # false: no, n, false, f, off, -, 0
14
+ # nil: treated as true (for bare flags like --flag)
15
+ #
16
+ # Examples:
17
+ # --flag → true
18
+ # --flag=yes → true
19
+ # --flag=no → false
20
+ # --flag yes → true
21
+ # --flag false → false
22
+ class BooleanType
23
+ # Values that map to true
24
+ TRUE_VALUES = %w[yes y true t on + 1].freeze
25
+
26
+ # Values that map to false
27
+ FALSE_VALUES = %w[no n false f off - 0].freeze
28
+
29
+ # All valid boolean string values
30
+ VALID_VALUES = TRUE_VALUES.zip(FALSE_VALUES).flatten.freeze # %w{yes no y n true false t f on off + - 1 0 }
31
+
32
+ # String representation for help messages ('yes/no/true/false/t/f/on/off/y/n/+/-/1/0')
33
+ BOOLEAN_VALUES_DISPLAY_STRING = VALID_VALUES.join('/').freeze
34
+
35
+ # Pattern object for OptionParser.
36
+ # Proc objects get treated as blocks, and Module instances are rejected outright,
37
+ # so we expose a singleton that only responds to #match like a regex.
38
+ IS_BOOLEAN_STRING_VALUE = Object.new
39
+ IS_BOOLEAN_STRING_VALUE.define_singleton_method(:match) do |value|
40
+ BooleanType.valid?(value) ? value : nil
41
+ end
42
+
43
+ class << self
44
+ # Parse a string value into a boolean.
45
+ #
46
+ # @param value [String, nil] The value to parse
47
+ # @return [Boolean] true or false
48
+ # @raise [ArgumentError] if the value is not a valid boolean string
49
+ def parse(value)
50
+ # nil means bare flag (e.g., --flag without a value) → true
51
+ return true if value.nil?
52
+
53
+ normalized = value.to_s.strip.downcase
54
+
55
+ return true if TRUE_VALUES.include?(normalized)
56
+ return false if FALSE_VALUES.include?(normalized)
57
+
58
+ raise ArgumentError, "invalid boolean value: #{value.inspect}. " \
59
+ "Valid values: #{VALID_VALUES.join(', ')}"
60
+ end
61
+
62
+ # Check if a value is a valid boolean string.
63
+ #
64
+ # @param value [String, nil] The value to check
65
+ # @return [Boolean] true if valid, false otherwise
66
+ def valid?(value)
67
+ return true if value.nil?
68
+
69
+ VALID_VALUES.include?(value.to_s.strip.downcase)
70
+ end
71
+
72
+ # Pattern matching for OptionParser (called via ===)
73
+ # This is called to determine if a token should be consumed as the option's argument.
74
+ # Returning nil signals OptionParser to NOT consume the token.
75
+ #
76
+ # @param value [String, nil] The value to match
77
+ # @return [Boolean, nil] The parsed boolean if valid, or nil to reject the token
78
+ def ===(value)
79
+ # nil means optional argument not provided - accept as match
80
+ return true if value.nil?
81
+
82
+ # Only consume the token if it's a valid boolean value
83
+ # This prevents consuming subcommand names or other arguments
84
+ return true if valid?(value)
85
+
86
+ # Invalid value - don't consume it, let OptionParser treat it as the next argument
87
+ nil
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'time'
5
+
6
+ module CovLoupe
7
+ class Logger
8
+ DEFAULT_LOG_FILESPEC = './cov_loupe.log'
9
+ FALLBACK_LOG_FILE = 'COV-LOUPE-LOG-ERROR.log'
10
+
11
+ attr_reader :target
12
+
13
+ def initialize(target:, mode: :library)
14
+ @mode = mode
15
+ @target = target
16
+ @init_error = nil
17
+ @stderr_warning_emitted = false
18
+
19
+ begin
20
+ @logger = build_logger(target)
21
+ rescue => e
22
+ @init_error = e
23
+ end
24
+ end
25
+
26
+ def info(msg)
27
+ log_with_level(:info, msg)
28
+ end
29
+
30
+ def warn(msg)
31
+ log_with_level(:warn, msg)
32
+ end
33
+
34
+ def error(msg)
35
+ log_with_level(:error, msg)
36
+ end
37
+
38
+ # Safe logging that never raises - use when logging should not interrupt execution.
39
+ def safe_log(msg)
40
+ info(msg)
41
+ rescue
42
+ # Silently ignore all logging failures
43
+ end
44
+
45
+ private def log_with_level(level, msg)
46
+ if @init_error
47
+ handle_logging_error(@init_error, msg)
48
+ else
49
+ @logger.send(level, msg)
50
+ end
51
+ rescue => e
52
+ handle_logging_error(e, msg)
53
+ end
54
+
55
+ private def build_logger(target)
56
+ io_or_path = case target
57
+ when 'stdout' then $stdout
58
+ when 'stderr' then $stderr
59
+ else
60
+ path = target || DEFAULT_LOG_FILESPEC
61
+ File.expand_path(path)
62
+ end
63
+
64
+ ::Logger.new(io_or_path).tap do |l|
65
+ l.formatter = ->(severity, datetime, _progname, msg) { "[#{datetime.iso8601}] #{severity}: #{msg}\n" }
66
+ end
67
+ end
68
+
69
+ private def handle_logging_error(error, original_msg)
70
+ write_fallback_file(error, original_msg)
71
+ warn_stderr_once if @mode == :cli
72
+ rescue
73
+ # Silently ignore all fallback failures
74
+ end
75
+
76
+ private def write_fallback_file(error, original_msg)
77
+ File.open(FALLBACK_LOG_FILE, 'a') do |f|
78
+ timestamp = Time.now.iso8601
79
+ f.puts "[#{timestamp}] MODE:#{@mode} ERROR:#{error.message} MSG:#{original_msg}"
80
+ end
81
+ rescue
82
+ # Best effort - ignore write failures
83
+ end
84
+
85
+ private def warn_stderr_once
86
+ return if @stderr_warning_emitted
87
+
88
+ @stderr_warning_emitted = true
89
+ $stderr.puts "Warning: Logging failed. See #{FALLBACK_LOG_FILE} for details."
90
+ end
91
+ end
92
+ end