cov-loupe 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +329 -0
  4. data/docs/dev/ARCHITECTURE.md +80 -0
  5. data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
  6. data/docs/dev/DEVELOPMENT.md +83 -0
  7. data/docs/dev/README.md +10 -0
  8. data/docs/dev/RELEASING.md +146 -0
  9. data/docs/dev/arch-decisions/001-x-arch-decision.md +95 -0
  10. data/docs/dev/arch-decisions/002-x-arch-decision.md +159 -0
  11. data/docs/dev/arch-decisions/003-x-arch-decision.md +165 -0
  12. data/docs/dev/arch-decisions/004-x-arch-decision.md +203 -0
  13. data/docs/dev/arch-decisions/005-x-arch-decision.md +189 -0
  14. data/docs/dev/arch-decisions/README.md +60 -0
  15. data/docs/dev/presentations/cov-loupe-presentation.md +255 -0
  16. data/docs/fixtures/demo_project/README.md +9 -0
  17. data/docs/user/ADVANCED_USAGE.md +777 -0
  18. data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
  19. data/docs/user/CLI_USAGE.md +750 -0
  20. data/docs/user/ERROR_HANDLING.md +93 -0
  21. data/docs/user/EXAMPLES.md +588 -0
  22. data/docs/user/INSTALLATION.md +130 -0
  23. data/docs/user/LIBRARY_API.md +693 -0
  24. data/docs/user/MCP_INTEGRATION.md +490 -0
  25. data/docs/user/README.md +14 -0
  26. data/docs/user/TROUBLESHOOTING.md +197 -0
  27. data/docs/user/V2-BREAKING-CHANGES.md +472 -0
  28. data/exe/cov-loupe +23 -0
  29. data/lib/cov_loupe/app_config.rb +56 -0
  30. data/lib/cov_loupe/app_context.rb +26 -0
  31. data/lib/cov_loupe/base_tool.rb +102 -0
  32. data/lib/cov_loupe/cli.rb +178 -0
  33. data/lib/cov_loupe/commands/base_command.rb +67 -0
  34. data/lib/cov_loupe/commands/command_factory.rb +45 -0
  35. data/lib/cov_loupe/commands/detailed_command.rb +38 -0
  36. data/lib/cov_loupe/commands/list_command.rb +13 -0
  37. data/lib/cov_loupe/commands/raw_command.rb +38 -0
  38. data/lib/cov_loupe/commands/summary_command.rb +41 -0
  39. data/lib/cov_loupe/commands/totals_command.rb +53 -0
  40. data/lib/cov_loupe/commands/uncovered_command.rb +45 -0
  41. data/lib/cov_loupe/commands/validate_command.rb +60 -0
  42. data/lib/cov_loupe/commands/version_command.rb +33 -0
  43. data/lib/cov_loupe/config_parser.rb +32 -0
  44. data/lib/cov_loupe/constants.rb +22 -0
  45. data/lib/cov_loupe/coverage_reporter.rb +31 -0
  46. data/lib/cov_loupe/error_handler.rb +165 -0
  47. data/lib/cov_loupe/error_handler_factory.rb +31 -0
  48. data/lib/cov_loupe/errors.rb +191 -0
  49. data/lib/cov_loupe/formatters/source_formatter.rb +152 -0
  50. data/lib/cov_loupe/formatters.rb +51 -0
  51. data/lib/cov_loupe/mcp_server.rb +42 -0
  52. data/lib/cov_loupe/mode_detector.rb +56 -0
  53. data/lib/cov_loupe/model.rb +339 -0
  54. data/lib/cov_loupe/option_normalizers.rb +113 -0
  55. data/lib/cov_loupe/option_parser_builder.rb +147 -0
  56. data/lib/cov_loupe/option_parsers/env_options_parser.rb +48 -0
  57. data/lib/cov_loupe/option_parsers/error_helper.rb +110 -0
  58. data/lib/cov_loupe/path_relativizer.rb +64 -0
  59. data/lib/cov_loupe/predicate_evaluator.rb +72 -0
  60. data/lib/cov_loupe/presenters/base_coverage_presenter.rb +42 -0
  61. data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +14 -0
  62. data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +14 -0
  63. data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +14 -0
  64. data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +14 -0
  65. data/lib/cov_loupe/presenters/project_coverage_presenter.rb +50 -0
  66. data/lib/cov_loupe/presenters/project_totals_presenter.rb +27 -0
  67. data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +122 -0
  68. data/lib/cov_loupe/resolvers/resolver_factory.rb +28 -0
  69. data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +76 -0
  70. data/lib/cov_loupe/resultset_loader.rb +131 -0
  71. data/lib/cov_loupe/staleness_checker.rb +247 -0
  72. data/lib/cov_loupe/table_formatter.rb +64 -0
  73. data/lib/cov_loupe/tools/all_files_coverage_tool.rb +51 -0
  74. data/lib/cov_loupe/tools/coverage_detailed_tool.rb +35 -0
  75. data/lib/cov_loupe/tools/coverage_raw_tool.rb +34 -0
  76. data/lib/cov_loupe/tools/coverage_summary_tool.rb +34 -0
  77. data/lib/cov_loupe/tools/coverage_table_tool.rb +50 -0
  78. data/lib/cov_loupe/tools/coverage_totals_tool.rb +44 -0
  79. data/lib/cov_loupe/tools/help_tool.rb +115 -0
  80. data/lib/cov_loupe/tools/uncovered_lines_tool.rb +34 -0
  81. data/lib/cov_loupe/tools/validate_tool.rb +72 -0
  82. data/lib/cov_loupe/tools/version_tool.rb +32 -0
  83. data/lib/cov_loupe/util.rb +88 -0
  84. data/lib/cov_loupe/version.rb +5 -0
  85. data/lib/cov_loupe.rb +140 -0
  86. data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
  87. data/spec/TIMESTAMPS.md +48 -0
  88. data/spec/all_files_coverage_tool_spec.rb +53 -0
  89. data/spec/app_config_spec.rb +142 -0
  90. data/spec/base_tool_spec.rb +62 -0
  91. data/spec/cli/show_default_report_spec.rb +33 -0
  92. data/spec/cli_enumerated_options_spec.rb +90 -0
  93. data/spec/cli_error_spec.rb +184 -0
  94. data/spec/cli_format_spec.rb +123 -0
  95. data/spec/cli_json_options_spec.rb +50 -0
  96. data/spec/cli_source_spec.rb +44 -0
  97. data/spec/cli_spec.rb +192 -0
  98. data/spec/cli_table_spec.rb +28 -0
  99. data/spec/cli_usage_spec.rb +42 -0
  100. data/spec/commands/base_command_spec.rb +107 -0
  101. data/spec/commands/command_factory_spec.rb +76 -0
  102. data/spec/commands/detailed_command_spec.rb +34 -0
  103. data/spec/commands/list_command_spec.rb +28 -0
  104. data/spec/commands/raw_command_spec.rb +69 -0
  105. data/spec/commands/summary_command_spec.rb +34 -0
  106. data/spec/commands/totals_command_spec.rb +34 -0
  107. data/spec/commands/uncovered_command_spec.rb +55 -0
  108. data/spec/commands/validate_command_spec.rb +213 -0
  109. data/spec/commands/version_command_spec.rb +38 -0
  110. data/spec/constants_spec.rb +61 -0
  111. data/spec/cov_loupe/formatters/source_formatter_spec.rb +267 -0
  112. data/spec/cov_loupe/formatters_spec.rb +76 -0
  113. data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +79 -0
  114. data/spec/cov_loupe_model_spec.rb +454 -0
  115. data/spec/cov_loupe_module_spec.rb +37 -0
  116. data/spec/cov_loupe_opts_spec.rb +185 -0
  117. data/spec/coverage_reporter_spec.rb +102 -0
  118. data/spec/coverage_table_tool_spec.rb +59 -0
  119. data/spec/coverage_totals_tool_spec.rb +37 -0
  120. data/spec/error_handler_spec.rb +197 -0
  121. data/spec/error_mode_spec.rb +139 -0
  122. data/spec/errors_edge_cases_spec.rb +312 -0
  123. data/spec/errors_stale_spec.rb +83 -0
  124. data/spec/file_based_mcp_tools_spec.rb +99 -0
  125. data/spec/fixtures/project1/lib/bar.rb +5 -0
  126. data/spec/fixtures/project1/lib/foo.rb +6 -0
  127. data/spec/help_tool_spec.rb +26 -0
  128. data/spec/integration_spec.rb +789 -0
  129. data/spec/logging_fallback_spec.rb +128 -0
  130. data/spec/mcp_logging_spec.rb +44 -0
  131. data/spec/mcp_server_integration_spec.rb +23 -0
  132. data/spec/mcp_server_spec.rb +106 -0
  133. data/spec/mode_detector_spec.rb +153 -0
  134. data/spec/model_error_handling_spec.rb +269 -0
  135. data/spec/model_staleness_spec.rb +79 -0
  136. data/spec/option_normalizers_spec.rb +203 -0
  137. data/spec/option_parsers/env_options_parser_spec.rb +221 -0
  138. data/spec/option_parsers/error_helper_spec.rb +222 -0
  139. data/spec/path_relativizer_spec.rb +98 -0
  140. data/spec/presenters/coverage_detailed_presenter_spec.rb +19 -0
  141. data/spec/presenters/coverage_raw_presenter_spec.rb +15 -0
  142. data/spec/presenters/coverage_summary_presenter_spec.rb +15 -0
  143. data/spec/presenters/coverage_uncovered_presenter_spec.rb +16 -0
  144. data/spec/presenters/project_coverage_presenter_spec.rb +87 -0
  145. data/spec/presenters/project_totals_presenter_spec.rb +144 -0
  146. data/spec/resolvers/coverage_line_resolver_spec.rb +282 -0
  147. data/spec/resolvers/resolver_factory_spec.rb +61 -0
  148. data/spec/resolvers/resultset_path_resolver_spec.rb +60 -0
  149. data/spec/resultset_loader_spec.rb +167 -0
  150. data/spec/shared_examples/README.md +115 -0
  151. data/spec/shared_examples/coverage_presenter_examples.rb +66 -0
  152. data/spec/shared_examples/file_based_mcp_tools.rb +179 -0
  153. data/spec/shared_examples/formatted_command_examples.rb +64 -0
  154. data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
  155. data/spec/spec_helper.rb +127 -0
  156. data/spec/staleness_checker_spec.rb +374 -0
  157. data/spec/staleness_more_spec.rb +42 -0
  158. data/spec/support/cli_helpers.rb +22 -0
  159. data/spec/support/control_flow_helpers.rb +20 -0
  160. data/spec/support/fake_mcp.rb +40 -0
  161. data/spec/support/io_helpers.rb +29 -0
  162. data/spec/support/mcp_helpers.rb +35 -0
  163. data/spec/support/mcp_runner.rb +66 -0
  164. data/spec/support/mocking_helpers.rb +30 -0
  165. data/spec/table_format_spec.rb +70 -0
  166. data/spec/tools/validate_tool_spec.rb +132 -0
  167. data/spec/tools_error_handling_spec.rb +130 -0
  168. data/spec/util_spec.rb +154 -0
  169. data/spec/version_spec.rb +123 -0
  170. data/spec/version_tool_spec.rb +141 -0
  171. metadata +290 -0
