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.
- checksums.yaml +4 -4
- data/AGENTS.md +230 -0
- data/CLAUDE.md +5 -0
- data/CODE_OF_CONDUCT.md +62 -0
- data/CONTRIBUTING.md +102 -0
- data/GEMINI.md +5 -0
- data/README.md +154 -51
- data/RELEASE_NOTES.md +452 -0
- data/dev/images/cov-loupe-icon-lores.png +0 -0
- data/dev/images/cov-loupe-icon-square.png +0 -0
- data/dev/images/cov-loupe-icon.png +0 -0
- data/dev/images/cov-loupe-logo.png +0 -0
- data/dev/prompts/README.md +74 -0
- data/dev/prompts/archive/architectural-review-and-actions-prompt.md +53 -0
- data/dev/prompts/archive/investigate-and-report-issues-prompt.md +33 -0
- data/dev/prompts/archive/produce-action-items-prompt.md +25 -0
- data/dev/prompts/guidelines/ai-code-evaluator-guidelines.md +337 -0
- data/dev/prompts/improve/refactor-test-suite.md +18 -0
- data/dev/prompts/improve/simplify-code-logic.md +133 -0
- data/dev/prompts/improve/update-documentation.md +21 -0
- data/dev/prompts/review/comprehensive-codebase-review.md +176 -0
- data/dev/prompts/review/identify-action-items.md +143 -0
- data/dev/prompts/review/verify-code-changes.md +54 -0
- data/dev/prompts/validate/create-screencast-outline.md +234 -0
- data/dev/prompts/validate/test-documentation-examples.md +180 -0
- data/docs/QUICKSTART.md +63 -0
- data/docs/assets/images/cov-loupe-logo-lores.png +0 -0
- data/docs/assets/images/cov-loupe-logo.png +0 -0
- data/docs/assets/images/favicon.png +0 -0
- data/docs/assets/stylesheets/branding.css +16 -0
- data/docs/assets/stylesheets/extra.css +15 -0
- data/docs/code_of_conduct.md +1 -0
- data/docs/contributing.md +1 -0
- data/docs/dev/ARCHITECTURE.md +56 -11
- data/docs/dev/DEVELOPMENT.md +116 -12
- data/docs/dev/FUTURE_ENHANCEMENTS.md +14 -0
- data/docs/dev/README.md +3 -2
- data/docs/dev/RELEASING.md +2 -0
- data/docs/dev/arch-decisions/README.md +10 -7
- data/docs/dev/arch-decisions/application-architecture.md +259 -0
- data/docs/dev/arch-decisions/coverage-data-quality.md +193 -0
- data/docs/dev/arch-decisions/output-character-mode.md +217 -0
- data/docs/dev/arch-decisions/path-resolution.md +90 -0
- data/docs/dev/arch-decisions/{004-x-arch-decision.md → policy-validation.md} +32 -28
- data/docs/dev/arch-decisions/{005-x-arch-decision.md → simplecov-integration.md} +47 -44
- data/docs/dev/presentations/cov-loupe-presentation.md +15 -13
- data/docs/examples/mcp-inputs.md +3 -0
- data/docs/examples/prompts.md +3 -0
- data/docs/examples/success_predicates.md +3 -0
- data/docs/fixtures/demo_project/.resultset.json +170 -0
- data/docs/fixtures/demo_project/README.md +6 -0
- data/docs/fixtures/demo_project/app/controllers/admin/audit_logs_controller.rb +19 -0
- data/docs/fixtures/demo_project/app/controllers/orders_controller.rb +26 -0
- data/docs/fixtures/demo_project/app/models/order.rb +20 -0
- data/docs/fixtures/demo_project/app/models/user.rb +19 -0
- data/docs/fixtures/demo_project/lib/api/client.rb +22 -0
- data/docs/fixtures/demo_project/lib/ops/jobs/cleanup_job.rb +16 -0
- data/docs/fixtures/demo_project/lib/ops/jobs/report_job.rb +17 -0
- data/docs/fixtures/demo_project/lib/payments/processor.rb +15 -0
- data/docs/fixtures/demo_project/lib/payments/refund_service.rb +15 -0
- data/docs/fixtures/demo_project/lib/payments/reporting/exporter.rb +16 -0
- data/docs/index.md +1 -0
- data/docs/license.md +3 -0
- data/docs/release_notes.md +3 -0
- data/docs/user/ADVANCED_USAGE.md +208 -115
- data/docs/user/CLI_FALLBACK_FOR_LLMS.md +2 -0
- data/docs/user/CLI_USAGE.md +276 -101
- data/docs/user/ERROR_HANDLING.md +4 -4
- data/docs/user/EXAMPLES.md +121 -128
- data/docs/user/INSTALLATION.md +9 -28
- data/docs/user/LIBRARY_API.md +227 -122
- data/docs/user/MCP_INTEGRATION.md +114 -203
- data/docs/user/README.md +5 -1
- data/docs/user/TROUBLESHOOTING.md +49 -27
- data/docs/user/installing-a-prelease-version-of-covloupe.md +43 -0
- data/docs/user/{V2-BREAKING-CHANGES.md → migrations/MIGRATING_TO_V2.md} +62 -72
- data/docs/user/migrations/MIGRATING_TO_V3.md +72 -0
- data/docs/user/migrations/MIGRATING_TO_V4.md +591 -0
- data/docs/user/migrations/README.md +22 -0
- data/docs/user/prompts/README.md +9 -0
- data/docs/user/prompts/non-web-coverage-analysis-prompt.md +103 -0
- data/docs/user/prompts/rails-coverage-analysis-prompt.md +94 -0
- data/docs/user/prompts/use-cli-not-mcp-prompt.md +53 -0
- data/examples/cli_demo.sh +77 -0
- data/examples/filter_and_table_demo-output.md +114 -0
- data/examples/filter_and_table_demo.rb +174 -0
- data/examples/fixtures/demo_project/coverage/.resultset.json +10 -0
- data/examples/mcp-inputs/README.md +66 -0
- data/examples/mcp-inputs/coverage_detailed.json +1 -0
- data/examples/mcp-inputs/coverage_raw.json +1 -0
- data/examples/mcp-inputs/coverage_summary.json +1 -0
- data/examples/mcp-inputs/list.json +1 -0
- data/examples/mcp-inputs/uncovered_lines.json +1 -0
- data/examples/prompts/README.md +27 -0
- data/examples/prompts/custom_resultset.txt +2 -0
- data/examples/prompts/detailed_with_source.txt +2 -0
- data/examples/prompts/list_lowest.txt +2 -0
- data/examples/prompts/summary.txt +2 -0
- data/examples/prompts/uncovered.txt +2 -0
- data/examples/success_predicates/README.md +198 -0
- data/examples/success_predicates/all_files_above_threshold_predicate.rb +21 -0
- data/examples/success_predicates/directory_specific_thresholds_predicate.rb +30 -0
- data/examples/success_predicates/project_coverage_minimum_predicate.rb +6 -0
- data/lib/cov_loupe/base_tool.rb +229 -20
- data/lib/cov_loupe/cli.rb +132 -23
- data/lib/cov_loupe/commands/base_command.rb +25 -6
- data/lib/cov_loupe/commands/command_factory.rb +0 -1
- data/lib/cov_loupe/commands/detailed_command.rb +10 -5
- data/lib/cov_loupe/commands/list_command.rb +2 -1
- data/lib/cov_loupe/commands/raw_command.rb +7 -5
- data/lib/cov_loupe/commands/summary_command.rb +12 -7
- data/lib/cov_loupe/commands/totals_command.rb +74 -10
- data/lib/cov_loupe/commands/uncovered_command.rb +7 -5
- data/lib/cov_loupe/commands/validate_command.rb +11 -3
- data/lib/cov_loupe/commands/version_command.rb +6 -4
- data/lib/cov_loupe/{app_config.rb → config/app_config.rb} +13 -5
- data/lib/cov_loupe/config/app_context.rb +43 -0
- data/lib/cov_loupe/config/boolean_type.rb +91 -0
- data/lib/cov_loupe/config/logger.rb +92 -0
- data/lib/cov_loupe/{option_normalizers.rb → config/option_normalizers.rb} +55 -24
- data/lib/cov_loupe/{option_parser_builder.rb → config/option_parser_builder.rb} +46 -24
- data/lib/cov_loupe/coverage/coverage_calculator.rb +53 -0
- data/lib/cov_loupe/coverage/coverage_reporter.rb +63 -0
- data/lib/cov_loupe/coverage/coverage_table_formatter.rb +133 -0
- data/lib/cov_loupe/{error_handler.rb → errors/error_handler.rb} +21 -33
- data/lib/cov_loupe/{errors.rb → errors/errors.rb} +48 -71
- data/lib/cov_loupe/formatters/formatters.rb +75 -0
- data/lib/cov_loupe/formatters/source_formatter.rb +18 -7
- data/lib/cov_loupe/formatters/table_formatter.rb +80 -0
- data/lib/cov_loupe/loaders/all.rb +15 -0
- data/lib/cov_loupe/loaders/all_cli.rb +10 -0
- data/lib/cov_loupe/loaders/all_mcp.rb +23 -0
- data/lib/cov_loupe/loaders/resultset_loader.rb +147 -0
- data/lib/cov_loupe/mcp_server.rb +3 -2
- data/lib/cov_loupe/model/model.rb +520 -0
- data/lib/cov_loupe/model/model_data.rb +13 -0
- data/lib/cov_loupe/model/model_data_cache.rb +116 -0
- data/lib/cov_loupe/option_parsers/env_options_parser.rb +17 -6
- data/lib/cov_loupe/option_parsers/error_helper.rb +16 -10
- data/lib/cov_loupe/output_chars.rb +192 -0
- data/lib/cov_loupe/paths/glob_utils.rb +100 -0
- data/lib/cov_loupe/{path_relativizer.rb → paths/path_relativizer.rb} +5 -13
- data/lib/cov_loupe/paths/path_utils.rb +265 -0
- data/lib/cov_loupe/paths/volume_case_sensitivity.rb +173 -0
- data/lib/cov_loupe/presenters/base_coverage_presenter.rb +9 -13
- data/lib/cov_loupe/presenters/coverage_payload_presenter.rb +21 -0
- data/lib/cov_loupe/presenters/payload_caching.rb +23 -0
- data/lib/cov_loupe/presenters/project_coverage_presenter.rb +73 -21
- data/lib/cov_loupe/presenters/project_totals_presenter.rb +16 -10
- data/lib/cov_loupe/repositories/coverage_repository.rb +149 -0
- data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +90 -76
- data/lib/cov_loupe/resolvers/{resolver_factory.rb → resolver_helpers.rb} +6 -5
- data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +40 -12
- data/lib/cov_loupe/scripts/command_execution.rb +113 -0
- data/lib/cov_loupe/scripts/latest_ci_status.rb +97 -0
- data/lib/cov_loupe/scripts/pre_release_check.rb +164 -0
- data/lib/cov_loupe/scripts/setup_doc_server.rb +23 -0
- data/lib/cov_loupe/scripts/start_doc_server.rb +24 -0
- data/lib/cov_loupe/staleness/stale_status.rb +23 -0
- data/lib/cov_loupe/staleness/staleness_checker.rb +328 -0
- data/lib/cov_loupe/staleness/staleness_message_formatter.rb +91 -0
- data/lib/cov_loupe/tools/coverage_detailed_tool.rb +14 -15
- data/lib/cov_loupe/tools/coverage_raw_tool.rb +14 -14
- data/lib/cov_loupe/tools/coverage_summary_tool.rb +16 -16
- data/lib/cov_loupe/tools/coverage_table_tool.rb +139 -21
- data/lib/cov_loupe/tools/coverage_totals_tool.rb +31 -13
- data/lib/cov_loupe/tools/help_tool.rb +16 -20
- data/lib/cov_loupe/tools/list_tool.rb +65 -0
- data/lib/cov_loupe/tools/uncovered_lines_tool.rb +14 -14
- data/lib/cov_loupe/tools/validate_tool.rb +18 -24
- data/lib/cov_loupe/tools/version_tool.rb +8 -3
- data/lib/cov_loupe/version.rb +1 -1
- data/lib/cov_loupe.rb +83 -55
- metadata +184 -154
- data/docs/dev/BRANCH_ONLY_COVERAGE.md +0 -158
- data/docs/dev/arch-decisions/001-x-arch-decision.md +0 -95
- data/docs/dev/arch-decisions/002-x-arch-decision.md +0 -159
- data/docs/dev/arch-decisions/003-x-arch-decision.md +0 -165
- data/lib/cov_loupe/app_context.rb +0 -26
- data/lib/cov_loupe/constants.rb +0 -22
- data/lib/cov_loupe/coverage_reporter.rb +0 -31
- data/lib/cov_loupe/formatters.rb +0 -51
- data/lib/cov_loupe/mode_detector.rb +0 -56
- data/lib/cov_loupe/model.rb +0 -339
- data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +0 -14
- data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +0 -14
- data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +0 -14
- data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +0 -14
- data/lib/cov_loupe/resultset_loader.rb +0 -131
- data/lib/cov_loupe/staleness_checker.rb +0 -247
- data/lib/cov_loupe/table_formatter.rb +0 -64
- data/lib/cov_loupe/tools/all_files_coverage_tool.rb +0 -51
- data/lib/cov_loupe/util.rb +0 -88
- data/spec/MCP_INTEGRATION_TESTS_README.md +0 -111
- data/spec/TIMESTAMPS.md +0 -48
- data/spec/all_files_coverage_tool_spec.rb +0 -53
- data/spec/app_config_spec.rb +0 -142
- data/spec/base_tool_spec.rb +0 -62
- data/spec/cli/show_default_report_spec.rb +0 -33
- data/spec/cli_enumerated_options_spec.rb +0 -90
- data/spec/cli_error_spec.rb +0 -184
- data/spec/cli_format_spec.rb +0 -123
- data/spec/cli_json_options_spec.rb +0 -50
- data/spec/cli_source_spec.rb +0 -44
- data/spec/cli_spec.rb +0 -192
- data/spec/cli_table_spec.rb +0 -28
- data/spec/cli_usage_spec.rb +0 -42
- data/spec/commands/base_command_spec.rb +0 -107
- data/spec/commands/command_factory_spec.rb +0 -76
- data/spec/commands/detailed_command_spec.rb +0 -34
- data/spec/commands/list_command_spec.rb +0 -28
- data/spec/commands/raw_command_spec.rb +0 -69
- data/spec/commands/summary_command_spec.rb +0 -34
- data/spec/commands/totals_command_spec.rb +0 -34
- data/spec/commands/uncovered_command_spec.rb +0 -55
- data/spec/commands/validate_command_spec.rb +0 -213
- data/spec/commands/version_command_spec.rb +0 -38
- data/spec/constants_spec.rb +0 -61
- data/spec/cov_loupe/formatters/source_formatter_spec.rb +0 -267
- data/spec/cov_loupe/formatters_spec.rb +0 -76
- data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +0 -79
- data/spec/cov_loupe_model_spec.rb +0 -454
- data/spec/cov_loupe_module_spec.rb +0 -37
- data/spec/cov_loupe_opts_spec.rb +0 -185
- data/spec/coverage_reporter_spec.rb +0 -102
- data/spec/coverage_table_tool_spec.rb +0 -59
- data/spec/coverage_totals_tool_spec.rb +0 -37
- data/spec/error_handler_spec.rb +0 -197
- data/spec/error_mode_spec.rb +0 -139
- data/spec/errors_edge_cases_spec.rb +0 -312
- data/spec/errors_stale_spec.rb +0 -83
- data/spec/file_based_mcp_tools_spec.rb +0 -99
- data/spec/help_tool_spec.rb +0 -26
- data/spec/integration_spec.rb +0 -789
- data/spec/logging_fallback_spec.rb +0 -128
- data/spec/mcp_logging_spec.rb +0 -44
- data/spec/mcp_server_integration_spec.rb +0 -23
- data/spec/mcp_server_spec.rb +0 -106
- data/spec/mode_detector_spec.rb +0 -153
- data/spec/model_error_handling_spec.rb +0 -269
- data/spec/model_staleness_spec.rb +0 -79
- data/spec/option_normalizers_spec.rb +0 -203
- data/spec/option_parsers/env_options_parser_spec.rb +0 -221
- data/spec/option_parsers/error_helper_spec.rb +0 -222
- data/spec/path_relativizer_spec.rb +0 -98
- data/spec/presenters/coverage_detailed_presenter_spec.rb +0 -19
- data/spec/presenters/coverage_raw_presenter_spec.rb +0 -15
- data/spec/presenters/coverage_summary_presenter_spec.rb +0 -15
- data/spec/presenters/coverage_uncovered_presenter_spec.rb +0 -16
- data/spec/presenters/project_coverage_presenter_spec.rb +0 -87
- data/spec/presenters/project_totals_presenter_spec.rb +0 -144
- data/spec/resolvers/coverage_line_resolver_spec.rb +0 -282
- data/spec/resolvers/resolver_factory_spec.rb +0 -61
- data/spec/resolvers/resultset_path_resolver_spec.rb +0 -60
- data/spec/resultset_loader_spec.rb +0 -167
- data/spec/shared_examples/README.md +0 -115
- data/spec/shared_examples/coverage_presenter_examples.rb +0 -66
- data/spec/shared_examples/file_based_mcp_tools.rb +0 -179
- data/spec/shared_examples/formatted_command_examples.rb +0 -64
- data/spec/shared_examples/mcp_tool_text_json_response.rb +0 -16
- data/spec/spec_helper.rb +0 -127
- data/spec/staleness_checker_spec.rb +0 -374
- data/spec/staleness_more_spec.rb +0 -42
- data/spec/support/cli_helpers.rb +0 -22
- data/spec/support/control_flow_helpers.rb +0 -20
- data/spec/support/fake_mcp.rb +0 -40
- data/spec/support/io_helpers.rb +0 -29
- data/spec/support/mcp_helpers.rb +0 -35
- data/spec/support/mcp_runner.rb +0 -66
- data/spec/support/mocking_helpers.rb +0 -30
- data/spec/table_format_spec.rb +0 -70
- data/spec/tools/validate_tool_spec.rb +0 -132
- data/spec/tools_error_handling_spec.rb +0 -130
- data/spec/util_spec.rb +0 -154
- data/spec/version_spec.rb +0 -123
- data/spec/version_tool_spec.rb +0 -141
- /data/{spec/fixtures/project1 → examples/fixtures/demo_project}/lib/bar.rb +0 -0
- /data/{spec/fixtures/project1 → examples/fixtures/demo_project}/lib/foo.rb +0 -0
- /data/lib/cov_loupe/{config_parser.rb → config/config_parser.rb} +0 -0
- /data/lib/cov_loupe/{predicate_evaluator.rb → config/predicate_evaluator.rb} +0 -0
- /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
|
data/lib/cov_loupe/constants.rb
DELETED
|
@@ -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
|
data/lib/cov_loupe/formatters.rb
DELETED
|
@@ -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
|