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,95 +0,0 @@
1
- # ADR 001: Dual-Mode Operation (CLI and MCP Server)
2
-
3
- [Back to main README](../../README.md)
4
-
5
- ## Status
6
-
7
- Accepted
8
-
9
- ## Context
10
-
11
- SimpleCov MCP needed to serve two distinct use cases:
12
-
13
- 1. **Human users** wanting a command-line tool to inspect coverage reports in their terminal
14
- 2. **AI agents and MCP clients** needing programmatic access to coverage data via the Model Context Protocol (MCP) over JSON-RPC
15
-
16
- We considered three approaches:
17
-
18
- 1. **Separate binaries/gems**: Create `simplecov-cli` and `cov-loupe` as separate projects
19
- 2. **Single binary with explicit mode flags**: Require users to pass `--mcp` or `--cli` to select mode
20
- 3. **Automatic mode detection**: Single binary that automatically detects the operating mode based on input
21
-
22
- ### Key Constraints
23
-
24
- - MCP servers communicate via JSON-RPC over stdin/stdout, so any human-readable output would corrupt the protocol
25
- - CLI users expect immediate, readable output without ceremony
26
- - The gem should be simple to install and use for both audiences
27
- - Mode detection must be reliable and unambiguous
28
-
29
- ## Decision
30
-
31
- We implemented **automatic mode detection** via a single entry point (`CovLoupe.run`) that routes to either CLI or MCP server mode based on the execution context.
32
-
33
- ### Mode Detection Algorithm
34
-
35
- The `ModeDetector` class (lib/cov_loupe/mode_detector.rb:6) implements a priority-based detection strategy:
36
-
37
- 1. **Explicit CLI flags** (`--force-cli`, `-h`, `--help`, `--version`) → CLI mode
38
- 2. **Presence of subcommands** (non-option arguments like `summary`, `list`) → CLI mode
39
- 3. **TTY detection** fallback: `stdin.tty?` returns true → CLI mode, false → MCP server mode
40
-
41
- The implementation is in `lib/cov_loupe.rb:34-52`:
42
-
43
- ```ruby
44
- def run(argv)
45
- env_opts = extract_env_opts
46
- full_argv = env_opts + argv
47
-
48
- if ModeDetector.cli_mode?(full_argv)
49
- CoverageCLI.new.run(argv)
50
- else
51
- CovLoupe.default_log_file = parse_log_file(full_argv)
52
- MCPServer.new.run
53
- end
54
- end
55
- ```
56
-
57
- ### Why This Works
58
-
59
- - **MCP clients** pipe JSON-RPC to stdin (not a TTY) and don't pass subcommands → routes to MCP server
60
- - **CLI users** run from an interactive terminal (TTY) or pass explicit subcommands → routes to CLI
61
- - **Edge cases** are covered by explicit flags (`--force-cli` for testing MCP mode from a TTY)
62
-
63
- ## Consequences
64
-
65
- ### Positive
66
-
67
- 1. **User convenience**: Single gem to install (`gem install cov-loupe`), single executable (`cov-loupe`)
68
- 2. **No ceremony**: Users don't need to remember mode flags or understand the MCP/CLI distinction
69
- 3. **Testable**: The `ModeDetector` class is a pure function that can be tested in isolation
70
- 4. **Clear separation**: CLI and MCP server implementations remain completely separate after routing
71
-
72
- ### Negative
73
-
74
- 1. **Complexity**: Requires maintaining the mode detection logic and keeping it accurate
75
- 2. **Potential ambiguity**: In unusual environments (non-TTY CLI execution without subcommands), users must understand `--force-cli`
76
- 3. **Shared dependencies**: Some components (error handling, coverage model) must work correctly in both modes
77
-
78
- ### Trade-offs
79
-
80
- - **Versus separate gems**: More initial complexity, but better DX (single installation, no confusion about which gem to use)
81
- - **Versus explicit mode flags**: Slightly more "magical", but eliminates user error and reduces boilerplate
82
-
83
- ### Future Constraints
84
-
85
- - Mode detection logic must remain stable and backward-compatible
86
- - Any new CLI subcommands must be registered in `ModeDetector::SUBCOMMANDS`
87
- - Shared components (like `CoverageModel`) must never output to stdout/stderr in ways that differ by mode
88
-
89
- ## References
90
-
91
- - Implementation: `lib/cov_loupe.rb:34-52`
92
- - Mode detection: `lib/cov_loupe/mode_detector.rb:6-63`
93
- - CLI implementation: `lib/cov_loupe/cli.rb`
94
- - MCP server implementation: `lib/cov_loupe/mcp_server.rb`
95
- - Related ADR: [002: Context-Aware Error Handling](002-x-arch-decision.md)
@@ -1,159 +0,0 @@
1
- # ADR 002: Context-Aware Error Handling Strategy
2
-
3
- [Back to main README](../../README.md)
4
-
5
- ## Status
6
-
7
- Accepted
8
-
9
- ## Context
10
-
11
- SimpleCov MCP operates in three distinct contexts, each with different error handling requirements:
12
-
13
- 1. **CLI mode**: Human users expect friendly error messages, exit codes, and optional debug traces
14
- 2. **MCP server mode**: AI agents/clients need structured error responses that don't crash the server
15
- 3. **Library mode**: Embedding applications need exceptions they can catch and handle programmatically
16
-
17
- Initially, we considered uniform error handling across all modes, but this created poor user experiences:
18
-
19
- - CLI users saw raw exceptions with stack traces (scary and unhelpful)
20
- - MCP servers crashed on errors instead of returning error responses
21
- - Library users got friendly messages logged to stderr (unwanted side effects in their applications)
22
-
23
- ### Key Requirements
24
-
25
- - **CLI**: User-friendly messages, meaningful exit codes, optional stack traces for debugging
26
- - **MCP Server**: Logged errors (to file, not stdout), structured JSON-RPC error responses, no server crashes
27
- - **Library**: Raise custom exceptions with no logging, allowing consumers to handle errors as needed
28
- - **Consistency**: Same underlying error types, but different presentation strategies
29
-
30
- ## Decision
31
-
32
- We implemented a **context-aware error handling strategy** using three components:
33
-
34
- ### 1. Custom Exception Hierarchy
35
-
36
- All errors inherit from `CovLoupe::Error` (lib/cov_loupe/errors.rb) with a `user_friendly_message` method:
37
-
38
- ```ruby
39
- class Error < StandardError
40
- def user_friendly_message
41
- message # Can be overridden in subclasses
42
- end
43
- end
44
-
45
- class FileNotFoundError < FileError; end
46
- class CoverageDataError < Error; end
47
- class ResultsetNotFoundError < CoverageDataError; end
48
- # ... etc
49
- ```
50
-
51
- This provides a unified interface for presenting errors to users while preserving exception types for programmatic handling.
52
-
53
- ### 2. ErrorHandler Class
54
-
55
- The `ErrorHandler` class (lib/cov_loupe/error_handler.rb:7) provides configurable error handling behavior:
56
-
57
- ```ruby
58
- class ErrorHandler
59
- attr_accessor :error_mode, :logger
60
-
61
- VALID_ERROR_MODES = [:off, :log, :debug].freeze
62
-
63
- def initialize(error_mode: :log, logger: nil)
64
- @error_mode = error_mode
65
- @logger = logger
66
- end
67
-
68
- def handle_error(error, context: nil, reraise: true)
69
- log_error(error, context)
70
- if reraise
71
- raise error.is_a?(CovLoupe::Error) ? error : convert_standard_error(error)
72
- end
73
- end
74
- end
75
- ```
76
-
77
- The `convert_standard_error` method (lib/cov_loupe/error_handler.rb:37) transforms Ruby's standard errors into user-friendly custom exceptions:
78
-
79
- - `Errno::ENOENT` → `FileNotFoundError`
80
- - `JSON::ParserError` → `CoverageDataError`
81
- - `Errno::EACCES` → `FilePermissionError`
82
-
83
- ### 3. ErrorHandlerFactory
84
-
85
- The `ErrorHandlerFactory` (lib/cov_loupe/error_handler_factory.rb:4) creates mode-specific handlers:
86
-
87
- ```ruby
88
- module ErrorHandlerFactory
89
- def self.for_cli(error_mode: :log)
90
- ErrorHandler.new(error_mode: error_mode)
91
- end
92
-
93
- def self.for_library(error_mode: :off)
94
- ErrorHandler.new(error_mode: :off) # No logging
95
- end
96
-
97
- def self.for_mcp_server(error_mode: :log)
98
- ErrorHandler.new(error_mode: :log) # Logs to file
99
- end
100
- end
101
- ```
102
-
103
- ### Error Flow by Mode
104
-
105
- **CLI Mode** (lib/cov_loupe/cli.rb):
106
- 1. Catches all exceptions in the main run loop
107
- 2. Uses `for_cli` handler to log errors if debug mode is enabled
108
- 3. Displays `user_friendly_message` to the user
109
- 4. Exits with appropriate code (1 for errors, 2 for usage errors)
110
-
111
- **MCP Server Mode** (lib/cov_loupe/base_tool.rb:46):
112
- 1. Each tool wraps execution in a rescue block
113
- 2. Uses `for_mcp_server` handler to log errors to `~/cov_loupe.log`
114
- 3. Returns structured JSON-RPC error response
115
- 4. Server continues running (no crashes)
116
-
117
- **Library Mode** (lib/cov_loupe.rb:75):
118
- 1. Uses `for_library` handler with `error_mode: :off` (no logging)
119
- 2. Raises custom exceptions directly
120
- 3. Consumers catch and handle `CovLoupe::Error` subclasses
121
-
122
- ## Consequences
123
-
124
- ### Positive
125
-
126
- 1. **Excellent UX**: Each context gets appropriate error handling behavior
127
- 2. **Robustness**: MCP server never crashes on tool errors
128
- 3. **Debuggability**: CLI users can enable stack traces with error modes, MCP errors are logged
129
- 4. **Clean library API**: No unwanted side effects (logging, stderr output) when used as a library
130
- 5. **Type safety**: Custom exceptions allow programmatic error handling by type
131
-
132
- ### Negative
133
-
134
- 1. **Complexity**: Three error handling paths to maintain and test
135
- 2. **Coordination required**: All error types must implement `user_friendly_message` consistently
136
- 3. **Error conversion overhead**: Standard errors must be converted to custom exceptions
137
-
138
- ### Trade-offs
139
-
140
- - **Versus uniform error handling**: More code complexity, but dramatically better UX in each context
141
- - **Versus separate error classes per mode**: Single error hierarchy is simpler, factory pattern adds mode-specific behavior
142
-
143
- ### Implementation Notes
144
-
145
- The `ErrorHandler.convert_standard_error` method (lib/cov_loupe/error_handler.rb:37) uses pattern matching on exception types and error messages to provide helpful, context-aware error messages. This includes:
146
-
147
- - Extracting filenames from system error messages
148
- - Detecting SimpleCov-specific error patterns
149
- - Providing actionable suggestions ("please run your tests first")
150
-
151
- ## References
152
-
153
- - Custom exceptions: `lib/cov_loupe/errors.rb`
154
- - ErrorHandler implementation: `lib/cov_loupe/error_handler.rb:7-124`
155
- - ErrorHandlerFactory: `lib/cov_loupe/error_handler_factory.rb:4-29`
156
- - CLI error handling: `lib/cov_loupe/cli.rb` (rescue block in run method)
157
- - MCP tool error handling: `lib/cov_loupe/base_tool.rb:46-54`
158
- - Library mode: `lib/cov_loupe.rb:75-86`
159
- - Related ADR: [001: Dual-Mode Operation](001-x-arch-decision.md)
@@ -1,165 +0,0 @@
1
- # ADR 003: Coverage Staleness Detection
2
-
3
- [Back to main README](../../README.md)
4
-
5
- ## Status
6
-
7
- Accepted
8
-
9
- ## Context
10
-
11
- Coverage data can become outdated when source files are modified after tests run. This creates misleading results:
12
-
13
- - Coverage percentages appear lower/higher than reality
14
- - Line numbers in coverage reports don't match the current source
15
- - AI agents and users may make decisions based on stale data
16
-
17
- We needed a staleness detection system that could:
18
-
19
- 1. Detect when source files have been modified since coverage was collected
20
- 2. Detect when source files have different line counts than coverage data
21
- 3. Handle edge cases (deleted files, files without trailing newlines)
22
- 4. Support both file-level and project-level checks
23
- 5. Allow users to control whether staleness is reported or causes errors
24
-
25
- ### Alternative Approaches Considered
26
-
27
- 1. **No staleness checking**: Simple, but leads to confusing/incorrect reports
28
- 2. **Single timestamp check**: Fast, but misses line count mismatches (files edited and reverted)
29
- 3. **Content hashing**: Accurate, but expensive for large projects
30
- 4. **Multi-type detection with modes**: More complex, but provides accurate detection with user control
31
-
32
- ## Decision
33
-
34
- We implemented a **three-type staleness detection system** with configurable error modes.
35
-
36
- ### Three Staleness Types
37
-
38
- The `StalenessChecker` class (lib/cov_loupe/staleness_checker.rb:8) detects three distinct types of staleness:
39
-
40
- 1. **Type 'M' (Missing)**: The source file exists in coverage but is now deleted/missing
41
- - Returned by `stale_for_file?` when `File.file?(file_abs)` returns false
42
- - Example: File was deleted after tests ran
43
-
44
- 2. **Type 'T' (Timestamp)**: The source file's mtime is newer than the coverage timestamp
45
- - Detected by comparing `File.mtime(file_abs)` with coverage timestamp
46
- - Example: File was edited after tests ran
47
-
48
- 3. **Type 'L' (Length)**: The source file line count doesn't match the coverage lines array length
49
- - Detected by comparing `File.foreach(path).count` with `coverage_lines.length`
50
- - Handles edge case: Files without trailing newlines (adjusts count by 1)
51
- - Example: Lines were added/removed without changing mtime (rare but possible with version control)
52
-
53
- ### Implementation Details
54
-
55
- The core algorithm is in `compute_file_staleness_details` (lib/cov_loupe/staleness_checker.rb:137):
56
-
57
- ```ruby
58
- def compute_file_staleness_details(file_abs, coverage_lines)
59
- coverage_ts = coverage_timestamp
60
- exists = File.file?(file_abs)
61
- file_mtime = exists ? File.mtime(file_abs) : nil
62
-
63
- cov_len = coverage_lines.respond_to?(:length) ? coverage_lines.length : 0
64
- src_len = exists ? safe_count_lines(file_abs) : 0
65
-
66
- newer = !!(file_mtime && file_mtime.to_i > coverage_ts.to_i)
67
-
68
- # Adjust for missing trailing newline edge case
69
- adjusted_src_len = src_len
70
- if exists && cov_len.positive? && src_len == cov_len + 1 && missing_trailing_newline?(file_abs)
71
- adjusted_src_len -= 1
72
- end
73
-
74
- len_mismatch = (cov_len.positive? && adjusted_src_len != cov_len)
75
- newer &&= !len_mismatch # Prioritize length mismatch over timestamp
76
-
77
- {
78
- exists: exists,
79
- file_mtime: file_mtime,
80
- coverage_timestamp: coverage_ts,
81
- cov_len: cov_len,
82
- src_len: src_len,
83
- newer: newer,
84
- len_mismatch: len_mismatch
85
- }
86
- end
87
- ```
88
-
89
- ### Staleness Modes
90
-
91
- The checker supports two modes (lib/cov_loupe/staleness_checker.rb:9):
92
-
93
- - **`:off`** (default): Staleness is detected but only reported in responses, never raises errors
94
- - **`:error`**: Staleness raises `CoverageDataStaleError` or `CoverageDataProjectStaleError`
95
-
96
- This allows:
97
- - Interactive tools to show warnings without crashing
98
- - CI systems to fail builds on stale coverage
99
- - AI agents to decide how to handle staleness based on their goals
100
-
101
- ### File-Level vs Project-Level Checks
102
-
103
- **File-level** (`check_file!` and `stale_for_file?`, lib/cov_loupe/staleness_checker.rb:25,49):
104
- - Checks a single file's staleness
105
- - Returns `false` or staleness type character ('M', 'T', 'L')
106
- - Used by single-file tools (summary, detailed, uncovered)
107
-
108
- **Project-level** (`check_project!`, lib/cov_loupe/staleness_checker.rb:59):
109
- - Checks all covered files plus optionally tracked files
110
- - Detects:
111
- - Files newer than coverage timestamp
112
- - Files deleted since coverage was collected
113
- - Tracked files missing from coverage (newly added files)
114
- - Raises `CoverageDataProjectStaleError` with lists of problematic files
115
- - Used by `all_files_coverage_tool` and `coverage_table_tool`
116
-
117
- ### Tracked Globs Feature
118
-
119
- The project-level check supports `tracked_globs` parameter to detect newly added files:
120
-
121
- ```ruby
122
- # Detects if lib/**/*.rb files exist that have no coverage data
123
- checker.check_project!(coverage_map) # with tracked_globs: ['lib/**/*.rb']
124
- ```
125
-
126
- This helps teams ensure new files are included in test runs.
127
-
128
- ## Consequences
129
-
130
- ### Positive
131
-
132
- 1. **Accurate detection**: Three types catch different staleness scenarios comprehensively
133
- 2. **Edge case handling**: Missing trailing newlines handled correctly
134
- 3. **User control**: Modes allow errors or warnings based on use case
135
- 4. **Detailed information**: Staleness errors include specific file lists and timestamps
136
- 5. **Project awareness**: Can detect newly added files that lack coverage
137
-
138
- ### Negative
139
-
140
- 1. **Complexity**: Three staleness types are harder to understand than a single timestamp check
141
- 2. **Performance**: Line counting and mtime checks for every file add overhead
142
- 3. **Maintenance burden**: Edge case logic (trailing newlines) requires careful testing
143
- 4. **Ambiguity**: When multiple staleness types apply, prioritization logic (length > timestamp) may surprise users
144
-
145
- ### Trade-offs
146
-
147
- - **Versus timestamp-only**: More accurate but slower and more complex
148
- - **Versus content hashing**: Fast enough for most projects, but can't detect "edit then revert" scenarios
149
- - **Versus no checking**: Essential for reliable coverage reporting, worth the complexity
150
-
151
- ### Edge Cases Handled
152
-
153
- 1. **Missing trailing newline**: Files without `\n` at EOF have `line_count == coverage_length + 1`, checker adjusts for this
154
- 2. **Deleted files**: Appear as 'M' (missing) type staleness
155
- 3. **Empty files**: `cov_len.positive?` guard prevents false positives
156
- 4. **No coverage timestamp**: Defaults to 0, effectively disabling timestamp checks
157
-
158
- ## References
159
-
160
- - Implementation: `lib/cov_loupe/staleness_checker.rb:8-168`
161
- - File-level checking: `lib/cov_loupe/staleness_checker.rb:25-55`
162
- - Project-level checking: `lib/cov_loupe/staleness_checker.rb:59-95`
163
- - Staleness detail computation: `lib/cov_loupe/staleness_checker.rb:137-166`
164
- - Error types: `lib/cov_loupe/errors.rb` (CoverageDataStaleError, CoverageDataProjectStaleError)
165
- - Usage in tools: `lib/cov_loupe/tools/all_files_coverage_tool.rb`, `lib/cov_loupe/model.rb`
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CovLoupe
4
- # Encapsulates per-request configuration such as error handling and logging.
5
- class AppContext
6
- attr_reader :error_handler, :log_target, :mode
7
-
8
- def initialize(error_handler:, log_target: nil, mode: :library)
9
- @error_handler = error_handler
10
- @log_target = log_target
11
- @mode = mode
12
- end
13
-
14
- def with_error_handler(handler)
15
- self.class.new(error_handler: handler, log_target: log_target, mode: mode)
16
- end
17
-
18
- def with_log_target(target)
19
- self.class.new(error_handler: error_handler, log_target: target, mode: mode)
20
- end
21
-
22
- def mcp_mode? = mode == :mcp
23
- def cli_mode? = mode == :cli
24
- def library_mode? = mode == :library
25
- end
26
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CovLoupe
4
- # Shared constants used across multiple components to avoid duplication.
5
- # This ensures consistency between CLI option parsing and mode detection.
6
- module Constants
7
- # CLI options that expect an argument value following them.
8
- # Used by both CoverageCLI and ModeDetector to correctly parse command-line arguments.
9
- OPTIONS_EXPECTING_ARGUMENT = %w[
10
- -r --resultset
11
- -R --root
12
- -f --format
13
- -o --sort-order
14
- -s --source
15
- -c --context-lines
16
- -S --staleness
17
- -g --tracked-globs
18
- -l --log-file
19
- --error-mode
20
- ].freeze
21
- end
22
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CovLoupe
4
- # Reports files with coverage below a specified threshold.
5
- # Useful for displaying low coverage files after test runs.
6
- #
7
- # @example Basic usage in spec_helper.rb
8
- # SimpleCov.at_exit do
9
- # SimpleCov.result.format!
10
- # report = CovLoupe::CoverageReporter.report(threshold: 80, count: 5)
11
- # puts report if report
12
- # end
13
- #
14
- module CoverageReporter
15
- module_function def report(threshold: 80, count: 5, model: nil)
16
- model ||= CoverageModel.new
17
- file_list = model.all_files(sort_order: :ascending)
18
- .select { |f| f['percentage'] < threshold }
19
- .first(count)
20
- file_list = model.relativize(file_list)
21
-
22
- return nil if file_list.empty?
23
-
24
- lines = ["\nLowest coverage files (< #{threshold}%):"]
25
- file_list.each do |f|
26
- lines << format(' %5.1f%% %s', f['percentage'], f['file'])
27
- end
28
- lines.join("\n")
29
- end
30
- end
31
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module CovLoupe
6
- module Formatters
7
- # Maps format symbols to their formatter lambdas
8
- # Following the rexe pattern for simple, extensible formatting
9
- FORMATTERS = {
10
- table: ->(obj) { obj }, # Pass through - table formatting handled elsewhere
11
- json: lambda(&:to_json),
12
- pretty_json: ->(obj) { JSON.pretty_generate(obj) },
13
- yaml: ->(obj) {
14
- require 'yaml'
15
- obj.to_yaml
16
- },
17
- awesome_print: ->(obj) {
18
- require 'awesome_print'
19
- obj.ai
20
- }
21
- }.freeze
22
-
23
- # Maps format symbols to their required libraries
24
- # Only loaded when the format is actually used
25
- FORMAT_REQUIRES = {
26
- yaml: 'yaml',
27
- awesome_print: 'awesome_print'
28
- }.freeze
29
-
30
- # Returns the formatter lambda for the given format
31
- def self.formatter_for(format)
32
- FORMATTERS[format] or raise ArgumentError, "Unknown format: #{format}"
33
- end
34
-
35
- # Ensures required libraries are loaded for the given format
36
- def self.ensure_requirements_for(format)
37
- requirement = FORMAT_REQUIRES[format]
38
- require requirement if requirement
39
- end
40
-
41
- # Formats an object using the specified format
42
- def self.format(obj, format)
43
- ensure_requirements_for(format)
44
- formatter_for(format).call(obj)
45
- rescue LoadError => e
46
- gem_name = e.message[/-- (\S+)/, 1] || 'required gem'
47
- raise LoadError, "The #{format} format requires the '#{gem_name}' gem. " \
48
- "Install it with: gem install #{gem_name}"
49
- end
50
- end
51
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'constants'
4
-
5
- module CovLoupe
6
- # Centralizes the logic for detecting whether to run in CLI or MCP server mode.
7
- # This makes the mode detection strategy explicit and testable.
8
- class ModeDetector
9
- SUBCOMMANDS = %w[list summary raw uncovered detailed totals validate version].freeze
10
-
11
- # Reference shared constant to avoid duplication with CoverageCLI
12
- OPTIONS_EXPECTING_ARGUMENT = Constants::OPTIONS_EXPECTING_ARGUMENT
13
-
14
- def self.cli_mode?(argv, stdin: $stdin)
15
- # 1. Explicit flags that force CLI mode always win
16
- cli_options = %w[--force-cli -h --help --version -v]
17
- return true if argv.intersect?(cli_options)
18
-
19
-
20
- # 2. Find the first non-option argument
21
- first_non_option = find_first_non_option(argv)
22
-
23
- # 3. If a non-option argument exists, it must be a CLI command (or an error)
24
- return true if first_non_option
25
-
26
- # 4. Fallback: If no non-option args, use TTY status to decide
27
- stdin.tty?
28
- end
29
-
30
- def self.mcp_server_mode?(argv, stdin: $stdin)
31
- !cli_mode?(argv, stdin: stdin)
32
- end
33
-
34
- # Scans argv and returns the first token that is not an option or a value for an option.
35
- def self.find_first_non_option(argv)
36
- pending_option = false
37
- argv.each do |token|
38
- if pending_option
39
- pending_option = false
40
- next
41
- end
42
-
43
- if token.start_with?('-')
44
- # Check if the option is one that takes a value and isn't using '=' syntax.
45
- pending_option = OPTIONS_EXPECTING_ARGUMENT.include?(token) && !token.include?('=')
46
- next
47
- end
48
-
49
- # Found the first token that is not an option
50
- return token
51
- end
52
- nil
53
- end
54
- private_class_method :find_first_non_option
55
- end
56
- end