@@ -0,0 +1,10 @@
1
+ # Developer Documentation
2
+
3
+ Resources for contributors working on SimpleCov MCP internals and release
4
+ engineering.
5
+
6
+ - [Architecture Overview](ARCHITECTURE.md) – subsystem layout and data flow
7
+ - [Development Guide](DEVELOPMENT.md) – setup, testing, release workflow
8
+ - [Branch Coverage Support](BRANCH_ONLY_COVERAGE.md) – implementation notes and limitations
9
+ - [Architecture Decision Records](arch-decisions/README.md) – design history
10
+ - [Presentations](presentations/cov-loupe-presentation.md) – slide deck for talks/demos
@@ -0,0 +1,146 @@
1
+ # Release Process
2
+
3
+ This document provides a checklist for releasing new versions of cov-loupe.
4
+
5
+ ## Pre-Release Checklist
6
+
7
+ ### 1. Documentation Review
8
+
9
+ - [ ] **RELEASE_NOTES.md**: Update version header
10
+ - Update the version section header to final version (e.g., `## v#{version}`)
11
+ - For major releases: Ensure all breaking changes are documented with migration examples
12
+ - Verify new features and bug fixes are listed
13
+
14
+ - [ ] **README.md**: Verify examples and feature list are current
15
+
16
+ - [ ] **Documentation**: Ensure all docs in `docs/` are up to date
17
+
18
+ ### 2. Code Quality
19
+
20
+ - [ ] **Tests**: All tests passing (`bundle exec rspec`)
21
+ - Verify via git hooks or run manually
22
+ - Check coverage is still excellent (>95%)
23
+
24
+ - [ ] **Linting**: No Rubocop violations (`bundle exec rubocop`)
25
+ - Verify via git hooks or run manually
26
+
27
+ - [ ] **Version**: Update `lib/cov_loupe/version.rb` to release version
28
+ - Remove `.pre.X` suffix for stable releases
29
+
30
+ ### 3. Cleanup
31
+
32
+ - [ ] **Untracked files**: Review `git status` for files that should be:
33
+ - Added to `.gitignore` (temp files, local experiments, AI reports)
34
+ - Committed (valuable documentation or examples)
35
+ - Deleted (obsolete files)
36
+
37
+ - [ ] **Temporary files**: Remove or ignore:
38
+ - `*.txt` files (r.txt, rubocop.txt, todo.txt, etc.)
39
+ - Experimental config files (`.rubocop.yml.new`, etc.)
40
+ - Local notes (CODING_AGENT_NOTES.md, architecture_insights.md, etc.)
41
+ - Work-in-progress directories (screencast/, untracked-ai-reports/, etc.)
42
+
43
+ ### 4. Build Verification
44
+
45
+ - [ ] **Build gem**: Verify gem builds without errors
46
+ ```bash
47
+ gem build cov-loupe.gemspec
48
+ ```
49
+
50
+ - [ ] **Test installation**: Install and test locally
51
+ ```bash
52
+ gem install cov-loupe-*.gem
53
+ cov-loupe --version
54
+ cov-loupe --help
55
+ # Test on actual project
56
+ cd /path/to/test/project
57
+ cov-loupe list
58
+ ```
59
+
60
+ ### 5. Git Release
61
+
62
+ - [ ] **Commit changes**: Commit version bump and RELEASE_NOTES.md updates
63
+ ```bash
64
+ git add lib/cov_loupe/version.rb RELEASE_NOTES.md
65
+ git commit -m "Release version #{version}"
66
+ ```
67
+
68
+ - [ ] **Create tag**: Tag the release
69
+ ```bash
70
+ git tag -a v#{version} -m "Version #{version}"
71
+ ```
72
+
73
+ - [ ] **Push**: Push commits and tags
74
+ ```bash
75
+ git push origin main --follow-tags
76
+ ```
77
+
78
+ ### 6. Publish Gem
79
+
80
+ - [ ] **Build final gem**: Build from tagged version
81
+ ```bash
82
+ gem build cov-loupe.gemspec
83
+ ```
84
+
85
+ - [ ] **Push to RubyGems**: Publish the gem
86
+ ```bash
87
+ gem push cov-loupe-#{version}.gem
88
+ ```
89
+
90
+ - [ ] **Verify publication**: Check gem appears on RubyGems.org
91
+ - Visit https://rubygems.org/gems/cov-loupe
92
+ - Verify new version is listed
93
+ - Check that documentation links work
94
+
95
+ ### 7. GitHub Release
96
+
97
+ - [ ] **Create GitHub release**: Go to https://github.com/keithrbennett/cov-loupe/releases/new
98
+ - Select the tag you just pushed
99
+ - Title: `Version #{version}`
100
+ - Description: Copy relevant sections from RELEASE_NOTES.md
101
+ - Attach the `.gem` file (optional)
102
+
103
+ ### 8. Post-Release
104
+
105
+ - [ ] **Announcement**: Consider announcing on:
106
+ - Ruby Weekly
107
+ - Reddit (r/ruby)
108
+ - Slack/Discord communities
109
+ - Social media
110
+
111
+ - [ ] **Update dependencies**: For projects using this gem
112
+ - Update your own projects to use new version
113
+ - Test integration
114
+
115
+ - [ ] **Prepare for next release**:
116
+ - Optionally create a new section in RELEASE_NOTES.md for next version
117
+ - Consider bumping to next pre-release version if starting new development cycle
118
+
119
+ ## Version Numbering
120
+
121
+ Follow [Semantic Versioning](https://semver.org/):
122
+
123
+ - **Major (X.0.0)**: Breaking changes
124
+ - **Minor (0.X.0)**: New features, backward compatible
125
+ - **Patch (0.0.X)**: Bug fixes, backward compatible
126
+ - **Pre-release (X.Y.Z.pre.N)**: Development versions
127
+
128
+ ## Rollback Procedure
129
+
130
+ If a critical issue is discovered after release:
131
+
132
+ 1. **Yank the gem** (removes from RubyGems but preserves install history):
133
+ ```bash
134
+ gem yank cov-loupe -v #{version}
135
+ ```
136
+
137
+ 2. **Fix the issue** in a new patch version
138
+
139
+ 3. **Release the fixed version** following this checklist
140
+
141
+ 4. **Communicate**: Update GitHub release notes and announce the issue + fix
142
+
143
+ ## Notes
144
+
145
+ - GitHub Actions runs tests and Rubocop on every commit (via hooks)
146
+ - Pre-commit hooks ensure code quality before commits
@@ -0,0 +1,95 @@
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)
@@ -0,0 +1,159 @@
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)
@@ -0,0 +1,165 @@
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`