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
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Application Architecture
|
|
2
|
+
|
|
3
|
+
[Back to main README](../../index.md)
|
|
4
|
+
|
|
5
|
+
This document describes the core architectural decisions that shape how cov-loupe operates: its dual-mode design and context-aware error handling strategy.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Dual-Mode Operation (CLI and MCP Server)](#dual-mode-operation-cli-and-mcp-server)
|
|
10
|
+
- [Context-Aware Error Handling](#context-aware-error-handling)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Dual-Mode Operation (CLI and MCP Server)
|
|
15
|
+
|
|
16
|
+
### Status
|
|
17
|
+
|
|
18
|
+
Accepted
|
|
19
|
+
|
|
20
|
+
### Context
|
|
21
|
+
|
|
22
|
+
cov-loupe needed to serve two distinct use cases:
|
|
23
|
+
|
|
24
|
+
1. **Human users** wanting a command-line tool to inspect coverage reports in their terminal
|
|
25
|
+
2. **AI agents and MCP clients** needing programmatic access to coverage data via the Model Context Protocol (MCP) over JSON-RPC
|
|
26
|
+
|
|
27
|
+
We considered three approaches:
|
|
28
|
+
|
|
29
|
+
1. **Separate binaries/gems**: Create `simplecov-cli` and `cov-loupe` as separate projects
|
|
30
|
+
2. **Single binary with explicit mode flags**: Require users to pass `--mode mcp` to run as MCP server
|
|
31
|
+
3. **Automatic mode detection**: Single binary that automatically detects the operating mode based on input (TTY status, stdin)
|
|
32
|
+
|
|
33
|
+
#### Key Constraints
|
|
34
|
+
|
|
35
|
+
- MCP servers communicate via JSON-RPC over stdin/stdout, so any human-readable output would corrupt the protocol
|
|
36
|
+
- CLI users expect immediate, readable output without ceremony
|
|
37
|
+
- The gem should be simple to install and use for both audiences
|
|
38
|
+
- Mode selection must be reliable and unambiguous
|
|
39
|
+
|
|
40
|
+
### Decision (v4.0.0+)
|
|
41
|
+
|
|
42
|
+
We implemented **explicit mode selection** via the `-m/--mode` flag. The default mode is `cli`, and MCP users must pass `-m mcp` or `--mode mcp` to run the server.
|
|
43
|
+
|
|
44
|
+
#### Mode Selection Logic
|
|
45
|
+
|
|
46
|
+
The mode is determined by parsing the `-m/--mode` flag from argv (including environment variables via `COV_LOUPE_OPTS`):
|
|
47
|
+
|
|
48
|
+
- **Default**: CLI mode (when `-m/--mode` is not specified)
|
|
49
|
+
- **MCP mode**: Must explicitly pass `-m mcp` or `--mode mcp`
|
|
50
|
+
|
|
51
|
+
The implementation parses the configuration from the command-line arguments and routes to either `CoverageCLI` or `MCPServer` based on the mode setting.
|
|
52
|
+
|
|
53
|
+
#### Why This Works
|
|
54
|
+
|
|
55
|
+
- **MCP clients** are configured once with `-m mcp` or `--mode mcp` in their server config → always routes to MCP server
|
|
56
|
+
- **CLI users** don't need to specify anything → defaults to CLI mode
|
|
57
|
+
- **No ambiguity**: Mode is explicit and deterministic based on the `-m/--mode` flag
|
|
58
|
+
|
|
59
|
+
#### Historical Note
|
|
60
|
+
|
|
61
|
+
Prior to v4.0.0, cov-loupe used automatic mode detection based on TTY status and presence of subcommands. This was removed because:
|
|
62
|
+
- Automatic detection caused issues with piped input (`cov-loupe --format json > output.json` would hang in MCP mode)
|
|
63
|
+
- CI environments and non-TTY contexts were unpredictable
|
|
64
|
+
- CLI-only flags without subcommands (`--format`, `--sort-order`) couldn't be reliably detected
|
|
65
|
+
- Explicit mode selection is more predictable and follows standard practice for language servers
|
|
66
|
+
|
|
67
|
+
### Consequences
|
|
68
|
+
|
|
69
|
+
#### Positive
|
|
70
|
+
|
|
71
|
+
1. **User convenience**: Single gem to install (`gem install cov-loupe`), single executable (`cov-loupe`)
|
|
72
|
+
2. **Predictable behavior**: Mode is explicit and deterministic - no surprises based on environment
|
|
73
|
+
3. **Simpler implementation**: No complex mode detection logic to maintain
|
|
74
|
+
4. **Clear separation**: CLI and MCP server implementations remain completely separate after routing
|
|
75
|
+
5. **Follows conventions**: Matches standard practice for language servers (e.g., `typescript-language-server --stdio`)
|
|
76
|
+
|
|
77
|
+
#### Negative
|
|
78
|
+
|
|
79
|
+
1. **Breaking change**: Users upgrading from v3.x must update MCP server configuration to include `-m mcp` or `--mode mcp`
|
|
80
|
+
2. **Slight verbosity**: MCP users must include `-m mcp` or `--mode mcp` in their server config (but this is one-time setup)
|
|
81
|
+
3. **Shared dependencies**: Some components (error handling, coverage model) must work correctly in both modes
|
|
82
|
+
|
|
83
|
+
#### Trade-offs
|
|
84
|
+
|
|
85
|
+
- **Versus automatic detection**: More explicit, but eliminates ambiguity and edge cases
|
|
86
|
+
- **Versus separate gems**: Single installation is simpler, but requires mode flag for MCP
|
|
87
|
+
|
|
88
|
+
#### Future Constraints
|
|
89
|
+
|
|
90
|
+
- Shared components (like `CoverageModel`) must never output to stdout/stderr in ways that differ by mode
|
|
91
|
+
- Default mode must remain `cli` for backward compatibility with existing CLI users
|
|
92
|
+
|
|
93
|
+
### References
|
|
94
|
+
|
|
95
|
+
- Implementation: `lib/cov_loupe.rb` (`CovLoupe.run`)
|
|
96
|
+
- Configuration: `lib/cov_loupe/app_config.rb`
|
|
97
|
+
- CLI implementation: `lib/cov_loupe/cli.rb`
|
|
98
|
+
- MCP server implementation: `lib/cov_loupe/mcp_server.rb`
|
|
99
|
+
- Related section: [Context-Aware Error Handling](#context-aware-error-handling)
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Context-Aware Error Handling
|
|
104
|
+
|
|
105
|
+
### Status
|
|
106
|
+
|
|
107
|
+
Accepted
|
|
108
|
+
|
|
109
|
+
### Context
|
|
110
|
+
|
|
111
|
+
cov-loupe operates in three distinct contexts, each with different error handling requirements:
|
|
112
|
+
|
|
113
|
+
1. **CLI mode**: Human users expect friendly error messages, exit codes, and optional debug traces
|
|
114
|
+
2. **MCP server mode**: AI agents/clients need structured error responses that don't crash the server
|
|
115
|
+
3. **Library mode**: Embedding applications need exceptions they can catch and handle programmatically
|
|
116
|
+
|
|
117
|
+
Initially, we considered uniform error handling across all modes, but this created poor user experiences:
|
|
118
|
+
|
|
119
|
+
- CLI users saw raw exceptions with stack traces (scary and unhelpful)
|
|
120
|
+
- MCP servers crashed on errors instead of returning error responses
|
|
121
|
+
- Library users got friendly messages logged to stderr (unwanted side effects in their applications)
|
|
122
|
+
|
|
123
|
+
#### Key Requirements
|
|
124
|
+
|
|
125
|
+
- **CLI**: User-friendly messages, meaningful exit codes, optional stack traces for debugging
|
|
126
|
+
- **MCP Server**: Logged errors (to file, not stdout), structured JSON-RPC error responses, no server crashes
|
|
127
|
+
- **Library**: Raise custom exceptions with no logging, allowing consumers to handle errors as needed
|
|
128
|
+
- **Consistency**: Same underlying error types, but different presentation strategies
|
|
129
|
+
|
|
130
|
+
### Decision
|
|
131
|
+
|
|
132
|
+
We implemented a **context-aware error handling strategy** using three components:
|
|
133
|
+
|
|
134
|
+
#### 1. Custom Exception Hierarchy
|
|
135
|
+
|
|
136
|
+
All errors inherit from `CovLoupe::Error` (lib/cov_loupe/errors.rb) with a `user_friendly_message` method:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
class Error < StandardError
|
|
140
|
+
def user_friendly_message
|
|
141
|
+
message # Can be overridden in subclasses
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
class FileNotFoundError < FileError; end
|
|
146
|
+
class CoverageDataError < Error; end
|
|
147
|
+
class ResultsetNotFoundError < CoverageDataError; end
|
|
148
|
+
# ... etc
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
This provides a unified interface for presenting errors to users while preserving exception types for programmatic handling.
|
|
152
|
+
|
|
153
|
+
#### 2. ErrorHandler Class
|
|
154
|
+
|
|
155
|
+
The `ErrorHandler` class (see `lib/cov_loupe/error_handler.rb`) provides configurable error handling behavior:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
class ErrorHandler
|
|
159
|
+
attr_accessor :error_mode, :logger
|
|
160
|
+
|
|
161
|
+
VALID_ERROR_MODES = [:off, :log, :debug].freeze
|
|
162
|
+
|
|
163
|
+
def initialize(error_mode: :log, logger: nil)
|
|
164
|
+
@error_mode = error_mode
|
|
165
|
+
@logger = logger
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def handle_error(error, context: nil, reraise: true)
|
|
169
|
+
log_error(error, context)
|
|
170
|
+
if reraise
|
|
171
|
+
raise error.is_a?(CovLoupe::Error) ? error : convert_standard_error(error)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The `convert_standard_error` method transforms Ruby's standard errors into user-friendly custom exceptions:
|
|
178
|
+
|
|
179
|
+
- `Errno::ENOENT` → `FileNotFoundError`
|
|
180
|
+
- `JSON::ParserError` → `CoverageDataError`
|
|
181
|
+
- `Errno::EACCES` → `FilePermissionError`
|
|
182
|
+
|
|
183
|
+
#### 3. ErrorHandlerFactory
|
|
184
|
+
|
|
185
|
+
The `ErrorHandlerFactory` (defined in `lib/cov_loupe/error_handler_factory.rb`) creates mode-specific handlers:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
module ErrorHandlerFactory
|
|
189
|
+
def self.for_cli(error_mode: :log)
|
|
190
|
+
ErrorHandler.new(error_mode: error_mode)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def self.for_library(error_mode: :off)
|
|
194
|
+
ErrorHandler.new(error_mode: :off) # No logging
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def self.for_mcp_server(error_mode: :log)
|
|
198
|
+
ErrorHandler.new(error_mode: :log) # Logs to file
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Error Flow by Mode
|
|
204
|
+
|
|
205
|
+
**CLI Mode** (lib/cov_loupe/cli.rb):
|
|
206
|
+
1. Catches all exceptions in the main run loop
|
|
207
|
+
2. Uses `for_cli` handler to log errors if debug mode is enabled
|
|
208
|
+
3. Displays `user_friendly_message` to the user
|
|
209
|
+
4. Exits with appropriate code (1 for errors, 2 for usage errors)
|
|
210
|
+
|
|
211
|
+
**MCP Server Mode** (`lib/cov_loupe/base_tool.rb`):
|
|
212
|
+
1. Each tool wraps execution in a rescue block
|
|
213
|
+
2. Uses `for_mcp_server` handler to log errors to `~/cov_loupe.log`
|
|
214
|
+
3. Returns structured JSON-RPC error response
|
|
215
|
+
4. Server continues running (no crashes)
|
|
216
|
+
|
|
217
|
+
**Library Mode** (`lib/cov_loupe.rb`):
|
|
218
|
+
1. Uses `for_library` handler with `error_mode: :off` (no logging)
|
|
219
|
+
2. Raises custom exceptions directly
|
|
220
|
+
3. Consumers catch and handle `CovLoupe::Error` subclasses
|
|
221
|
+
|
|
222
|
+
### Consequences
|
|
223
|
+
|
|
224
|
+
#### Positive
|
|
225
|
+
|
|
226
|
+
1. **Excellent UX**: Each context gets appropriate error handling behavior
|
|
227
|
+
2. **Robustness**: MCP server never crashes on tool errors
|
|
228
|
+
3. **Debuggability**: CLI users can enable stack traces with error modes, MCP errors are logged
|
|
229
|
+
4. **Clean library API**: No unwanted side effects (logging, stderr output) when used as a library
|
|
230
|
+
5. **Type safety**: Custom exceptions allow programmatic error handling by type
|
|
231
|
+
|
|
232
|
+
#### Negative
|
|
233
|
+
|
|
234
|
+
1. **Complexity**: Three error handling paths to maintain and test
|
|
235
|
+
2. **Coordination required**: All error types must implement `user_friendly_message` consistently
|
|
236
|
+
3. **Error conversion overhead**: Standard errors must be converted to custom exceptions
|
|
237
|
+
|
|
238
|
+
#### Trade-offs
|
|
239
|
+
|
|
240
|
+
- **Versus uniform error handling**: More code complexity, but dramatically better UX in each context
|
|
241
|
+
- **Versus separate error classes per mode**: Single error hierarchy is simpler, factory pattern adds mode-specific behavior
|
|
242
|
+
|
|
243
|
+
#### Implementation Notes
|
|
244
|
+
|
|
245
|
+
The `ErrorHandler.convert_standard_error` method uses pattern matching on exception types and error messages to provide helpful, context-aware error messages. This includes:
|
|
246
|
+
|
|
247
|
+
- Extracting filenames from system error messages
|
|
248
|
+
- Detecting SimpleCov-specific error patterns
|
|
249
|
+
- Providing actionable suggestions ("please run your tests first")
|
|
250
|
+
|
|
251
|
+
### References
|
|
252
|
+
|
|
253
|
+
- Custom exceptions: `lib/cov_loupe/errors.rb`
|
|
254
|
+
- ErrorHandler implementation: `lib/cov_loupe/error_handler.rb`
|
|
255
|
+
- ErrorHandlerFactory: `lib/cov_loupe/error_handler_factory.rb`
|
|
256
|
+
- CLI error handling: `lib/cov_loupe/cli.rb` (rescue block in `CoverageCLI#run`)
|
|
257
|
+
- MCP tool error handling: `lib/cov_loupe/base_tool.rb` (`BaseTool#call`)
|
|
258
|
+
- Library mode: `lib/cov_loupe.rb` (error handling within `CovLoupe.run`)
|
|
259
|
+
- Related section: [Dual-Mode Operation](#dual-mode-operation-cli-and-mcp-server)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Coverage Data Quality
|
|
2
|
+
|
|
3
|
+
[Back to main README](../../index.md)
|
|
4
|
+
|
|
5
|
+
This document describes how cov-loupe ensures the accuracy and reliability of coverage data through staleness detection.
|
|
6
|
+
|
|
7
|
+
## Coverage Staleness Detection
|
|
8
|
+
|
|
9
|
+
### Status
|
|
10
|
+
|
|
11
|
+
Accepted
|
|
12
|
+
|
|
13
|
+
### Context
|
|
14
|
+
|
|
15
|
+
Coverage data can become outdated when source files are modified after tests run. This creates misleading results:
|
|
16
|
+
|
|
17
|
+
- Coverage percentages appear lower/higher than reality
|
|
18
|
+
- Line numbers in coverage reports don't match the current source
|
|
19
|
+
- AI agents and users may make decisions based on stale data
|
|
20
|
+
|
|
21
|
+
We needed a staleness detection system that could:
|
|
22
|
+
|
|
23
|
+
1. Detect when source files have been modified since coverage was collected
|
|
24
|
+
2. Detect when source files have different line counts than coverage data
|
|
25
|
+
3. Handle edge cases (deleted files)
|
|
26
|
+
4. Support both file-level and project-level checks
|
|
27
|
+
5. Allow users to control whether staleness is reported or causes errors
|
|
28
|
+
|
|
29
|
+
#### Alternative Approaches Considered
|
|
30
|
+
|
|
31
|
+
1. **No staleness checking**: Simple, but leads to confusing/incorrect reports
|
|
32
|
+
2. **Single timestamp check**: Fast, but misses line count mismatches (files edited and reverted)
|
|
33
|
+
3. **Content hashing**: Accurate, but expensive for large projects
|
|
34
|
+
4. **Multi-type detection with modes**: More complex, but provides accurate detection with user control
|
|
35
|
+
|
|
36
|
+
### Decision
|
|
37
|
+
|
|
38
|
+
We implemented a **staleness detection system** with configurable error modes that can identify four distinct staleness conditions.
|
|
39
|
+
|
|
40
|
+
#### Four Staleness Types
|
|
41
|
+
|
|
42
|
+
The `StalenessChecker` class (defined in `lib/cov_loupe/staleness/staleness_checker.rb`) detects four distinct types of staleness:
|
|
43
|
+
|
|
44
|
+
1. **Type "error" (Error)**: The staleness check itself failed
|
|
45
|
+
- Returned by `CoverageModel#staleness_for` when an exception is raised during staleness checking
|
|
46
|
+
- Example: File permission errors, resolver failures, or other unexpected issues
|
|
47
|
+
- The error is logged but execution continues with an "error" status instead of crashing
|
|
48
|
+
|
|
49
|
+
2. **Type "missing" (Missing)**: The source file exists in coverage but is now deleted/missing
|
|
50
|
+
- Returned by `file_staleness_status` when `File.file?(file_abs)` returns false
|
|
51
|
+
- Example: File was deleted after tests ran
|
|
52
|
+
|
|
53
|
+
3. **Type "newer" (Timestamp)**: The source file's mtime is newer than coverage timestamp
|
|
54
|
+
- Detected by comparing `File.mtime(file_abs)` with coverage timestamp
|
|
55
|
+
- Example: File was edited after tests ran
|
|
56
|
+
|
|
57
|
+
4. **Type "length_mismatch" (Length)**: The source file line count doesn't match the coverage lines array length
|
|
58
|
+
- Detected by comparing `File.foreach(path).count` with `coverage_lines.length`
|
|
59
|
+
- Example: Lines were added/removed without changing mtime (rare but possible with version control)
|
|
60
|
+
|
|
61
|
+
5. **Type "ok" (Not stale)**: The file is not stale
|
|
62
|
+
- Returned when none of the above staleness conditions apply
|
|
63
|
+
- Indicates the coverage data is current and accurate
|
|
64
|
+
|
|
65
|
+
#### Implementation Details
|
|
66
|
+
|
|
67
|
+
The core algorithm lives in `CovLoupe::StalenessChecker#compute_file_staleness_details`:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
def compute_file_staleness_details(file_abs, coverage_lines)
|
|
71
|
+
coverage_ts = coverage_timestamp
|
|
72
|
+
exists = File.file?(file_abs)
|
|
73
|
+
file_mtime = exists ? File.mtime(file_abs) : nil
|
|
74
|
+
|
|
75
|
+
cov_len = coverage_lines.respond_to?(:length) ? coverage_lines.length : 0
|
|
76
|
+
src_len = exists ? safe_count_lines(file_abs) : 0
|
|
77
|
+
|
|
78
|
+
# If coverage timestamp is 0 (missing/invalid), we cannot determine if file is newer
|
|
79
|
+
newer = if coverage_ts.to_i > 0
|
|
80
|
+
!!(file_mtime && file_mtime.to_i > coverage_ts.to_i)
|
|
81
|
+
else
|
|
82
|
+
false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
len_mismatch = (cov_len.positive? && src_len != cov_len)
|
|
86
|
+
newer &&= !len_mismatch # Prioritize length mismatch over timestamp
|
|
87
|
+
|
|
88
|
+
{
|
|
89
|
+
exists: exists,
|
|
90
|
+
file_mtime: file_mtime,
|
|
91
|
+
coverage_timestamp: coverage_ts,
|
|
92
|
+
cov_len: cov_len,
|
|
93
|
+
src_len: src_len,
|
|
94
|
+
newer: newer,
|
|
95
|
+
len_mismatch: len_mismatch
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Staleness Modes
|
|
101
|
+
|
|
102
|
+
The checker supports two modes, configured when instantiating `StalenessChecker`:
|
|
103
|
+
|
|
104
|
+
- **`:off`** (default): Staleness is detected but only reported in responses, never raises errors
|
|
105
|
+
- **"error"**: Staleness raises `CoverageDataStaleError` or `CoverageDataProjectStaleError`
|
|
106
|
+
|
|
107
|
+
This allows:
|
|
108
|
+
- Interactive tools to show warnings without crashing
|
|
109
|
+
- CI systems to fail builds on stale coverage
|
|
110
|
+
- AI agents to decide how to handle staleness based on their goals
|
|
111
|
+
|
|
112
|
+
#### File-Level vs Project-Level Checks
|
|
113
|
+
|
|
114
|
+
**File-level** (`check_file!` and `file_staleness_status`):
|
|
115
|
+
- Checks a single file's staleness
|
|
116
|
+
- Returns one of the staleness status strings ("ok", "missing", "newer", "length_mismatch", "error")
|
|
117
|
+
- Used by single-file tools (summary, detailed, uncovered)
|
|
118
|
+
|
|
119
|
+
**Project-level** (`check_project!`):
|
|
120
|
+
- Checks all covered files plus optionally tracked files
|
|
121
|
+
- Detects:
|
|
122
|
+
- Files newer than coverage timestamp
|
|
123
|
+
- Files deleted since coverage was collected
|
|
124
|
+
- Tracked files missing from coverage (newly added files)
|
|
125
|
+
- Raises `CoverageDataProjectStaleError` with lists of problematic files
|
|
126
|
+
- Used by `list_tool` and `coverage_table_tool`
|
|
127
|
+
|
|
128
|
+
**Totals behavior**:
|
|
129
|
+
- `project_totals` excludes any stale files ("missing", "newer", "length_mismatch", "error") from aggregate counts.
|
|
130
|
+
- Totals include explicit `with_coverage`/`without_coverage` breakdowns so callers can reconcile what was omitted.
|
|
131
|
+
- The `without_coverage` payload includes counts for three categories:
|
|
132
|
+
- `missing_from_coverage`: Tracked files that have no coverage data in the resultset
|
|
133
|
+
- `unreadable`: Files that exist but could not be read (e.g., due to permission errors, I/O issues, or staleness check failures)
|
|
134
|
+
- `skipped`: Files that were skipped during list processing due to coverage data errors (e.g., malformed entries)
|
|
135
|
+
- The `unreadable` count is populated from `list_result['unreadable_files']`, which is collected during staleness checking when files exist but cannot be accessed or validated.
|
|
136
|
+
|
|
137
|
+
#### Tracked Globs Feature
|
|
138
|
+
|
|
139
|
+
The project-level check supports `tracked_globs` parameter to detect newly added files:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# Detects if lib/**/*.rb files exist that have no coverage data
|
|
143
|
+
checker.check_project!(coverage_map) # with tracked_globs: ['lib/**/*.rb']
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This helps teams ensure new files are included in test runs.
|
|
147
|
+
|
|
148
|
+
#### Resultset Path Consistency (SimpleCov)
|
|
149
|
+
|
|
150
|
+
SimpleCov can emit mixed path forms for the same file when resultsets are merged across suites or
|
|
151
|
+
environments (for example, absolute vs relative paths, or different roots). This is a SimpleCov
|
|
152
|
+
data consistency risk, not a cov-loupe behavior. Downstream tools that normalize paths may treat
|
|
153
|
+
one entry as overriding another when multiple keys map to the same absolute path.
|
|
154
|
+
|
|
155
|
+
**Guidance:** Keep `SimpleCov.root` consistent across all suites and avoid manual path rewriting
|
|
156
|
+
before merging resultsets.
|
|
157
|
+
|
|
158
|
+
### Consequences
|
|
159
|
+
|
|
160
|
+
#### Positive
|
|
161
|
+
|
|
162
|
+
1. **Accurate detection**: Three types catch different staleness scenarios comprehensively
|
|
163
|
+
2. **User control**: Modes allow errors or warnings based on use case
|
|
164
|
+
3. **Detailed information**: Staleness errors include specific file lists and timestamps
|
|
165
|
+
4. **Project awareness**: Can detect newly added files that lack coverage
|
|
166
|
+
5. **Conservative totals**: Aggregate totals only include fresh coverage data
|
|
167
|
+
|
|
168
|
+
#### Negative
|
|
169
|
+
|
|
170
|
+
1. **Complexity**: Three staleness types are harder to understand than a single timestamp check
|
|
171
|
+
2. **Performance**: Line counting and mtime checks for every file add overhead
|
|
172
|
+
3. **Ambiguity**: When multiple staleness types apply, prioritization logic (length > timestamp) may surprise users
|
|
173
|
+
|
|
174
|
+
#### Trade-offs
|
|
175
|
+
|
|
176
|
+
- **Versus timestamp-only**: More accurate but slower and more complex
|
|
177
|
+
- **Versus content hashing**: Fast enough for most projects, but can't detect "edit then revert" scenarios
|
|
178
|
+
- **Versus no checking**: Essential for reliable coverage reporting, worth the complexity
|
|
179
|
+
|
|
180
|
+
#### Edge Cases Handled
|
|
181
|
+
|
|
182
|
+
1. **Deleted files**: Appear as "missing" type staleness
|
|
183
|
+
2. **Empty files**: `cov_len.positive?` guard prevents false positives
|
|
184
|
+
3. **No coverage timestamp**: Defaults to 0, effectively disabling timestamp checks
|
|
185
|
+
|
|
186
|
+
### References
|
|
187
|
+
|
|
188
|
+
- Implementation: `lib/cov_loupe/staleness_checker.rb` (`StalenessChecker` class)
|
|
189
|
+
- File-level checking: `StalenessChecker#check_file!` and `#file_staleness_status`
|
|
190
|
+
- Project-level checking: `StalenessChecker#check_project!`
|
|
191
|
+
- Staleness detail computation: `StalenessChecker#compute_file_staleness_details`
|
|
192
|
+
- Error types: `lib/cov_loupe/errors.rb` (`CoverageDataStaleError`, `CoverageDataProjectStaleError`)
|
|
193
|
+
- Usage in tools: `lib/cov_loupe/tools/list_tool.rb`, `lib/cov_loupe/model.rb`
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Output Character Mode
|
|
2
|
+
|
|
3
|
+
[Back to main README](../../index.md)
|
|
4
|
+
|
|
5
|
+
This document describes the architectural decision for implementing a global output character mode that controls ASCII vs Unicode output across CLI and MCP interfaces.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Accepted
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
cov-loupe outputs data in multiple formats across two interfaces:
|
|
14
|
+
|
|
15
|
+
1. **CLI mode**: Human users read terminal output including tables, error messages, and formatted coverage reports
|
|
16
|
+
2. **MCP server mode**: AI agents receive JSON responses containing coverage data and metadata
|
|
17
|
+
|
|
18
|
+
### The Problem
|
|
19
|
+
|
|
20
|
+
Modern projects often contain file paths with Unicode characters (e.g., accented characters, non-Latin scripts). The original implementation used Unicode characters throughout:
|
|
21
|
+
|
|
22
|
+
- Table borders using box-drawing characters (│ ─ ┌ ┐ └ ┘ ├ ┤ ┬ ┴ ┼)
|
|
23
|
+
- Source code markers (✓ for covered, · for uncovered)
|
|
24
|
+
- Error messages with file paths preserved as-is
|
|
25
|
+
|
|
26
|
+
This caused issues in environments that don't support Unicode:
|
|
27
|
+
|
|
28
|
+
- Windows terminals with legacy encoding
|
|
29
|
+
- CI/CD systems with ASCII-only terminals
|
|
30
|
+
- Piped output to files or tools expecting ASCII
|
|
31
|
+
- Legacy systems without UTF-8 support
|
|
32
|
+
|
|
33
|
+
Users experienced garbled output, corrupted tables, and unreadable error messages.
|
|
34
|
+
|
|
35
|
+
### Requirements
|
|
36
|
+
|
|
37
|
+
- **ASCII mode**: Must produce ASCII-only output (0-127 characters) when requested
|
|
38
|
+
- **Fancy mode**: Should use Unicode characters for enhanced readability when supported
|
|
39
|
+
- **Auto-detection**: Default mode should intelligently choose based on environment
|
|
40
|
+
- **MCP integration**: MCP tools must support the same output modes as CLI
|
|
41
|
+
- **Comprehensive coverage**: All output channels must respect the mode setting
|
|
42
|
+
- **Backward compatibility**: Existing behavior (Unicode) should remain the default when supported
|
|
43
|
+
|
|
44
|
+
### Considered Approaches
|
|
45
|
+
|
|
46
|
+
1. **Separate ASCII formatters**: Create duplicate formatter implementations for ASCII output
|
|
47
|
+
- Too much code duplication
|
|
48
|
+
- Maintenance burden (two implementations of each formatter)
|
|
49
|
+
|
|
50
|
+
2. **Post-process all output**: Apply ASCII conversion after formatting
|
|
51
|
+
- Inefficient (convert entire formatted output)
|
|
52
|
+
- Could corrupt already-encoded data (JSON structure)
|
|
53
|
+
|
|
54
|
+
3. **Centralized conversion with charsets**: Define separate charsets and convert at formatting time
|
|
55
|
+
- Clean separation of concerns
|
|
56
|
+
- Efficient (convert only what's displayed)
|
|
57
|
+
- Consistent across all formatters
|
|
58
|
+
|
|
59
|
+
## Decision
|
|
60
|
+
|
|
61
|
+
We implemented **global output character mode** with centralized conversion using charset definitions.
|
|
62
|
+
|
|
63
|
+
### Mode Options
|
|
64
|
+
|
|
65
|
+
Three modes are available:
|
|
66
|
+
- `default`: Auto-detects terminal UTF-8 support at runtime → fancy if supported, otherwise ASCII
|
|
67
|
+
- `fancy`: Forces Unicode output with box-drawing characters and fancy markers
|
|
68
|
+
- `ascii`: Forces ASCII-only output with transliteration fallback to `?` for unknown characters
|
|
69
|
+
|
|
70
|
+
### Configuration
|
|
71
|
+
|
|
72
|
+
- **CLI**: `-O/--output-chars MODE` flag (case-insensitive, short forms `d|f|a`)
|
|
73
|
+
- **MCP**: Optional `output_chars` parameter in tool requests (overrides server default)
|
|
74
|
+
- **No environment variable**: Intentionally omitted to keep configuration simple and explicit
|
|
75
|
+
|
|
76
|
+
### Core Implementation
|
|
77
|
+
|
|
78
|
+
The `OutputChars` module (`lib/cov_loupe/output_chars.rb`) provides:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
module OutputChars
|
|
82
|
+
# Mode resolution
|
|
83
|
+
def self.resolve_mode(mode)
|
|
84
|
+
return :fancy if mode == :fancy
|
|
85
|
+
return :ascii if mode == :ascii
|
|
86
|
+
# default: detect terminal UTF-8 support
|
|
87
|
+
stdout_utf8? ? :fancy : :ascii
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Character conversion using transliteration map
|
|
91
|
+
def self.convert(text, mode)
|
|
92
|
+
return text unless mode == :ascii
|
|
93
|
+
text.chars.map { |c| TRANSLITERATIONS[c] || c.ascii_only? ? c : '?' }.join
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Charset selection
|
|
97
|
+
def self.charset_for(mode)
|
|
98
|
+
mode == :fancy ? FANCY_CHARSET : ASCII_CHARSET
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Transliteration Strategy
|
|
104
|
+
|
|
105
|
+
Instead of a generic library (like `ActiveSupport::Multibyte`), we use an internal `TRANSLITERATIONS` hash mapping common characters to ASCII equivalents:
|
|
106
|
+
|
|
107
|
+
- Accented Latin characters (á → a, é → e, ñ → n, etc.)
|
|
108
|
+
- Symbols and punctuation (→ ->, — --, © (C), etc.)
|
|
109
|
+
- Box-drawing characters (│ → |, ─ → -, ┌ → +, etc.)
|
|
110
|
+
|
|
111
|
+
Characters without defined mappings fall back to `?` to maintain ASCII-only output.
|
|
112
|
+
|
|
113
|
+
### Formatter Integration
|
|
114
|
+
|
|
115
|
+
All formatters respect the `output_chars` parameter:
|
|
116
|
+
|
|
117
|
+
1. **JSON**: Uses `JSON.generate(..., ascii_only: true)` in ASCII mode
|
|
118
|
+
2. **YAML**: Post-processes through `OutputChars.convert`
|
|
119
|
+
3. **AmazingPrint**: Post-processes through `OutputChars.convert`
|
|
120
|
+
4. **Tables**: Uses appropriate charset (`OutputChars.charset_for`) and converts cell contents
|
|
121
|
+
5. **Source**: Uses ASCII-safe markers (`+`/`-` instead of `✓`/`·`) and converts source code
|
|
122
|
+
|
|
123
|
+
### Error Message Integration
|
|
124
|
+
|
|
125
|
+
- CLI error handlers convert messages via `OutputChars.convert`
|
|
126
|
+
- Staleness error messages convert file paths via `convert_path` lambda
|
|
127
|
+
- Option parser errors converted before display
|
|
128
|
+
- Backtrace lines converted in debug mode
|
|
129
|
+
|
|
130
|
+
### Scope of Conversion
|
|
131
|
+
|
|
132
|
+
**Converted in ASCII mode:**
|
|
133
|
+
- All CLI error messages and option parser errors
|
|
134
|
+
- Staleness error messages and file paths
|
|
135
|
+
- Command literal strings (via `convert_text` helper in BaseCommand)
|
|
136
|
+
- MCP tool JSON responses (via `respond_json` with `ascii_only: true`)
|
|
137
|
+
- All formatted output (tables, source, JSON, YAML)
|
|
138
|
+
|
|
139
|
+
**Not converted in ASCII mode:**
|
|
140
|
+
- **Log files**: Preserved in original encoding for debugging fidelity. Log files are system/debugging artifacts, not user-facing output. Converting would lose exact file paths and error details needed for troubleshooting, create inconsistency between logged paths and actual filesystem paths, and provides no user value since logs are developer artifacts.
|
|
141
|
+
- **Gem post-install message**: Intentionally left unchanged per requirements
|
|
142
|
+
|
|
143
|
+
## Consequences
|
|
144
|
+
|
|
145
|
+
### Positive
|
|
146
|
+
|
|
147
|
+
1. **Broad compatibility**: Works in any terminal environment, including legacy systems
|
|
148
|
+
2. **Better UX**: Fancy mode provides enhanced readability when Unicode is supported
|
|
149
|
+
3. **Auto-detection**: Default mode adapts to environment without user configuration
|
|
150
|
+
4. **Comprehensive coverage**: All output channels respect the mode setting
|
|
151
|
+
5. **MCP parity**: CLI and MCP interfaces have identical behavior
|
|
152
|
+
6. **No dependencies**: Internal transliteration map avoids external dependencies
|
|
153
|
+
7. **Consistent behavior**: Single source of truth for character conversion
|
|
154
|
+
|
|
155
|
+
### Negative
|
|
156
|
+
|
|
157
|
+
1. **Complexity**: Additional configuration option and conversion logic to maintain
|
|
158
|
+
2. **Transliteration coverage**: Not all Unicode characters have mappings (falls back to `?`)
|
|
159
|
+
3. **Performance**: Conversion overhead for every output operation (minimal in practice)
|
|
160
|
+
4. **Test burden**: Comprehensive tests needed across all formatters and modes
|
|
161
|
+
|
|
162
|
+
### Trade-offs
|
|
163
|
+
|
|
164
|
+
- **Internal vs external transliteration**: Internal map is less comprehensive but avoids dependencies and keeps behavior predictable
|
|
165
|
+
- **Charset vs post-processing**: Charsets are cleaner but require formatter awareness; post-processing is simpler but can corrupt structured data
|
|
166
|
+
- **Auto-detection vs explicit default**: Auto-detection is more convenient but less predictable; explicit default is clearer but requires configuration
|
|
167
|
+
|
|
168
|
+
### Future Constraints
|
|
169
|
+
|
|
170
|
+
- Any new formatters must respect `output_chars` parameter
|
|
171
|
+
- New output channels (e.g., HTML) need ASCII mode support
|
|
172
|
+
- Transliteration map must be maintained as new characters are encountered
|
|
173
|
+
- Log files must never be converted (documented design decision)
|
|
174
|
+
|
|
175
|
+
## Implementation Notes
|
|
176
|
+
|
|
177
|
+
### Mode Precedence
|
|
178
|
+
|
|
179
|
+
1. Explicit mode parameter (CLI flag or MCP tool parameter)
|
|
180
|
+
2. Server default (for MCP)
|
|
181
|
+
3. Built-in default (auto-detect UTF-8 support)
|
|
182
|
+
|
|
183
|
+
### Performance Considerations
|
|
184
|
+
|
|
185
|
+
- Conversion only applies in ASCII mode (fancy mode is a no-op)
|
|
186
|
+
- Transliteration map lookup is O(1) per character
|
|
187
|
+
- JSON `ascii_only: true` is optimized by the json gem
|
|
188
|
+
- Overall performance impact is negligible (< 1ms for typical outputs)
|
|
189
|
+
|
|
190
|
+
### Testing Strategy
|
|
191
|
+
|
|
192
|
+
Comprehensive test coverage ensures correctness:
|
|
193
|
+
|
|
194
|
+
- Mode resolution and normalization tests
|
|
195
|
+
- Formatter tests for both ASCII and fancy modes
|
|
196
|
+
- CLI option parsing tests for `--output-chars` flag
|
|
197
|
+
- MCP tool output mode tests
|
|
198
|
+
- Staleness error message tests with Unicode file paths
|
|
199
|
+
- Integration tests across all subcommands with Unicode file names
|
|
200
|
+
|
|
201
|
+
## References
|
|
202
|
+
|
|
203
|
+
- Core implementation: `lib/cov_loupe/output_chars.rb`
|
|
204
|
+
- Configuration: `lib/cov_loupe/config/app_config.rb`, `lib/cov_loupe/config/option_normalizers.rb`
|
|
205
|
+
- Formatters:
|
|
206
|
+
- `lib/cov_loupe/formatters/formatters.rb` (JSON, YAML, AmazingPrint)
|
|
207
|
+
- `lib/cov_loupe/formatters/table_formatter.rb` (tables)
|
|
208
|
+
- `lib/cov_loupe/formatters/source_formatter.rb` (source code)
|
|
209
|
+
- Error handling: `lib/cov_loupe/cli.rb`, `lib/cov_loupe/errors/error_handler.rb`
|
|
210
|
+
- MCP integration: `lib/cov_loupe/base_tool.rb`, `lib/cov_loupe/tools/*.rb`
|
|
211
|
+
- CLI option parsing: `lib/cov_loupe/config/option_parser_builder.rb`
|
|
212
|
+
- Tests:
|
|
213
|
+
- `spec/cov_loupe/output_chars_spec.rb`
|
|
214
|
+
- `spec/cov_loupe/formatters/*_spec.rb`
|
|
215
|
+
- `spec/cov_loupe/cli/cli_output_chars_spec.rb`
|
|
216
|
+
- `spec/cov_loupe/tools/*_spec.rb`
|
|
217
|
+
- Review document: `docs/dev/output-chars-review.md`
|