mdcontext 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/README.md +28 -0
- package/.changeset/config.json +11 -0
- package/.claude/settings.local.json +25 -0
- package/.github/workflows/ci.yml +83 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +85 -0
- package/.github/workflows/release.yml +113 -0
- package/.tldrignore +112 -0
- package/BACKLOG.md +338 -0
- package/CONTRIBUTING.md +186 -0
- package/NOTES/NOTES +44 -0
- package/README.md +434 -11
- package/biome.json +36 -0
- package/cspell.config.yaml +14 -0
- package/dist/chunk-23UPXDNL.js +3044 -0
- package/dist/chunk-2W7MO2DL.js +1366 -0
- package/dist/chunk-3NUAZGMA.js +1689 -0
- package/dist/chunk-7TOWB2XB.js +366 -0
- package/dist/chunk-7XOTOADQ.js +3065 -0
- package/dist/chunk-AH2PDM2K.js +3042 -0
- package/dist/chunk-BNXWSZ63.js +3742 -0
- package/dist/chunk-BTL5DJVU.js +3222 -0
- package/dist/chunk-HDHYG7E4.js +104 -0
- package/dist/chunk-HLR4KZBP.js +3234 -0
- package/dist/chunk-IP3FRFEB.js +1045 -0
- package/dist/chunk-KHU56VDO.js +3042 -0
- package/dist/chunk-KRYIFLQR.js +88 -0
- package/dist/chunk-LBSDNLEM.js +287 -0
- package/dist/chunk-MNTQ7HCP.js +2643 -0
- package/dist/chunk-MUJELQQ6.js +1387 -0
- package/dist/chunk-MXJGMSLV.js +2199 -0
- package/dist/chunk-N6QJGC3Z.js +2636 -0
- package/dist/chunk-OBELGBPM.js +1713 -0
- package/dist/chunk-OT7R5XTA.js +3192 -0
- package/dist/chunk-P7X4RA2T.js +106 -0
- package/dist/chunk-PIDUQNC2.js +3185 -0
- package/dist/chunk-POGCDIH4.js +3187 -0
- package/dist/chunk-PSIEOQGZ.js +3043 -0
- package/dist/chunk-PVRT3IHA.js +3238 -0
- package/dist/chunk-QNN4TT23.js +1430 -0
- package/dist/chunk-RE3R45RJ.js +3042 -0
- package/dist/chunk-S7E6TFX6.js +803 -0
- package/dist/chunk-SG6GLU4U.js +1378 -0
- package/dist/chunk-SJCDV2ST.js +274 -0
- package/dist/chunk-SYE5XLF3.js +104 -0
- package/dist/chunk-T5VLYBZD.js +103 -0
- package/dist/chunk-TOQB7VWU.js +3238 -0
- package/dist/chunk-VFNMZ4ZQ.js +3228 -0
- package/dist/chunk-VVTGZNBT.js +1629 -0
- package/dist/chunk-W7Q4RFEV.js +104 -0
- package/dist/chunk-XTYYVRLO.js +3190 -0
- package/dist/chunk-Y6MDYVJD.js +3063 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +5458 -0
- package/dist/index.d.ts +653 -0
- package/dist/index.js +79 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +472 -0
- package/dist/schema-BAWSG7KY.js +22 -0
- package/dist/schema-E3QUPL26.js +20 -0
- package/dist/schema-EHL7WUT6.js +20 -0
- package/docs/019-USAGE.md +625 -0
- package/docs/020-current-implementation.md +364 -0
- package/docs/021-DOGFOODING-FINDINGS.md +175 -0
- package/docs/BACKLOG.md +80 -0
- package/docs/CONFIG.md +1123 -0
- package/docs/DESIGN.md +439 -0
- package/docs/ERRORS.md +383 -0
- package/docs/PROJECT.md +88 -0
- package/docs/ROADMAP.md +407 -0
- package/docs/summarization.md +320 -0
- package/docs/test-links.md +9 -0
- package/justfile +40 -0
- package/package.json +74 -9
- package/pnpm-workspace.yaml +5 -0
- package/research/INDEX.md +315 -0
- package/research/code-review/README.md +90 -0
- package/research/code-review/cli-error-handling-review.md +979 -0
- package/research/code-review/code-review-validation-report.md +464 -0
- package/research/code-review/main-ts-review.md +1128 -0
- package/research/config-analysis/01-current-implementation.md +470 -0
- package/research/config-analysis/02-strategy-recommendation.md +428 -0
- package/research/config-analysis/03-task-candidates.md +715 -0
- package/research/config-analysis/033-research-configuration-management.md +828 -0
- package/research/config-analysis/034-research-effect-cli-config.md +1504 -0
- package/research/config-analysis/04-consolidated-task-candidates.md +277 -0
- package/research/config-docs/SUMMARY.md +357 -0
- package/research/config-docs/TEST-RESULTS.md +776 -0
- package/research/config-docs/TODO.md +542 -0
- package/research/config-docs/analysis.md +744 -0
- package/research/config-docs/fix-validation.md +502 -0
- package/research/config-docs/help-audit.md +264 -0
- package/research/config-docs/help-system-analysis.md +890 -0
- package/research/dogfood/consolidated-tool-evaluation.md +373 -0
- package/research/dogfood/strategy-a/a-synthesis.md +184 -0
- package/research/dogfood/strategy-a/a1-docs.md +226 -0
- package/research/dogfood/strategy-a/a2-amorphic.md +156 -0
- package/research/dogfood/strategy-a/a3-llm.md +164 -0
- package/research/dogfood/strategy-b/b-synthesis.md +228 -0
- package/research/dogfood/strategy-b/b1-architecture.md +207 -0
- package/research/dogfood/strategy-b/b2-gaps.md +258 -0
- package/research/dogfood/strategy-b/b3-workflows.md +250 -0
- package/research/dogfood/strategy-c/c-synthesis.md +451 -0
- package/research/dogfood/strategy-c/c1-explorer.md +192 -0
- package/research/dogfood/strategy-c/c2-diver-memory.md +145 -0
- package/research/dogfood/strategy-c/c3-diver-control.md +148 -0
- package/research/dogfood/strategy-c/c4-diver-failure.md +151 -0
- package/research/dogfood/strategy-c/c5-diver-execution.md +221 -0
- package/research/dogfood/strategy-c/c6-diver-org.md +221 -0
- package/research/effect-cli-error-handling.md +845 -0
- package/research/effect-errors-as-values.md +943 -0
- package/research/errors-task-analysis/00-consolidated-tasks.md +207 -0
- package/research/errors-task-analysis/cli-commands-analysis.md +909 -0
- package/research/errors-task-analysis/embeddings-analysis.md +709 -0
- package/research/errors-task-analysis/index-search-analysis.md +812 -0
- package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
- package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
- package/research/issue-review.md +603 -0
- package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
- package/research/llm-summarization/alternative-providers-2026.md +1428 -0
- package/research/llm-summarization/anthropic-2026.md +367 -0
- package/research/llm-summarization/claude-cli-integration.md +1706 -0
- package/research/llm-summarization/cli-integration-patterns.md +3155 -0
- package/research/llm-summarization/openai-2026.md +473 -0
- package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
- package/research/llm-summarization/opencode-cli-integration.md +1552 -0
- package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
- package/research/llm-summarization/prototype-results.md +56 -0
- package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
- package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
- package/research/mdcontext-error-analysis.md +521 -0
- package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
- package/research/mdcontext-pudding/01-index-embed.md +956 -0
- package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
- package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
- package/research/mdcontext-pudding/02-search.md +970 -0
- package/research/mdcontext-pudding/03-context.md +779 -0
- package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
- package/research/mdcontext-pudding/04-tree.md +704 -0
- package/research/mdcontext-pudding/05-config.md +1038 -0
- package/research/mdcontext-pudding/06-links-summary.txt +87 -0
- package/research/mdcontext-pudding/06-links.md +679 -0
- package/research/mdcontext-pudding/07-stats.md +693 -0
- package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
- package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
- package/research/mdcontext-pudding/README.md +168 -0
- package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
- package/research/npm_publish/011-npm-workflow-research-agent2.md +792 -0
- package/research/npm_publish/012-npm-workflow-research-agent1.md +530 -0
- package/research/npm_publish/013-npm-workflow-research-agent3.md +722 -0
- package/research/npm_publish/014-npm-workflow-synthesis.md +556 -0
- package/research/npm_publish/031-npm-workflow-task-analysis.md +134 -0
- package/research/research-quality-review.md +834 -0
- package/research/semantic-search/002-research-embedding-models.md +490 -0
- package/research/semantic-search/003-research-rag-alternatives.md +523 -0
- package/research/semantic-search/004-research-vector-search.md +841 -0
- package/research/semantic-search/032-research-semantic-search.md +427 -0
- package/research/semantic-search/embedding-text-analysis.md +156 -0
- package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
- package/research/semantic-search/query-processing-analysis.md +207 -0
- package/research/semantic-search/root-cause-and-solution.md +114 -0
- package/research/semantic-search/threshold-validation-report.md +69 -0
- package/research/semantic-search/vector-search-analysis.md +63 -0
- package/research/task-management-2026/00-synthesis-recommendations.md +295 -0
- package/research/task-management-2026/01-ai-workflow-tools.md +416 -0
- package/research/task-management-2026/02-agent-framework-patterns.md +476 -0
- package/research/task-management-2026/03-lightweight-file-based.md +567 -0
- package/research/task-management-2026/04-established-tools-ai-features.md +541 -0
- package/research/task-management-2026/linear/01-core-features-workflow.md +771 -0
- package/research/task-management-2026/linear/02-api-integrations.md +930 -0
- package/research/task-management-2026/linear/03-ai-features.md +368 -0
- package/research/task-management-2026/linear/04-pricing-setup.md +205 -0
- package/research/task-management-2026/linear/05-usage-patterns-best-practices.md +605 -0
- package/research/test-path-issues.md +276 -0
- package/review/ALP-76/1-error-type-design.md +962 -0
- package/review/ALP-76/2-error-handling-patterns.md +906 -0
- package/review/ALP-76/3-error-presentation.md +624 -0
- package/review/ALP-76/4-test-coverage.md +625 -0
- package/review/ALP-76/5-migration-completeness.md +440 -0
- package/review/ALP-76/6-effect-best-practices.md +755 -0
- package/scripts/apply-branch-protection.sh +47 -0
- package/scripts/branch-protection-templates.json +79 -0
- package/scripts/prototype-summarization.ts +346 -0
- package/scripts/rebuild-hnswlib.js +58 -0
- package/scripts/setup-branch-protection.sh +64 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
- package/src/cli/argv-preprocessor.test.ts +210 -0
- package/src/cli/argv-preprocessor.ts +202 -0
- package/src/cli/cli.test.ts +627 -0
- package/src/cli/commands/backlinks.ts +54 -0
- package/src/cli/commands/config-cmd.ts +642 -0
- package/src/cli/commands/context.ts +285 -0
- package/src/cli/commands/duplicates.ts +122 -0
- package/src/cli/commands/embeddings.ts +529 -0
- package/src/cli/commands/index-cmd.ts +480 -0
- package/src/cli/commands/index.ts +16 -0
- package/src/cli/commands/links.ts +52 -0
- package/src/cli/commands/search.ts +1281 -0
- package/src/cli/commands/stats.ts +149 -0
- package/src/cli/commands/tree.ts +128 -0
- package/src/cli/config-layer.ts +176 -0
- package/src/cli/error-handler.test.ts +235 -0
- package/src/cli/error-handler.ts +655 -0
- package/src/cli/flag-schemas.ts +341 -0
- package/src/cli/help.ts +588 -0
- package/src/cli/index.ts +9 -0
- package/src/cli/main.ts +435 -0
- package/src/cli/options.ts +41 -0
- package/src/cli/shared-error-handling.ts +199 -0
- package/src/cli/typo-suggester.test.ts +105 -0
- package/src/cli/typo-suggester.ts +130 -0
- package/src/cli/utils.ts +259 -0
- package/src/config/file-provider.test.ts +320 -0
- package/src/config/file-provider.ts +273 -0
- package/src/config/index.ts +72 -0
- package/src/config/integration.test.ts +667 -0
- package/src/config/precedence.test.ts +277 -0
- package/src/config/precedence.ts +451 -0
- package/src/config/schema.test.ts +414 -0
- package/src/config/schema.ts +603 -0
- package/src/config/service.test.ts +320 -0
- package/src/config/service.ts +243 -0
- package/src/config/testing.test.ts +264 -0
- package/src/config/testing.ts +110 -0
- package/src/core/index.ts +1 -0
- package/src/core/types.ts +113 -0
- package/src/duplicates/detector.test.ts +183 -0
- package/src/duplicates/detector.ts +414 -0
- package/src/duplicates/index.ts +18 -0
- package/src/embeddings/embedding-namespace.test.ts +300 -0
- package/src/embeddings/embedding-namespace.ts +947 -0
- package/src/embeddings/heading-boost.test.ts +222 -0
- package/src/embeddings/hnsw-build-options.test.ts +198 -0
- package/src/embeddings/hyde.test.ts +272 -0
- package/src/embeddings/hyde.ts +264 -0
- package/src/embeddings/index.ts +10 -0
- package/src/embeddings/openai-provider.ts +414 -0
- package/src/embeddings/pricing.json +22 -0
- package/src/embeddings/provider-constants.ts +204 -0
- package/src/embeddings/provider-errors.test.ts +967 -0
- package/src/embeddings/provider-errors.ts +565 -0
- package/src/embeddings/provider-factory.test.ts +240 -0
- package/src/embeddings/provider-factory.ts +225 -0
- package/src/embeddings/provider-integration.test.ts +788 -0
- package/src/embeddings/query-preprocessing.test.ts +187 -0
- package/src/embeddings/semantic-search-threshold.test.ts +508 -0
- package/src/embeddings/semantic-search.ts +1270 -0
- package/src/embeddings/types.ts +359 -0
- package/src/embeddings/vector-store.ts +708 -0
- package/src/embeddings/voyage-provider.ts +313 -0
- package/src/errors/errors.test.ts +845 -0
- package/src/errors/index.ts +533 -0
- package/src/index/ignore-patterns.test.ts +354 -0
- package/src/index/ignore-patterns.ts +305 -0
- package/src/index/index.ts +4 -0
- package/src/index/indexer.ts +684 -0
- package/src/index/storage.ts +260 -0
- package/src/index/types.ts +147 -0
- package/src/index/watcher.ts +189 -0
- package/src/index.ts +30 -0
- package/src/integration/search-keyword.test.ts +678 -0
- package/src/mcp/server.ts +612 -0
- package/src/parser/index.ts +1 -0
- package/src/parser/parser.test.ts +291 -0
- package/src/parser/parser.ts +394 -0
- package/src/parser/section-filter.test.ts +277 -0
- package/src/parser/section-filter.ts +392 -0
- package/src/search/__tests__/hybrid-search.test.ts +650 -0
- package/src/search/bm25-store.ts +366 -0
- package/src/search/cross-encoder.test.ts +253 -0
- package/src/search/cross-encoder.ts +406 -0
- package/src/search/fuzzy-search.test.ts +419 -0
- package/src/search/fuzzy-search.ts +273 -0
- package/src/search/hybrid-search.ts +448 -0
- package/src/search/path-matcher.test.ts +276 -0
- package/src/search/path-matcher.ts +33 -0
- package/src/search/query-parser.test.ts +260 -0
- package/src/search/query-parser.ts +319 -0
- package/src/search/searcher.test.ts +280 -0
- package/src/search/searcher.ts +724 -0
- package/src/search/wink-bm25.d.ts +30 -0
- package/src/summarization/cli-providers/claude.ts +202 -0
- package/src/summarization/cli-providers/detection.test.ts +273 -0
- package/src/summarization/cli-providers/detection.ts +118 -0
- package/src/summarization/cli-providers/index.ts +8 -0
- package/src/summarization/cost.test.ts +139 -0
- package/src/summarization/cost.ts +102 -0
- package/src/summarization/error-handler.test.ts +127 -0
- package/src/summarization/error-handler.ts +111 -0
- package/src/summarization/index.ts +102 -0
- package/src/summarization/pipeline.test.ts +498 -0
- package/src/summarization/pipeline.ts +231 -0
- package/src/summarization/prompts.test.ts +269 -0
- package/src/summarization/prompts.ts +133 -0
- package/src/summarization/provider-factory.test.ts +396 -0
- package/src/summarization/provider-factory.ts +178 -0
- package/src/summarization/types.ts +184 -0
- package/src/summarize/budget-bugs.test.ts +620 -0
- package/src/summarize/formatters.ts +419 -0
- package/src/summarize/index.ts +20 -0
- package/src/summarize/summarizer.test.ts +275 -0
- package/src/summarize/summarizer.ts +597 -0
- package/src/summarize/verify-bugs.test.ts +238 -0
- package/src/types/huggingface-transformers.d.ts +66 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/tokens.test.ts +142 -0
- package/src/utils/tokens.ts +186 -0
- package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
- package/tests/fixtures/cli/.mdcontext/config.json +8 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/indexes/documents.json +33 -0
- package/tests/fixtures/cli/.mdcontext/indexes/links.json +12 -0
- package/tests/fixtures/cli/.mdcontext/indexes/sections.json +247 -0
- package/tests/fixtures/cli/README.md +9 -0
- package/tests/fixtures/cli/api-reference.md +11 -0
- package/tests/fixtures/cli/getting-started.md +11 -0
- package/tests/integration/embed-index.test.ts +712 -0
- package/tests/integration/search-context.test.ts +469 -0
- package/tests/integration/search-semantic.test.ts +522 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +16 -0
- package/vitest.setup.ts +12 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
# Effect CLI Error Handling Research
|
|
2
|
+
|
|
3
|
+
This document explores best practices for error handling in Effect-based CLI applications, with specific recommendations for mdcontext.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [@effect/cli Error Patterns](#1-effectcli-error-patterns)
|
|
8
|
+
2. [Layered Error Handling](#2-layered-error-handling)
|
|
9
|
+
3. [Error Presentation](#3-error-presentation)
|
|
10
|
+
4. [Real-world Examples](#4-real-world-examples)
|
|
11
|
+
5. [Recommendations for mdcontext](#5-recommendations-for-mdcontext)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. @effect/cli Error Patterns
|
|
16
|
+
|
|
17
|
+
### Command Type Signature
|
|
18
|
+
|
|
19
|
+
The @effect/cli `Command` type uses three type parameters:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
Command<R, E, A>;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- **R** (Environment): Dependencies or context needed by the command's handler
|
|
26
|
+
- **E** (Expected Errors): Types of errors the command might produce during execution
|
|
27
|
+
- **A** (Arguments/Configuration): Configuration object the handler receives
|
|
28
|
+
|
|
29
|
+
### ValidationError Types
|
|
30
|
+
|
|
31
|
+
@effect/cli provides built-in validation errors through the `ValidationError` module. These are returned when parsing fails:
|
|
32
|
+
|
|
33
|
+
| Error Type | Description |
|
|
34
|
+
| ------------------------ | ---------------------------------- |
|
|
35
|
+
| `CommandMismatch` | Command name doesn't match |
|
|
36
|
+
| `CorrectedFlag` | Auto-corrected misspelled flag |
|
|
37
|
+
| `HelpRequested` | User requested `--help` |
|
|
38
|
+
| `InvalidArgument` | Argument failed validation |
|
|
39
|
+
| `InvalidValue` | Value doesn't meet constraints |
|
|
40
|
+
| `MissingValue` | Required value not provided |
|
|
41
|
+
| `MissingFlag` | Required flag not provided |
|
|
42
|
+
| `MultipleValuesDetected` | Multiple values where one expected |
|
|
43
|
+
| `MissingSubcommand` | Subcommand required but missing |
|
|
44
|
+
| `UnclusteredFlag` | Cluster format issue |
|
|
45
|
+
|
|
46
|
+
Each `ValidationError` contains an `error: HelpDoc` field with formatted documentation.
|
|
47
|
+
|
|
48
|
+
### Built-in Error Handling
|
|
49
|
+
|
|
50
|
+
The `Command.run` method handles:
|
|
51
|
+
|
|
52
|
+
1. **Argument parsing** - Validates against command structure
|
|
53
|
+
2. **Help text generation** - Automatic `--help` and `--version`
|
|
54
|
+
3. **Error formatting** - User-friendly error messages
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { Command } from "@effect/cli";
|
|
58
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
59
|
+
import { Effect } from "effect";
|
|
60
|
+
|
|
61
|
+
const myCommand = Command.make(
|
|
62
|
+
"myapp",
|
|
63
|
+
{
|
|
64
|
+
// args and options...
|
|
65
|
+
},
|
|
66
|
+
(args) =>
|
|
67
|
+
Effect.gen(function* () {
|
|
68
|
+
// handler logic
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const cli = Command.run(myCommand, {
|
|
73
|
+
name: "myapp",
|
|
74
|
+
version: "1.0.0",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
cli(process.argv).pipe(
|
|
78
|
+
Effect.provide(NodeContext.layer),
|
|
79
|
+
NodeRuntime.runMain, // Pretty error formatting by default
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Excess Arguments Protection
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
npx tsx myapp.ts unexpected extra args
|
|
87
|
+
# Output: Received unknown argument: 'extra'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This prevents execution of malformed commands.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 2. Layered Error Handling
|
|
95
|
+
|
|
96
|
+
### The Three-Layer Model
|
|
97
|
+
|
|
98
|
+
Effect applications typically organize errors into three layers:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
+----------------------------------+
|
|
102
|
+
| User-Facing Errors | CLI output, JSON responses
|
|
103
|
+
+----------------------------------+
|
|
104
|
+
|
|
|
105
|
+
v (transform)
|
|
106
|
+
+----------------------------------+
|
|
107
|
+
| Domain Errors | Business logic failures
|
|
108
|
+
+----------------------------------+
|
|
109
|
+
|
|
|
110
|
+
v (transform)
|
|
111
|
+
+----------------------------------+
|
|
112
|
+
| Infrastructure Errors | API calls, file system, DB
|
|
113
|
+
+----------------------------------+
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Tagged Errors with Data.TaggedError
|
|
117
|
+
|
|
118
|
+
Create type-safe, discriminated union errors:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { Data, Effect, Console } from "effect";
|
|
122
|
+
|
|
123
|
+
// Domain errors
|
|
124
|
+
class FileNotFoundError extends Data.TaggedError("FileNotFoundError")<{
|
|
125
|
+
path: string;
|
|
126
|
+
}> {}
|
|
127
|
+
|
|
128
|
+
class ParseError extends Data.TaggedError("ParseError")<{
|
|
129
|
+
path: string;
|
|
130
|
+
message: string;
|
|
131
|
+
line?: number;
|
|
132
|
+
}> {}
|
|
133
|
+
|
|
134
|
+
class IndexNotFoundError extends Data.TaggedError("IndexNotFoundError")<{
|
|
135
|
+
directory: string;
|
|
136
|
+
}> {}
|
|
137
|
+
|
|
138
|
+
// Infrastructure errors
|
|
139
|
+
class ApiKeyMissingError extends Data.TaggedError("ApiKeyMissingError")<{}> {}
|
|
140
|
+
|
|
141
|
+
class ApiKeyInvalidError extends Data.TaggedError("ApiKeyInvalidError")<{
|
|
142
|
+
details: string;
|
|
143
|
+
}> {}
|
|
144
|
+
|
|
145
|
+
class NetworkError extends Data.TaggedError("NetworkError")<{
|
|
146
|
+
url: string;
|
|
147
|
+
statusCode?: number;
|
|
148
|
+
}> {}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Transforming Errors Between Layers
|
|
152
|
+
|
|
153
|
+
Use `Effect.mapError` to transform errors at layer boundaries:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { Effect } from "effect";
|
|
157
|
+
|
|
158
|
+
// Infrastructure layer: raw file system errors
|
|
159
|
+
const readFileRaw = (
|
|
160
|
+
path: string,
|
|
161
|
+
): Effect.Effect<string, NodeJS.ErrnoException> =>
|
|
162
|
+
Effect.tryPromise({
|
|
163
|
+
try: () => fs.readFile(path, "utf-8"),
|
|
164
|
+
catch: (e) => e as NodeJS.ErrnoException,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Domain layer: transform to domain error
|
|
168
|
+
const readFile = (path: string): Effect.Effect<string, FileNotFoundError> =>
|
|
169
|
+
readFileRaw(path).pipe(
|
|
170
|
+
Effect.mapError((e) => new FileNotFoundError({ path })),
|
|
171
|
+
);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Catching Specific Errors with catchTag
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { Effect, Data } from "effect";
|
|
178
|
+
|
|
179
|
+
class HttpError extends Data.TaggedError("HttpError")<{}> {}
|
|
180
|
+
class ValidationError extends Data.TaggedError("ValidationError")<{
|
|
181
|
+
field: string;
|
|
182
|
+
message: string;
|
|
183
|
+
}> {}
|
|
184
|
+
|
|
185
|
+
const program = Effect.gen(function* () {
|
|
186
|
+
// ... operations that may fail
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Handle specific error types
|
|
190
|
+
const recovered = program.pipe(
|
|
191
|
+
Effect.catchTag("HttpError", (error) =>
|
|
192
|
+
Effect.succeed("Recovered from HTTP error"),
|
|
193
|
+
),
|
|
194
|
+
Effect.catchTag("ValidationError", (error) =>
|
|
195
|
+
Effect.succeed(`Validation failed: ${error.message}`),
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Handling Multiple Errors with catchTags
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const recovered = program.pipe(
|
|
204
|
+
Effect.catchTags({
|
|
205
|
+
HttpError: (error) => Effect.succeed("HTTP recovery"),
|
|
206
|
+
ValidationError: (error) => Effect.succeed(`Validation: ${error.message}`),
|
|
207
|
+
NetworkError: (error) => Effect.succeed(`Network issue: ${error.url}`),
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Expected Errors vs Defects
|
|
213
|
+
|
|
214
|
+
**Expected Errors (E channel):**
|
|
215
|
+
|
|
216
|
+
- Recoverable failures the caller can handle
|
|
217
|
+
- Validation errors, "not found", permission denied
|
|
218
|
+
- Tracked in the type system: `Effect<A, E, R>`
|
|
219
|
+
|
|
220
|
+
**Defects (Unexpected Errors):**
|
|
221
|
+
|
|
222
|
+
- Unrecoverable situations: bugs, invariant violations
|
|
223
|
+
- Use `Effect.die` or `Effect.dieMessage`
|
|
224
|
+
- Handle once at system boundary (logging, crash reporting)
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Expected error - caller can recover
|
|
228
|
+
const divide = (a: number, b: number): Effect.Effect<number, DivisionError> =>
|
|
229
|
+
b === 0
|
|
230
|
+
? Effect.fail(new DivisionError({ dividend: a }))
|
|
231
|
+
: Effect.succeed(a / b);
|
|
232
|
+
|
|
233
|
+
// Defect - unrecoverable, terminates fiber
|
|
234
|
+
const assertPositive = (n: number): Effect.Effect<number, never> =>
|
|
235
|
+
n < 0
|
|
236
|
+
? Effect.dieMessage("Invariant violated: expected positive number")
|
|
237
|
+
: Effect.succeed(n);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Converting Errors to Defects
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// Convert all failures to defects
|
|
244
|
+
const program = divide(1, 0).pipe(Effect.orDie);
|
|
245
|
+
|
|
246
|
+
// Convert with custom transformation
|
|
247
|
+
const programWithContext = divide(1, 0).pipe(
|
|
248
|
+
Effect.orDieWith((error) => new Error(`Critical: ${error.message}`)),
|
|
249
|
+
);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 3. Error Presentation
|
|
255
|
+
|
|
256
|
+
### Terminal Output with runMain
|
|
257
|
+
|
|
258
|
+
The `runMain` function from `@effect/platform-node` provides built-in pretty error formatting:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { NodeRuntime } from "@effect/platform-node";
|
|
262
|
+
|
|
263
|
+
cli(process.argv).pipe(
|
|
264
|
+
Effect.provide(NodeContext.layer),
|
|
265
|
+
NodeRuntime.runMain, // Pretty logger enabled by default
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Or disable pretty formatting:
|
|
269
|
+
NodeRuntime.runMain({ disablePrettyLogger: true });
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Custom Error Formatting
|
|
273
|
+
|
|
274
|
+
For CLI-specific error messages:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
const formatCliError = (error: unknown): string => {
|
|
278
|
+
if (error && typeof error === "object") {
|
|
279
|
+
const err = error as Record<string, unknown>;
|
|
280
|
+
|
|
281
|
+
// Handle ValidationError from @effect/cli
|
|
282
|
+
if (err._tag === "ValidationError" && err.error) {
|
|
283
|
+
// Extract HelpDoc content
|
|
284
|
+
return extractHelpDocText(err.error);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Handle custom tagged errors
|
|
288
|
+
if (err._tag && typeof err._tag === "string") {
|
|
289
|
+
return formatTaggedError(err);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return String(error);
|
|
293
|
+
};
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### JSON Error Output for Machine Consumption
|
|
297
|
+
|
|
298
|
+
Support `--json` flag for structured error output:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
interface JsonError {
|
|
302
|
+
error: {
|
|
303
|
+
code: string;
|
|
304
|
+
message: string;
|
|
305
|
+
details?: Record<string, unknown>;
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const formatJsonError = (error: TaggedError): JsonError => ({
|
|
310
|
+
error: {
|
|
311
|
+
code: error._tag,
|
|
312
|
+
message: error.message ?? String(error),
|
|
313
|
+
details: { ...error }, // Include all error properties
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// In command handler:
|
|
318
|
+
if (jsonOutput) {
|
|
319
|
+
yield * Console.log(JSON.stringify(formatJsonError(error)));
|
|
320
|
+
} else {
|
|
321
|
+
yield * Console.error(formatUserFriendlyError(error));
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Verbosity Levels
|
|
326
|
+
|
|
327
|
+
Implement `--verbose` and `--debug` flags:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
type LogLevel = "quiet" | "normal" | "verbose" | "debug";
|
|
331
|
+
|
|
332
|
+
const logError = (error: Error, level: LogLevel): Effect.Effect<void> =>
|
|
333
|
+
Effect.gen(function* () {
|
|
334
|
+
switch (level) {
|
|
335
|
+
case "quiet":
|
|
336
|
+
// Just exit code
|
|
337
|
+
break;
|
|
338
|
+
case "normal":
|
|
339
|
+
yield* Console.error(error.message);
|
|
340
|
+
break;
|
|
341
|
+
case "verbose":
|
|
342
|
+
yield* Console.error(error.message);
|
|
343
|
+
yield* Console.error(` Type: ${error.constructor.name}`);
|
|
344
|
+
if ("path" in error) {
|
|
345
|
+
yield* Console.error(` Path: ${error.path}`);
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
case "debug":
|
|
349
|
+
yield* Console.error(error.message);
|
|
350
|
+
yield* Console.error(error.stack ?? "");
|
|
351
|
+
yield* Console.error("Full error object:");
|
|
352
|
+
yield* Console.error(JSON.stringify(error, null, 2));
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Color and ANSI Formatting
|
|
359
|
+
|
|
360
|
+
@effect/cli integrates with terminal styling:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// Check terminal capabilities
|
|
364
|
+
const supportsColor = process.stdout.isTTY;
|
|
365
|
+
|
|
366
|
+
// ANSI codes for styling
|
|
367
|
+
const bold = (text: string) => (supportsColor ? `\x1b[1m${text}\x1b[0m` : text);
|
|
368
|
+
const red = (text: string) => (supportsColor ? `\x1b[31m${text}\x1b[0m` : text);
|
|
369
|
+
const yellow = (text: string) =>
|
|
370
|
+
supportsColor ? `\x1b[33m${text}\x1b[0m` : text;
|
|
371
|
+
|
|
372
|
+
const formatError = (error: Error) => `${red(bold("Error:"))} ${error.message}`;
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Schema Error Formatting
|
|
376
|
+
|
|
377
|
+
Effect Schema provides formatters for validation errors:
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
import { Schema, ParseResult, Either } from "effect";
|
|
381
|
+
|
|
382
|
+
// Tree formatter - hierarchical view
|
|
383
|
+
const result = Schema.decodeUnknownEither(MySchema)(data);
|
|
384
|
+
if (Either.isLeft(result)) {
|
|
385
|
+
console.error(ParseResult.TreeFormatter.formatErrorSync(result.left));
|
|
386
|
+
}
|
|
387
|
+
/* Output:
|
|
388
|
+
{ readonly name: string; readonly age: number }
|
|
389
|
+
└─ ["name"]
|
|
390
|
+
└─ is missing
|
|
391
|
+
*/
|
|
392
|
+
|
|
393
|
+
// Array formatter - flat list for programmatic use
|
|
394
|
+
if (Either.isLeft(result)) {
|
|
395
|
+
const errors = ParseResult.ArrayFormatter.formatErrorSync(result.left);
|
|
396
|
+
// [{ _tag: 'Missing', path: ['name'], message: 'is missing' }]
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## 4. Real-world Examples
|
|
403
|
+
|
|
404
|
+
### Example 1: File Operation with Layered Errors
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import { Data, Effect, Console } from "effect";
|
|
408
|
+
import * as fs from "node:fs/promises";
|
|
409
|
+
|
|
410
|
+
// Error types
|
|
411
|
+
class FileReadError extends Data.TaggedError("FileReadError")<{
|
|
412
|
+
path: string;
|
|
413
|
+
cause: Error;
|
|
414
|
+
}> {}
|
|
415
|
+
|
|
416
|
+
class ConfigParseError extends Data.TaggedError("ConfigParseError")<{
|
|
417
|
+
path: string;
|
|
418
|
+
message: string;
|
|
419
|
+
}> {}
|
|
420
|
+
|
|
421
|
+
// Infrastructure layer
|
|
422
|
+
const readFileRaw = (path: string) =>
|
|
423
|
+
Effect.tryPromise({
|
|
424
|
+
try: () => fs.readFile(path, "utf-8"),
|
|
425
|
+
catch: (e) => new FileReadError({ path, cause: e as Error }),
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Domain layer
|
|
429
|
+
const parseConfig = (content: string, path: string) =>
|
|
430
|
+
Effect.try({
|
|
431
|
+
try: () => JSON.parse(content),
|
|
432
|
+
catch: (e) =>
|
|
433
|
+
new ConfigParseError({
|
|
434
|
+
path,
|
|
435
|
+
message: e instanceof Error ? e.message : "Invalid JSON",
|
|
436
|
+
}),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Combined operation
|
|
440
|
+
const loadConfig = (path: string) =>
|
|
441
|
+
Effect.gen(function* () {
|
|
442
|
+
const content = yield* readFileRaw(path);
|
|
443
|
+
return yield* parseConfig(content, path);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// CLI layer - transform to user-facing messages
|
|
447
|
+
const loadConfigForCli = (path: string, json: boolean) =>
|
|
448
|
+
loadConfig(path).pipe(
|
|
449
|
+
Effect.catchTags({
|
|
450
|
+
FileReadError: (e) =>
|
|
451
|
+
json
|
|
452
|
+
? Effect.fail({
|
|
453
|
+
code: "FILE_NOT_FOUND",
|
|
454
|
+
message: `Cannot read: ${e.path}`,
|
|
455
|
+
})
|
|
456
|
+
: Effect.gen(function* () {
|
|
457
|
+
yield* Console.error(`Error: Cannot read file "${e.path}"`);
|
|
458
|
+
yield* Console.error(` ${e.cause.message}`);
|
|
459
|
+
return yield* Effect.fail(new Error("File read failed"));
|
|
460
|
+
}),
|
|
461
|
+
ConfigParseError: (e) =>
|
|
462
|
+
json
|
|
463
|
+
? Effect.fail({ code: "PARSE_ERROR", message: e.message })
|
|
464
|
+
: Effect.gen(function* () {
|
|
465
|
+
yield* Console.error(`Error: Invalid config file "${e.path}"`);
|
|
466
|
+
yield* Console.error(` ${e.message}`);
|
|
467
|
+
return yield* Effect.fail(new Error("Parse failed"));
|
|
468
|
+
}),
|
|
469
|
+
}),
|
|
470
|
+
);
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Example 2: API Key Validation Pattern
|
|
474
|
+
|
|
475
|
+
From a real CLI (similar to mdcontext's approach):
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
import { Console, Effect } from "effect";
|
|
479
|
+
|
|
480
|
+
class MissingApiKeyError extends Error {
|
|
481
|
+
readonly _tag = "MissingApiKeyError";
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
class InvalidApiKeyError extends Error {
|
|
485
|
+
readonly _tag = "InvalidApiKeyError";
|
|
486
|
+
constructor(readonly details: string) {
|
|
487
|
+
super("Invalid API key");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Reusable error handler
|
|
492
|
+
const handleApiKeyError = <A, E>(
|
|
493
|
+
effect: Effect.Effect<A, E | MissingApiKeyError | InvalidApiKeyError>,
|
|
494
|
+
): Effect.Effect<A, E | Error> =>
|
|
495
|
+
effect.pipe(
|
|
496
|
+
Effect.catchIf(
|
|
497
|
+
(e): e is MissingApiKeyError => e instanceof MissingApiKeyError,
|
|
498
|
+
() =>
|
|
499
|
+
Effect.gen(function* () {
|
|
500
|
+
yield* Console.error("");
|
|
501
|
+
yield* Console.error("Error: API key not set");
|
|
502
|
+
yield* Console.error("");
|
|
503
|
+
yield* Console.error("Set your API key:");
|
|
504
|
+
yield* Console.error(" export OPENAI_API_KEY=sk-...");
|
|
505
|
+
yield* Console.error("");
|
|
506
|
+
yield* Console.error("Or add to .env file.");
|
|
507
|
+
return yield* Effect.fail(new Error("Missing API key"));
|
|
508
|
+
}),
|
|
509
|
+
),
|
|
510
|
+
Effect.catchIf(
|
|
511
|
+
(e): e is InvalidApiKeyError => e instanceof InvalidApiKeyError,
|
|
512
|
+
(e) =>
|
|
513
|
+
Effect.gen(function* () {
|
|
514
|
+
yield* Console.error("");
|
|
515
|
+
yield* Console.error("Error: Invalid API key");
|
|
516
|
+
yield* Console.error("");
|
|
517
|
+
yield* Console.error("The API key was rejected.");
|
|
518
|
+
yield* Console.error(`Details: ${e.details}`);
|
|
519
|
+
return yield* Effect.fail(new Error("Invalid API key"));
|
|
520
|
+
}),
|
|
521
|
+
),
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
// Usage in command
|
|
525
|
+
const searchCommand = Command.make(
|
|
526
|
+
"search",
|
|
527
|
+
{
|
|
528
|
+
/* ... */
|
|
529
|
+
},
|
|
530
|
+
(args) =>
|
|
531
|
+
Effect.gen(function* () {
|
|
532
|
+
const results = yield* semanticSearch(args.query).pipe(handleApiKeyError);
|
|
533
|
+
// ...
|
|
534
|
+
}),
|
|
535
|
+
);
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Example 3: Graceful Degradation
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
const searchWithFallback = (query: string, options: SearchOptions) =>
|
|
542
|
+
Effect.gen(function* () {
|
|
543
|
+
// Try semantic search first
|
|
544
|
+
const semanticResult = yield* semanticSearch(query).pipe(
|
|
545
|
+
Effect.catchTag("MissingApiKeyError", () => Effect.succeed(null)),
|
|
546
|
+
Effect.catchTag("IndexNotFoundError", () => Effect.succeed(null)),
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
if (semanticResult) {
|
|
550
|
+
return { mode: "semantic", results: semanticResult };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Fall back to keyword search
|
|
554
|
+
yield* Console.log("Falling back to keyword search...");
|
|
555
|
+
const keywordResult = yield* keywordSearch(query);
|
|
556
|
+
return { mode: "keyword", results: keywordResult };
|
|
557
|
+
});
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## 5. Recommendations for mdcontext
|
|
563
|
+
|
|
564
|
+
### Current State Analysis
|
|
565
|
+
|
|
566
|
+
Based on code review, mdcontext currently:
|
|
567
|
+
|
|
568
|
+
1. **Uses `Effect.fail(new Error(...))` directly** - Loses type safety
|
|
569
|
+
2. **Has ad-hoc error handling** in `main.ts` with manual `_tag` checking
|
|
570
|
+
3. **Mixes infrastructure and domain errors** - `IoError` and `ParseError` at same level
|
|
571
|
+
4. **Has good API key error handling** in `openai-provider.ts` as a pattern to follow
|
|
572
|
+
|
|
573
|
+
### Current Issues
|
|
574
|
+
|
|
575
|
+
1. **Untyped errors**: Using `new Error()` loses discriminated union benefits
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
// Current
|
|
579
|
+
yield * Effect.fail(new Error("At least one file is required..."));
|
|
580
|
+
|
|
581
|
+
// Better
|
|
582
|
+
yield * Effect.fail(new MissingArgumentError({ argument: "files" }));
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
2. **Manual error formatting**: The `formatCliError` function manually inspects error structure
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
// Current - fragile, relies on internal structure
|
|
589
|
+
if (err._tag === "ValidationError" && err.error) {
|
|
590
|
+
const validationError = err.error as Record<string, unknown>;
|
|
591
|
+
// ...deeply nested extraction
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
3. **Inconsistent error transformation**: Some places use `Effect.mapError`, others use `Effect.catchAll`
|
|
596
|
+
|
|
597
|
+
4. **Silent failures**: `catch (_e)` in `assembleContext` swallows errors
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
} catch (_e) {
|
|
601
|
+
// Skip files that can't be processed
|
|
602
|
+
overflow.push(sourcePath)
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Proposed Error Hierarchy
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
// src/errors/index.ts
|
|
610
|
+
|
|
611
|
+
import { Data } from "effect";
|
|
612
|
+
|
|
613
|
+
// Base error for all mdcontext errors
|
|
614
|
+
export class MdcontextError extends Data.TaggedError("MdcontextError")<{
|
|
615
|
+
message: string;
|
|
616
|
+
}> {}
|
|
617
|
+
|
|
618
|
+
// ============================================================================
|
|
619
|
+
// Infrastructure Errors
|
|
620
|
+
// ============================================================================
|
|
621
|
+
|
|
622
|
+
export class FileNotFoundError extends Data.TaggedError("FileNotFoundError")<{
|
|
623
|
+
path: string;
|
|
624
|
+
}> {}
|
|
625
|
+
|
|
626
|
+
export class FileReadError extends Data.TaggedError("FileReadError")<{
|
|
627
|
+
path: string;
|
|
628
|
+
cause: string;
|
|
629
|
+
}> {}
|
|
630
|
+
|
|
631
|
+
export class ApiKeyMissingError extends Data.TaggedError(
|
|
632
|
+
"ApiKeyMissingError",
|
|
633
|
+
)<{}> {}
|
|
634
|
+
|
|
635
|
+
export class ApiKeyInvalidError extends Data.TaggedError("ApiKeyInvalidError")<{
|
|
636
|
+
details: string;
|
|
637
|
+
}> {}
|
|
638
|
+
|
|
639
|
+
export class NetworkError extends Data.TaggedError("NetworkError")<{
|
|
640
|
+
url: string;
|
|
641
|
+
statusCode?: number;
|
|
642
|
+
message: string;
|
|
643
|
+
}> {}
|
|
644
|
+
|
|
645
|
+
// ============================================================================
|
|
646
|
+
// Domain Errors
|
|
647
|
+
// ============================================================================
|
|
648
|
+
|
|
649
|
+
export class ParseError extends Data.TaggedError("ParseError")<{
|
|
650
|
+
path: string;
|
|
651
|
+
message: string;
|
|
652
|
+
line?: number;
|
|
653
|
+
}> {}
|
|
654
|
+
|
|
655
|
+
export class IndexNotFoundError extends Data.TaggedError("IndexNotFoundError")<{
|
|
656
|
+
directory: string;
|
|
657
|
+
}> {}
|
|
658
|
+
|
|
659
|
+
export class IndexOutdatedError extends Data.TaggedError("IndexOutdatedError")<{
|
|
660
|
+
directory: string;
|
|
661
|
+
indexAge: number;
|
|
662
|
+
}> {}
|
|
663
|
+
|
|
664
|
+
export class SectionNotFoundError extends Data.TaggedError(
|
|
665
|
+
"SectionNotFoundError",
|
|
666
|
+
)<{
|
|
667
|
+
selector: string;
|
|
668
|
+
availableSections: string[];
|
|
669
|
+
}> {}
|
|
670
|
+
|
|
671
|
+
export class SearchError extends Data.TaggedError("SearchError")<{
|
|
672
|
+
query: string;
|
|
673
|
+
message: string;
|
|
674
|
+
}> {}
|
|
675
|
+
|
|
676
|
+
// ============================================================================
|
|
677
|
+
// CLI/User-Facing Errors
|
|
678
|
+
// ============================================================================
|
|
679
|
+
|
|
680
|
+
export class MissingArgumentError extends Data.TaggedError(
|
|
681
|
+
"MissingArgumentError",
|
|
682
|
+
)<{
|
|
683
|
+
argument: string;
|
|
684
|
+
usage: string;
|
|
685
|
+
}> {}
|
|
686
|
+
|
|
687
|
+
export class InvalidOptionError extends Data.TaggedError("InvalidOptionError")<{
|
|
688
|
+
option: string;
|
|
689
|
+
value: string;
|
|
690
|
+
expected: string;
|
|
691
|
+
}> {}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### Proposed Error Handler
|
|
695
|
+
|
|
696
|
+
```typescript
|
|
697
|
+
// src/cli/error-handler.ts
|
|
698
|
+
|
|
699
|
+
import { Effect, Console } from "effect";
|
|
700
|
+
import type {
|
|
701
|
+
FileNotFoundError,
|
|
702
|
+
ParseError,
|
|
703
|
+
IndexNotFoundError,
|
|
704
|
+
ApiKeyMissingError,
|
|
705
|
+
MissingArgumentError,
|
|
706
|
+
} from "../errors/index.js";
|
|
707
|
+
|
|
708
|
+
type CliError =
|
|
709
|
+
| FileNotFoundError
|
|
710
|
+
| ParseError
|
|
711
|
+
| IndexNotFoundError
|
|
712
|
+
| ApiKeyMissingError
|
|
713
|
+
| MissingArgumentError;
|
|
714
|
+
|
|
715
|
+
interface ErrorOutput {
|
|
716
|
+
json: boolean;
|
|
717
|
+
verbose: boolean;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export const handleCliError =
|
|
721
|
+
(options: ErrorOutput) =>
|
|
722
|
+
<A, E extends CliError>(
|
|
723
|
+
effect: Effect.Effect<A, E>,
|
|
724
|
+
): Effect.Effect<A, never> =>
|
|
725
|
+
effect.pipe(
|
|
726
|
+
Effect.catchTags({
|
|
727
|
+
FileNotFoundError: (e) => formatFileNotFound(e, options),
|
|
728
|
+
ParseError: (e) => formatParseError(e, options),
|
|
729
|
+
IndexNotFoundError: (e) => formatIndexNotFound(e, options),
|
|
730
|
+
ApiKeyMissingError: (e) => formatApiKeyMissing(e, options),
|
|
731
|
+
MissingArgumentError: (e) => formatMissingArgument(e, options),
|
|
732
|
+
}),
|
|
733
|
+
Effect.catchAll((e) => formatUnknownError(e, options)),
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
const formatFileNotFound = (error: FileNotFoundError, options: ErrorOutput) =>
|
|
737
|
+
Effect.gen(function* () {
|
|
738
|
+
if (options.json) {
|
|
739
|
+
yield* Console.log(
|
|
740
|
+
JSON.stringify({
|
|
741
|
+
error: { code: "FILE_NOT_FOUND", path: error.path },
|
|
742
|
+
}),
|
|
743
|
+
);
|
|
744
|
+
} else {
|
|
745
|
+
yield* Console.error(`Error: File not found: ${error.path}`);
|
|
746
|
+
}
|
|
747
|
+
return yield* Effect.die("exit");
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// ... similar handlers for each error type
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
### Migration Path
|
|
754
|
+
|
|
755
|
+
**Phase 1: Define Error Types (Low Risk)**
|
|
756
|
+
|
|
757
|
+
1. Create `src/errors/index.ts` with tagged error classes
|
|
758
|
+
2. Export from main index
|
|
759
|
+
|
|
760
|
+
**Phase 2: Update Infrastructure Layer**
|
|
761
|
+
|
|
762
|
+
1. Update `parser.ts` to use new `ParseError`/`FileReadError`
|
|
763
|
+
2. Update `openai-provider.ts` to use standard tagged errors
|
|
764
|
+
3. Update file system operations in indexer, storage
|
|
765
|
+
|
|
766
|
+
**Phase 3: Update Domain Layer**
|
|
767
|
+
|
|
768
|
+
1. Add `mapError` at service boundaries
|
|
769
|
+
2. Ensure consistent error transformation
|
|
770
|
+
|
|
771
|
+
**Phase 4: Update CLI Layer**
|
|
772
|
+
|
|
773
|
+
1. Create centralized error handler
|
|
774
|
+
2. Update `main.ts` to use new handler
|
|
775
|
+
3. Add JSON error output support
|
|
776
|
+
|
|
777
|
+
**Phase 5: Add Verbosity Support**
|
|
778
|
+
|
|
779
|
+
1. Add `--verbose` and `--debug` global options
|
|
780
|
+
2. Adjust error output detail based on level
|
|
781
|
+
|
|
782
|
+
### Example Refactored Command
|
|
783
|
+
|
|
784
|
+
```typescript
|
|
785
|
+
// Before
|
|
786
|
+
export const contextCommand = Command.make(
|
|
787
|
+
"context",
|
|
788
|
+
{ files: Args.file({ name: "files" }).pipe(Args.repeated) },
|
|
789
|
+
({ files }) =>
|
|
790
|
+
Effect.gen(function* () {
|
|
791
|
+
if (files.length === 0) {
|
|
792
|
+
yield* Effect.fail(new Error("At least one file is required..."));
|
|
793
|
+
}
|
|
794
|
+
const document = yield* parseFile(filePath).pipe(
|
|
795
|
+
Effect.mapError((e) => new Error(`${e._tag}: ${e.message}`)),
|
|
796
|
+
);
|
|
797
|
+
// ...
|
|
798
|
+
}),
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
// After
|
|
802
|
+
export const contextCommand = Command.make(
|
|
803
|
+
"context",
|
|
804
|
+
{
|
|
805
|
+
files: Args.file({ name: "files" }).pipe(Args.repeated),
|
|
806
|
+
json: jsonOption,
|
|
807
|
+
verbose: verboseOption,
|
|
808
|
+
},
|
|
809
|
+
({ files, json, verbose }) =>
|
|
810
|
+
Effect.gen(function* () {
|
|
811
|
+
if (files.length === 0) {
|
|
812
|
+
yield* Effect.fail(
|
|
813
|
+
new MissingArgumentError({
|
|
814
|
+
argument: "files",
|
|
815
|
+
usage: "mdcontext context <file> [files...]",
|
|
816
|
+
}),
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const document = yield* parseFile(filePath);
|
|
821
|
+
// Errors automatically typed and handled by outer handler
|
|
822
|
+
|
|
823
|
+
// ...
|
|
824
|
+
}).pipe(handleCliError({ json, verbose })),
|
|
825
|
+
);
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
---
|
|
829
|
+
|
|
830
|
+
## Sources
|
|
831
|
+
|
|
832
|
+
- [Effect Documentation - Expected Errors](https://effect.website/docs/error-management/expected-errors/)
|
|
833
|
+
- [Effect Documentation - Error Channel Operations](https://effect.website/docs/error-management/error-channel-operations/)
|
|
834
|
+
- [Effect Documentation - Unexpected Errors](https://effect.website/docs/error-management/unexpected-errors/)
|
|
835
|
+
- [Effect Documentation - Schema Error Formatters](https://effect.website/docs/schema/error-formatters/)
|
|
836
|
+
- [Effect Documentation - Logging](https://effect.website/docs/observability/logging/)
|
|
837
|
+
- [Effect Documentation - Cause](https://effect.website/docs/data-types/cause/)
|
|
838
|
+
- [@effect/cli npm package](https://www.npmjs.com/package/@effect/cli)
|
|
839
|
+
- [@effect/cli API - ValidationError](https://effect-ts.github.io/effect/cli/ValidationError.ts.html)
|
|
840
|
+
- [CLI Framework - DeepWiki](https://deepwiki.com/Effect-TS/effect/8.1-cli-framework)
|
|
841
|
+
- [Effect-TS Examples Repository](https://github.com/Effect-TS/examples)
|
|
842
|
+
- [EffectPatterns Community Repository](https://github.com/PaulJPhilp/EffectPatterns)
|
|
843
|
+
- [Exploring Effect in TypeScript - Tweag](https://www.tweag.io/blog/2024-11-07-typescript-effect/)
|
|
844
|
+
- [How to Effect TS - DTech Vision](https://dtech.vision/blog/how-to-effect-ts-best-practices/)
|
|
845
|
+
- [Effect Solutions - CLI Documentation](https://www.effect.solutions/cli)
|