diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9081435..a69bf52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,8 @@ env: RUST_BACKTRACE: short RUSTFLAGS: -D warnings # Crate publish order (dependencies first) - CRATES: "masterror-template masterror-derive masterror" + # template → derive → knowledge → masterror → cli + CRATES: "masterror-template masterror-derive masterror-knowledge masterror masterror-cli" jobs: # ════════════════════════════════════════════════════════════════════════════ @@ -58,13 +59,160 @@ jobs: echo "version=$MSRV" >> "$GITHUB_OUTPUT" echo "MSRV: $MSRV" + # ════════════════════════════════════════════════════════════════════════════ + # Detect changed crates (dependency-aware) + # ════════════════════════════════════════════════════════════════════════════ + + changes: + name: Detect Changes + runs-on: ubuntu-latest + outputs: + # Individual crate changes + template: ${{ steps.filter.outputs.template }} + derive: ${{ steps.filter.outputs.derive }} + knowledge: ${{ steps.filter.outputs.knowledge }} + masterror: ${{ steps.filter.outputs.masterror }} + cli: ${{ steps.filter.outputs.cli }} + # Dependency-aware: need to rebuild these + need-template: ${{ steps.deps.outputs.need-template }} + need-derive: ${{ steps.deps.outputs.need-derive }} + need-knowledge: ${{ steps.deps.outputs.need-knowledge }} + need-masterror: ${{ steps.deps.outputs.need-masterror }} + need-cli: ${{ steps.deps.outputs.need-cli }} + # Any library changed (for full workspace checks) + any-lib: ${{ steps.deps.outputs.any-lib }} + # CI/config changed (force full rebuild) + ci: ${{ steps.filter.outputs.ci }} + steps: + - uses: actions/checkout@v5 + + - name: Detect file changes + uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + template: + - 'masterror-template/**' + derive: + - 'masterror-derive/**' + knowledge: + - 'masterror-knowledge/**' + masterror: + - 'src/**' + - 'Cargo.toml' + - 'build.rs' + cli: + - 'masterror-cli/**' + ci: + - '.github/workflows/**' + - 'Cargo.lock' + - 'deny.toml' + - 'cliff.toml' + - '.cargo/**' + + - name: Compute dependency graph + id: deps + run: | + # Dependency graph: + # template ← derive ← masterror + # ↗ + # knowledge ← cli + # ↘ masterror (optional feature) + + TEMPLATE="${{ steps.filter.outputs.template }}" + DERIVE="${{ steps.filter.outputs.derive }}" + KNOWLEDGE="${{ steps.filter.outputs.knowledge }}" + MASTERROR="${{ steps.filter.outputs.masterror }}" + CLI="${{ steps.filter.outputs.cli }}" + CI="${{ steps.filter.outputs.ci }}" + + # Force all if CI config changed or workflow_dispatch + if [[ "$CI" == "true" ]] || [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "CI config changed or manual trigger - rebuilding all" + echo "need-template=true" >> "$GITHUB_OUTPUT" + echo "need-derive=true" >> "$GITHUB_OUTPUT" + echo "need-knowledge=true" >> "$GITHUB_OUTPUT" + echo "need-masterror=true" >> "$GITHUB_OUTPUT" + echo "need-cli=true" >> "$GITHUB_OUTPUT" + echo "any-lib=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # template: standalone + NEED_TEMPLATE="$TEMPLATE" + + # derive: depends on template + if [[ "$DERIVE" == "true" ]] || [[ "$TEMPLATE" == "true" ]]; then + NEED_DERIVE=true + else + NEED_DERIVE=false + fi + + # knowledge: standalone + NEED_KNOWLEDGE="$KNOWLEDGE" + + # masterror: depends on template, derive, optionally knowledge + if [[ "$MASTERROR" == "true" ]] || [[ "$TEMPLATE" == "true" ]] || \ + [[ "$DERIVE" == "true" ]] || [[ "$KNOWLEDGE" == "true" ]]; then + NEED_MASTERROR=true + else + NEED_MASTERROR=false + fi + + # cli: depends on knowledge + if [[ "$CLI" == "true" ]] || [[ "$KNOWLEDGE" == "true" ]]; then + NEED_CLI=true + else + NEED_CLI=false + fi + + # Any library changed? + if [[ "$NEED_TEMPLATE" == "true" ]] || [[ "$NEED_DERIVE" == "true" ]] || \ + [[ "$NEED_KNOWLEDGE" == "true" ]] || [[ "$NEED_MASTERROR" == "true" ]] || \ + [[ "$NEED_CLI" == "true" ]]; then + ANY_LIB=true + else + ANY_LIB=false + fi + + echo "need-template=$NEED_TEMPLATE" >> "$GITHUB_OUTPUT" + echo "need-derive=$NEED_DERIVE" >> "$GITHUB_OUTPUT" + echo "need-knowledge=$NEED_KNOWLEDGE" >> "$GITHUB_OUTPUT" + echo "need-masterror=$NEED_MASTERROR" >> "$GITHUB_OUTPUT" + echo "need-cli=$NEED_CLI" >> "$GITHUB_OUTPUT" + echo "any-lib=$ANY_LIB" >> "$GITHUB_OUTPUT" + + echo "Summary:" + echo " template: $TEMPLATE → need: $NEED_TEMPLATE" + echo " derive: $DERIVE → need: $NEED_DERIVE" + echo " knowledge: $KNOWLEDGE → need: $NEED_KNOWLEDGE" + echo " masterror: $MASTERROR → need: $NEED_MASTERROR" + echo " cli: $CLI → need: $NEED_CLI" + echo " any-lib: $ANY_LIB" + + # GitHub Step Summary + cat >> $GITHUB_STEP_SUMMARY << EOF + ## 🔍 Change Detection + + | Crate | Changed | Rebuild | + |-------|---------|---------| + | masterror-template | $([[ "$TEMPLATE" == "true" ]] && echo "✅" || echo "—") | $([[ "$NEED_TEMPLATE" == "true" ]] && echo "🔨" || echo "⏭️") | + | masterror-derive | $([[ "$DERIVE" == "true" ]] && echo "✅" || echo "—") | $([[ "$NEED_DERIVE" == "true" ]] && echo "🔨" || echo "⏭️") | + | masterror-knowledge | $([[ "$KNOWLEDGE" == "true" ]] && echo "✅" || echo "—") | $([[ "$NEED_KNOWLEDGE" == "true" ]] && echo "🔨" || echo "⏭️") | + | masterror | $([[ "$MASTERROR" == "true" ]] && echo "✅" || echo "—") | $([[ "$NEED_MASTERROR" == "true" ]] && echo "🔨" || echo "⏭️") | + | masterror-cli | $([[ "$CLI" == "true" ]] && echo "✅" || echo "—") | $([[ "$NEED_CLI" == "true" ]] && echo "🔨" || echo "⏭️") | + + **Legend:** ✅ = changed, 🔨 = will rebuild, ⏭️ = skipped, — = no changes + EOF + # ════════════════════════════════════════════════════════════════════════════ # STAGE 1: CHECKS (parallel matrix) # ════════════════════════════════════════════════════════════════════════════ check: name: Check (${{ matrix.rust }} / ${{ matrix.os }}) - needs: msrv + needs: [msrv, changes] + if: needs.changes.outputs.any-lib == 'true' runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -124,6 +272,8 @@ jobs: fmt: name: Format + needs: changes + if: needs.changes.outputs.any-lib == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -138,6 +288,8 @@ jobs: docs: name: Documentation + needs: changes + if: needs.changes.outputs.any-lib == 'true' runs-on: ubuntu-latest env: RUSTDOCFLAGS: -D warnings @@ -155,6 +307,8 @@ jobs: no-std: name: no_std (${{ matrix.name }}) + needs: changes + if: needs.changes.outputs.need-masterror == 'true' runs-on: ubuntu-latest strategy: fail-fast: false @@ -185,7 +339,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - name: Check ${{ matrix.name }} - run: cargo check ${{ matrix.args }} + run: cargo check -p masterror ${{ matrix.args }} security: name: Security Audit @@ -225,8 +379,16 @@ jobs: test: name: Test Suite - needs: [check, fmt, no-std, security, reuse] - if: ${{ !inputs.skip_tests }} + needs: [changes, check, fmt, no-std, security, reuse] + if: | + always() && + !inputs.skip_tests && + needs.changes.outputs.any-lib == 'true' && + (needs.check.result == 'success' || needs.check.result == 'skipped') && + (needs.fmt.result == 'success' || needs.fmt.result == 'skipped') && + (needs['no-std'].result == 'success' || needs['no-std'].result == 'skipped') && + needs.security.result == 'success' && + needs.reuse.result == 'success' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -262,7 +424,11 @@ jobs: coverage: name: Coverage - needs: test + needs: [changes, test] + if: | + always() && + needs.changes.outputs.any-lib == 'true' && + needs.test.result == 'success' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -309,9 +475,13 @@ jobs: benchmarks: name: Benchmarks - needs: test + needs: [changes, test] runs-on: ubuntu-latest - if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' + if: | + always() && + needs.changes.outputs.need-masterror == 'true' && + needs.test.result == 'success' && + (github.event_name == 'pull_request' || github.ref == 'refs/heads/main') steps: - uses: actions/checkout@v5 @@ -340,12 +510,18 @@ jobs: changelog: name: Update Changelog - needs: [check, fmt, no-std, security, reuse] + needs: [changes, check, fmt, no-std, security, reuse] runs-on: ubuntu-latest if: | + always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && - !contains(github.event.head_commit.message || '', '[skip ci]') + !contains(github.event.head_commit.message || '', '[skip ci]') && + (needs.check.result == 'success' || needs.check.result == 'skipped') && + (needs.fmt.result == 'success' || needs.fmt.result == 'skipped') && + (needs['no-std'].result == 'success' || needs['no-std'].result == 'skipped') && + needs.security.result == 'success' && + needs.reuse.result == 'success' steps: - uses: actions/checkout@v5 with: @@ -412,10 +588,12 @@ jobs: release: name: Release - needs: [test, changelog] + needs: [changes, test, changelog] if: | always() && + needs.changes.outputs.any-lib == 'true' && (needs.test.result == 'success' || needs.test.result == 'skipped') && + (needs.changelog.result == 'success' || needs.changelog.result == 'skipped') && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message || '', '[skip ci]') @@ -508,7 +686,7 @@ jobs: declare -A LOCAL REMOTE NEEDS_PUBLISH PUBLISHED_ANY=false - for crate in masterror-template masterror-derive masterror; do + for crate in $CRATES; do LOCAL[$crate]=$(get_local_version "$crate") REMOTE[$crate]=$(get_remote_version "$crate") @@ -527,16 +705,21 @@ jobs: # Dependency-aware publishing # ══════════════════════════════════════════════════════════════════ - # If derive changes, masterror should also be republished - # (it depends on derive, users need consistent versions) + # Dependency consistency warnings if [[ "${NEEDS_PUBLISH[masterror-derive]}" == "true" ]] && \ [[ "${NEEDS_PUBLISH[masterror]}" == "false" ]]; then warn "masterror-derive changed but masterror version unchanged" warn "Consider bumping masterror version for dependency consistency" fi - # Publish in order: template → derive → masterror - for crate in masterror-template masterror-derive masterror; do + if [[ "${NEEDS_PUBLISH[masterror-knowledge]}" == "true" ]] && \ + [[ "${NEEDS_PUBLISH[masterror-cli]}" == "false" ]]; then + warn "masterror-knowledge changed but masterror-cli version unchanged" + warn "Consider bumping masterror-cli version for dependency consistency" + fi + + # Publish in order: template → derive → knowledge → masterror → cli + for crate in $CRATES; do if [[ "${NEEDS_PUBLISH[$crate]}" == "true" ]]; then if publish_crate "$crate"; then PUBLISHED_ANY=true @@ -605,7 +788,9 @@ jobs: |-------|--------| | masterror-template | ${{ steps.publish.outputs.published == 'true' && '✅ Published' || '⏭️ Skipped' }} | | masterror-derive | ${{ steps.publish.outputs.published == 'true' && '✅ Published' || '⏭️ Skipped' }} | + | masterror-knowledge | ${{ steps.publish.outputs.published == 'true' && '✅ Published' || '⏭️ Skipped' }} | | masterror | ${{ steps.publish.outputs.published == 'true' && '✅ Published' || '⏭️ Skipped' }} | + | masterror-cli | ${{ steps.publish.outputs.published == 'true' && '✅ Published' || '⏭️ Skipped' }} | **Version:** `${{ steps.publish.outputs.version }}` EOF @@ -681,19 +866,48 @@ jobs: ci-success: name: CI Success - needs: [check, fmt, docs, no-std, security, reuse, test] + needs: [changes, check, fmt, docs, no-std, security, reuse, test] if: always() runs-on: ubuntu-latest steps: - name: Check all jobs run: | - results="${{ needs.check.result }} ${{ needs.fmt.result }} ${{ needs.docs.result }} ${{ needs.no-std.result }} ${{ needs.security.result }} ${{ needs.reuse.result }} ${{ needs.test.result }}" - - for r in $results; do - if [[ "$r" == "failure" ]]; then - echo "::error::One or more required jobs failed" - exit 1 - fi - done + echo "Job results:" + echo " changes: ${{ needs.changes.result }}" + echo " check: ${{ needs.check.result }}" + echo " fmt: ${{ needs.fmt.result }}" + echo " docs: ${{ needs.docs.result }}" + echo " no-std: ${{ needs['no-std'].result }}" + echo " security: ${{ needs.security.result }}" + echo " reuse: ${{ needs.reuse.result }}" + echo " test: ${{ needs.test.result }}" + + FAILED=false + + # Changes detection must succeed + [[ "${{ needs.changes.result }}" != "success" ]] && \ + echo "::error::Changes detection failed" && FAILED=true + + # Security and REUSE must always pass + [[ "${{ needs.security.result }}" == "failure" ]] && \ + echo "::error::Security audit failed" && FAILED=true + [[ "${{ needs.reuse.result }}" == "failure" ]] && \ + echo "::error::REUSE compliance failed" && FAILED=true + + # Other jobs: failure is not OK (skipped is fine) + [[ "${{ needs.check.result }}" == "failure" ]] && \ + echo "::error::Check job failed" && FAILED=true + [[ "${{ needs.fmt.result }}" == "failure" ]] && \ + echo "::error::Format job failed" && FAILED=true + [[ "${{ needs.docs.result }}" == "failure" ]] && \ + echo "::error::Docs job failed" && FAILED=true + [[ "${{ needs['no-std'].result }}" == "failure" ]] && \ + echo "::error::no-std job failed" && FAILED=true + [[ "${{ needs.test.result }}" == "failure" ]] && \ + echo "::error::Test job failed" && FAILED=true + + if [[ "$FAILED" == "true" ]]; then + exit 1 + fi - echo "✅ All CI checks passed!" + echo "✅ All CI checks passed (some may have been skipped due to no changes)" diff --git a/.hooks/pre-commit b/.hooks/pre-commit index eeacd39..ccc498a 100755 --- a/.hooks/pre-commit +++ b/.hooks/pre-commit @@ -20,6 +20,25 @@ else echo " - pip: pip install reuse" fi +echo "🔧 Linting GitHub Actions..." +if command -v actionlint &> /dev/null; then + actionlint +else + echo "⚠️ Warning: actionlint not installed, skipping Actions linting" + echo " Install with:" + echo " - Arch Linux: paru -S actionlint" + echo " - Go: go install github.com/rhysd/actionlint/cmd/actionlint@latest" + echo " - Homebrew: brew install actionlint" +fi + +echo "🔒 Checking no_std compatibility..." +cargo check --no-default-features -q +cargo check --features std -q +cargo check --no-default-features --features tracing -q +cargo check --no-default-features --features metrics -q +cargo check --no-default-features --features colored -q +cargo check --all-features -q + echo "🔍 Running clippy (all features, all targets)..." cargo clippy --workspace --all-targets --all-features -- -D warnings diff --git a/Cargo.lock b/Cargo.lock index a97000b..9187276 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,7 +156,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.1", + "socket2 0.6.2", "time", "tracing", "url", @@ -225,12 +225,56 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -258,6 +302,27 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "assert_cmd" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -435,6 +500,17 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -482,9 +558,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.53" +version = "1.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", "shlex", @@ -544,6 +620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -552,8 +629,23 @@ version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -562,6 +654,12 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "combine" version = "4.6.7" @@ -788,9 +886,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.0-rc.11" +version = "0.2.0-rc.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d2bcc93d5cde6659e8649fc412894417ebc14dee54cfc6ee439c683a4a58342" +checksum = "a6dcdb44f2c3ee25689ca12a4c19e664fd09f97aeae0bc5043b2dbab6389e308" dependencies = [ "hybrid-array", ] @@ -972,6 +1070,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.10.7" @@ -992,10 +1096,31 @@ checksum = "ca14c221bd9052fd2da7c34a2eeb5ae54732db28be47c35937be71793d675422" dependencies = [ "block-buffer 0.11.0", "const-oid 0.10.2", - "crypto-common 0.2.0-rc.11", + "crypto-common 0.2.0-rc.12", "subtle", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1185,6 +1310,15 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "flume" version = "0.11.1" @@ -1551,9 +1685,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0" +checksum = "b41fb3dc24fe72c2e3a4685eed55917c2fb228851257f4a8f2d985da9443c3e5" dependencies = [ "typenum", ] @@ -1612,7 +1746,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.2", "tokio", "tower-service", "tracing", @@ -1838,6 +1972,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -1897,9 +2037,9 @@ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" @@ -1964,7 +2104,7 @@ checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" [[package]] name = "masterror" -version = "0.27.2" +version = "0.28.0" dependencies = [ "actix-web", "anyhow", @@ -1978,6 +2118,7 @@ dependencies = [ "log", "log-mdc", "masterror-derive", + "masterror-knowledge", "masterror-template", "metrics", "owo-colors", @@ -2005,6 +2146,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "masterror-cli" +version = "0.1.0" +dependencies = [ + "assert_cmd", + "clap", + "dirs", + "masterror-knowledge", + "owo-colors", + "predicates", + "serde", + "serde_json", + "toml", +] + [[package]] name = "masterror-derive" version = "0.11.2" @@ -2015,6 +2171,24 @@ dependencies = [ "syn", ] +[[package]] +name = "masterror-knowledge" +version = "0.1.0" +dependencies = [ + "aho-corasick", + "arrayvec", +] + +[[package]] +name = "masterror-rustc" +version = "0.1.0" +dependencies = [ + "libc", + "masterror-knowledge", + "owo-colors", + "windows-sys 0.59.0", +] + [[package]] name = "masterror-template" version = "0.4.1" @@ -2122,6 +2296,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2182,9 +2362,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -2242,6 +2422,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oorandom" version = "11.1.5" @@ -2292,6 +2478,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -2532,6 +2724,36 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -2566,9 +2788,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2585,9 +2807,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -2697,7 +2919,7 @@ dependencies = [ "itoa", "percent-encoding", "ryu", - "socket2 0.6.1", + "socket2 0.6.2", "url", "xxhash-rust", ] @@ -2720,6 +2942,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -3223,9 +3456,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -3612,6 +3845,22 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "2.0.18" @@ -3643,9 +3892,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" dependencies = [ "deranged", "itoa", @@ -3658,15 +3907,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" dependencies = [ "num-conv", "time-core", @@ -3728,7 +3977,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] @@ -3826,7 +4075,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "socket2 0.6.1", + "socket2 0.6.2", "sync_wrapper", "tokio", "tokio-stream", @@ -4074,6 +4323,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "utoipa" version = "5.4.0" @@ -4157,6 +4412,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 3b7d835..3bf1709 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "masterror" -version = "0.27.2" +version = "0.28.0" rust-version = "1.92" edition = "2024" license = "MIT" @@ -42,6 +42,9 @@ include = [ members = [ "masterror-derive", "masterror-template", + "masterror-knowledge", + "masterror-cli", + "masterror-rustc", "examples/axum-rest-api", "examples/custom-domain-errors", "examples/sqlx-database", @@ -87,10 +90,12 @@ turnkey = ["std"] tonic = ["dep:tonic", "std"] openapi = ["dep:utoipa", "std"] benchmarks = ["std"] +knowledge = ["dep:masterror-knowledge"] [workspace.dependencies] masterror-derive = { version = "0.11" } masterror-template = { version = "0.4" } +masterror-knowledge = { version = "0.1" } [dependencies] masterror-derive = { version = "0.11" } @@ -147,6 +152,7 @@ tonic = { version = "0.14", optional = true } owo-colors = { version = "4", optional = true, default-features = false, features = [ "supports-colors", ] } +masterror-knowledge = { path = "masterror-knowledge", optional = true } [dev-dependencies] anyhow = { version = "1", default-features = false, features = ["std"] } @@ -200,6 +206,7 @@ feature_order = [ "tonic", "frontend", "turnkey", + "knowledge", "benchmarks", ] feature_snippet_group = 4 @@ -283,6 +290,9 @@ description = "Log to the browser console and convert to JsValue on WASM" [package.metadata.masterror.readme.features.turnkey] description = "Ship Turnkey-specific error taxonomy and conversions" +[package.metadata.masterror.readme.features.knowledge] +description = "Rust compiler error explanations and best practices (en/ru/ko)" + [package.metadata.masterror.readme.features.benchmarks] description = "Enable Criterion benchmarks and CI baseline tooling" extra = ["Primarily used for local profiling and continuous benchmarking runs"] diff --git a/README.ko.md b/README.ko.md index 0fe7d7d..16d27a2 100644 --- a/README.ko.md +++ b/README.ko.md @@ -25,6 +25,8 @@ SPDX-License-Identifier: MIT > 🇬🇧 [Read README in English](README.md) > 🇷🇺 [Читайте README на русском языке](README.ru.md) + **참고:** [masterror-cli](https://github.com/RAprogramm/masterror-cli) — Rust 컴파일러 오류를 상세한 해결책, 모범 사례 및 다국어 지원과 함께 설명하는 CLI 도구입니다. `cargo install masterror-cli` 또는 [AUR](https://aur.archlinux.org/packages/masterror-cli)에서 설치하세요. + > [!IMPORTANT] diff --git a/README.md b/README.md index bcafb01..6601cc5 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ SPDX-License-Identifier: MIT > 🇷🇺 [Читайте README на русском языке](README.ru.md) > 🇰🇷 [한국어 README](README.ko.md) + **See also:** [masterror-cli](https://github.com/RAprogramm/masterror-cli) — CLI tool that explains Rust compiler errors with detailed solutions, best practices, and multi-language support. Install via `cargo install masterror-cli` or from [AUR](https://aur.archlinux.org/packages/masterror-cli). + --- @@ -113,6 +115,7 @@ of redaction and metadata. | [`masterror`](https://crates.io/crates/masterror) | Core error types, metadata builders, transports, integrations and the prelude. | Application crates, services and libraries that want a stable error surface. | | [`masterror-derive`](masterror-derive/README.md) | Proc-macros backing `#[derive(Error)]`, `#[derive(Masterror)]`, `#[app_error]` and `#[provide]`. | Brought in automatically via `masterror`; depend directly only for macro hacking. | | [`masterror-template`](masterror-template/README.md) | Shared template parser used by the derive macros for formatter analysis. | Internal dependency; reuse when you need the template parser elsewhere. | +| [`masterror-knowledge`](masterror-knowledge/README.md) | Knowledge base with 31+ error explanations and 15 best practices in 3 languages. | Used by [masterror-cli](https://github.com/RAprogramm/masterror-cli); depend directly for custom tooling. |
` for details.");
+ }
+
+ if !status.success() {
+ match status.code() {
+ Some(code) => {
+ return Err(AppError::CargoFailed {
+ code
+ });
+ }
+ None => return Err(AppError::CargoSignaled)
+ }
+ }
+
+ Ok(())
+}
diff --git a/masterror-cli/src/commands/explain.rs b/masterror-cli/src/commands/explain.rs
new file mode 100644
index 0000000..0a6dcf7
--- /dev/null
+++ b/masterror-cli/src/commands/explain.rs
@@ -0,0 +1,174 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Explain command - explain a specific error code or best practice.
+
+use masterror_knowledge::{
+ BestPractice, ErrorEntry, ErrorRegistry, Lang, PracticeRegistry, UiMsg
+};
+use owo_colors::OwoColorize;
+
+use crate::{
+ error::{AppError, Result},
+ options::DisplayOptions
+};
+
+/// Explain a specific error code (E0382) or best practice (RA001).
+pub fn run(lang: Lang, code: &str, opts: &DisplayOptions) -> Result<()> {
+ let upper = code.to_uppercase();
+
+ if upper.starts_with("RA") {
+ let registry = PracticeRegistry::new();
+ if let Some(practice) = registry.find(&upper) {
+ print_practice(lang, practice, opts);
+ return Ok(());
+ }
+ }
+
+ let registry = ErrorRegistry::new();
+ if let Some(entry) = registry.find(code) {
+ print_error(lang, entry, opts);
+ return Ok(());
+ }
+
+ Err(AppError::UnknownErrorCode {
+ code: code.to_string()
+ })
+}
+
+fn print_error(lang: Lang, entry: &ErrorEntry, opts: &DisplayOptions) {
+ println!();
+
+ let title = entry.title.get(lang.code());
+ if opts.colored {
+ println!("{} - {}", entry.code.yellow().bold(), title.bold());
+ } else {
+ println!("{} - {title}", entry.code);
+ }
+
+ let category = entry.category.name(lang.code());
+ if opts.colored {
+ println!("{}: {}", UiMsg::Category.get(lang), category.dimmed());
+ } else {
+ println!("{}: {category}", UiMsg::Category.get(lang));
+ }
+
+ println!();
+ let why_label = UiMsg::LabelWhy.get(lang);
+ if opts.colored {
+ println!("{}", why_label.green().bold());
+ } else {
+ println!("{why_label}");
+ }
+ println!("{}", entry.explanation.get(lang.code()));
+
+ if !entry.fixes.is_empty() {
+ println!();
+ let fix_label = UiMsg::LabelFix.get(lang);
+ if opts.colored {
+ println!("{}", fix_label.green().bold());
+ } else {
+ println!("{fix_label}");
+ }
+ for (i, fix) in entry.fixes.iter().enumerate() {
+ println!();
+ println!("{}. {}", i + 1, fix.description.get(lang.code()));
+ println!("```rust");
+ println!("{}", fix.code);
+ println!("```");
+ }
+ }
+
+ if !entry.links.is_empty() {
+ println!();
+ let link_label = UiMsg::LabelLink.get(lang);
+ if opts.colored {
+ println!("{}", link_label.cyan().bold());
+ } else {
+ println!("{link_label}");
+ }
+ for link in entry.links {
+ if opts.colored {
+ println!(" - {} {}", link.title, link.url.dimmed());
+ } else {
+ println!(" - {} {}", link.title, link.url);
+ }
+ }
+ }
+
+ println!();
+}
+
+/// Print best practice details.
+pub fn print_practice(lang: Lang, practice: &BestPractice, opts: &DisplayOptions) {
+ println!();
+
+ let title = practice.title.get(lang.code());
+ if opts.colored {
+ println!("{} - {}", practice.code.yellow().bold(), title.bold());
+ } else {
+ println!("{} - {title}", practice.code);
+ }
+
+ let category = practice.category.name(lang.code());
+ if opts.colored {
+ println!("{}: {}", UiMsg::Category.get(lang), category.dimmed());
+ } else {
+ println!("{}: {category}", UiMsg::Category.get(lang));
+ }
+
+ println!();
+ let why_label = UiMsg::LabelWhyMatters.get(lang);
+ if opts.colored {
+ println!("{}", why_label.green().bold());
+ } else {
+ println!("{why_label}");
+ }
+ println!("{}", practice.explanation.get(lang.code()));
+
+ println!();
+ let how_label = UiMsg::LabelHowToApply.get(lang);
+ if opts.colored {
+ println!("{}", how_label.green().bold());
+ } else {
+ println!("{how_label}");
+ }
+
+ println!();
+ let avoid_label = UiMsg::LabelAvoid.get(lang);
+ if opts.colored {
+ println!("{}. {}", "1".cyan(), avoid_label.red());
+ } else {
+ println!("1. {avoid_label}");
+ }
+ println!("```rust");
+ println!("{}", practice.bad_example);
+ println!("```");
+
+ println!();
+ let prefer_label = UiMsg::LabelPrefer.get(lang);
+ if opts.colored {
+ println!("{}. {}", "2".cyan(), prefer_label.green());
+ } else {
+ println!("2. {prefer_label}");
+ }
+ println!("```rust");
+ println!("{}", practice.good_example);
+ println!("```");
+
+ println!();
+ let link_label = UiMsg::LabelLink.get(lang);
+ if opts.colored {
+ println!("{}", link_label.cyan().bold());
+ } else {
+ println!("{link_label}");
+ }
+ if opts.colored {
+ println!(" - RustManifest {}", practice.source.dimmed());
+ } else {
+ println!(" - RustManifest {}", practice.source);
+ }
+
+ println!();
+}
diff --git a/masterror-cli/src/commands/init.rs b/masterror-cli/src/commands/init.rs
new file mode 100644
index 0000000..59d9d0e
--- /dev/null
+++ b/masterror-cli/src/commands/init.rs
@@ -0,0 +1,295 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Init command - create configuration file interactively.
+
+use std::io::{self, Write};
+
+use masterror_knowledge::{Lang, UiMsg};
+use owo_colors::OwoColorize;
+
+use crate::{
+ config::{Config, DisplayConfig, GeneralConfig},
+ error::Result
+};
+
+/// Run init command - create configuration interactively.
+pub fn run(_lang: Lang, colored: bool) -> Result<()> {
+ println!();
+ print_welcome(colored);
+ println!();
+
+ // First ask language - this affects all subsequent prompts
+ let lang_value = prompt_language(colored)?;
+ let lang = masterror_knowledge::Lang::from_code(&lang_value);
+
+ let color_value = prompt_colors(lang, colored)?;
+ let display = prompt_display(lang, colored)?;
+ let save_location = prompt_save_location(lang, colored)?;
+
+ let config = Config {
+ general: GeneralConfig {
+ lang: lang_value,
+ colored: color_value
+ },
+ display,
+ aliases: default_aliases()
+ };
+
+ let saved_path = match save_location {
+ SaveLocation::Global => {
+ config.save()?;
+ Config::path()
+ .map(|p| p.display().to_string())
+ .unwrap_or_else(|| "~/.config/masterror/config.toml".to_string())
+ }
+ SaveLocation::Local => {
+ let path = config.save_local()?;
+ path.display().to_string()
+ }
+ };
+
+ println!();
+ print_success(lang, colored, &saved_path);
+ print_tips(lang, colored, &save_location);
+
+ Ok(())
+}
+
+/// Check if first run and prompt for setup.
+pub fn check_first_run(colored: bool) -> Result {
+ if !Config::is_first_run() {
+ return Ok(false);
+ }
+
+ println!();
+ if colored {
+ println!(
+ "{}",
+ "Welcome to masterror! Let's set up your preferences.".cyan()
+ );
+ println!(
+ "{}",
+ "(This only happens once. Run `masterror init` to reconfigure later.)".dimmed()
+ );
+ } else {
+ println!("Welcome to masterror! Let's set up your preferences.");
+ println!("(This only happens once. Run `masterror init` to reconfigure later.)");
+ }
+ println!();
+
+ run(Lang::En, colored)?;
+ Ok(true)
+}
+
+#[derive(Clone, Copy)]
+enum SaveLocation {
+ Global,
+ Local
+}
+
+fn print_welcome(colored: bool) {
+ let title = "masterror - Rust compiler error explainer";
+ let subtitle = "Let's configure your preferences";
+
+ if colored {
+ println!("{}", title.bold().cyan());
+ println!("{}", subtitle.dimmed());
+ } else {
+ println!("{title}");
+ println!("{subtitle}");
+ }
+}
+
+fn prompt_language(colored: bool) -> Result {
+ println!();
+ if colored {
+ println!(
+ "{}",
+ "Select your language / Выберите язык / 언어 선택".bold()
+ );
+ println!(" {} - English", "en".cyan());
+ println!(" {} - Русский", "ru".cyan());
+ println!(" {} - 한국어", "ko".cyan());
+ } else {
+ println!("Select your language / Выберите язык / 언어 선택");
+ println!(" en - English");
+ println!(" ru - Русский");
+ println!(" ko - 한국어");
+ }
+
+ if colored {
+ print!("{} [en/ru/ko] ({}): ", "Choice".bold(), "en".dimmed());
+ } else {
+ print!("Choice [en/ru/ko] (en): ");
+ }
+ io::stdout().flush()?;
+
+ let mut input = String::new();
+ io::stdin().read_line(&mut input)?;
+ let choice = input.trim().to_lowercase();
+
+ Ok(
+ if choice.is_empty() || !["en", "ru", "ko"].contains(&choice.as_str()) {
+ "en".to_string()
+ } else {
+ choice
+ }
+ )
+}
+
+fn prompt_colors(lang: Lang, colored: bool) -> Result {
+ let prompt = UiMsg::InitColorPrompt.get(lang);
+
+ if colored {
+ print!("{} [y/n] ({}): ", prompt.bold(), "y".dimmed());
+ } else {
+ print!("{prompt} [y/n] (y): ");
+ }
+ io::stdout().flush()?;
+
+ let mut input = String::new();
+ io::stdin().read_line(&mut input)?;
+ Ok(!matches!(input.trim().to_lowercase().as_str(), "n" | "no"))
+}
+
+fn prompt_display(lang: Lang, colored: bool) -> Result {
+ let prompt = UiMsg::InitDisplayPrompt.get(lang);
+ println!();
+ if colored {
+ println!("{}", prompt.bold());
+ } else {
+ println!("{prompt}");
+ }
+
+ let translation = prompt_bool(lang, colored, UiMsg::InitShowTranslation, true)?;
+ let why = prompt_bool(lang, colored, UiMsg::InitShowWhy, true)?;
+ let fix = prompt_bool(lang, colored, UiMsg::InitShowFix, true)?;
+ let links = prompt_bool(lang, colored, UiMsg::InitShowLinks, true)?;
+ let original = prompt_bool(lang, colored, UiMsg::InitShowOriginal, false)?;
+
+ Ok(DisplayConfig {
+ translation,
+ why,
+ fix,
+ links,
+ original
+ })
+}
+
+fn prompt_save_location(lang: Lang, colored: bool) -> Result {
+ println!();
+ let global_label = UiMsg::InitSaveGlobal.get(lang);
+ let local_label = UiMsg::InitSaveLocal.get(lang);
+
+ if colored {
+ println!("{}", UiMsg::InitSavePrompt.get(lang).bold());
+ println!(" {} - {}", "1".cyan(), global_label);
+ println!(" {} - {}", "2".cyan(), local_label);
+ } else {
+ println!("{}", UiMsg::InitSavePrompt.get(lang));
+ println!(" 1 - {global_label}");
+ println!(" 2 - {local_label}");
+ }
+
+ if colored {
+ print!("{} [1/2] ({}): ", "Choice".bold(), "1".dimmed());
+ } else {
+ print!("Choice [1/2] (1): ");
+ }
+ io::stdout().flush()?;
+
+ let mut input = String::new();
+ io::stdin().read_line(&mut input)?;
+
+ Ok(match input.trim() {
+ "2" => SaveLocation::Local,
+ _ => SaveLocation::Global
+ })
+}
+
+fn prompt_bool(lang: Lang, colored: bool, msg: UiMsg, default: bool) -> Result {
+ let label = msg.get(lang);
+ let default_str = if default { "y" } else { "n" };
+
+ if colored {
+ print!(" {} [y/n] ({}): ", label, default_str.dimmed());
+ } else {
+ print!(" {label} [y/n] ({default_str}): ");
+ }
+ io::stdout().flush()?;
+
+ let mut input = String::new();
+ io::stdin().read_line(&mut input)?;
+ let trimmed = input.trim().to_lowercase();
+
+ Ok(if trimmed.is_empty() {
+ default
+ } else {
+ matches!(trimmed.as_str(), "y" | "yes" | "д" | "да")
+ })
+}
+
+fn print_success(lang: Lang, colored: bool, path: &str) {
+ let msg = UiMsg::InitSuccess.get(lang);
+
+ if colored {
+ println!("{} {}", msg.green(), path.dimmed());
+ } else {
+ println!("{msg} {path}");
+ }
+}
+
+fn print_tips(lang: Lang, colored: bool, location: &SaveLocation) {
+ println!();
+ let tip = UiMsg::InitTip.get(lang);
+
+ if colored {
+ println!("{}", tip.dimmed());
+ } else {
+ println!("{tip}");
+ }
+
+ match location {
+ SaveLocation::Global => {
+ let global_path = Config::path()
+ .map(|p| p.display().to_string())
+ .unwrap_or_else(|| "~/.config/masterror/config.toml".to_string());
+ if colored {
+ println!(" {} {}", "Global:".dimmed(), global_path.dimmed());
+ } else {
+ println!(" Global: {global_path}");
+ }
+ }
+ SaveLocation::Local => {
+ if colored {
+ println!(" {} .masterror.toml", "Local:".dimmed());
+ } else {
+ println!(" Local: .masterror.toml");
+ }
+ }
+ }
+
+ println!();
+ let usage = UiMsg::InitUsage.get(lang);
+ if colored {
+ println!("{}", usage.cyan());
+ println!(" {} cargo masterror check", "$".dimmed());
+ println!(" {} masterror explain E0382", "$".dimmed());
+ } else {
+ println!("{usage}");
+ println!(" $ cargo masterror check");
+ println!(" $ masterror explain E0382");
+ }
+ println!();
+}
+
+fn default_aliases() -> std::collections::HashMap {
+ let mut aliases = std::collections::HashMap::new();
+ aliases.insert("c".to_string(), "check".to_string());
+ aliases.insert("e".to_string(), "explain".to_string());
+ aliases.insert("l".to_string(), "list".to_string());
+ aliases.insert("p".to_string(), "practice".to_string());
+ aliases
+}
diff --git a/masterror-cli/src/commands/list.rs b/masterror-cli/src/commands/list.rs
new file mode 100644
index 0000000..6c4436e
--- /dev/null
+++ b/masterror-cli/src/commands/list.rs
@@ -0,0 +1,89 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! List command - list all known error codes.
+
+use masterror_knowledge::{Category, ErrorRegistry, Lang};
+use owo_colors::OwoColorize;
+
+use crate::{
+ error::{AppError, Result},
+ options::DisplayOptions
+};
+
+/// List all known error codes.
+pub fn run(lang: Lang, category: Option<&str>, opts: &DisplayOptions) -> Result<()> {
+ let registry = ErrorRegistry::new();
+
+ println!();
+ if opts.colored {
+ println!("{}", "Known Rust Compiler Errors".bold());
+ } else {
+ println!("Known Rust Compiler Errors");
+ }
+ println!();
+
+ let mut entries: Vec<_> = if let Some(cat) = category {
+ let cat = parse_category(cat);
+ if let Some(c) = cat {
+ registry.by_category(c)
+ } else {
+ return Err(AppError::InvalidCategory {
+ name: category.unwrap_or("").to_string()
+ });
+ }
+ } else {
+ registry.all().collect()
+ };
+
+ if entries.is_empty() {
+ println!(" No errors found.");
+ return Ok(());
+ }
+
+ entries.sort_by_key(|e| e.code);
+
+ let mut current_cat: Option = None;
+ for entry in &entries {
+ if current_cat != Some(entry.category) {
+ current_cat = Some(entry.category);
+ println!();
+ let cat_name = entry.category.name(lang.code());
+ if opts.colored {
+ println!(" {}", cat_name.yellow().bold());
+ } else {
+ println!(" {cat_name}");
+ }
+ println!();
+ }
+
+ let title = entry.title.get(lang.code());
+ if opts.colored {
+ println!(" {} - {title}", entry.code.cyan());
+ } else {
+ println!(" {} - {title}", entry.code);
+ }
+ }
+
+ println!();
+ println!("Total: {} errors", entries.len());
+ println!();
+ println!("Use `masterror explain ` to see details.");
+ println!("Use `masterror practice` to see best practices.");
+ println!();
+
+ Ok(())
+}
+
+fn parse_category(s: &str) -> Option {
+ match s.to_lowercase().as_str() {
+ "ownership" | "own" => Some(Category::Ownership),
+ "borrowing" | "borrow" => Some(Category::Borrowing),
+ "lifetimes" | "lifetime" | "life" => Some(Category::Lifetimes),
+ "types" | "type" => Some(Category::Types),
+ "traits" | "trait" => Some(Category::Traits),
+ "resolution" | "resolve" | "names" => Some(Category::Resolution),
+ _ => None
+ }
+}
diff --git a/masterror-cli/src/commands/mod.rs b/masterror-cli/src/commands/mod.rs
new file mode 100644
index 0000000..7f2c260
--- /dev/null
+++ b/masterror-cli/src/commands/mod.rs
@@ -0,0 +1,16 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! CLI commands.
+
+mod check;
+mod explain;
+pub mod init;
+mod list;
+pub mod practice;
+
+pub use check::run as check;
+pub use explain::run as explain;
+pub use init::run as init;
+pub use list::run as list;
diff --git a/masterror-cli/src/commands/practice.rs b/masterror-cli/src/commands/practice.rs
new file mode 100644
index 0000000..e3ed893
--- /dev/null
+++ b/masterror-cli/src/commands/practice.rs
@@ -0,0 +1,106 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Practice command - show best practices from RustManifest.
+
+use masterror_knowledge::{Lang, PracticeCategory, PracticeRegistry};
+use owo_colors::OwoColorize;
+
+use super::explain::print_practice;
+use crate::{
+ error::{AppError, Result},
+ options::DisplayOptions
+};
+
+/// List all best practices or filter by category.
+pub fn list(lang: Lang, category: Option<&str>, opts: &DisplayOptions) -> Result<()> {
+ let registry = PracticeRegistry::new();
+
+ println!();
+ if opts.colored {
+ println!("{}", "RustManifest Best Practices".bold());
+ } else {
+ println!("RustManifest Best Practices");
+ }
+ println!();
+
+ let practices: Vec<_> = if let Some(cat) = category {
+ let cat = parse_category(cat);
+ if let Some(c) = cat {
+ registry.by_category(c)
+ } else {
+ return Err(AppError::InvalidCategory {
+ name: category.unwrap_or("").to_string()
+ });
+ }
+ } else {
+ registry.all().collect()
+ };
+
+ if practices.is_empty() {
+ println!(" No practices found.");
+ return Ok(());
+ }
+
+ let mut sorted = practices;
+ sorted.sort_by_key(|p| p.code);
+
+ let mut current_cat: Option = None;
+ for practice in &sorted {
+ if current_cat != Some(practice.category) {
+ current_cat = Some(practice.category);
+ println!();
+ if opts.colored {
+ println!(" {}", practice.category.name(lang.code()).yellow().bold());
+ } else {
+ println!(" {}", practice.category.name(lang.code()));
+ }
+ println!();
+ }
+
+ let title = practice.title.get(lang.code());
+ if opts.colored {
+ println!(" {} - {title}", practice.code.cyan());
+ } else {
+ println!(" {} - {title}", practice.code);
+ }
+ }
+
+ println!();
+ println!("Total: {} practices", sorted.len());
+ println!();
+ println!("Use `masterror practice ` to see details.");
+ println!();
+
+ Ok(())
+}
+
+/// Show a specific best practice.
+pub fn show(lang: Lang, code: &str, opts: &DisplayOptions) -> Result<()> {
+ let registry = PracticeRegistry::new();
+
+ let Some(practice) = registry.find(code) else {
+ return Err(AppError::UnknownPracticeCode {
+ code: code.to_string()
+ });
+ };
+
+ print_practice(lang, practice, opts);
+ Ok(())
+}
+
+fn parse_category(s: &str) -> Option {
+ match s.to_lowercase().as_str() {
+ "error-handling" | "error_handling" | "errorhandling" | "errors" => {
+ Some(PracticeCategory::ErrorHandling)
+ }
+ "performance" | "perf" => Some(PracticeCategory::Performance),
+ "naming" | "names" => Some(PracticeCategory::Naming),
+ "documentation" | "docs" | "doc" => Some(PracticeCategory::Documentation),
+ "design" | "architecture" | "arch" => Some(PracticeCategory::Design),
+ "testing" | "tests" | "test" => Some(PracticeCategory::Testing),
+ "security" | "sec" => Some(PracticeCategory::Security),
+ _ => None
+ }
+}
diff --git a/masterror-cli/src/config.rs b/masterror-cli/src/config.rs
new file mode 100644
index 0000000..b9b0cb0
--- /dev/null
+++ b/masterror-cli/src/config.rs
@@ -0,0 +1,203 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Configuration management for masterror-cli.
+//!
+//! Supports layered configuration (highest priority first):
+//! 1. CLI arguments
+//! 2. Environment variables
+//! 3. Local project config (.masterror.toml in current directory)
+//! 4. Global config (~/.config/masterror/config.toml)
+//! 5. Default values
+
+use std::{collections::HashMap, env, fs, path::PathBuf};
+
+use serde::{Deserialize, Serialize};
+
+use crate::error::{AppError, Result};
+
+/// Global config file name.
+const CONFIG_FILE: &str = "config.toml";
+
+/// Local project config file name.
+const LOCAL_CONFIG_FILE: &str = ".masterror.toml";
+
+/// Config directory name.
+const CONFIG_DIR: &str = "masterror";
+
+/// Application configuration.
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(default)]
+pub struct Config {
+ /// General settings.
+ pub general: GeneralConfig,
+ /// Display settings.
+ pub display: DisplayConfig,
+ /// Command aliases.
+ pub aliases: HashMap
+}
+
+/// General configuration options.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(default)]
+pub struct GeneralConfig {
+ /// Language code (en, ru, ko).
+ pub lang: String,
+ /// Enable colored output.
+ pub colored: bool
+}
+
+/// Display section toggles.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(default)]
+pub struct DisplayConfig {
+ /// Show translated error message.
+ pub translation: bool,
+ /// Show "why this happens" explanation.
+ pub why: bool,
+ /// Show fix suggestions.
+ pub fix: bool,
+ /// Show documentation links.
+ pub links: bool,
+ /// Show original compiler output.
+ pub original: bool
+}
+
+impl Default for GeneralConfig {
+ fn default() -> Self {
+ Self {
+ lang: "en".to_string(),
+ colored: true
+ }
+ }
+}
+
+impl Default for DisplayConfig {
+ fn default() -> Self {
+ Self {
+ translation: true,
+ why: true,
+ fix: true,
+ links: true,
+ original: false
+ }
+ }
+}
+
+impl Config {
+ /// Get global config directory path.
+ pub fn dir() -> Option {
+ dirs::config_dir().map(|p| p.join(CONFIG_DIR))
+ }
+
+ /// Get global config file path.
+ pub fn path() -> Option {
+ Self::dir().map(|p| p.join(CONFIG_FILE))
+ }
+
+ /// Get local project config path (.masterror.toml in current directory).
+ pub fn local_path() -> Option {
+ env::current_dir().ok().map(|p| p.join(LOCAL_CONFIG_FILE))
+ }
+
+ /// Check if this is the first run (no global config exists).
+ pub fn is_first_run() -> bool {
+ Self::path().is_none_or(|p| !p.exists())
+ }
+
+ /// Load config with layered priority:
+ /// 1. Local .masterror.toml (if exists)
+ /// 2. Global ~/.config/masterror/config.toml
+ /// 3. Defaults
+ pub fn load() -> Result {
+ // Try local config first
+ if let Some(local_path) = Self::local_path()
+ && local_path.exists()
+ {
+ let content = fs::read_to_string(&local_path)?;
+ let config: Config = toml::from_str(&content).map_err(|e| AppError::ConfigParse {
+ path: local_path.clone(),
+ message: e.message().to_string()
+ })?;
+ return Ok(config);
+ }
+
+ // Try global config
+ let Some(path) = Self::path() else {
+ return Ok(Self::default());
+ };
+
+ if !path.exists() {
+ return Ok(Self::default());
+ }
+
+ let content = fs::read_to_string(&path)?;
+ let config: Config = toml::from_str(&content).map_err(|e| AppError::ConfigParse {
+ path: path.clone(),
+ message: e.message().to_string()
+ })?;
+
+ Ok(config)
+ }
+
+ /// Save config to global file.
+ pub fn save(&self) -> Result<()> {
+ let Some(dir) = Self::dir() else {
+ return Err(AppError::Io(std::io::Error::other(
+ "could not determine config directory"
+ )));
+ };
+
+ fs::create_dir_all(&dir)?;
+
+ let path = dir.join(CONFIG_FILE);
+ let content = toml::to_string_pretty(self).map_err(|e| AppError::ConfigParse {
+ path: path.clone(),
+ message: e.to_string()
+ })?;
+
+ fs::write(&path, content)?;
+ Ok(())
+ }
+
+ /// Save config to local project file (.masterror.toml).
+ pub fn save_local(&self) -> Result {
+ let path = env::current_dir()?.join(LOCAL_CONFIG_FILE);
+ let content = toml::to_string_pretty(self).map_err(|e| AppError::ConfigParse {
+ path: path.clone(),
+ message: e.to_string()
+ })?;
+
+ fs::write(&path, &content)?;
+ Ok(path)
+ }
+
+ /// Resolve command alias.
+ #[allow(dead_code)]
+ pub fn resolve_alias<'a>(&'a self, cmd: &'a str) -> &'a str {
+ self.aliases.get(cmd).map(String::as_str).unwrap_or(cmd)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_default_config() {
+ let config = Config::default();
+ assert_eq!(config.general.lang, "en");
+ assert!(config.general.colored);
+ assert!(config.display.why);
+ assert!(config.display.fix);
+ }
+
+ #[test]
+ fn test_toml_roundtrip() {
+ let config = Config::default();
+ let toml = toml::to_string_pretty(&config).unwrap();
+ let parsed: Config = toml::from_str(&toml).unwrap();
+ assert_eq!(parsed.general.lang, config.general.lang);
+ }
+}
diff --git a/masterror-cli/src/error.rs b/masterror-cli/src/error.rs
new file mode 100644
index 0000000..bfeb345
--- /dev/null
+++ b/masterror-cli/src/error.rs
@@ -0,0 +1,169 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Application error types for masterror-cli.
+
+use std::{fmt, io, path::PathBuf};
+
+/// Application-wide error type.
+#[derive(Debug)]
+pub enum AppError {
+ /// I/O error (file operations, process spawning).
+ Io(io::Error),
+ /// JSON parsing error from cargo output.
+ Json(serde_json::Error),
+ /// Cargo check process failed with exit code.
+ CargoFailed {
+ /// Exit code from cargo process.
+ code: i32
+ },
+ /// Cargo check process was terminated by signal.
+ CargoSignaled,
+ /// Unknown error code requested.
+ UnknownErrorCode {
+ /// The requested error code.
+ code: String
+ },
+ /// Unknown practice code requested.
+ UnknownPracticeCode {
+ /// The requested practice code.
+ code: String
+ },
+ /// Invalid category name.
+ InvalidCategory {
+ /// The invalid category name.
+ name: String
+ },
+ /// Invalid command-line argument.
+ InvalidArgument {
+ /// The invalid argument.
+ arg: String
+ },
+ /// Config file parse error.
+ ConfigParse {
+ /// Path to config file.
+ path: PathBuf,
+ /// Error message.
+ message: String
+ },
+ /// Error with additional context.
+ #[allow(dead_code)]
+ WithContext {
+ /// Context message.
+ context: String,
+ /// Original error.
+ source: Box
+ }
+}
+
+impl fmt::Display for AppError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Io(e) => write!(f, "I/O error: {e}"),
+ Self::Json(e) => write!(f, "JSON parse error: {e}"),
+ Self::CargoFailed {
+ code
+ } => write!(f, "cargo check failed with exit code {code}"),
+ Self::CargoSignaled => write!(f, "cargo check was terminated by signal"),
+ Self::UnknownErrorCode {
+ code
+ } => write!(f, "unknown error code: {code}"),
+ Self::UnknownPracticeCode {
+ code
+ } => write!(f, "unknown practice code: {code}"),
+ Self::InvalidCategory {
+ name
+ } => write!(f, "invalid category: {name}"),
+ Self::InvalidArgument {
+ arg
+ } => write!(f, "invalid argument: {arg}"),
+ Self::ConfigParse {
+ path,
+ message
+ } => write!(f, "config error in {}: {message}", path.display()),
+ Self::WithContext {
+ context,
+ source
+ } => write!(f, "{context}: {source}")
+ }
+ }
+}
+
+impl std::error::Error for AppError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ Self::Io(e) => Some(e),
+ Self::Json(e) => Some(e),
+ Self::WithContext {
+ source, ..
+ } => Some(source.as_ref()),
+ _ => None
+ }
+ }
+}
+
+impl From for AppError {
+ fn from(err: io::Error) -> Self {
+ Self::Io(err)
+ }
+}
+
+impl From for AppError {
+ fn from(err: serde_json::Error) -> Self {
+ Self::Json(err)
+ }
+}
+
+/// Result type alias for AppError.
+pub type Result = std::result::Result;
+
+/// Extension trait for adding context to Results.
+///
+/// # Example
+///
+/// ```ignore
+/// use masterror::error::ResultExt;
+///
+/// fs::read_to_string(path).context("reading config")?;
+/// ```
+#[allow(dead_code)]
+pub trait ResultExt {
+ /// Add static context to an error.
+ fn context(self, ctx: &'static str) -> Result;
+
+ /// Add dynamic context to an error.
+ fn with_context(self, f: F) -> Result
+ where
+ F: FnOnce() -> S,
+ S: Into;
+}
+
+impl ResultExt for std::result::Result
+where
+ E: Into
+{
+ fn context(self, ctx: &'static str) -> Result {
+ self.map_err(|e| {
+ let inner = e.into();
+ AppError::WithContext {
+ context: ctx.to_string(),
+ source: Box::new(inner)
+ }
+ })
+ }
+
+ fn with_context(self, f: F) -> Result
+ where
+ F: FnOnce() -> S,
+ S: Into
+ {
+ self.map_err(|e| {
+ let inner = e.into();
+ AppError::WithContext {
+ context: f().into(),
+ source: Box::new(inner)
+ }
+ })
+ }
+}
diff --git a/masterror-cli/src/main.rs b/masterror-cli/src/main.rs
new file mode 100644
index 0000000..47b006a
--- /dev/null
+++ b/masterror-cli/src/main.rs
@@ -0,0 +1,131 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! masterror CLI - Rust compiler error explainer.
+
+mod commands;
+mod config;
+mod error;
+mod options;
+mod output;
+mod parser;
+mod sections;
+
+use clap::{Parser, Subcommand};
+use masterror_knowledge::Lang;
+
+use crate::{config::Config, options::DisplayOptions};
+
+#[derive(Parser)]
+#[command(name = "masterror")]
+#[command(author, version, about = "Rust compiler error explainer")]
+struct Cli {
+ /// Language for explanations (en, ru, ko)
+ #[arg(short, long, env = "MASTERROR_LANG")]
+ lang: Option,
+
+ /// Disable colored output
+ #[arg(long, env = "NO_COLOR")]
+ no_color: bool,
+
+ #[command(subcommand)]
+ command: Commands
+}
+
+#[derive(Subcommand)]
+enum Commands {
+ /// Initialize configuration file
+ Init,
+ /// Run cargo check and explain errors
+ #[command(visible_alias = "c")]
+ Check {
+ #[arg(trailing_var_arg = true)]
+ args: Vec
+ },
+ /// Explain a specific error code (E0382) or best practice (RA001)
+ #[command(visible_alias = "e")]
+ Explain { code: String },
+ /// List all known error codes
+ #[command(visible_alias = "l")]
+ List {
+ #[arg(short, long)]
+ category: Option
+ },
+ /// Show RustManifest best practices
+ #[command(visible_alias = "p")]
+ Practice {
+ /// Practice code (RA001-RA015) or empty for list
+ code: Option,
+ /// Filter by category
+ #[arg(short, long)]
+ category: Option
+ }
+}
+
+fn main() {
+ let args: Vec = std::env::args().collect();
+ let cli = if args.get(1).is_some_and(|a| a == "masterror") {
+ Cli::parse_from(
+ args.into_iter()
+ .enumerate()
+ .filter_map(|(i, a)| if i == 1 { None } else { Some(a) })
+ )
+ } else {
+ Cli::parse()
+ };
+
+ // Check for first run and run setup if needed (except for init command)
+ if !matches!(cli.command, Commands::Init)
+ && let Err(e) = commands::init::check_first_run(true)
+ {
+ eprintln!("Setup failed: {e}");
+ std::process::exit(1);
+ }
+
+ let config = Config::load().unwrap_or_default();
+ let lang_str = cli.lang.as_deref().unwrap_or(&config.general.lang);
+ let lang = Lang::from_code(lang_str);
+ let colored = if cli.no_color {
+ false
+ } else {
+ config.general.colored
+ };
+
+ let opts = DisplayOptions {
+ colored,
+ show_translation: config.display.translation,
+ show_why: config.display.why,
+ show_fix: config.display.fix,
+ show_links: config.display.links,
+ show_original: config.display.original
+ };
+
+ let result = match &cli.command {
+ Commands::Init => commands::init(lang, opts.colored),
+ Commands::Check {
+ args
+ } => commands::check(lang, args, &opts),
+ Commands::Explain {
+ code
+ } => commands::explain(lang, code, &opts),
+ Commands::List {
+ category
+ } => commands::list(lang, category.as_deref(), &opts),
+ Commands::Practice {
+ code,
+ category
+ } => {
+ if let Some(c) = code {
+ commands::practice::show(lang, c, &opts)
+ } else {
+ commands::practice::list(lang, category.as_deref(), &opts)
+ }
+ }
+ };
+
+ if let Err(e) = result {
+ eprintln!("Error: {e}");
+ std::process::exit(1);
+ }
+}
diff --git a/masterror-cli/src/options.rs b/masterror-cli/src/options.rs
new file mode 100644
index 0000000..8256a87
--- /dev/null
+++ b/masterror-cli/src/options.rs
@@ -0,0 +1,138 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Display options for masterror output.
+
+/// What sections to show in masterror block.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct DisplayOptions {
+ /// Enable colored output.
+ pub colored: bool,
+ /// Show translated error message.
+ pub show_translation: bool,
+ /// Show "why this happens" explanation.
+ pub show_why: bool,
+ /// Show fix suggestions.
+ pub show_fix: bool,
+ /// Show documentation links.
+ pub show_links: bool,
+ /// Show original compiler output.
+ pub show_original: bool
+}
+
+impl DisplayOptions {
+ /// Default options as const value.
+ pub const DEFAULT: Self = Self {
+ colored: true,
+ show_translation: true,
+ show_why: true,
+ show_fix: true,
+ show_links: true,
+ show_original: false
+ };
+
+ /// Create new builder for constructing DisplayOptions.
+ #[allow(dead_code)]
+ pub const fn builder() -> DisplayOptionsBuilder {
+ DisplayOptionsBuilder::new()
+ }
+}
+
+impl Default for DisplayOptions {
+ fn default() -> Self {
+ Self::DEFAULT
+ }
+}
+
+/// Builder for constructing DisplayOptions with const support.
+///
+/// # Example
+///
+/// ```ignore
+/// use masterror_cli::options::DisplayOptions;
+///
+/// const OPTS: DisplayOptions = DisplayOptions::builder()
+/// .colored(false)
+/// .show_original(true)
+/// .build();
+/// ```
+#[derive(Clone, Copy, Debug)]
+#[allow(dead_code)]
+pub struct DisplayOptionsBuilder {
+ colored: bool,
+ show_translation: bool,
+ show_why: bool,
+ show_fix: bool,
+ show_links: bool,
+ show_original: bool
+}
+
+#[allow(dead_code)]
+impl DisplayOptionsBuilder {
+ /// Create new builder with default values.
+ pub const fn new() -> Self {
+ Self {
+ colored: true,
+ show_translation: true,
+ show_why: true,
+ show_fix: true,
+ show_links: true,
+ show_original: false
+ }
+ }
+
+ /// Set colored output.
+ pub const fn colored(mut self, value: bool) -> Self {
+ self.colored = value;
+ self
+ }
+
+ /// Set show translation.
+ pub const fn show_translation(mut self, value: bool) -> Self {
+ self.show_translation = value;
+ self
+ }
+
+ /// Set show why explanation.
+ pub const fn show_why(mut self, value: bool) -> Self {
+ self.show_why = value;
+ self
+ }
+
+ /// Set show fix suggestions.
+ pub const fn show_fix(mut self, value: bool) -> Self {
+ self.show_fix = value;
+ self
+ }
+
+ /// Set show documentation links.
+ pub const fn show_links(mut self, value: bool) -> Self {
+ self.show_links = value;
+ self
+ }
+
+ /// Set show original compiler output.
+ pub const fn show_original(mut self, value: bool) -> Self {
+ self.show_original = value;
+ self
+ }
+
+ /// Build the DisplayOptions.
+ pub const fn build(self) -> DisplayOptions {
+ DisplayOptions {
+ colored: self.colored,
+ show_translation: self.show_translation,
+ show_why: self.show_why,
+ show_fix: self.show_fix,
+ show_links: self.show_links,
+ show_original: self.show_original
+ }
+ }
+}
+
+impl Default for DisplayOptionsBuilder {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/masterror-cli/src/output.rs b/masterror-cli/src/output.rs
new file mode 100644
index 0000000..4e0167c
--- /dev/null
+++ b/masterror-cli/src/output.rs
@@ -0,0 +1,78 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Terminal output formatting for errors.
+
+use masterror_knowledge::{ErrorEntry, ErrorRegistry, Lang, UiMsg};
+use owo_colors::OwoColorize;
+
+use crate::{options::DisplayOptions, parser::CargoMessage, sections};
+
+const SEPARATOR: &str = "--- masterror ----------------------------------------";
+const SEPARATOR_END: &str = "------------------------------------------------------";
+
+/// Print error with masterror explanation.
+pub fn print_error(lang: Lang, msg: &CargoMessage, opts: &DisplayOptions) {
+ let rendered = msg.rendered_output();
+
+ if opts.show_original
+ && let Some(r) = rendered
+ {
+ print!("{}", r.trim_end());
+ }
+
+ let Some(code) = msg.error_code() else {
+ if opts.show_original {
+ println!();
+ }
+ return;
+ };
+
+ let registry = ErrorRegistry::new();
+ let Some(entry) = registry.find(code) else {
+ if opts.show_original {
+ println!();
+ }
+ return;
+ };
+
+ println!();
+ print_block(lang, entry, rendered, opts);
+}
+
+fn print_block(lang: Lang, entry: &ErrorEntry, rendered: Option<&str>, opts: &DisplayOptions) {
+ if opts.colored {
+ println!("{}", SEPARATOR.dimmed());
+ } else {
+ println!("{SEPARATOR}");
+ }
+
+ if opts.show_translation {
+ sections::translation::print(lang, rendered, opts.colored);
+ }
+
+ if opts.show_why {
+ let label = UiMsg::LabelWhy.get(lang);
+ if opts.colored {
+ println!("{}", label.green().bold());
+ } else {
+ println!("{label}");
+ }
+ println!("{}", entry.explanation.get(lang.code()));
+ }
+
+ if opts.show_fix {
+ sections::fix::print(lang, entry.fixes, opts.colored);
+ }
+
+ if opts.show_links {
+ sections::link::print(lang, entry.links, opts.colored);
+ }
+
+ if opts.colored {
+ println!("{}", SEPARATOR_END.dimmed());
+ } else {
+ println!("{SEPARATOR_END}");
+ }
+}
diff --git a/masterror-cli/src/parser.rs b/masterror-cli/src/parser.rs
new file mode 100644
index 0000000..6b25c8f
--- /dev/null
+++ b/masterror-cli/src/parser.rs
@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Cargo JSON output parser.
+
+use serde::Deserialize;
+
+/// Top-level cargo message.
+#[derive(Deserialize)]
+pub struct CargoMessage {
+ pub reason: String,
+ pub message: Option,
+ /// Full rendered compiler output.
+ pub rendered: Option
+}
+
+/// Compiler diagnostic message.
+#[derive(Deserialize)]
+pub struct DiagnosticMessage {
+ pub level: String,
+ #[allow(dead_code)]
+ pub message: String,
+ pub code: Option,
+ pub rendered: Option
+}
+
+/// Error code info.
+#[derive(Deserialize)]
+pub struct DiagnosticCode {
+ pub code: String
+}
+
+impl CargoMessage {
+ /// Check if this is a compiler error message.
+ pub fn is_error(&self) -> bool {
+ self.reason == "compiler-message"
+ && self.message.as_ref().is_some_and(|m| m.level == "error")
+ }
+
+ /// Get the error code if present.
+ pub fn error_code(&self) -> Option<&str> {
+ self.message
+ .as_ref()
+ .and_then(|m| m.code.as_ref())
+ .map(|c| c.code.as_str())
+ }
+
+ /// Get the error message.
+ #[allow(dead_code)]
+ pub fn error_message(&self) -> Option<&str> {
+ self.message.as_ref().map(|m| m.message.as_str())
+ }
+
+ /// Get rendered output (from message or top-level).
+ pub fn rendered_output(&self) -> Option<&str> {
+ self.message
+ .as_ref()
+ .and_then(|m| m.rendered.as_deref())
+ .or(self.rendered.as_deref())
+ }
+}
diff --git a/masterror-cli/src/sections/fix.rs b/masterror-cli/src/sections/fix.rs
new file mode 100644
index 0000000..113c62d
--- /dev/null
+++ b/masterror-cli/src/sections/fix.rs
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Fix section - shows fix suggestions with code examples.
+
+use masterror_knowledge::{FixSuggestion, Lang, UiMsg};
+use owo_colors::OwoColorize;
+
+/// Print fix suggestions with code examples.
+pub fn print(lang: Lang, fixes: &[FixSuggestion], colored: bool) {
+ if fixes.is_empty() {
+ return;
+ }
+
+ let label = UiMsg::LabelFix.get(lang);
+
+ if colored {
+ println!("{}", label.green().bold());
+ } else {
+ println!("{label}");
+ }
+
+ for (i, fix) in fixes.iter().enumerate() {
+ let desc = fix.description.get(lang.code());
+ let num = i + 1;
+ if colored {
+ println!(" {}. {}", num.cyan(), desc);
+ println!(" {}", fix.code.dimmed());
+ } else {
+ println!(" {num}. {desc}");
+ println!(" {}", fix.code);
+ }
+ }
+}
diff --git a/masterror-cli/src/sections/link.rs b/masterror-cli/src/sections/link.rs
new file mode 100644
index 0000000..d8e8843
--- /dev/null
+++ b/masterror-cli/src/sections/link.rs
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Link section - shows documentation URLs.
+
+use masterror_knowledge::{DocLink, Lang, UiMsg};
+use owo_colors::OwoColorize;
+
+/// Print documentation links with titles.
+pub fn print(lang: Lang, links: &[DocLink], colored: bool) {
+ if links.is_empty() {
+ return;
+ }
+
+ let label = UiMsg::LabelLink.get(lang);
+
+ if colored {
+ println!("{}", label.blue().bold());
+ } else {
+ println!("{label}");
+ }
+
+ for link in links {
+ if colored {
+ println!(" {} {}", link.title.cyan(), link.url.underline().dimmed());
+ } else {
+ println!(" {} {}", link.title, link.url);
+ }
+ }
+}
diff --git a/masterror-cli/src/sections/mod.rs b/masterror-cli/src/sections/mod.rs
new file mode 100644
index 0000000..82c32d3
--- /dev/null
+++ b/masterror-cli/src/sections/mod.rs
@@ -0,0 +1,9 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Output sections for masterror block.
+
+pub mod fix;
+pub mod link;
+pub mod translation;
diff --git a/masterror-cli/src/sections/translation.rs b/masterror-cli/src/sections/translation.rs
new file mode 100644
index 0000000..c9a4758
--- /dev/null
+++ b/masterror-cli/src/sections/translation.rs
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Translation section - shows full translated compiler error.
+
+use masterror_knowledge::{Lang, UiMsg, phrases::translate_rendered};
+use owo_colors::OwoColorize;
+
+/// Print full translated copy of compiler error.
+pub fn print(lang: Lang, rendered: Option<&str>, colored: bool) {
+ if matches!(lang, Lang::En) {
+ return;
+ }
+
+ let Some(rendered) = rendered else {
+ return;
+ };
+
+ let translated = translate_rendered(rendered, lang);
+
+ let label = UiMsg::LabelTranslation.get(lang);
+
+ if colored {
+ println!("{}", label.cyan().bold());
+ } else {
+ println!("{label}");
+ }
+
+ for line in translated.lines() {
+ println!(" {line}");
+ }
+}
diff --git a/masterror-knowledge/Cargo.toml b/masterror-knowledge/Cargo.toml
new file mode 100644
index 0000000..fce37d1
--- /dev/null
+++ b/masterror-knowledge/Cargo.toml
@@ -0,0 +1,25 @@
+# SPDX-FileCopyrightText: 2025-2026 RAprogramm
+#
+# SPDX-License-Identifier: MIT
+
+[package]
+name = "masterror-knowledge"
+version = "0.1.0"
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "Knowledge base for Rust compiler errors and best practices"
+keywords = ["rust", "compiler", "errors", "explain", "learning"]
+categories = ["development-tools", "command-line-utilities"]
+
+[features]
+default = ["lang-ru", "lang-ko"]
+
+# Languages
+lang-ru = []
+lang-ko = []
+
+[dependencies]
+arrayvec = "0.7"
+aho-corasick = "1"
diff --git a/masterror-knowledge/README.md b/masterror-knowledge/README.md
new file mode 100644
index 0000000..bf83138
--- /dev/null
+++ b/masterror-knowledge/README.md
@@ -0,0 +1,88 @@
+
+
+
+
+
+ masterror-knowledge
+ Knowledge base for Rust compiler errors and best practices
+
+ [](https://crates.io/crates/masterror-knowledge)
+ [](https://docs.rs/masterror-knowledge)
+ 
+
+
+---
+
+## Overview
+
+`masterror-knowledge` provides a comprehensive knowledge base of Rust compiler error explanations and best practices. It powers the [masterror-cli](https://github.com/RAprogramm/masterror-cli) tool, enabling developers to quickly understand and fix compiler errors.
+
+## Features
+
+- **31+ Error Explanations** — Detailed explanations for common Rust compiler errors (E0001-E0792)
+- **15 RustManifest Best Practices** — Guidelines for writing idiomatic Rust code
+- **Multi-language Support** — Available in English, Russian, and Korean
+- **Zero Dependencies** — Lightweight, no runtime overhead
+- **Compile-time Lookup** — Fast pattern matching using Aho-Corasick algorithm
+
+## Supported Languages
+
+| Feature | Language |
+|---------|----------|
+| (default) | English |
+| `lang-ru` | Russian |
+| `lang-ko` | Korean |
+
+## Installation
+
+```toml
+[dependencies]
+masterror-knowledge = "0.1"
+```
+
+With additional languages:
+
+```toml
+[dependencies]
+masterror-knowledge = { version = "0.1", features = ["lang-ru", "lang-ko"] }
+```
+
+## Usage
+
+```rust
+use masterror_knowledge::{Lang, lookup_error, lookup_practice};
+
+// Get explanation for error E0382 (borrow of moved value)
+if let Some(explanation) = lookup_error("E0382", Lang::En) {
+ println!("{}", explanation);
+}
+
+// Get best practice by ID
+if let Some(practice) = lookup_practice("RM001", Lang::En) {
+ println!("{}", practice);
+}
+```
+
+## Error Codes Covered
+
+The knowledge base includes explanations for errors related to:
+
+- **Ownership & Borrowing** — E0382, E0499, E0502, E0505, E0507
+- **Lifetimes** — E0106, E0621, E0759, E0792
+- **Type System** — E0277, E0308, E0412, E0425
+- **Traits** — E0046, E0119, E0277
+- **Patterns** — E0004, E0005, E0026, E0027
+- **And more...**
+
+## Related
+
+- [masterror](https://github.com/RAprogramm/masterror) — Framework-agnostic application error types
+- [masterror-cli](https://github.com/RAprogramm/masterror-cli) — CLI tool using this knowledge base
+
+## License
+
+MIT
diff --git a/masterror-knowledge/src/errors/borrowing.rs b/masterror-knowledge/src/errors/borrowing.rs
new file mode 100644
index 0000000..d857c8b
--- /dev/null
+++ b/masterror-knowledge/src/errors/borrowing.rs
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Borrowing-related errors.
+
+mod e0499;
+mod e0500;
+mod e0501;
+mod e0502;
+mod e0503;
+mod e0506;
+mod e0508;
+mod e0596;
+
+use super::ErrorEntry;
+
+static ENTRIES: &[&ErrorEntry] = &[
+ &e0499::ENTRY,
+ &e0500::ENTRY,
+ &e0501::ENTRY,
+ &e0502::ENTRY,
+ &e0503::ENTRY,
+ &e0506::ENTRY,
+ &e0508::ENTRY,
+ &e0596::ENTRY
+];
+
+pub fn entries() -> &'static [&'static ErrorEntry] {
+ ENTRIES
+}
diff --git a/masterror-knowledge/src/errors/borrowing/e0499.rs b/masterror-knowledge/src/errors/borrowing/e0499.rs
new file mode 100644
index 0000000..b43ca0b
--- /dev/null
+++ b/masterror-knowledge/src/errors/borrowing/e0499.rs
@@ -0,0 +1,68 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0499: cannot borrow as mutable more than once
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0499",
+ title: LocalizedText::new(
+ "Cannot borrow as mutable more than once",
+ "Нельзя заимствовать как изменяемое более одного раза",
+ "가변으로 두 번 이상 빌릴 수 없음"
+ ),
+ category: Category::Borrowing,
+ explanation: LocalizedText::new(
+ "\
+Rust allows only ONE mutable reference to data at a time. This is stricter
+than the immutable borrowing rule and prevents all aliased mutation.
+
+Why? Two mutable references to the same data could lead to:
+- Data races in concurrent code
+- Iterator invalidation
+- Dangling pointers after reallocation
+
+This rule is checked at compile time, giving you fearless concurrency.",
+ "\
+Rust разрешает только ОДНУ изменяемую ссылку на данные одновременно.
+Это строже правила неизменяемого заимствования.
+
+Почему? Две изменяемые ссылки на одни данные могут привести к:
+- Гонкам данных в конкурентном коде
+- Инвалидации итераторов
+- Висячим указателям после реаллокации",
+ "\
+Rust는 데이터에 대해 한 번에 하나의 가변 참조만 허용합니다.
+이는 불변 빌림 규칙보다 엄격합니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Use scopes to limit borrow lifetime",
+ "Использовать области видимости",
+ "스코프를 사용하여 빌림 수명 제한"
+ ),
+ code: "{ let r1 = &mut x; *r1 += 1; } // r1 dropped\nlet r2 = &mut x;"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Use RefCell for interior mutability",
+ "Использовать RefCell",
+ "내부 가변성을 위해 RefCell 사용"
+ ),
+ code: "use std::cell::RefCell;\nlet x = RefCell::new(value);"
+ }
+ ],
+ links: &[
+ DocLink {
+ title: "Rust Book: Mutable References",
+ url: "https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references"
+ },
+ DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0499.html"
+ }
+ ]
+};
diff --git a/masterror-knowledge/src/errors/borrowing/e0500.rs b/masterror-knowledge/src/errors/borrowing/e0500.rs
new file mode 100644
index 0000000..8721f48
--- /dev/null
+++ b/masterror-knowledge/src/errors/borrowing/e0500.rs
@@ -0,0 +1,50 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0500: closure requires unique access but X is already borrowed
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0500",
+ title: LocalizedText::new(
+ "Closure requires unique access but value is already borrowed",
+ "Замыкание требует уникальный доступ, но значение уже заимствовано",
+ "클로저가 고유 접근을 필요로 하지만 값이 이미 빌려짐"
+ ),
+ category: Category::Borrowing,
+ explanation: LocalizedText::new(
+ "\
+A closure that mutates a captured variable needs exclusive access to it.
+But you've already borrowed the value elsewhere, creating a conflict.
+
+Closures that capture by mutable reference act like mutable borrows.",
+ "\
+Замыкание, изменяющее захваченную переменную, требует эксклюзивного доступа.",
+ "\
+캡처된 변수를 변경하는 클로저는 독점적인 접근이 필요합니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new(
+ "End the borrow before the closure",
+ "Завершить заимствование перед замыканием",
+ "클로저 전에 빌림 종료"
+ ),
+ code: "{ let r = &x; use(r); }\nlet c = || x += 1;"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Move the value into the closure",
+ "Переместить значение в замыкание",
+ "클로저로 값 이동"
+ ),
+ code: "let c = move || { x += 1; };"
+ }
+ ],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0500.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/borrowing/e0501.rs b/masterror-knowledge/src/errors/borrowing/e0501.rs
new file mode 100644
index 0000000..34298ab
--- /dev/null
+++ b/masterror-knowledge/src/errors/borrowing/e0501.rs
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0501: cannot borrow X as mutable because previous closure requires unique
+//! access
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0501",
+ title: LocalizedText::new(
+ "Cannot borrow because closure requires unique access",
+ "Нельзя заимствовать, так как замыкание требует уникальный доступ",
+ "클로저가 고유 접근을 필요로 하여 빌릴 수 없음"
+ ),
+ category: Category::Borrowing,
+ explanation: LocalizedText::new(
+ "\
+A closure has captured a variable mutably, and now you're trying to borrow
+that same variable again. The closure's capture acts like a mutable borrow
+that lasts for the closure's entire lifetime.",
+ "\
+Замыкание захватило переменную изменяемо, и теперь вы пытаетесь заимствовать
+ту же переменную снова.",
+ "\
+클로저가 변수를 가변으로 캡처했고, 이제 같은 변수를 다시 빌리려고 합니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new(
+ "Use the closure before borrowing again",
+ "Использовать замыкание перед повторным заимствованием",
+ "다시 빌리기 전에 클로저 사용"
+ ),
+ code: "let mut c = || x += 1;\nc(); // use closure\nlet r = &x; // now safe"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0501.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/borrowing/e0502.rs b/masterror-knowledge/src/errors/borrowing/e0502.rs
new file mode 100644
index 0000000..fa372c0
--- /dev/null
+++ b/masterror-knowledge/src/errors/borrowing/e0502.rs
@@ -0,0 +1,63 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0502: cannot borrow as mutable because also borrowed as immutable
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0502",
+ title: LocalizedText::new(
+ "Cannot borrow as mutable (already borrowed as immutable)",
+ "Нельзя заимствовать как изменяемое (уже заимствовано как неизменяемое)",
+ "가변으로 빌릴 수 없음 (이미 불변으로 빌림)"
+ ),
+ category: Category::Borrowing,
+ explanation: LocalizedText::new(
+ "\
+Rust enforces a strict borrowing rule: you can have EITHER one mutable
+reference OR any number of immutable references, but never both at once.
+
+This prevents data races at compile time. If you could mutate data while
+someone else is reading it, the reader might see inconsistent state.
+
+The immutable borrow is still \"active\" because it's used later in code.",
+ "\
+Rust применяет строгое правило: можно иметь ЛИБО одну изменяемую ссылку,
+ЛИБО любое количество неизменяемых, но никогда обе одновременно.
+
+Это предотвращает гонки данных.",
+ "\
+Rust는 엄격한 빌림 규칙을 적용합니다: 하나의 가변 참조 또는 여러 불변 참조를
+가질 수 있지만, 동시에 둘 다 가질 수는 없습니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new(
+ "End the immutable borrow before mutating",
+ "Завершить неизменяемое заимствование",
+ "변경 전에 불변 빌림 종료"
+ ),
+ code: "{ let r = &x; println!(\"{}\", r); } // r dropped\nx.push(1);"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Clone before mutation",
+ "Клонировать перед изменением",
+ "변경 전에 복제"
+ ),
+ code: "let copy = x[0].clone();\nx.push(copy);"
+ }
+ ],
+ links: &[
+ DocLink {
+ title: "Rust Book: References and Borrowing",
+ url: "https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html"
+ },
+ DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0502.html"
+ }
+ ]
+};
diff --git a/masterror-knowledge/src/errors/borrowing/e0503.rs b/masterror-knowledge/src/errors/borrowing/e0503.rs
new file mode 100644
index 0000000..083a5c3
--- /dev/null
+++ b/masterror-knowledge/src/errors/borrowing/e0503.rs
@@ -0,0 +1,42 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0503: cannot use X because it was mutably borrowed
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0503",
+ title: LocalizedText::new(
+ "Cannot use value because it was mutably borrowed",
+ "Нельзя использовать значение, так как оно изменяемо заимствовано",
+ "가변으로 빌려져서 값을 사용할 수 없음"
+ ),
+ category: Category::Borrowing,
+ explanation: LocalizedText::new(
+ "\
+While a mutable borrow is active, you cannot access the original value
+in any way. This prevents you from observing partially modified state
+or creating aliased mutable references.
+
+The mutable borrow has exclusive access until it ends.",
+ "\
+Пока активно изменяемое заимствование, вы не можете обращаться к
+исходному значению никак.",
+ "\
+가변 빌림이 활성화된 동안 원래 값에 어떤 방식으로도 접근할 수 없습니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new(
+ "End the mutable borrow first",
+ "Сначала завершить изменяемое заимствование",
+ "먼저 가변 빌림 종료"
+ ),
+ code: "{ let r = &mut x; modify(r); } // r dropped\nuse_value(&x);"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0503.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/borrowing/e0506.rs b/masterror-knowledge/src/errors/borrowing/e0506.rs
new file mode 100644
index 0000000..8c3a482
--- /dev/null
+++ b/masterror-knowledge/src/errors/borrowing/e0506.rs
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0506: cannot assign to X because it is borrowed
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0506",
+ title: LocalizedText::new(
+ "Cannot assign because it is borrowed",
+ "Нельзя присвоить, так как значение заимствовано",
+ "빌려져 있어서 할당할 수 없음"
+ ),
+ category: Category::Borrowing,
+ explanation: LocalizedText::new(
+ "\
+You're trying to assign to a value while a borrow of it exists.
+This would invalidate the existing reference.
+
+You must wait for all borrows to end before assigning a new value.",
+ "\
+Вы пытаетесь присвоить значение, пока существует его заимствование.",
+ "\
+빌림이 존재하는 동안 값에 할당하려고 합니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new(
+ "End the borrow before assigning",
+ "Завершить заимствование перед присваиванием",
+ "할당 전에 빌림 종료"
+ ),
+ code: "{ let r = &x; use(r); } // borrow ends\nx = new_value;"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0506.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/borrowing/e0508.rs b/masterror-knowledge/src/errors/borrowing/e0508.rs
new file mode 100644
index 0000000..4b0449c
--- /dev/null
+++ b/masterror-knowledge/src/errors/borrowing/e0508.rs
@@ -0,0 +1,50 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0508: cannot move out of type `[T]`, a non-copy slice
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0508",
+ title: LocalizedText::new(
+ "Cannot move out of type, a non-copy slice",
+ "Нельзя переместить из типа — это не-Copy срез",
+ "타입에서 이동할 수 없음, 비복사 슬라이스"
+ ),
+ category: Category::Borrowing,
+ explanation: LocalizedText::new(
+ "\
+You're trying to move a value out of a slice, but slices don't own their data.
+They're just views into an array or Vec.
+
+Moving out would leave a \"hole\" in the slice, which isn't allowed.",
+ "\
+Вы пытаетесь переместить значение из среза, но срезы не владеют данными.",
+ "\
+슬라이스에서 값을 이동하려고 하지만, 슬라이스는 데이터를 소유하지 않습니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Clone the element",
+ "Клонировать элемент",
+ "요소 복제"
+ ),
+ code: "let elem = slice[i].clone();"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Use into_iter() on Vec",
+ "Использовать into_iter() на Vec",
+ "Vec에 into_iter() 사용"
+ ),
+ code: "for elem in vec.into_iter() { ... }"
+ }
+ ],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0508.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/borrowing/e0596.rs b/masterror-knowledge/src/errors/borrowing/e0596.rs
new file mode 100644
index 0000000..3f17af3
--- /dev/null
+++ b/masterror-knowledge/src/errors/borrowing/e0596.rs
@@ -0,0 +1,51 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0596: cannot borrow as mutable, as it is not declared as mutable
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0596",
+ title: LocalizedText::new(
+ "Cannot borrow as mutable (not declared as mutable)",
+ "Нельзя заимствовать как изменяемое (не объявлено как изменяемое)",
+ "가변으로 빌릴 수 없음 (가변으로 선언되지 않음)"
+ ),
+ category: Category::Borrowing,
+ explanation: LocalizedText::new(
+ "\
+You're trying to get a mutable reference to something that wasn't declared
+as mutable. To modify through a reference, the original binding must be `mut`.
+
+This is Rust's way of making mutation explicit and visible in the code.",
+ "\
+Вы пытаетесь получить изменяемую ссылку на то, что не было объявлено
+как изменяемое. Для изменения через ссылку оригинал должен быть `mut`.",
+ "\
+가변으로 선언되지 않은 것에 대한 가변 참조를 얻으려고 합니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Add mut to the variable declaration",
+ "Добавить mut к объявлению переменной",
+ "변수 선언에 mut 추가"
+ ),
+ code: "let mut x = vec![1, 2, 3];"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Add mut to function parameter",
+ "Добавить mut к параметру функции",
+ "함수 매개변수에 mut 추가"
+ ),
+ code: "fn process(data: &mut Vec) { ... }"
+ }
+ ],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0596.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/lifetimes.rs b/masterror-knowledge/src/errors/lifetimes.rs
new file mode 100644
index 0000000..8132ece
--- /dev/null
+++ b/masterror-knowledge/src/errors/lifetimes.rs
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Lifetime-related errors.
+
+mod e0106;
+mod e0495;
+mod e0515;
+mod e0597;
+mod e0621;
+mod e0623;
+mod e0700;
+mod e0716;
+
+use super::ErrorEntry;
+
+static ENTRIES: &[&ErrorEntry] = &[
+ &e0106::ENTRY,
+ &e0495::ENTRY,
+ &e0515::ENTRY,
+ &e0597::ENTRY,
+ &e0621::ENTRY,
+ &e0623::ENTRY,
+ &e0700::ENTRY,
+ &e0716::ENTRY
+];
+
+pub fn entries() -> &'static [&'static ErrorEntry] {
+ ENTRIES
+}
diff --git a/masterror-knowledge/src/errors/lifetimes/e0106.rs b/masterror-knowledge/src/errors/lifetimes/e0106.rs
new file mode 100644
index 0000000..3d10310
--- /dev/null
+++ b/masterror-knowledge/src/errors/lifetimes/e0106.rs
@@ -0,0 +1,68 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0106: missing lifetime specifier
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0106",
+ title: LocalizedText::new(
+ "Missing lifetime specifier",
+ "Отсутствует спецификатор времени жизни",
+ "라이프타임 지정자 누락"
+ ),
+ category: Category::Lifetimes,
+ explanation: LocalizedText::new(
+ "\
+References in Rust have lifetimes - they describe how long the reference
+is valid. Usually the compiler infers lifetimes, but sometimes you must
+be explicit.
+
+Lifetime annotations don't change how long values live. They describe
+relationships between references so the compiler can verify safety.",
+ "\
+Ссылки в Rust имеют времена жизни — они описывают, как долго ссылка
+действительна. Обычно компилятор выводит времена жизни, но иногда нужно
+указать явно.",
+ "\
+Rust의 참조에는 라이프타임이 있습니다 - 참조가 얼마나 오래 유효한지 설명합니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Add explicit lifetime parameter",
+ "Добавить явный параметр времени жизни",
+ "명시적 라이프타임 매개변수 추가"
+ ),
+ code: "struct Foo<'a> { x: &'a str }"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Use owned type instead",
+ "Использовать владеющий тип",
+ "소유 타입 사용"
+ ),
+ code: "struct Foo { x: String }"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Use 'static for compile-time constants",
+ "Использовать 'static для констант",
+ "컴파일 시간 상수에 'static 사용"
+ ),
+ code: "fn get_str() -> &'static str { \"hello\" }"
+ }
+ ],
+ links: &[
+ DocLink {
+ title: "Rust Book: Lifetimes",
+ url: "https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html"
+ },
+ DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0106.html"
+ }
+ ]
+};
diff --git a/masterror-knowledge/src/errors/lifetimes/e0495.rs b/masterror-knowledge/src/errors/lifetimes/e0495.rs
new file mode 100644
index 0000000..a5ce0a0
--- /dev/null
+++ b/masterror-knowledge/src/errors/lifetimes/e0495.rs
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0495: cannot infer an appropriate lifetime
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0495",
+ title: LocalizedText::new(
+ "Cannot infer an appropriate lifetime",
+ "Невозможно вывести подходящее время жизни",
+ "적절한 라이프타임을 추론할 수 없음"
+ ),
+ category: Category::Lifetimes,
+ explanation: LocalizedText::new(
+ "\
+The compiler found conflicting lifetime requirements and couldn't
+determine which one to use.",
+ "\
+Компилятор обнаружил конфликтующие требования времён жизни.",
+ "\
+컴파일러가 충돌하는 라이프타임 요구사항을 찾았습니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new(
+ "Add explicit lifetime bounds",
+ "Добавить явные ограничения времени жизни",
+ "명시적 라이프타임 바운드 추가"
+ ),
+ code: "fn process<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0495.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/lifetimes/e0515.rs b/masterror-knowledge/src/errors/lifetimes/e0515.rs
new file mode 100644
index 0000000..f8ca6a2
--- /dev/null
+++ b/masterror-knowledge/src/errors/lifetimes/e0515.rs
@@ -0,0 +1,51 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0515: cannot return reference to temporary value
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0515",
+ title: LocalizedText::new(
+ "Cannot return reference to temporary value",
+ "Нельзя вернуть ссылку на временное значение",
+ "임시 값에 대한 참조를 반환할 수 없음"
+ ),
+ category: Category::Lifetimes,
+ explanation: LocalizedText::new(
+ "\
+You're trying to return a reference to a value that was created inside
+the function. When the function returns, that value is dropped.
+
+The reference would point to freed memory - a dangling pointer.",
+ "\
+Вы пытаетесь вернуть ссылку на значение, созданное внутри функции.
+При возврате из функции это значение будет уничтожено.",
+ "\
+함수 내에서 생성된 값에 대한 참조를 반환하려고 합니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Return owned value instead of reference",
+ "Вернуть владеющее значение вместо ссылки",
+ "참조 대신 소유 값 반환"
+ ),
+ code: "fn create() -> String { String::from(\"hello\") }"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Use a parameter lifetime",
+ "Использовать время жизни параметра",
+ "매개변수 라이프타임 사용"
+ ),
+ code: "fn longest<'a>(x: &'a str, y: &'a str) -> &'a str"
+ }
+ ],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0515.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/lifetimes/e0597.rs b/masterror-knowledge/src/errors/lifetimes/e0597.rs
new file mode 100644
index 0000000..bf3f11f
--- /dev/null
+++ b/masterror-knowledge/src/errors/lifetimes/e0597.rs
@@ -0,0 +1,57 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0597: value does not live long enough
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0597",
+ title: LocalizedText::new(
+ "Value does not live long enough",
+ "Значение живёт недостаточно долго",
+ "값이 충분히 오래 살지 않음"
+ ),
+ category: Category::Lifetimes,
+ explanation: LocalizedText::new(
+ "\
+You're creating a reference to something that will be destroyed before
+the reference is used. This would create a dangling pointer.
+
+Rust prevents this at compile time. The referenced value must live at
+least as long as the reference itself.",
+ "\
+Вы создаёте ссылку на что-то, что будет уничтожено до использования ссылки.",
+ "\
+참조가 사용되기 전에 파괴될 것에 대한 참조를 만들고 있습니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Move value to outer scope",
+ "Переместить значение во внешнюю область",
+ "값을 외부 스코프로 이동"
+ ),
+ code: "let s = String::from(\"hello\"); // declare before use"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Return owned value instead",
+ "Вернуть владеющее значение",
+ "소유 값 반환"
+ ),
+ code: "fn get() -> String { s.to_string() }"
+ }
+ ],
+ links: &[
+ DocLink {
+ title: "Rust Book: Lifetimes",
+ url: "https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html"
+ },
+ DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0597.html"
+ }
+ ]
+};
diff --git a/masterror-knowledge/src/errors/lifetimes/e0621.rs b/masterror-knowledge/src/errors/lifetimes/e0621.rs
new file mode 100644
index 0000000..b7247fc
--- /dev/null
+++ b/masterror-knowledge/src/errors/lifetimes/e0621.rs
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0621: explicit lifetime required in the type of X
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0621",
+ title: LocalizedText::new(
+ "Explicit lifetime required in the type",
+ "Требуется явное время жизни в типе",
+ "타입에 명시적 라이프타임이 필요함"
+ ),
+ category: Category::Lifetimes,
+ explanation: LocalizedText::new(
+ "\
+The compiler cannot infer lifetimes in this context. You need to add
+explicit lifetime annotations to show how references relate.",
+ "\
+Компилятор не может вывести времена жизни в этом контексте.",
+ "\
+컴파일러가 이 컨텍스트에서 라이프타임을 추론할 수 없습니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new(
+ "Add lifetime parameter to function",
+ "Добавить параметр времени жизни к функции",
+ "함수에 라이프타임 매개변수 추가"
+ ),
+ code: "fn process<'a>(data: &'a str) -> &'a str { data }"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0621.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/lifetimes/e0623.rs b/masterror-knowledge/src/errors/lifetimes/e0623.rs
new file mode 100644
index 0000000..152bb3b
--- /dev/null
+++ b/masterror-knowledge/src/errors/lifetimes/e0623.rs
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0623: lifetime mismatch
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0623",
+ title: LocalizedText::new(
+ "Lifetime mismatch",
+ "Несоответствие времён жизни",
+ "라이프타임 불일치"
+ ),
+ category: Category::Lifetimes,
+ explanation: LocalizedText::new(
+ "\
+Two lifetimes in your code don't match where they should.",
+ "\
+Два времени жизни в коде не совпадают там, где должны.",
+ "\
+코드에서 두 라이프타임이 일치해야 하는 곳에서 일치하지 않습니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new(
+ "Ensure consistent lifetime annotations",
+ "Обеспечить согласованные аннотации",
+ "일관된 라이프타임 어노테이션 확보"
+ ),
+ code: "fn foo<'a>(x: &'a str) -> &'a str { x }"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0623.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/lifetimes/e0700.rs b/masterror-knowledge/src/errors/lifetimes/e0700.rs
new file mode 100644
index 0000000..f0ec8d0
--- /dev/null
+++ b/masterror-knowledge/src/errors/lifetimes/e0700.rs
@@ -0,0 +1,39 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0700: hidden type captures lifetime
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0700",
+ title: LocalizedText::new(
+ "Hidden type captures lifetime",
+ "Скрытый тип захватывает время жизни",
+ "숨겨진 타입이 라이프타임을 캡처함"
+ ),
+ category: Category::Lifetimes,
+ explanation: LocalizedText::new(
+ "\
+When using `impl Trait` return type, the hidden concrete type captures
+a lifetime that isn't declared in the function signature.",
+ "\
+При использовании типа возврата `impl Trait` скрытый конкретный тип
+захватывает время жизни, не объявленное в сигнатуре.",
+ "\
+`impl Trait` 반환 타입을 사용할 때, 숨겨진 구체적 타입이 선언되지 않은 라이프타임을 캡처합니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new(
+ "Declare the captured lifetime",
+ "Объявить захваченное время жизни",
+ "캡처된 라이프타임 선언"
+ ),
+ code: "fn foo<'a>(x: &'a str) -> impl Iterator- + 'a"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0700.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/lifetimes/e0716.rs b/masterror-knowledge/src/errors/lifetimes/e0716.rs
new file mode 100644
index 0000000..f12a054
--- /dev/null
+++ b/masterror-knowledge/src/errors/lifetimes/e0716.rs
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0716: temporary value dropped while borrowed
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0716",
+ title: LocalizedText::new(
+ "Temporary value dropped while borrowed",
+ "Временное значение уничтожено во время заимствования",
+ "빌린 동안 임시 값이 삭제됨"
+ ),
+ category: Category::Lifetimes,
+ explanation: LocalizedText::new(
+ "\
+A temporary value was created, borrowed, and then immediately dropped.
+The borrow outlives the temporary.
+
+Temporaries only live until the end of the statement by default.",
+ "\
+Было создано временное значение, заимствовано и сразу уничтожено.",
+ "\
+임시 값이 생성되고, 빌려지고, 즉시 삭제되었습니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new(
+ "Bind temporary to a variable",
+ "Привязать временное значение к переменной",
+ "임시 값을 변수에 바인딩"
+ ),
+ code: "let value = create_value();\nlet reference = &value;"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0716.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/mod.rs b/masterror-knowledge/src/errors/mod.rs
new file mode 100644
index 0000000..df40cc0
--- /dev/null
+++ b/masterror-knowledge/src/errors/mod.rs
@@ -0,0 +1,213 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Rust compiler error explanations organized by category.
+
+pub mod borrowing;
+pub mod lifetimes;
+pub mod ownership;
+pub mod raprogramm;
+pub mod resolution;
+pub mod traits;
+pub mod types;
+
+use std::{collections::HashMap, sync::LazyLock};
+
+use arrayvec::ArrayString;
+
+/// Global error registry singleton.
+static ERROR_REGISTRY: LazyLock = LazyLock::new(ErrorRegistry::build);
+
+/// Link with title for documentation.
+#[derive(Debug, Clone, Copy)]
+pub struct DocLink {
+ /// Link display title.
+ pub title: &'static str,
+ /// URL to documentation.
+ pub url: &'static str
+}
+
+/// Fix suggestion with code example.
+#[derive(Debug, Clone, Copy)]
+pub struct FixSuggestion {
+ /// Description of the fix approach.
+ pub description: LocalizedText,
+ /// Code example showing the fix.
+ pub code: &'static str
+}
+
+/// Localized text with translations.
+///
+/// All fields are `&'static str` for zero-copy access.
+#[derive(Debug, Clone, Copy)]
+pub struct LocalizedText {
+ /// English text (always present).
+ pub en: &'static str,
+ /// Russian translation.
+ pub ru: &'static str,
+ /// Korean translation.
+ pub ko: &'static str
+}
+
+impl LocalizedText {
+ pub const fn new(en: &'static str, ru: &'static str, ko: &'static str) -> Self {
+ Self {
+ en,
+ ru,
+ ko
+ }
+ }
+
+ pub fn get(&self, lang: &str) -> &'static str {
+ match lang {
+ "ru" => self.ru,
+ "ko" => self.ko,
+ _ => self.en
+ }
+ }
+}
+
+/// Error category.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Category {
+ Ownership,
+ Borrowing,
+ Lifetimes,
+ Types,
+ Traits,
+ Resolution
+}
+
+impl Category {
+ pub fn name(&self, lang: &str) -> &'static str {
+ match (self, lang) {
+ (Self::Ownership, "ru") => "Владение",
+ (Self::Ownership, "ko") => "소유권",
+ (Self::Ownership, _) => "Ownership",
+
+ (Self::Borrowing, "ru") => "Заимствование",
+ (Self::Borrowing, "ko") => "빌림",
+ (Self::Borrowing, _) => "Borrowing",
+
+ (Self::Lifetimes, "ru") => "Времена жизни",
+ (Self::Lifetimes, "ko") => "라이프타임",
+ (Self::Lifetimes, _) => "Lifetimes",
+
+ (Self::Types, "ru") => "Типы",
+ (Self::Types, "ko") => "타입",
+ (Self::Types, _) => "Types",
+
+ (Self::Traits, "ru") => "Трейты",
+ (Self::Traits, "ko") => "트레이트",
+ (Self::Traits, _) => "Traits",
+
+ (Self::Resolution, "ru") => "Разрешение имён",
+ (Self::Resolution, "ko") => "이름 확인",
+ (Self::Resolution, _) => "Name Resolution"
+ }
+ }
+}
+
+/// Complete error entry.
+///
+/// Fields ordered by size (largest first) to minimize padding.
+#[derive(Debug, Clone)]
+pub struct ErrorEntry {
+ /// Error explanation text.
+ pub explanation: LocalizedText,
+ /// Short error title.
+ pub title: LocalizedText,
+ /// Suggested fixes.
+ pub fixes: &'static [FixSuggestion],
+ /// Documentation links.
+ pub links: &'static [DocLink],
+ /// Error code (E0382).
+ pub code: &'static str,
+ /// Error category.
+ pub category: Category
+}
+
+/// Registry of all known errors.
+pub struct ErrorRegistry {
+ errors: HashMap<&'static str, &'static ErrorEntry>
+}
+
+impl ErrorRegistry {
+ /// Get the global registry instance.
+ pub fn new() -> &'static Self {
+ &ERROR_REGISTRY
+ }
+
+ /// Build registry from all modules.
+ fn build() -> Self {
+ let mut errors = HashMap::with_capacity(34);
+
+ for entry in ownership::entries() {
+ errors.insert(entry.code, *entry);
+ }
+ for entry in borrowing::entries() {
+ errors.insert(entry.code, *entry);
+ }
+ for entry in lifetimes::entries() {
+ errors.insert(entry.code, *entry);
+ }
+ for entry in types::entries() {
+ errors.insert(entry.code, *entry);
+ }
+ for entry in traits::entries() {
+ errors.insert(entry.code, *entry);
+ }
+ for entry in resolution::entries() {
+ errors.insert(entry.code, *entry);
+ }
+
+ Self {
+ errors
+ }
+ }
+
+ /// Find error by code.
+ ///
+ /// Accepts formats: "E0382", "e0382", "0382".
+ /// Uses stack-allocated buffer to avoid heap allocation.
+ pub fn find(&self, code: &str) -> Option<&'static ErrorEntry> {
+ // Fast path: try exact match first (covers "E0382" case)
+ if let Some(entry) = self.errors.get(code) {
+ return Some(*entry);
+ }
+
+ // Normalize to uppercase with 'E' prefix using stack buffer
+ let mut buf: ArrayString<8> = ArrayString::new();
+
+ if !code.starts_with('E') && !code.starts_with('e') {
+ buf.push('E');
+ }
+
+ for c in code.chars().take(7) {
+ buf.push(c.to_ascii_uppercase());
+ }
+
+ self.errors.get(buf.as_str()).copied()
+ }
+
+ /// Get all errors.
+ pub fn all(&self) -> impl Iterator- + '_ {
+ self.errors.values().copied()
+ }
+
+ /// Get errors by category.
+ pub fn by_category(&self, cat: Category) -> Vec<&'static ErrorEntry> {
+ self.errors
+ .values()
+ .filter(|e| e.category == cat)
+ .copied()
+ .collect()
+ }
+}
+
+impl Default for &'static ErrorRegistry {
+ fn default() -> Self {
+ ErrorRegistry::new()
+ }
+}
diff --git a/masterror-knowledge/src/errors/ownership.rs b/masterror-knowledge/src/errors/ownership.rs
new file mode 100644
index 0000000..516a81b
--- /dev/null
+++ b/masterror-knowledge/src/errors/ownership.rs
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Ownership-related errors.
+
+mod e0381;
+mod e0382;
+mod e0383;
+mod e0384;
+mod e0505;
+mod e0507;
+mod e0509;
+
+use super::ErrorEntry;
+
+static ENTRIES: &[&ErrorEntry] = &[
+ &e0381::ENTRY,
+ &e0382::ENTRY,
+ &e0383::ENTRY,
+ &e0384::ENTRY,
+ &e0505::ENTRY,
+ &e0507::ENTRY,
+ &e0509::ENTRY
+];
+
+pub fn entries() -> &'static [&'static ErrorEntry] {
+ ENTRIES
+}
diff --git a/masterror-knowledge/src/errors/ownership/e0381.rs b/masterror-knowledge/src/errors/ownership/e0381.rs
new file mode 100644
index 0000000..8518c29
--- /dev/null
+++ b/masterror-knowledge/src/errors/ownership/e0381.rs
@@ -0,0 +1,55 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0381: borrow of possibly-uninitialized variable
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0381",
+ title: LocalizedText::new(
+ "Borrow of possibly-uninitialized variable",
+ "Заимствование возможно неинициализированной переменной",
+ "초기화되지 않았을 수 있는 변수의 빌림"
+ ),
+ category: Category::Ownership,
+ explanation: LocalizedText::new(
+ "\
+Rust requires all variables to be initialized before use. You're trying
+to use a variable that might not have been assigned a value yet.
+
+This prevents reading garbage memory. The compiler tracks initialization
+through all possible code paths.",
+ "\
+Rust требует инициализации всех переменных перед использованием.
+Вы пытаетесь использовать переменную, которая может быть не инициализирована.
+
+Это предотвращает чтение мусора из памяти.",
+ "\
+Rust는 사용 전에 모든 변수를 초기화해야 합니다.
+아직 값이 할당되지 않았을 수 있는 변수를 사용하려고 합니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Initialize the variable",
+ "Инициализировать переменную",
+ "변수 초기화"
+ ),
+ code: "let x = 0; // or any default value"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Use Option for maybe-uninitialized",
+ "Использовать Option для возможно неинициализированных",
+ "초기화되지 않을 수 있는 경우 Option 사용"
+ ),
+ code: "let x: Option = None;\nif condition { x = Some(42); }"
+ }
+ ],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0381.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/ownership/e0382.rs b/masterror-knowledge/src/errors/ownership/e0382.rs
new file mode 100644
index 0000000..f5371d6
--- /dev/null
+++ b/masterror-knowledge/src/errors/ownership/e0382.rs
@@ -0,0 +1,80 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0382: borrow of moved value
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0382",
+ title: LocalizedText::new(
+ "Borrow of moved value",
+ "Заимствование перемещённого значения",
+ "이동된 값의 빌림"
+ ),
+ category: Category::Ownership,
+ explanation: LocalizedText::new(
+ "\
+In Rust, each value has exactly one owner at a time. This is the foundation
+of Rust's memory safety guarantees without garbage collection.
+
+When you assign a value to another variable or pass it to a function,
+ownership MOVES to the new location. The original variable becomes invalid
+and cannot be used anymore.
+
+This happens because Rust needs to know exactly when to free memory.
+With one owner, there's no ambiguity about who is responsible for cleanup.",
+ "\
+В Rust каждое значение имеет ровно одного владельца. Это основа
+гарантий безопасности памяти без сборщика мусора.
+
+Когда вы присваиваете значение другой переменной или передаёте в функцию,
+владение ПЕРЕМЕЩАЕТСЯ. Исходная переменная становится недействительной.
+
+Rust должен точно знать, когда освобождать память.
+С одним владельцем нет неоднозначности в том, кто отвечает за очистку.",
+ "\
+Rust에서 각 값은 정확히 하나의 소유자를 가집니다. 이것이 가비지 컬렉터 없이
+메모리 안전성을 보장하는 기반입니다.
+
+값을 다른 변수에 할당하거나 함수에 전달하면 소유권이 새 위치로 이동합니다.
+원래 변수는 무효화되어 더 이상 사용할 수 없습니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Clone the value (creates a deep copy)",
+ "Клонировать значение (глубокая копия)",
+ "값을 복제 (깊은 복사)"
+ ),
+ code: "let s2 = s.clone();"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Borrow with a reference (no copy)",
+ "Заимствовать по ссылке (без копии)",
+ "참조로 빌림 (복사 없음)"
+ ),
+ code: "let s2 = &s;"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Implement Copy trait (for small types)",
+ "Реализовать Copy (для маленьких типов)",
+ "Copy 트레이트 구현 (작은 타입용)"
+ ),
+ code: "#[derive(Copy, Clone)]"
+ }
+ ],
+ links: &[
+ DocLink {
+ title: "Rust Book: Ownership",
+ url: "https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html"
+ },
+ DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0382.html"
+ }
+ ]
+};
diff --git a/masterror-knowledge/src/errors/ownership/e0383.rs b/masterror-knowledge/src/errors/ownership/e0383.rs
new file mode 100644
index 0000000..ebf37f9
--- /dev/null
+++ b/masterror-knowledge/src/errors/ownership/e0383.rs
@@ -0,0 +1,46 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0383: partial reinitialization of uninitialized structure
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0383",
+ title: LocalizedText::new(
+ "Partial reinitialization of uninitialized structure",
+ "Частичная переинициализация неинициализированной структуры",
+ "초기화되지 않은 구조체의 부분 재초기화"
+ ),
+ category: Category::Ownership,
+ explanation: LocalizedText::new(
+ "\
+You're trying to partially reinitialize a struct that was moved from.
+After a move, the entire struct is invalid - you can't assign to just
+one field.
+
+You must reinitialize the entire struct.",
+ "\
+Вы пытаетесь частично переинициализировать структуру после перемещения.
+После перемещения вся структура недействительна - нельзя присвоить
+только одно поле.
+
+Нужно переинициализировать всю структуру.",
+ "\
+이동된 구조체를 부분적으로 재초기화하려고 합니다.
+이동 후 전체 구조체가 무효화됩니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new(
+ "Reinitialize the entire struct",
+ "Переинициализировать всю структуру",
+ "전체 구조체 재초기화"
+ ),
+ code: "s = MyStruct { field1: val1, field2: val2 };"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0383.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/ownership/e0384.rs b/masterror-knowledge/src/errors/ownership/e0384.rs
new file mode 100644
index 0000000..19696a4
--- /dev/null
+++ b/masterror-knowledge/src/errors/ownership/e0384.rs
@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0384: cannot assign twice to immutable variable
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0384",
+ title: LocalizedText::new(
+ "Cannot assign twice to immutable variable",
+ "Нельзя присвоить дважды неизменяемой переменной",
+ "불변 변수에 두 번 할당할 수 없음"
+ ),
+ category: Category::Ownership,
+ explanation: LocalizedText::new(
+ "\
+Variables in Rust are immutable by default. Once a value is bound to a name,
+you cannot change it unless you explicitly mark it as mutable with `mut`.
+
+This is a deliberate design choice that makes code easier to reason about.
+When you see a variable without `mut`, you know it won't change.",
+ "\
+Переменные в Rust неизменяемы по умолчанию. После привязки значения
+к имени вы не можете его изменить без явного указания `mut`.
+
+Это осознанное решение, упрощающее понимание кода.
+Если переменная без `mut`, она не изменится.",
+ "\
+Rust의 변수는 기본적으로 불변입니다. 값이 이름에 바인딩되면
+`mut`로 명시적으로 표시하지 않는 한 변경할 수 없습니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Make the variable mutable",
+ "Сделать переменную изменяемой",
+ "변수를 가변으로 만들기"
+ ),
+ code: "let mut x = 5;\nx = 10;"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Use shadowing (create new binding)",
+ "Использовать затенение (новая привязка)",
+ "섀도잉 사용 (새 바인딩 생성)"
+ ),
+ code: "let x = 5;\nlet x = 10; // shadows the first x"
+ }
+ ],
+ links: &[
+ DocLink {
+ title: "Rust Book: Variables and Mutability",
+ url: "https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html"
+ },
+ DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0384.html"
+ }
+ ]
+};
diff --git a/masterror-knowledge/src/errors/ownership/e0505.rs b/masterror-knowledge/src/errors/ownership/e0505.rs
new file mode 100644
index 0000000..e76eec9
--- /dev/null
+++ b/masterror-knowledge/src/errors/ownership/e0505.rs
@@ -0,0 +1,56 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0505: cannot move out of X because it is borrowed
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0505",
+ title: LocalizedText::new(
+ "Cannot move out because it is borrowed",
+ "Нельзя переместить, так как значение заимствовано",
+ "빌려져 있어서 이동할 수 없음"
+ ),
+ category: Category::Ownership,
+ explanation: LocalizedText::new(
+ "\
+You're trying to move a value while a borrow of it still exists.
+This would invalidate the reference, creating a dangling pointer.
+
+The borrow must end (go out of scope) before you can move the value.
+
+Rust tracks the lifetime of all borrows to prevent this at compile time.",
+ "\
+Вы пытаетесь переместить значение, пока существует его заимствование.
+Это сделает ссылку недействительной.
+
+Заимствование должно закончиться до перемещения значения.",
+ "\
+빌림이 존재하는 동안 값을 이동하려고 합니다.
+이것은 참조를 무효화하여 댕글링 포인터를 만듭니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new(
+ "End the borrow before moving",
+ "Завершить заимствование перед перемещением",
+ "이동 전에 빌림 종료"
+ ),
+ code: "{ let r = &x; use(r); } // borrow ends\nmove_value(x);"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Clone before borrowing",
+ "Клонировать перед заимствованием",
+ "빌리기 전에 복제"
+ ),
+ code: "let cloned = x.clone();\nlet r = &cloned;\nmove_value(x);"
+ }
+ ],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0505.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/ownership/e0507.rs b/masterror-knowledge/src/errors/ownership/e0507.rs
new file mode 100644
index 0000000..3df095f
--- /dev/null
+++ b/masterror-knowledge/src/errors/ownership/e0507.rs
@@ -0,0 +1,71 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0507: cannot move out of borrowed content
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0507",
+ title: LocalizedText::new(
+ "Cannot move out of borrowed content",
+ "Нельзя переместить из заимствованного содержимого",
+ "빌린 내용에서 이동할 수 없음"
+ ),
+ category: Category::Ownership,
+ explanation: LocalizedText::new(
+ "\
+You're trying to take ownership of a value that you only have a reference to.
+References are borrows - they don't own the data.
+
+Moving out of a reference would leave the original owner with invalid data,
+violating Rust's memory safety guarantees.
+
+Common cases:
+- Indexing into a Vec or array with `vec[i]` and trying to own the element
+- Dereferencing a reference and trying to move the value
+- Pattern matching on borrowed data with ownership patterns",
+ "\
+Вы пытаетесь забрать владение значением, на которое у вас только ссылка.
+Ссылки - это заимствования, они не владеют данными.
+
+Перемещение из ссылки оставит исходного владельца с недействительными данными.
+
+Частые случаи:
+- Индексация Vec с попыткой забрать элемент
+- Разыменование ссылки с попыткой переместить
+- Pattern matching на заимствованных данных",
+ "\
+참조만 있는 값의 소유권을 가져오려고 합니다.
+참조는 빌림입니다 - 데이터를 소유하지 않습니다.
+
+참조에서 이동하면 원래 소유자가 무효한 데이터를 갖게 됩니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new("Clone the value", "Клонировать значение", "값 복제"),
+ code: "let owned = borrowed.clone();"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Use mem::take or mem::replace",
+ "Использовать mem::take или mem::replace",
+ "mem::take 또는 mem::replace 사용"
+ ),
+ code: "let owned = std::mem::take(&mut vec[i]);"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Use swap_remove for Vec",
+ "Использовать swap_remove для Vec",
+ "Vec에 swap_remove 사용"
+ ),
+ code: "let owned = vec.swap_remove(i);"
+ }
+ ],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0507.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/ownership/e0509.rs b/masterror-knowledge/src/errors/ownership/e0509.rs
new file mode 100644
index 0000000..4d39ab1
--- /dev/null
+++ b/masterror-knowledge/src/errors/ownership/e0509.rs
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! E0509: cannot move out of type X, which implements the Drop trait
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0509",
+ title: LocalizedText::new(
+ "Cannot move out of type which implements Drop",
+ "Нельзя переместить из типа, реализующего Drop",
+ "Drop을 구현하는 타입에서 이동할 수 없음"
+ ),
+ category: Category::Ownership,
+ explanation: LocalizedText::new(
+ "\
+Types that implement Drop have custom cleanup logic that runs when they're
+destroyed. Moving a field out would leave the struct in a partially valid
+state, and Drop wouldn't know what to clean up.
+
+Rust prevents this to ensure Drop always sees a valid value.",
+ "\
+Типы с Drop имеют пользовательскую логику очистки при уничтожении.
+Перемещение поля оставит структуру в частично валидном состоянии,
+и Drop не будет знать, что очищать.
+
+Rust предотвращает это для гарантии валидности значения в Drop.",
+ "\
+Drop을 구현하는 타입은 파괴될 때 실행되는 사용자 정의 정리 로직이 있습니다.
+필드를 이동하면 구조체가 부분적으로 유효한 상태가 됩니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new("Clone the field", "Клонировать поле", "필드 복제"),
+ code: "let field = self.field.clone();"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Use Option and take()",
+ "Использовать Option и take()",
+ "Option과 take() 사용"
+ ),
+ code: "struct S { field: Option }\nlet field = self.field.take();"
+ }
+ ],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0509.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/raprogramm.rs b/masterror-knowledge/src/errors/raprogramm.rs
new file mode 100644
index 0000000..7e81024
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm.rs
@@ -0,0 +1,175 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Best practices from RAprogramm's RustManifest.
+//!
+//! These are not compiler errors but recommendations and patterns
+//! from
+
+mod ra001;
+mod ra002;
+mod ra003;
+mod ra004;
+mod ra005;
+mod ra006;
+mod ra007;
+mod ra008;
+mod ra009;
+mod ra010;
+mod ra011;
+mod ra012;
+mod ra013;
+mod ra014;
+mod ra015;
+
+use std::{collections::HashMap, sync::LazyLock};
+
+use arrayvec::ArrayString;
+
+pub use crate::errors::LocalizedText;
+
+/// Global practice registry singleton.
+static PRACTICE_REGISTRY: LazyLock = LazyLock::new(PracticeRegistry::build);
+
+/// Best practice category.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum PracticeCategory {
+ ErrorHandling,
+ Performance,
+ Naming,
+ Documentation,
+ Design,
+ Testing,
+ Security
+}
+
+impl PracticeCategory {
+ pub fn name(&self, lang: &str) -> &'static str {
+ match (self, lang) {
+ (Self::ErrorHandling, "ru") => "Обработка ошибок",
+ (Self::ErrorHandling, "ko") => "오류 처리",
+ (Self::ErrorHandling, _) => "Error Handling",
+
+ (Self::Performance, "ru") => "Производительность",
+ (Self::Performance, "ko") => "성능",
+ (Self::Performance, _) => "Performance",
+
+ (Self::Naming, "ru") => "Именование",
+ (Self::Naming, "ko") => "명명",
+ (Self::Naming, _) => "Naming",
+
+ (Self::Documentation, "ru") => "Документация",
+ (Self::Documentation, "ko") => "문서화",
+ (Self::Documentation, _) => "Documentation",
+
+ (Self::Design, "ru") => "Проектирование",
+ (Self::Design, "ko") => "설계",
+ (Self::Design, _) => "Design",
+
+ (Self::Testing, "ru") => "Тестирование",
+ (Self::Testing, "ko") => "테스트",
+ (Self::Testing, _) => "Testing",
+
+ (Self::Security, "ru") => "Безопасность",
+ (Self::Security, "ko") => "보안",
+ (Self::Security, _) => "Security"
+ }
+ }
+}
+
+/// A best practice recommendation.
+#[derive(Debug, Clone)]
+pub struct BestPractice {
+ pub code: &'static str,
+ pub title: LocalizedText,
+ pub category: PracticeCategory,
+ pub explanation: LocalizedText,
+ pub good_example: &'static str,
+ pub bad_example: &'static str,
+ pub source: &'static str
+}
+
+static ENTRIES: &[&BestPractice] = &[
+ &ra001::ENTRY,
+ &ra002::ENTRY,
+ &ra003::ENTRY,
+ &ra004::ENTRY,
+ &ra005::ENTRY,
+ &ra006::ENTRY,
+ &ra007::ENTRY,
+ &ra008::ENTRY,
+ &ra009::ENTRY,
+ &ra010::ENTRY,
+ &ra011::ENTRY,
+ &ra012::ENTRY,
+ &ra013::ENTRY,
+ &ra014::ENTRY,
+ &ra015::ENTRY
+];
+
+pub fn entries() -> &'static [&'static BestPractice] {
+ ENTRIES
+}
+
+/// Registry for best practices.
+pub struct PracticeRegistry {
+ practices: HashMap<&'static str, &'static BestPractice>
+}
+
+impl PracticeRegistry {
+ /// Get the global registry instance.
+ pub fn new() -> &'static Self {
+ &PRACTICE_REGISTRY
+ }
+
+ /// Build registry from all practices.
+ fn build() -> Self {
+ let mut practices = HashMap::with_capacity(15);
+ for entry in entries() {
+ practices.insert(entry.code, *entry);
+ }
+ Self {
+ practices
+ }
+ }
+
+ /// Find practice by code (RA001, etc.).
+ ///
+ /// Accepts formats: "RA001", "ra001".
+ /// Uses stack-allocated buffer to avoid heap allocation.
+ pub fn find(&self, code: &str) -> Option<&'static BestPractice> {
+ // Fast path: try exact match first
+ if let Some(entry) = self.practices.get(code) {
+ return Some(*entry);
+ }
+
+ // Normalize to uppercase using stack buffer
+ let mut buf: ArrayString<8> = ArrayString::new();
+ for c in code.chars().take(8) {
+ buf.push(c.to_ascii_uppercase());
+ }
+
+ self.practices.get(buf.as_str()).copied()
+ }
+
+ /// Get all practices.
+ pub fn all(&self) -> impl Iterator- + '_ {
+ self.practices.values().copied()
+ }
+
+ /// Get practices by category.
+ pub fn by_category(&self, cat: PracticeCategory) -> Vec<&'static BestPractice> {
+ self.practices
+ .values()
+ .filter(|p| p.category == cat)
+ .copied()
+ .collect()
+ }
+}
+
+impl Default for &'static PracticeRegistry {
+ fn default() -> Self {
+ PracticeRegistry::new()
+ }
+}
diff --git a/masterror-knowledge/src/errors/raprogramm/ra001.rs b/masterror-knowledge/src/errors/raprogramm/ra001.rs
new file mode 100644
index 0000000..774eed4
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra001.rs
@@ -0,0 +1,39 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA001: Never use unwrap() in production code
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA001",
+ title: LocalizedText::new(
+ "Never use unwrap() in production code",
+ "Никогда не используйте unwrap() в продакшене",
+ "프로덕션 코드에서 unwrap() 사용 금지"
+ ),
+ category: PracticeCategory::ErrorHandling,
+ explanation: LocalizedText::new(
+ "\
+The Cloudflare November 2025 outage affected 330+ datacenters due to a single
+.unwrap(). Configuration change exposed an error case that was never handled.
+Result: ChatGPT, X, Canva offline for ~3 hours.
+
+Always use proper error propagation with Result and the ? operator.
+Implement detailed error messages with map_err().",
+ "\
+Сбой Cloudflare в ноябре 2025 затронул 330+ дата-центров из-за одного .unwrap().
+Изменение конфигурации обнажило случай ошибки, который не был обработан.
+Результат: ChatGPT, X, Canva недоступны ~3 часа.
+
+Всегда используйте правильное распространение ошибок с Result и оператором ?.",
+ "\
+2025년 11월 Cloudflare 장애는 단일 .unwrap()으로 인해 330개 이상의 데이터센터에
+영향을 미쳤습니다. 구성 변경으로 처리되지 않은 오류 케이스가 노출되었습니다."
+ ),
+ good_example: r#"let config = Config::from_file("config.toml")
+ .map_err(|e| format!("Failed to load config: {}", e))?;"#,
+ bad_example: r#"let config = Config::from_file("config.toml").unwrap();"#,
+ source: "https://github.com/RAprogramm/RustManifest#6-panic-avoidance-in-production"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra002.rs b/masterror-knowledge/src/errors/raprogramm/ra002.rs
new file mode 100644
index 0000000..52c5391
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra002.rs
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA002: Use ? operator for error propagation
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA002",
+ title: LocalizedText::new(
+ "Use ? operator for error propagation",
+ "Используйте оператор ? для распространения ошибок",
+ "오류 전파에 ? 연산자 사용"
+ ),
+ category: PracticeCategory::ErrorHandling,
+ explanation: LocalizedText::new(
+ "\
+The ? operator is the idiomatic way to handle errors in Rust.
+It automatically converts errors and propagates them up the call stack.
+
+Use ok_or() or ok_or_else() to convert Option to Result with meaningful messages.",
+ "\
+Оператор ? — идиоматический способ обработки ошибок в Rust.
+Он автоматически конвертирует ошибки и распространяет их вверх по стеку вызовов.",
+ "\
+? 연산자는 Rust에서 오류를 처리하는 관용적인 방법입니다."
+ ),
+ good_example: r#"let value = some_option.ok_or("Expected a value")?;
+let data = fetch_data().map_err(|e| AppError::Network(e))?;"#,
+ bad_example: r#"let value = some_option.unwrap();
+let data = fetch_data().expect("fetch failed");"#,
+ source: "https://github.com/RAprogramm/RustManifest#5-best-practices"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra003.rs b/masterror-knowledge/src/errors/raprogramm/ra003.rs
new file mode 100644
index 0000000..14b249c
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra003.rs
@@ -0,0 +1,39 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA003: Avoid unnecessary clone() calls
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA003",
+ title: LocalizedText::new(
+ "Avoid unnecessary clone() calls",
+ "Избегайте ненужных вызовов clone()",
+ "불필요한 clone() 호출 피하기"
+ ),
+ category: PracticeCategory::Performance,
+ explanation: LocalizedText::new(
+ "\
+Cloning allocates memory and copies data. Often you can use references instead.
+Only clone when you actually need ownership of the data.
+
+Common anti-patterns:
+- Cloning just to satisfy the borrow checker (restructure instead)
+- Cloning in a loop (clone once before the loop)
+- Cloning when a reference would work",
+ "\
+Клонирование выделяет память и копирует данные. Часто можно использовать ссылки.
+Клонируйте только когда действительно нужно владение данными.",
+ "\
+클론은 메모리를 할당하고 데이터를 복사합니다. 종종 참조를 대신 사용할 수 있습니다."
+ ),
+ good_example: r#"fn process(data: &str) { /* use reference */ }
+let owned = expensive_data.clone(); // clone once
+for item in &items { process(item); }"#,
+ bad_example: r#"for item in items {
+ process(item.clone()); // clones every iteration!
+}"#,
+ source: "https://github.com/RAprogramm/RustManifest#3-code-quality"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra004.rs b/masterror-knowledge/src/errors/raprogramm/ra004.rs
new file mode 100644
index 0000000..ab1caae
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra004.rs
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA004: Use descriptive, meaningful names
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA004",
+ title: LocalizedText::new(
+ "Use descriptive, meaningful names",
+ "Используйте описательные, значимые имена",
+ "설명적이고 의미 있는 이름 사용"
+ ),
+ category: PracticeCategory::Naming,
+ explanation: LocalizedText::new(
+ "\
+Names must reflect purpose. Avoid generic terms like 'create', 'handle', 'data'.
+Descriptive names reduce ambiguity, facilitate easier onboarding, and improve
+maintainability.
+
+Conventions:
+- snake_case for variables and functions
+- PascalCase for structs and enums
+- SCREAMING_SNAKE_CASE for constants",
+ "\
+Имена должны отражать назначение. Избегайте общих терминов вроде 'create', 'handle', 'data'.
+Описательные имена уменьшают неоднозначность и улучшают поддерживаемость.",
+ "\
+이름은 목적을 반영해야 합니다. 'create', 'handle', 'data' 같은 일반적인 용어를 피하세요."
+ ),
+ good_example: r#"fn create_user_handler(req: CreateUserRequest) -> Result
+const MAX_RETRY_ATTEMPTS: u32 = 3;
+struct UserAuthenticationService { ... }"#,
+ bad_example: r#"fn create(r: Request) -> Result
+const MAX: u32 = 3;
+struct Service { ... }"#,
+ source: "https://github.com/RAprogramm/RustManifest#2-naming-conventions"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra005.rs b/masterror-knowledge/src/errors/raprogramm/ra005.rs
new file mode 100644
index 0000000..0087cc2
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra005.rs
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA005: No inline comments - use docblocks only
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA005",
+ title: LocalizedText::new(
+ "No inline comments - use docblocks only",
+ "Никаких инлайн комментариев - только docblocks",
+ "인라인 주석 금지 - docblock만 사용"
+ ),
+ category: PracticeCategory::Documentation,
+ explanation: LocalizedText::new(
+ "\
+Avoid // and /* */ explanations in code. All documentation lives in docblocks:
+/// for items, //! for modules.
+
+Standardized headings for IDE/LSP stability:
+- # Overview - Short purpose statement
+- # Examples - Minimal, compilable samples
+- # Errors - Precise failure modes for Result types
+- # Panics - Only if unavoidable
+- # Safety - Required if unsafe code present",
+ "\
+Избегайте // и /* */ объяснений в коде. Вся документация живёт в docblocks:
+/// для элементов, //! для модулей.",
+ "\
+코드에서 // 및 /* */ 설명을 피하세요. 모든 문서는 docblock에."
+ ),
+ good_example: r#"/// Fetches user data from the database.
+///
+/// # Errors
+/// Returns `DbError::NotFound` if user doesn't exist.
+///
+/// # Examples
+/// ```
+/// let user = fetch_user(42)?;
+/// ```
+pub fn fetch_user(id: u64) -> Result"#,
+ bad_example: r#"// This function fetches user data from the database
+// It returns an error if user is not found
+pub fn fetch_user(id: u64) -> Result"#,
+ source: "https://github.com/RAprogramm/RustManifest#8-code-documentation"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra006.rs b/masterror-knowledge/src/errors/raprogramm/ra006.rs
new file mode 100644
index 0000000..ae7a5e1
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra006.rs
@@ -0,0 +1,46 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA006: Entity naming - no -er suffixes
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA006",
+ title: LocalizedText::new(
+ "Entity naming: avoid -er, -or, -manager suffixes",
+ "Именование сущностей: избегайте суффиксов -er, -or, -manager",
+ "엔티티 명명: -er, -or, -manager 접미사 피하기"
+ ),
+ category: PracticeCategory::Naming,
+ explanation: LocalizedText::new(
+ "\
+Structures represent entities, not actions. The -er suffix encourages procedural
+thinking that separates data from behavior, creating anemic domain models.
+Entity naming naturally unifies data and operations.
+
+Transforms:
+- ConfigLoader → Config
+- MessageParser → Message
+- RequestHandler → Request
+- DataValidator → Data
+- ConnectionManager → ConnectionPool
+
+Exceptions: Iterator, Builder, Visitor, Formatter (established patterns).",
+ "\
+Структуры представляют сущности, не действия. Суффикс -er поощряет процедурное
+мышление, разделяющее данные и поведение. Именование сущностей объединяет их.",
+ "\
+구조체는 동작이 아닌 엔티티를 나타냅니다. -er 접미사는 절차적 사고를 장려합니다."
+ ),
+ good_example: r#"struct Config { ... }
+struct Message { ... }
+struct Request { ... }
+struct ConnectionPool { ... }"#,
+ bad_example: r#"struct ConfigLoader { ... }
+struct MessageParser { ... }
+struct RequestHandler { ... }
+struct ConnectionManager { ... }"#,
+ source: "https://github.com/RAprogramm/RustManifest/blob/main/STRUCTURE.md#1-entity-naming"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra007.rs b/masterror-knowledge/src/errors/raprogramm/ra007.rs
new file mode 100644
index 0000000..b585557
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra007.rs
@@ -0,0 +1,44 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA007: Method naming - nouns for accessors, verbs for mutators
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA007",
+ title: LocalizedText::new(
+ "Method naming: nouns for accessors, verbs for mutators",
+ "Именование методов: существительные для accessors, глаголы для mutators",
+ "메서드 명명: accessor는 명사, mutator는 동사"
+ ),
+ category: PracticeCategory::Naming,
+ explanation: LocalizedText::new(
+ "\
+Method names reflect purpose through grammatical form:
+- Accessors (nouns): name(), length(), value() — not get_name()
+- Predicates (adjectives): empty(), valid(), published() — not is_empty()
+- Mutators (verbs): save(), publish(), delete()
+
+The get_ prefix adds noise without information. Omitting verbs signals pure
+accessors. Adjective predicates read more naturally than is_ constructions.",
+ "\
+Имена методов отражают назначение через грамматическую форму:
+- Accessors: name(), length() — не get_name()
+- Predicates: empty(), valid() — не is_empty()
+- Mutators: save(), publish(), delete()",
+ "\
+메서드 이름은 문법적 형태로 목적을 반영합니다."
+ ),
+ good_example: r#"impl User {
+ fn name(&self) -> &str { &self.name }
+ fn empty(&self) -> bool { self.data.is_empty() }
+ fn save(&mut self) { ... }
+}"#,
+ bad_example: r#"impl User {
+ fn get_name(&self) -> &str { &self.name }
+ fn is_empty(&self) -> bool { self.data.is_empty() }
+}"#,
+ source: "https://github.com/RAprogramm/RustManifest/blob/main/STRUCTURE.md#2-method-naming"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra008.rs b/masterror-knowledge/src/errors/raprogramm/ra008.rs
new file mode 100644
index 0000000..9439153
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra008.rs
@@ -0,0 +1,50 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA008: Structure size - maximum 4 fields
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA008",
+ title: LocalizedText::new(
+ "Structure size: maximum 4 fields",
+ "Размер структуры: максимум 4 поля",
+ "구조체 크기: 최대 4개 필드"
+ ),
+ category: PracticeCategory::Design,
+ explanation: LocalizedText::new(
+ "\
+A structure should have no more than 4 fields. More fields indicate multiple
+responsibilities requiring composition.
+
+Problems with large structures:
+- Complex testing with many combinations
+- Changes ripple through unrelated code
+- Purpose becomes unclear
+- Parts cannot be reused independently
+
+Solution: Decompose into focused sub-structures.",
+ "\
+Структура должна иметь не более 4 полей. Больше полей указывает на
+множественные ответственности, требующие композиции.",
+ "\
+구조체는 4개 이하의 필드를 가져야 합니다. 더 많은 필드는 분해가 필요함을 나타냅니다."
+ ),
+ good_example: r#"struct User {
+ identity: UserIdentity,
+ credentials: Credentials,
+ profile: UserProfile,
+ access: AccessControl,
+}"#,
+ bad_example: r#"struct User {
+ id: u64, email: String, password_hash: String,
+ name: String, avatar: Option, bio: String,
+ created_at: DateTime, updated_at: DateTime,
+ role: Role, permissions: Vec,
+ last_login: Option, login_count: u32,
+ is_verified: bool, verification_token: Option,
+}"#,
+ source: "https://github.com/RAprogramm/RustManifest/blob/main/STRUCTURE.md#3-structure-size"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra009.rs b/masterror-knowledge/src/errors/raprogramm/ra009.rs
new file mode 100644
index 0000000..91573b8
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra009.rs
@@ -0,0 +1,58 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA009: Public API size - maximum 5 methods
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA009",
+ title: LocalizedText::new(
+ "Public API size: maximum 5 methods",
+ "Размер публичного API: максимум 5 методов",
+ "공개 API 크기: 최대 5개 메서드"
+ ),
+ category: PracticeCategory::Design,
+ explanation: LocalizedText::new(
+ "\
+A structure's public interface should have no more than 5 methods.
+More methods signal the structure does too much.
+
+Large APIs indicate mixed responsibilities, forcing users to understand more,
+expanding documentation and testing complexity.
+
+Solution: Extract secondary concerns into separate types.
+Excludes: trait implementations (Display, Debug, From) and generic new().",
+ "\
+Публичный интерфейс структуры должен иметь не более 5 методов.
+Больше методов означает, что структура делает слишком много.",
+ "\
+구조체의 공개 인터페이스는 5개 이하의 메서드를 가져야 합니다."
+ ),
+ good_example: r#"impl Document {
+ pub fn new() -> Self { ... }
+ pub fn load(path: &Path) -> Result { ... }
+ pub fn save(&self) -> Result<()> { ... }
+ pub fn content(&self) -> &str { ... }
+ pub fn metadata(&self) -> &Metadata { ... }
+}
+
+// Rendering is separate
+impl Renderer { ... }
+// Export is separate
+impl Exporter { ... }"#,
+ bad_example: r#"impl Document {
+ pub fn new() -> Self { ... }
+ pub fn load() -> Result { ... }
+ pub fn save() -> Result<()> { ... }
+ pub fn content(&self) -> &str { ... }
+ pub fn metadata(&self) -> &Metadata { ... }
+ pub fn render_html(&self) -> String { ... }
+ pub fn render_pdf(&self) -> Vec { ... }
+ pub fn export_json(&self) -> String { ... }
+ pub fn validate(&self) -> Result<()> { ... }
+ // ... 10+ more methods
+}"#,
+ source: "https://github.com/RAprogramm/RustManifest/blob/main/STRUCTURE.md#4-public-api-size"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra010.rs b/masterror-knowledge/src/errors/raprogramm/ra010.rs
new file mode 100644
index 0000000..16397f6
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra010.rs
@@ -0,0 +1,56 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA010: Constructors should only assign fields
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA010",
+ title: LocalizedText::new(
+ "Constructors: assignment only, no logic",
+ "Конструкторы: только присваивание, никакой логики",
+ "생성자: 할당만, 로직 없음"
+ ),
+ category: PracticeCategory::Design,
+ explanation: LocalizedText::new(
+ "\
+Constructors should only assign fields. All processing, validation, and I/O
+belong in methods.
+
+Problems with logic in constructors:
+- Constructors can fail, complicating object creation
+- Work happens eagerly even if unused
+- Inflexible creation paths
+- Hard to test without real resources
+
+Benefits of assignment-only constructors:
+- Infallible object creation
+- Lazy evaluation of expensive work
+- Multiple creation paths (from_data() for tests)",
+ "\
+Конструкторы должны только присваивать поля. Вся обработка, валидация и I/O
+принадлежат методам.",
+ "\
+생성자는 필드만 할당해야 합니다. 모든 처리, 검증, I/O는 메서드에."
+ ),
+ good_example: r#"impl Server {
+ pub fn new(config: Config) -> Self {
+ Self { config, connection: None }
+ }
+
+ pub fn connect(&mut self) -> Result<()> {
+ self.connection = Some(Connection::establish(&self.config)?);
+ Ok(())
+ }
+}"#,
+ bad_example: r#"impl Server {
+ pub fn new(config: Config) -> Result {
+ let connection = Connection::establish(&config)?; // I/O in constructor!
+ validate_config(&config)?; // Logic in constructor!
+ Ok(Self { config, connection })
+ }
+}"#,
+ source: "https://github.com/RAprogramm/RustManifest/blob/main/STRUCTURE.md#5-constructor-design"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra011.rs b/masterror-knowledge/src/errors/raprogramm/ra011.rs
new file mode 100644
index 0000000..a837c1b
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra011.rs
@@ -0,0 +1,44 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA011: Immutability first - prefer self over &mut self
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA011",
+ title: LocalizedText::new(
+ "Immutability first: prefer self over &mut self",
+ "Сначала неизменяемость: предпочитайте self вместо &mut self",
+ "불변성 우선: &mut self보다 self 선호"
+ ),
+ category: PracticeCategory::Design,
+ explanation: LocalizedText::new(
+ "\
+Prefer returning new objects over mutating existing ones. Use `self` instead
+of `&mut self` where practical.
+
+Problems with mutable objects:
+- Shared state bugs from unexpected modifications
+- Thread safety requires synchronization
+- Temporal coupling makes operation order matter
+- Incomplete state during configuration
+
+Exceptions: Large data structures, I/O, performance-critical loops, Iterator::next",
+ "\
+Предпочитайте возврат новых объектов вместо изменения существующих.
+Используйте `self` вместо `&mut self` где возможно.",
+ "\
+기존 객체를 변경하는 것보다 새 객체를 반환하는 것을 선호하세요."
+ ),
+ good_example: r#"Request::new(url)
+ .header("Content-Type", "application/json")
+ .body(payload)
+ .send()"#,
+ bad_example: r#"let mut req = Request::new(url);
+req.set_header("Content-Type", "application/json");
+req.set_body(payload);
+req.send()"#,
+ source: "https://github.com/RAprogramm/RustManifest/blob/main/STRUCTURE.md#7-immutability-first"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra012.rs b/masterror-knowledge/src/errors/raprogramm/ra012.rs
new file mode 100644
index 0000000..8274d8c
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra012.rs
@@ -0,0 +1,49 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA012: Constant encapsulation - associate with types
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA012",
+ title: LocalizedText::new(
+ "Encapsulate constants in their types",
+ "Инкапсулируйте константы в их типы",
+ "상수를 타입에 캡슐화"
+ ),
+ category: PracticeCategory::Design,
+ explanation: LocalizedText::new(
+ "\
+Constants belong to structures using them, not global scope. This improves
+discoverability and prevents namespace pollution.
+
+Benefits:
+- Clear discovery location
+- Built-in documentation
+- Automatic namespacing
+- Encapsulation
+- Easy refactoring",
+ "\
+Константы принадлежат структурам, которые их используют, а не глобальной области.
+Это улучшает обнаруживаемость и предотвращает загрязнение пространства имён.",
+ "\
+상수는 전역 범위가 아닌 사용하는 구조체에 속합니다."
+ ),
+ good_example: r#"impl ConnectionPool {
+ pub const MAX_SIZE: usize = 100;
+}
+
+impl Client {
+ pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
+}
+
+// Usage: ConnectionPool::MAX_SIZE"#,
+ bad_example: r#"const MAX_POOL_SIZE: usize = 100;
+const DEFAULT_CLIENT_TIMEOUT: Duration = Duration::from_secs(30);
+const MAX_RETRIES: u32 = 3;
+const DEFAULT_PORT: u16 = 8080;
+// ... scattered constants"#,
+ source: "https://github.com/RAprogramm/RustManifest/blob/main/STRUCTURE.md#8-constant-encapsulation"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra013.rs b/masterror-knowledge/src/errors/raprogramm/ra013.rs
new file mode 100644
index 0000000..306b810
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra013.rs
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA013: Testing with fakes over mocks
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA013",
+ title: LocalizedText::new(
+ "Use fakes over mocks for testing",
+ "Используйте fakes вместо mocks для тестирования",
+ "테스트에 mock보다 fake 사용"
+ ),
+ category: PracticeCategory::Testing,
+ explanation: LocalizedText::new(
+ "\
+Use simple fake implementations instead of mock libraries. Fakes provide real
+behavior; mocks verify call sequences.
+
+| Aspect | Mocks | Fakes |
+|--------|-------|-------|
+| Coupling | High | Low |
+| Maintenance | Breaks on refactoring | Survives changes |
+| Behavior | Simulates | Provides real |
+| Debugging | Cryptic | Standard |
+
+Mock appropriateness: Verifying external system interactions, ensuring methods
+are NOT called, testing strict interaction ordering.",
+ "\
+Используйте простые fake-реализации вместо mock-библиотек. Fakes обеспечивают
+реальное поведение; mocks проверяют последовательности вызовов.",
+ "\
+mock 라이브러리 대신 간단한 fake 구현을 사용하세요."
+ ),
+ good_example: r#"struct FakeDatabase {
+ users: HashMap,
+}
+
+impl FakeDatabase {
+ fn new() -> Self { Self { users: HashMap::new() } }
+ fn insert(&mut self, user: User) { self.users.insert(user.id, user); }
+}
+
+impl Database for FakeDatabase {
+ fn find_user(&self, id: u64) -> Option<&User> {
+ self.users.get(&id)
+ }
+}"#,
+ bad_example: r#"#[test]
+fn test_user_service() {
+ let mut mock = MockDatabase::new();
+ mock.expect_find_user()
+ .with(eq(42))
+ .times(1)
+ .returning(|_| Some(User::default()));
+ // Breaks when implementation changes
+}"#,
+ source: "https://github.com/RAprogramm/RustManifest/blob/main/STRUCTURE.md#9-testing-with-fakes"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra014.rs b/masterror-knowledge/src/errors/raprogramm/ra014.rs
new file mode 100644
index 0000000..4301b08
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra014.rs
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA014: Pre-allocate with Vec::with_capacity
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA014",
+ title: LocalizedText::new(
+ "Pre-allocate with Vec::with_capacity",
+ "Предвыделяйте память с Vec::with_capacity",
+ "Vec::with_capacity로 사전 할당"
+ ),
+ category: PracticeCategory::Performance,
+ explanation: LocalizedText::new(
+ "\
+When you know the approximate size of a Vec, pre-allocate to avoid reallocations.
+Each reallocation copies all existing elements to new memory.
+
+This is especially important in hot paths and loops.",
+ "\
+Когда знаете примерный размер Vec, предвыделяйте чтобы избежать реаллокаций.
+Каждая реаллокация копирует все элементы в новую память.",
+ "\
+Vec의 대략적인 크기를 알 때, 재할당을 피하기 위해 사전 할당하세요."
+ ),
+ good_example: r#"let mut results = Vec::with_capacity(items.len());
+for item in items {
+ results.push(process(item));
+}"#,
+ bad_example: r#"let mut results = Vec::new(); // starts with 0 capacity
+for item in items {
+ results.push(process(item)); // reallocates multiple times!
+}"#,
+ source: "https://github.com/RAprogramm/RustManifest#9-code-review-methodology"
+};
diff --git a/masterror-knowledge/src/errors/raprogramm/ra015.rs b/masterror-knowledge/src/errors/raprogramm/ra015.rs
new file mode 100644
index 0000000..0bfd79c
--- /dev/null
+++ b/masterror-knowledge/src/errors/raprogramm/ra015.rs
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RA015: Avoid O(n²) algorithms
+
+use crate::errors::raprogramm::{BestPractice, LocalizedText, PracticeCategory};
+
+pub static ENTRY: BestPractice = BestPractice {
+ code: "RA015",
+ title: LocalizedText::new(
+ "Avoid O(n²) algorithms",
+ "Избегайте алгоритмов O(n²)",
+ "O(n²) 알고리즘 피하기"
+ ),
+ category: PracticeCategory::Performance,
+ explanation: LocalizedText::new(
+ "\
+Nested loops over the same data often indicate O(n²) complexity.
+Use HashSet/HashMap for lookups, or sort + binary search.
+
+What looks fine with 100 items becomes unusable with 10,000.",
+ "\
+Вложенные циклы по одним данным часто указывают на O(n²) сложность.
+Используйте HashSet/HashMap для поиска или сортировку + бинарный поиск.",
+ "\
+같은 데이터에 대한 중첩 루프는 종종 O(n²) 복잡도를 나타냅니다."
+ ),
+ good_example: r#"let seen: HashSet<_> = items.iter().collect();
+for item in other_items {
+ if seen.contains(&item) { // O(1) lookup
+ // ...
+ }
+}"#,
+ bad_example: r#"for item in other_items {
+ for existing in &items { // O(n) for each = O(n²) total
+ if item == existing {
+ // ...
+ }
+ }
+}"#,
+ source: "https://github.com/RAprogramm/RustManifest#9-code-review-methodology"
+};
diff --git a/masterror-knowledge/src/errors/resolution.rs b/masterror-knowledge/src/errors/resolution.rs
new file mode 100644
index 0000000..42f17d8
--- /dev/null
+++ b/masterror-knowledge/src/errors/resolution.rs
@@ -0,0 +1,17 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Name resolution errors.
+
+mod e0412;
+mod e0425;
+mod e0433;
+
+use super::ErrorEntry;
+
+static ENTRIES: &[&ErrorEntry] = &[&e0412::ENTRY, &e0425::ENTRY, &e0433::ENTRY];
+
+pub fn entries() -> &'static [&'static ErrorEntry] {
+ ENTRIES
+}
diff --git a/masterror-knowledge/src/errors/resolution/e0412.rs b/masterror-knowledge/src/errors/resolution/e0412.rs
new file mode 100644
index 0000000..32a9aa1
--- /dev/null
+++ b/masterror-knowledge/src/errors/resolution/e0412.rs
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0412",
+ title: LocalizedText::new(
+ "Cannot find type in this scope",
+ "Не удаётся найти тип в этой области видимости",
+ "이 스코프에서 타입을 찾을 수 없음"
+ ),
+ category: Category::Resolution,
+ explanation: LocalizedText::new(
+ "The type you're referencing doesn't exist or isn't in scope.",
+ "Тип, на который вы ссылаетесь, не существует или не в области видимости.",
+ "참조하는 타입이 존재하지 않거나 스코프에 없습니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new("Import the type", "Импортировать тип", "타입 import"),
+ code: "use crate::types::MyType;"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0412.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/resolution/e0425.rs b/masterror-knowledge/src/errors/resolution/e0425.rs
new file mode 100644
index 0000000..b973d07
--- /dev/null
+++ b/masterror-knowledge/src/errors/resolution/e0425.rs
@@ -0,0 +1,32 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0425",
+ title: LocalizedText::new(
+ "Cannot find value in this scope",
+ "Не удаётся найти значение в этой области видимости",
+ "이 스코프에서 값을 찾을 수 없음"
+ ),
+ category: Category::Resolution,
+ explanation: LocalizedText::new(
+ "You're using a variable, function, or constant that doesn't exist in scope.",
+ "Вы используете переменную, функцию или константу, которая не существует в текущей области.",
+ "스코프에 존재하지 않는 변수, 함수 또는 상수를 사용하고 있습니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new(
+ "Declare the variable",
+ "Объявить переменную",
+ "변수 선언"
+ ),
+ code: "let x = 10;"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0425.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/resolution/e0433.rs b/masterror-knowledge/src/errors/resolution/e0433.rs
new file mode 100644
index 0000000..b9e4d19
--- /dev/null
+++ b/masterror-knowledge/src/errors/resolution/e0433.rs
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0433",
+ title: LocalizedText::new(
+ "Failed to resolve: use of undeclared crate or module",
+ "Не удалось разрешить: необъявленный крейт или модуль",
+ "해결 실패: 선언되지 않은 크레이트 또는 모듈"
+ ),
+ category: Category::Resolution,
+ explanation: LocalizedText::new(
+ "Rust can't find the crate, module, or type you're trying to use.",
+ "Rust не может найти крейт, модуль или тип.",
+ "Rust가 크레이트, 모듈 또는 타입을 찾을 수 없습니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new("Add use statement", "Добавить use", "use 문 추가"),
+ code: "use std::collections::HashMap;"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Add dependency",
+ "Добавить зависимость",
+ "의존성 추가"
+ ),
+ code: "[dependencies]\nserde = \"1.0\""
+ }
+ ],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0433.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/traits.rs b/masterror-knowledge/src/errors/traits.rs
new file mode 100644
index 0000000..d402807
--- /dev/null
+++ b/masterror-knowledge/src/errors/traits.rs
@@ -0,0 +1,16 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Trait-related errors.
+
+mod e0038;
+mod e0282;
+
+use super::ErrorEntry;
+
+static ENTRIES: &[&ErrorEntry] = &[&e0038::ENTRY, &e0282::ENTRY];
+
+pub fn entries() -> &'static [&'static ErrorEntry] {
+ ENTRIES
+}
diff --git a/masterror-knowledge/src/errors/traits/e0038.rs b/masterror-knowledge/src/errors/traits/e0038.rs
new file mode 100644
index 0000000..9055739
--- /dev/null
+++ b/masterror-knowledge/src/errors/traits/e0038.rs
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0038",
+ title: LocalizedText::new(
+ "Trait cannot be made into an object",
+ "Трейт не может быть превращён в объект",
+ "트레이트를 객체로 만들 수 없음"
+ ),
+ category: Category::Traits,
+ explanation: LocalizedText::new(
+ "This trait is not object-safe - it can't be used as `dyn Trait`.",
+ "Этот трейт не объектно-безопасен - его нельзя использовать как `dyn Trait`.",
+ "이 트레이트는 객체 안전하지 않습니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new("Use generics", "Использовать обобщения", "제네릭 사용"),
+ code: "fn process(item: T) { ... }"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0038.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/traits/e0282.rs b/masterror-knowledge/src/errors/traits/e0282.rs
new file mode 100644
index 0000000..1861285
--- /dev/null
+++ b/masterror-knowledge/src/errors/traits/e0282.rs
@@ -0,0 +1,32 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0282",
+ title: LocalizedText::new(
+ "Type annotations needed",
+ "Требуются аннотации типа",
+ "타입 어노테이션이 필요함"
+ ),
+ category: Category::Traits,
+ explanation: LocalizedText::new(
+ "The compiler cannot infer the type. Provide an explicit type annotation.",
+ "Компилятор не может вывести тип. Укажите явную аннотацию типа.",
+ "컴파일러가 타입을 추론할 수 없습니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new(
+ "Add type annotation",
+ "Добавить аннотацию",
+ "타입 어노테이션 추가"
+ ),
+ code: "let numbers: Vec = input.parse().unwrap();"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0282.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/types.rs b/masterror-knowledge/src/errors/types.rs
new file mode 100644
index 0000000..8cf39da
--- /dev/null
+++ b/masterror-knowledge/src/errors/types.rs
@@ -0,0 +1,17 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Type-related errors.
+
+mod e0277;
+mod e0308;
+mod e0599;
+
+use super::ErrorEntry;
+
+static ENTRIES: &[&ErrorEntry] = &[&e0277::ENTRY, &e0308::ENTRY, &e0599::ENTRY];
+
+pub fn entries() -> &'static [&'static ErrorEntry] {
+ ENTRIES
+}
diff --git a/masterror-knowledge/src/errors/types/e0277.rs b/masterror-knowledge/src/errors/types/e0277.rs
new file mode 100644
index 0000000..e054ab2
--- /dev/null
+++ b/masterror-knowledge/src/errors/types/e0277.rs
@@ -0,0 +1,42 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0277",
+ title: LocalizedText::new(
+ "Trait bound not satisfied",
+ "Ограничение трейта не выполнено",
+ "트레이트 바운드가 충족되지 않음"
+ ),
+ category: Category::Types,
+ explanation: LocalizedText::new(
+ "A generic function or type requires a trait bound that your type doesn't satisfy.",
+ "Обобщённая функция или тип требует ограничение трейта, которому ваш тип не удовлетворяет.",
+ "제네릭 함수나 타입이 당신의 타입이 충족하지 않는 트레이트 바운드를 요구합니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Derive the trait",
+ "Получить через derive",
+ "트레이트 derive"
+ ),
+ code: "#[derive(Hash, Eq, PartialEq)]"
+ },
+ FixSuggestion {
+ description: LocalizedText::new(
+ "Implement manually",
+ "Реализовать вручную",
+ "수동 구현"
+ ),
+ code: "impl MyTrait for MyType { ... }"
+ }
+ ],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0277.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/types/e0308.rs b/masterror-knowledge/src/errors/types/e0308.rs
new file mode 100644
index 0000000..a01246f
--- /dev/null
+++ b/masterror-knowledge/src/errors/types/e0308.rs
@@ -0,0 +1,30 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0308",
+ title: LocalizedText::new("Mismatched types", "Несовпадение типов", "타입 불일치"),
+ category: Category::Types,
+ explanation: LocalizedText::new(
+ "Rust is statically typed and does NOT perform implicit type conversions.",
+ "Rust статически типизирован и НЕ выполняет неявные преобразования типов.",
+ "Rust는 정적 타입 언어이며 암시적 타입 변환을 수행하지 않습니다."
+ ),
+ fixes: &[
+ FixSuggestion {
+ description: LocalizedText::new("Use parse()", "Использовать parse()", "parse() 사용"),
+ code: "let n: i32 = s.parse().unwrap();"
+ },
+ FixSuggestion {
+ description: LocalizedText::new("Use 'as'", "Использовать 'as'", "'as' 사용"),
+ code: "let n = x as i32;"
+ }
+ ],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0308.html"
+ }]
+};
diff --git a/masterror-knowledge/src/errors/types/e0599.rs b/masterror-knowledge/src/errors/types/e0599.rs
new file mode 100644
index 0000000..84a2802
--- /dev/null
+++ b/masterror-knowledge/src/errors/types/e0599.rs
@@ -0,0 +1,32 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+use crate::errors::{Category, DocLink, ErrorEntry, FixSuggestion, LocalizedText};
+
+pub static ENTRY: ErrorEntry = ErrorEntry {
+ code: "E0599",
+ title: LocalizedText::new(
+ "No method named X found for type",
+ "Метод не найден для типа",
+ "타입에서 메서드를 찾을 수 없음"
+ ),
+ category: Category::Types,
+ explanation: LocalizedText::new(
+ "You're calling a method that doesn't exist on this type. Check trait imports.",
+ "Вы вызываете метод, который не существует для этого типа. Проверьте импорт трейтов.",
+ "이 타입에 존재하지 않는 메서드를 호출하고 있습니다."
+ ),
+ fixes: &[FixSuggestion {
+ description: LocalizedText::new(
+ "Import the trait",
+ "Импортировать трейт",
+ "트레이트 import"
+ ),
+ code: "use std::io::Read;"
+ }],
+ links: &[DocLink {
+ title: "Error Code Reference",
+ url: "https://doc.rust-lang.org/error_codes/E0599.html"
+ }]
+};
diff --git a/masterror-knowledge/src/i18n/messages.rs b/masterror-knowledge/src/i18n/messages.rs
new file mode 100644
index 0000000..99ebba6
--- /dev/null
+++ b/masterror-knowledge/src/i18n/messages.rs
@@ -0,0 +1,206 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! UI messages for masterror-cli.
+
+use crate::define_messages;
+
+define_messages! {
+ pub enum UiMsg {
+ LabelTranslation {
+ en: "Translation:",
+ ru: "Перевод:",
+ ko: "번역:",
+ }
+ LabelWhy {
+ en: "Why this happens:",
+ ru: "Почему это происходит:",
+ ko: "왜 이런 일이 발생하나요:",
+ }
+ LabelFix {
+ en: "How to fix:",
+ ru: "Как исправить:",
+ ko: "해결 방법:",
+ }
+ LabelLink {
+ en: "Learn more:",
+ ru: "Подробнее:",
+ ko: "더 알아보기:",
+ }
+ LabelWhyMatters {
+ en: "Why this matters:",
+ ru: "Почему это важно:",
+ ko: "왜 중요한가:",
+ }
+ LabelHowToApply {
+ en: "How to apply:",
+ ru: "Как применять:",
+ ko: "적용 방법:",
+ }
+ LabelAvoid {
+ en: "Avoid",
+ ru: "Избегайте",
+ ko: "피하세요",
+ }
+ LabelPrefer {
+ en: "Prefer",
+ ru: "Предпочитайте",
+ ko: "선호하세요",
+ }
+
+ CategoryOwnership {
+ en: "Ownership",
+ ru: "Владение",
+ ko: "소유권",
+ }
+ CategoryBorrowing {
+ en: "Borrowing",
+ ru: "Заимствование",
+ ko: "빌림",
+ }
+ CategoryLifetimes {
+ en: "Lifetimes",
+ ru: "Времена жизни",
+ ko: "라이프타임",
+ }
+ CategoryTypes {
+ en: "Types",
+ ru: "Типы",
+ ko: "타입",
+ }
+ CategoryTraits {
+ en: "Traits",
+ ru: "Трейты",
+ ko: "트레이트",
+ }
+ CategoryResolution {
+ en: "Name Resolution",
+ ru: "Разрешение имён",
+ ko: "이름 확인",
+ }
+
+ CategoryErrorHandling {
+ en: "Error Handling",
+ ru: "Обработка ошибок",
+ ko: "에러 처리",
+ }
+ CategoryPerformance {
+ en: "Performance",
+ ru: "Производительность",
+ ko: "성능",
+ }
+ CategoryNaming {
+ en: "Naming",
+ ru: "Именование",
+ ko: "명명",
+ }
+ CategoryDocumentation {
+ en: "Documentation",
+ ru: "Документация",
+ ko: "문서화",
+ }
+ CategoryDesign {
+ en: "Design",
+ ru: "Дизайн",
+ ko: "설계",
+ }
+ CategoryTesting {
+ en: "Testing",
+ ru: "Тестирование",
+ ko: "테스트",
+ }
+ CategorySecurity {
+ en: "Security",
+ ru: "Безопасность",
+ ko: "보안",
+ }
+
+ UnknownCode {
+ en: "Unknown code",
+ ru: "Неизвестный код",
+ ko: "알 수 없는 코드",
+ }
+ Category {
+ en: "Category",
+ ru: "Категория",
+ ko: "카테고리",
+ }
+
+ InitTitle {
+ en: "masterror configuration",
+ ru: "Настройка masterror",
+ ko: "masterror 설정",
+ }
+ InitSuccess {
+ en: "Configuration saved to",
+ ru: "Конфигурация сохранена в",
+ ko: "설정이 저장됨:",
+ }
+ InitLangPrompt {
+ en: "Language",
+ ru: "Язык",
+ ko: "언어",
+ }
+ InitColorPrompt {
+ en: "Colored output",
+ ru: "Цветной вывод",
+ ko: "색상 출력",
+ }
+ InitDisplayPrompt {
+ en: "Display sections:",
+ ru: "Секции для отображения:",
+ ko: "표시 섹션:",
+ }
+ InitShowTranslation {
+ en: "Show translation",
+ ru: "Показывать перевод",
+ ko: "번역 표시",
+ }
+ InitShowWhy {
+ en: "Show explanation",
+ ru: "Показывать объяснение",
+ ko: "설명 표시",
+ }
+ InitShowFix {
+ en: "Show fix suggestions",
+ ru: "Показывать исправления",
+ ko: "수정 제안 표시",
+ }
+ InitShowLinks {
+ en: "Show documentation links",
+ ru: "Показывать ссылки",
+ ko: "문서 링크 표시",
+ }
+ InitShowOriginal {
+ en: "Show original compiler output",
+ ru: "Показывать оригинальный вывод",
+ ko: "원본 컴파일러 출력 표시",
+ }
+ InitSavePrompt {
+ en: "Where to save configuration?",
+ ru: "Где сохранить настройки?",
+ ko: "설정을 어디에 저장할까요?",
+ }
+ InitSaveGlobal {
+ en: "Global (~/.config/masterror/) - applies to all projects",
+ ru: "Глобально (~/.config/masterror/) - для всех проектов",
+ ko: "전역 (~/.config/masterror/) - 모든 프로젝트에 적용",
+ }
+ InitSaveLocal {
+ en: "Local (.masterror.toml) - only this project",
+ ru: "Локально (.masterror.toml) - только этот проект",
+ ko: "로컬 (.masterror.toml) - 이 프로젝트만",
+ }
+ InitTip {
+ en: "You can change settings anytime by editing:",
+ ru: "Вы можете изменить настройки в любой момент, отредактировав:",
+ ko: "언제든지 다음 파일을 편집하여 설정을 변경할 수 있습니다:",
+ }
+ InitUsage {
+ en: "Start using masterror:",
+ ru: "Начните использовать masterror:",
+ ko: "masterror 사용 시작:",
+ }
+ }
+}
diff --git a/masterror-knowledge/src/i18n/mod.rs b/masterror-knowledge/src/i18n/mod.rs
new file mode 100644
index 0000000..a7c6afc
--- /dev/null
+++ b/masterror-knowledge/src/i18n/mod.rs
@@ -0,0 +1,152 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Internationalization system for masterror-cli.
+//!
+//! Provides compile-time localization with zero runtime allocation.
+
+pub mod messages;
+pub mod phrases;
+
+/// Supported languages.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
+#[repr(u8)]
+pub enum Lang {
+ /// English (default).
+ #[default]
+ En = 0,
+ /// Russian.
+ #[cfg(feature = "lang-ru")]
+ Ru = 1,
+ /// Korean.
+ #[cfg(feature = "lang-ko")]
+ Ko = 2
+}
+
+impl Lang {
+ /// Parse language from string, fallback to English.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use masterror_knowledge::Lang;
+ ///
+ /// assert_eq!(Lang::from_code("ru"), Lang::Ru);
+ /// assert_eq!(Lang::from_code("unknown"), Lang::En);
+ /// ```
+ pub fn from_code(s: &str) -> Self {
+ match s {
+ #[cfg(feature = "lang-ru")]
+ "ru" | "RU" | "Ru" => Self::Ru,
+ #[cfg(feature = "lang-ko")]
+ "ko" | "KO" | "Ko" => Self::Ko,
+ _ => Self::En
+ }
+ }
+
+ /// Get language code as string.
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::En => "en",
+ #[cfg(feature = "lang-ru")]
+ Self::Ru => "ru",
+ #[cfg(feature = "lang-ko")]
+ Self::Ko => "ko"
+ }
+ }
+
+ /// Get language display name.
+ pub const fn name(self) -> &'static str {
+ match self {
+ Self::En => "English",
+ #[cfg(feature = "lang-ru")]
+ Self::Ru => "Русский",
+ #[cfg(feature = "lang-ko")]
+ Self::Ko => "한국어"
+ }
+ }
+}
+
+/// Define localized messages with compile-time validation.
+///
+/// Creates an enum with localized strings accessible via `.get(lang)` method.
+/// All strings are `&'static str` with zero runtime allocation.
+///
+/// # Example
+///
+/// ```ignore
+/// define_messages! {
+/// pub enum UiMsg {
+/// LabelWhy {
+/// en: "Why:",
+/// ru: "Почему:",
+/// ko: "왜:",
+/// }
+/// }
+/// }
+///
+/// let msg = UiMsg::LabelWhy.get(Lang::Ru);
+/// assert_eq!(msg, "Почему:");
+/// ```
+#[macro_export]
+macro_rules! define_messages {
+ (
+ $vis:vis enum $name:ident {
+ $(
+ $key:ident {
+ en: $en:literal
+ $(, ru: $ru:literal)?
+ $(, ko: $ko:literal)?
+ $(,)?
+ }
+ )*
+ }
+ ) => {
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+ #[repr(u16)]
+ $vis enum $name {
+ $($key,)*
+ }
+
+ impl $name {
+ /// Total number of messages.
+ pub const COUNT: usize = {
+ let mut count = 0usize;
+ $(let _ = stringify!($key); count += 1;)*
+ count
+ };
+
+ /// Get localized string for this message key.
+ #[inline]
+ pub const fn get(self, lang: $crate::i18n::Lang) -> &'static str {
+ match self {
+ $(
+ Self::$key => {
+ match lang {
+ $crate::i18n::Lang::En => $en,
+ $(
+ #[cfg(feature = "lang-ru")]
+ $crate::i18n::Lang::Ru => $ru,
+ )?
+ $(
+ #[cfg(feature = "lang-ko")]
+ $crate::i18n::Lang::Ko => $ko,
+ )?
+ #[allow(unreachable_patterns)]
+ _ => $en,
+ }
+ }
+ )*
+ }
+ }
+
+ /// Get all message keys as static slice.
+ pub const fn all() -> &'static [Self] {
+ &[$(Self::$key,)*]
+ }
+ }
+ };
+}
+
+pub use define_messages;
diff --git a/masterror-knowledge/src/i18n/phrases.rs b/masterror-knowledge/src/i18n/phrases.rs
new file mode 100644
index 0000000..f0db428
--- /dev/null
+++ b/masterror-knowledge/src/i18n/phrases.rs
@@ -0,0 +1,241 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Phrase translations for cargo compiler output.
+//!
+//! Uses Aho-Corasick algorithm for O(n+m) multi-pattern replacement.
+
+use std::sync::LazyLock;
+
+use aho_corasick::AhoCorasick;
+
+use super::Lang;
+
+/// Russian phrase translations (sorted alphabetically by English key).
+#[cfg(feature = "lang-ru")]
+static PHRASES_RU: &[(&str, &str)] = &[
+ ("aborting due to", "прерывание из-за"),
+ (
+ "as mutable because it is also borrowed as immutable",
+ "как изменяемое, т.к. уже заимствовано как неизменяемое"
+ ),
+ (
+ "as mutable more than once at a time",
+ "как изменяемое больше одного раза одновременно"
+ ),
+ (
+ "borrow of moved value",
+ "заимствование перемещённого значения"
+ ),
+ (
+ "borrowed value does not live long enough",
+ "заимствованное значение живёт недостаточно долго"
+ ),
+ ("cannot borrow", "нельзя заимствовать"),
+ ("consider", "рассмотрите"),
+ (
+ "consider cloning the value if the performance cost is acceptable",
+ "рассмотрите клонирование значения, если допустима потеря производительности"
+ ),
+ ("could not compile", "не удалось скомпилировать"),
+ ("does not live long enough", "живёт недостаточно долго"),
+ (
+ "dropped here while still borrowed",
+ "удалено здесь, пока ещё заимствовано"
+ ),
+ ("due to", "из-за"),
+ ("error", "ошибка"),
+ ("expected", "ожидается"),
+ (
+ "expected named lifetime parameter",
+ "ожидается именованный параметр времени жизни"
+ ),
+ ("expected type", "ожидаемый тип"),
+ (
+ "first borrow later used here",
+ "первое заимствование используется здесь"
+ ),
+ (
+ "first mutable borrow occurs here",
+ "первое изменяемое заимствование здесь"
+ ),
+ (
+ "for more info about this issue",
+ "для информации об этой ошибке"
+ ),
+ ("found", "найдено"),
+ ("found type", "найденный тип"),
+ ("has type", "имеет тип"),
+ ("help", "подсказка"),
+ (
+ "immutable borrow later used here",
+ "неизменяемое заимствование используется здесь"
+ ),
+ (
+ "immutable borrow occurs here",
+ "неизменяемое заимствование здесь"
+ ),
+ ("mismatched types", "несовпадение типов"),
+ (
+ "missing lifetime specifier",
+ "отсутствует спецификатор времени жизни"
+ ),
+ ("move occurs because", "перемещение происходит потому что"),
+ (
+ "mutable borrow occurs here",
+ "изменяемое заимствование здесь"
+ ),
+ ("note", "примечание"),
+ ("previous error", "предыдущей ошибки"),
+ ("previous errors", "предыдущих ошибок"),
+ ("run with", "запустите с"),
+ (
+ "second mutable borrow occurs here",
+ "второе изменяемое заимствование здесь"
+ ),
+ (
+ "this error originates in the macro",
+ "эта ошибка возникла в макросе"
+ ),
+ ("this expression has type", "это выражение имеет тип"),
+ (
+ "value borrowed here after move",
+ "значение заимствовано здесь после перемещения"
+ ),
+ ("value moved here", "значение перемещено здесь"),
+ ("warning", "предупреждение"),
+ (
+ "which does not implement the `Copy` trait",
+ "который не реализует трейт `Copy`"
+ )
+];
+
+/// Korean phrase translations (sorted alphabetically by English key).
+#[cfg(feature = "lang-ko")]
+static PHRASES_KO: &[(&str, &str)] = &[
+ ("borrow of moved value", "이동된 값의 빌림"),
+ ("cannot borrow", "빌릴 수 없습니다"),
+ ("error", "에러"),
+ ("help", "도움말"),
+ ("mismatched types", "타입 불일치"),
+ ("note", "참고"),
+ ("warning", "경고")
+];
+
+/// Pre-built Aho-Corasick automaton for Russian translations.
+///
+/// Patterns are sorted by length (longest first) to ensure correct replacement
+/// when shorter patterns are substrings of longer ones.
+#[cfg(feature = "lang-ru")]
+static AC_RU: LazyLock<(AhoCorasick, Vec<&'static str>)> = LazyLock::new(|| {
+ let mut sorted: Vec<_> = PHRASES_RU.iter().collect();
+ sorted.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
+
+ let patterns: Vec<_> = sorted.iter().map(|(k, _)| *k).collect();
+ let replacements: Vec<_> = sorted.iter().map(|(_, v)| *v).collect();
+
+ let ac = AhoCorasick::new(&patterns).expect("valid patterns");
+ (ac, replacements)
+});
+
+/// Pre-built Aho-Corasick automaton for Korean translations.
+#[cfg(feature = "lang-ko")]
+static AC_KO: LazyLock<(AhoCorasick, Vec<&'static str>)> = LazyLock::new(|| {
+ let mut sorted: Vec<_> = PHRASES_KO.iter().collect();
+ sorted.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
+
+ let patterns: Vec<_> = sorted.iter().map(|(k, _)| *k).collect();
+ let replacements: Vec<_> = sorted.iter().map(|(_, v)| *v).collect();
+
+ let ac = AhoCorasick::new(&patterns).expect("valid patterns");
+ (ac, replacements)
+});
+
+/// Translate a phrase to the target language.
+///
+/// Returns `None` if no translation exists or language is English.
+pub fn translate_phrase(phrase: &str, lang: Lang) -> Option<&'static str> {
+ let phrases: &[(&str, &str)] = match lang {
+ Lang::En => return None,
+ #[cfg(feature = "lang-ru")]
+ Lang::Ru => PHRASES_RU,
+ #[cfg(feature = "lang-ko")]
+ Lang::Ko => PHRASES_KO,
+ #[allow(unreachable_patterns)]
+ _ => return None
+ };
+
+ phrases
+ .binary_search_by_key(&phrase, |(k, _)| *k)
+ .ok()
+ .map(|i| phrases[i].1)
+}
+
+/// Translate full rendered compiler output.
+///
+/// Uses pre-built Aho-Corasick automaton for O(n+m) replacement
+/// instead of O(n*m) naive string replacement.
+pub fn translate_rendered(rendered: &str, lang: Lang) -> String {
+ match lang {
+ Lang::En => rendered.to_string(),
+ #[cfg(feature = "lang-ru")]
+ Lang::Ru => {
+ let (ac, replacements) = &*AC_RU;
+ ac.replace_all(rendered, replacements)
+ }
+ #[cfg(feature = "lang-ko")]
+ Lang::Ko => {
+ let (ac, replacements) = &*AC_KO;
+ ac.replace_all(rendered, replacements)
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ #[cfg(feature = "lang-ru")]
+ fn test_translate_phrase_ru() {
+ assert_eq!(
+ translate_phrase("borrow of moved value", Lang::Ru),
+ Some("заимствование перемещённого значения")
+ );
+ assert_eq!(translate_phrase("unknown phrase", Lang::Ru), None);
+ }
+
+ #[test]
+ fn test_translate_phrase_en() {
+ assert_eq!(translate_phrase("borrow of moved value", Lang::En), None);
+ }
+
+ #[test]
+ #[cfg(feature = "lang-ru")]
+ fn test_phrases_sorted() {
+ for window in PHRASES_RU.windows(2) {
+ assert!(
+ window[0].0 < window[1].0,
+ "Phrases not sorted: {:?} should come before {:?}",
+ window[1].0,
+ window[0].0
+ );
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "lang-ru")]
+ fn test_translate_rendered_ru() {
+ let input = "error: borrow of moved value";
+ let output = translate_rendered(input, Lang::Ru);
+ assert_eq!(output, "ошибка: заимствование перемещённого значения");
+ }
+
+ #[test]
+ fn test_translate_rendered_en_passthrough() {
+ let input = "error: borrow of moved value";
+ let output = translate_rendered(input, Lang::En);
+ assert_eq!(output, input);
+ }
+}
diff --git a/masterror-knowledge/src/lib.rs b/masterror-knowledge/src/lib.rs
new file mode 100644
index 0000000..3ac7174
--- /dev/null
+++ b/masterror-knowledge/src/lib.rs
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Knowledge base for Rust compiler errors and best practices.
+//!
+//! This crate provides structured explanations for Rust compiler errors
+//! with translations (en/ru/ko) and actionable fix suggestions.
+//!
+//! # Example
+//!
+//! ```rust,ignore
+//! use masterror_knowledge::{ErrorRegistry, PracticeRegistry};
+//!
+//! let registry = ErrorRegistry::new();
+//! if let Some(entry) = registry.find("E0502") {
+//! println!("Error: {}", entry.title.en);
+//! println!("Explanation: {}", entry.explanation.en);
+//! }
+//!
+//! let practices = PracticeRegistry::new();
+//! if let Some(practice) = practices.find("RA001") {
+//! println!("Practice: {}", practice.title.en);
+//! }
+//! ```
+
+pub mod errors;
+pub mod i18n;
+
+pub use errors::{
+ Category, DocLink, ErrorEntry, ErrorRegistry, FixSuggestion, LocalizedText,
+ raprogramm::{BestPractice, PracticeCategory, PracticeRegistry}
+};
+pub use i18n::{Lang, messages::UiMsg, phrases};
diff --git a/masterror-rustc/Cargo.toml b/masterror-rustc/Cargo.toml
new file mode 100644
index 0000000..2d2665f
--- /dev/null
+++ b/masterror-rustc/Cargo.toml
@@ -0,0 +1,28 @@
+# SPDX-FileCopyrightText: 2025-2026 RAprogramm
+#
+# SPDX-License-Identifier: MIT
+
+[package]
+name = "masterror-rustc"
+version = "0.1.0"
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "RUSTC_WRAPPER that adds translated explanations to Rust compiler errors"
+keywords = ["rust", "compiler", "errors", "translation", "rustc"]
+categories = ["command-line-utilities", "development-tools"]
+
+[[bin]]
+name = "masterror-rustc"
+path = "src/main.rs"
+
+[dependencies]
+masterror-knowledge = { version = "0.1", path = "../masterror-knowledge", features = ["lang-ru", "lang-ko"] }
+owo-colors = { version = "4", features = ["supports-colors"] }
+
+[target.'cfg(unix)'.dependencies]
+libc = "0.2"
+
+[target.'cfg(windows)'.dependencies]
+windows-sys = { version = "0.59", features = ["Win32_System_Console"] }
diff --git a/masterror-rustc/src/main.rs b/masterror-rustc/src/main.rs
new file mode 100644
index 0000000..5cec597
--- /dev/null
+++ b/masterror-rustc/src/main.rs
@@ -0,0 +1,198 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! RUSTC_WRAPPER that adds translated explanations to Rust compiler errors.
+//!
+//! # Usage
+//!
+//! Add to `.cargo/config.toml`:
+//! ```toml
+//! [build]
+//! rustc-wrapper = "masterror-rustc"
+//! ```
+//!
+//! Or set environment variable:
+//! ```bash
+//! export RUSTC_WRAPPER=masterror-rustc
+//! ```
+//!
+//! Then use cargo as usual:
+//! ```bash
+//! cargo build
+//! cargo run
+//! cargo test
+//! ```
+
+use std::{
+ env,
+ io::{BufRead, BufReader, Write},
+ process::{Command, Stdio, exit}
+};
+
+use masterror_knowledge::{ErrorRegistry, Lang};
+use owo_colors::OwoColorize;
+
+fn main() {
+ let args: Vec = env::args().skip(1).collect();
+
+ if args.is_empty() {
+ eprintln!("masterror-rustc: no arguments provided");
+ eprintln!("This is a RUSTC_WRAPPER, not meant to be called directly.");
+ eprintln!();
+ eprintln!("Usage: Add to .cargo/config.toml:");
+ eprintln!(" [build]");
+ eprintln!(" rustc-wrapper = \"masterror-rustc\"");
+ exit(1);
+ }
+
+ // Cargo passes: $RUSTC_WRAPPER $RUSTC
+ // First argument is the path to rustc, rest are rustc arguments
+ let rustc = &args[0];
+ let rustc_args = &args[1..];
+
+ let lang = detect_lang();
+ let colored = supports_color();
+ let registry = ErrorRegistry::new();
+
+ let mut child = Command::new(rustc)
+ .args(rustc_args)
+ .stdout(Stdio::inherit())
+ .stderr(Stdio::piped())
+ .spawn()
+ .unwrap_or_else(|e| {
+ eprintln!("masterror-rustc: failed to run rustc: {e}");
+ exit(1);
+ });
+
+ let stderr = child.stderr.take().expect("failed to capture stderr");
+ let reader = BufReader::new(stderr);
+
+ let mut stderr_handle = std::io::stderr().lock();
+
+ for line in reader.lines() {
+ let line = match line {
+ Ok(l) => l,
+ Err(e) => {
+ eprintln!("masterror-rustc: read error: {e}");
+ continue;
+ }
+ };
+
+ // Output original line
+ let _ = writeln!(stderr_handle, "{line}");
+
+ // Check for error code pattern: error[E0308]
+ if let Some(code) = extract_error_code(&line)
+ && let Some(explanation) = get_explanation(registry, &code, lang, colored)
+ {
+ let _ = writeln!(stderr_handle, "{explanation}");
+ }
+ }
+
+ drop(stderr_handle);
+
+ let status = child.wait().unwrap_or_else(|e| {
+ eprintln!("masterror-rustc: failed to wait for rustc: {e}");
+ exit(1);
+ });
+
+ exit(status.code().unwrap_or(1));
+}
+
+/// Extract error code from line like "error[E0308]: mismatched types"
+fn extract_error_code(line: &str) -> Option {
+ let start = line.find("error[E")?;
+ let code_start = start + 6; // skip "error["
+ let end = line[code_start..].find(']')?;
+ Some(line[code_start..code_start + end].to_string())
+}
+
+/// Get translated explanation for error code
+fn get_explanation(
+ registry: &'static ErrorRegistry,
+ code: &str,
+ lang: Lang,
+ colored: bool
+) -> Option {
+ let entry = registry.find(code)?;
+ let lang_code = lang.code();
+ let mut output = String::new();
+
+ // Header
+ let title = entry.title.get(lang_code);
+ if colored {
+ output.push_str(&format!("\n {} {}\n", "💡".bold(), title.bold().cyan()));
+ } else {
+ output.push_str(&format!("\n 💡 {title}\n"));
+ }
+
+ // Description
+ let desc = entry.explanation.get(lang_code);
+ if !desc.is_empty() {
+ for line in desc.lines() {
+ output.push_str(&format!(" {line}\n"));
+ }
+ }
+
+ // Fix suggestions
+ if !entry.fixes.is_empty() {
+ output.push('\n');
+ let fix_header = match lang.code() {
+ "ru" => "Как исправить:",
+ "ko" => "해결 방법:",
+ _ => "How to fix:"
+ };
+ if colored {
+ output.push_str(&format!(" {}\n", fix_header.bold().green()));
+ } else {
+ output.push_str(&format!(" {fix_header}\n"));
+ }
+ for fix in entry.fixes {
+ let fix_desc = fix.description.get(lang_code);
+ output.push_str(&format!(" • {fix_desc}\n"));
+ }
+ }
+
+ Some(output)
+}
+
+/// Detect language from environment
+fn detect_lang() -> Lang {
+ if let Ok(lang) = env::var("MASTERROR_LANG") {
+ return Lang::from_code(&lang);
+ }
+
+ if let Ok(lang) = env::var("LANG") {
+ if lang.starts_with("ru") {
+ return Lang::from_code("ru");
+ }
+ if lang.starts_with("ko") {
+ return Lang::from_code("ko");
+ }
+ }
+
+ Lang::En
+}
+
+/// Check if terminal supports colors
+fn supports_color() -> bool {
+ if env::var("NO_COLOR").is_ok() {
+ return false;
+ }
+ if env::var("CLICOLOR_FORCE").is_ok() {
+ return true;
+ }
+ is_stderr_tty()
+}
+
+/// Check if stderr is a TTY
+#[cfg(unix)]
+fn is_stderr_tty() -> bool {
+ unsafe { libc::isatty(libc::STDERR_FILENO) != 0 }
+}
+
+#[cfg(not(unix))]
+fn is_stderr_tty() -> bool {
+ false
+}
diff --git a/pkg/aur/.SRCINFO b/pkg/aur/.SRCINFO
new file mode 100644
index 0000000..fe1f3cd
--- /dev/null
+++ b/pkg/aur/.SRCINFO
@@ -0,0 +1,14 @@
+pkgbase = masterror
+ pkgdesc = CLI tool for explaining Rust compiler errors in human-friendly language
+ pkgver = 0.1.0
+ pkgrel = 1
+ url = https://github.com/RAprogramm/masterror
+ arch = x86_64
+ arch = aarch64
+ license = MIT
+ makedepends = cargo
+ depends = gcc-libs
+ source = masterror-0.1.0.tar.gz::https://github.com/RAprogramm/masterror/archive/v0.1.0.tar.gz
+ sha256sums = SKIP
+
+pkgname = masterror
diff --git a/pkg/aur/PKGBUILD b/pkg/aur/PKGBUILD
new file mode 100644
index 0000000..0fbc002
--- /dev/null
+++ b/pkg/aur/PKGBUILD
@@ -0,0 +1,44 @@
+# SPDX-FileCopyrightText: 2025-2026 RAprogramm
+#
+# SPDX-License-Identifier: MIT
+
+# Maintainer: RAprogramm
+pkgname=masterror
+pkgver=0.1.0
+pkgrel=1
+pkgdesc="CLI tool for explaining Rust compiler errors in human-friendly language"
+arch=('x86_64' 'aarch64')
+url="https://github.com/RAprogramm/masterror"
+license=('MIT')
+depends=('gcc-libs')
+makedepends=('cargo')
+source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz")
+sha256sums=('SKIP')
+
+prepare() {
+ cd "$pkgname-$pkgver"
+ export RUSTUP_TOOLCHAIN=stable
+ cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')"
+}
+
+build() {
+ cd "$pkgname-$pkgver/masterror-cli"
+ export RUSTUP_TOOLCHAIN=stable
+ export CARGO_TARGET_DIR=target
+ cargo build --frozen --release --all-features
+}
+
+check() {
+ cd "$pkgname-$pkgver/masterror-cli"
+ export RUSTUP_TOOLCHAIN=stable
+ export CARGO_TARGET_DIR=target
+ cargo test --frozen --release
+}
+
+package() {
+ cd "$pkgname-$pkgver"
+ install -Dm755 "masterror-cli/target/release/masterror" "$pkgdir/usr/bin/masterror"
+ install -Dm755 "masterror-cli/target/release/cargo-masterror" "$pkgdir/usr/bin/cargo-masterror"
+ install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
+ install -Dm644 masterror-cli/README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
+}
diff --git a/src/app_error.rs b/src/app_error.rs
index fffd18a..87e20f6 100644
--- a/src/app_error.rs
+++ b/src/app_error.rs
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2025 RAprogramm
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
//
// SPDX-License-Identifier: MIT
@@ -69,6 +69,7 @@
mod constructors;
mod context;
mod core;
+pub mod diagnostics;
mod inline_vec;
mod metadata;
@@ -77,6 +78,7 @@ pub use core::{AppError, AppResult, DisplayMode, Error, ErrorChain, MessageEditP
pub(crate) use core::{reset_backtrace_preference, set_backtrace_preference_override};
pub use context::Context;
+pub use diagnostics::{DiagnosticVisibility, Diagnostics, DocLink, Hint, Suggestion};
pub(crate) use metadata::duration_to_string;
pub use metadata::{Field, FieldRedaction, FieldValue, Metadata, field};
diff --git a/src/app_error/core/builder.rs b/src/app_error/core/builder.rs
deleted file mode 100644
index 2af4589..0000000
--- a/src/app_error/core/builder.rs
+++ /dev/null
@@ -1,420 +0,0 @@
-// SPDX-FileCopyrightText: 2025 RAprogramm
-//
-// SPDX-License-Identifier: MIT
-
-use alloc::{borrow::Cow, string::String, sync::Arc};
-use core::error::Error as CoreError;
-#[cfg(feature = "backtrace")]
-use std::backtrace::Backtrace;
-
-#[cfg(feature = "serde_json")]
-use serde::Serialize;
-#[cfg(feature = "serde_json")]
-use serde_json::{Value as JsonValue, to_value};
-
-use super::{
- error::Error,
- types::{CapturedBacktrace, ContextAttachment, MessageEditPolicy}
-};
-use crate::{
- AppCode, AppErrorKind, RetryAdvice,
- app_error::metadata::{Field, FieldRedaction, Metadata}
-};
-
-impl Error {
- /// Create a new [`Error`] with a kind and message.
- ///
- /// This is equivalent to [`Error::with`], provided for API symmetry and to
- /// keep doctests readable.
- ///
- /// # Examples
- ///
- /// ```rust
- /// use masterror::{AppError, AppErrorKind};
- /// let err = AppError::new(AppErrorKind::BadRequest, "invalid payload");
- /// assert!(err.message.is_some());
- /// ```
- #[must_use]
- pub fn new(kind: AppErrorKind, msg: impl Into>) -> Self {
- Self::with(kind, msg)
- }
-
- /// Create an error with the given kind and message.
- ///
- /// Prefer named helpers (e.g. [`Error::not_found`]) where it clarifies
- /// intent.
- ///
- /// # Examples
- ///
- /// ```rust
- /// use masterror::{AppError, AppErrorKind};
- /// let err = AppError::with(AppErrorKind::Validation, "bad input");
- /// assert_eq!(err.kind, AppErrorKind::Validation);
- /// ```
- #[must_use]
- pub fn with(kind: AppErrorKind, msg: impl Into>) -> Self {
- let err = Self::new_raw(kind, Some(msg.into()));
- err.emit_telemetry();
- err
- }
-
- /// Create a message-less error with the given kind.
- ///
- /// Useful when the kind alone conveys sufficient information to the client.
- ///
- /// # Examples
- ///
- /// ```rust
- /// use masterror::{AppError, AppErrorKind};
- /// let err = AppError::bare(AppErrorKind::NotFound);
- /// assert!(err.message.is_none());
- /// ```
- #[must_use]
- pub fn bare(kind: AppErrorKind) -> Self {
- let err = Self::new_raw(kind, None);
- err.emit_telemetry();
- err
- }
-
- /// Override the machine-readable [`AppCode`].
- ///
- /// # Examples
- ///
- /// ```rust
- /// use masterror::{AppCode, AppError, AppErrorKind};
- /// let err = AppError::new(AppErrorKind::BadRequest, "test").with_code(AppCode::NotFound);
- /// assert_eq!(err.code, AppCode::NotFound);
- /// ```
- #[must_use]
- pub fn with_code(mut self, code: AppCode) -> Self {
- self.code = code;
- self.mark_dirty();
- self
- }
-
- /// Attach retry advice to the error.
- ///
- /// When mapped to HTTP, this becomes the `Retry-After` header.
- ///
- /// # Examples
- ///
- /// ```rust
- /// use masterror::{AppError, AppErrorKind};
- /// let err = AppError::new(AppErrorKind::RateLimited, "slow down").with_retry_after_secs(60);
- /// assert_eq!(err.retry.map(|r| r.after_seconds), Some(60));
- /// ```
- #[must_use]
- pub fn with_retry_after_secs(mut self, secs: u64) -> Self {
- self.retry = Some(RetryAdvice {
- after_seconds: secs
- });
- self.mark_dirty();
- self
- }
-
- /// Attach a `WWW-Authenticate` challenge string.
- ///
- /// # Examples
- ///
- /// ```rust
- /// use masterror::{AppError, AppErrorKind};
- /// let err = AppError::new(AppErrorKind::Unauthorized, "auth required")
- /// .with_www_authenticate("Bearer realm=\"api\"");
- /// assert!(err.www_authenticate.is_some());
- /// ```
- #[must_use]
- pub fn with_www_authenticate(mut self, value: impl Into) -> Self {
- self.www_authenticate = Some(value.into());
- self.mark_dirty();
- self
- }
-
- /// Attach additional metadata to the error.
- ///
- /// # Examples
- ///
- /// ```rust
- /// use masterror::{AppError, AppErrorKind, field};
- /// let err = AppError::new(AppErrorKind::Validation, "bad field")
- /// .with_field(field::str("field_name", "email"));
- /// assert!(err.metadata().get("field_name").is_some());
- /// ```
- #[must_use]
- pub fn with_field(mut self, field: Field) -> Self {
- self.metadata.insert(field);
- self.mark_dirty();
- self
- }
-
- /// Extend metadata from an iterator of fields.
- ///
- /// # Examples
- ///
- /// ```rust
- /// use masterror::{AppError, AppErrorKind, field};
- /// let fields = vec![field::str("key1", "value1"), field::str("key2", "value2")];
- /// let err = AppError::new(AppErrorKind::BadRequest, "test").with_fields(fields);
- /// assert!(err.metadata().get("key1").is_some());
- /// ```
- #[must_use]
- pub fn with_fields(mut self, fields: impl IntoIterator- ) -> Self {
- self.metadata.extend(fields);
- self.mark_dirty();
- self
- }
-
- /// Override the redaction policy for a stored metadata field.
- ///
- /// # Examples
- ///
- /// ```rust
- /// use masterror::{AppError, AppErrorKind, FieldRedaction, field};
- ///
- /// let err = AppError::new(AppErrorKind::Internal, "test")
- /// .with_field(field::str("password", "secret"))
- /// .redact_field("password", FieldRedaction::Redact);
- /// ```
- #[must_use]
- pub fn redact_field(mut self, name: &'static str, redaction: FieldRedaction) -> Self {
- self.metadata.set_redaction(name, redaction);
- self.mark_dirty();
- self
- }
-
- /// Replace metadata entirely.
- ///
- /// # Examples
- ///
- /// ```rust
- /// use masterror::{AppError, AppErrorKind, Metadata};
- ///
- /// let metadata = Metadata::new();
- /// let err = AppError::new(AppErrorKind::Internal, "test").with_metadata(metadata);
- /// ```
- #[must_use]
- pub fn with_metadata(mut self, metadata: Metadata) -> Self {
- self.metadata = metadata;
- self.mark_dirty();
- self
- }
-
- /// Mark the message as redactable.
- ///
- /// # Examples
- ///
- /// ```rust
- /// use masterror::{AppError, AppErrorKind, MessageEditPolicy};
- ///
- /// let err = AppError::new(AppErrorKind::Internal, "secret").redactable();
- /// assert_eq!(err.edit_policy, MessageEditPolicy::Redact);
- /// ```
- #[must_use]
- pub fn redactable(mut self) -> Self {
- self.edit_policy = MessageEditPolicy::Redact;
- self.mark_dirty();
- self
- }
-
- /// Attach upstream diagnostics using [`with_source`](Self::with_source) or
- /// an existing [`Arc`].
- ///
- /// This is the preferred alias for capturing upstream errors. It accepts
- /// either an owned error implementing [`core::error::Error`] or a
- /// shared [`Arc`] produced by other APIs, reusing the allocation when
- /// possible.
- ///
- /// # Examples
- ///
- /// ```rust
- /// # #[cfg(feature = "std")] {
- /// use masterror::AppError;
- ///
- /// let err = AppError::service("downstream degraded")
- /// .with_context(std::io::Error::new(std::io::ErrorKind::Other, "boom"));
- /// assert!(err.source_ref().is_some());
- /// # }
- /// ```
- #[must_use]
- pub fn with_context(self, context: impl Into
) -> Self {
- match context.into() {
- ContextAttachment::Owned(source) => {
- match source.downcast::>() {
- Ok(shared) => self.with_source_arc(*shared),
- Err(source) => self.with_source_arc(Arc::from(source))
- }
- }
- ContextAttachment::Shared(source) => self.with_source_arc(source)
- }
- }
-
- /// Attach a source error for diagnostics.
- ///
- /// Prefer [`with_context`](Self::with_context) when capturing upstream
- /// diagnostics without additional `Arc` allocations.
- ///
- /// # Examples
- ///
- /// ```rust
- /// # #[cfg(feature = "std")] {
- /// use masterror::{AppError, AppErrorKind};
- ///
- /// let io_err = std::io::Error::new(std::io::ErrorKind::Other, "boom");
- /// let err = AppError::internal("boom").with_source(io_err);
- /// assert!(err.source_ref().is_some());
- /// # }
- /// ```
- #[must_use]
- pub fn with_source(mut self, source: impl CoreError + Send + Sync + 'static) -> Self {
- self.source = Some(Arc::new(source));
- self.mark_dirty();
- self
- }
-
- /// Attach a shared source error without cloning the underlying `Arc`.
- ///
- /// # Examples
- ///
- /// ```rust
- /// # #[cfg(feature = "std")] {
- /// use std::sync::Arc;
- ///
- /// use masterror::{AppError, AppErrorKind};
- ///
- /// let source = Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "boom"));
- /// let err = AppError::internal("boom").with_source_arc(source.clone());
- /// assert!(err.source_ref().is_some());
- /// assert_eq!(Arc::strong_count(&source), 2);
- /// # }
- /// ```
- #[must_use]
- pub fn with_source_arc(mut self, source: Arc) -> Self {
- self.source = Some(source);
- self.mark_dirty();
- self
- }
-
- /// Attach a captured backtrace.
- ///
- /// # Examples
- ///
- /// ```rust
- /// # #[cfg(feature = "backtrace")]
- /// # {
- /// use std::backtrace::Backtrace;
- ///
- /// use masterror::AppError;
- ///
- /// let bt = Backtrace::capture();
- /// let err = AppError::internal("test").with_backtrace(bt);
- /// # }
- /// ```
- #[must_use]
- pub fn with_backtrace(mut self, backtrace: CapturedBacktrace) -> Self {
- #[cfg(feature = "backtrace")]
- {
- self.set_backtrace_slot(Arc::new(backtrace));
- }
- #[cfg(not(feature = "backtrace"))]
- {
- self.set_backtrace_slot(backtrace);
- }
- self.mark_dirty();
- self
- }
-
- /// Attach a shared backtrace without cloning.
- ///
- /// Internal method for sharing backtraces between errors.
- #[cfg(feature = "backtrace")]
- pub(crate) fn with_shared_backtrace(mut self, backtrace: Arc) -> Self {
- self.set_backtrace_slot(backtrace);
- self.mark_dirty();
- self
- }
-
- /// Attach structured JSON details for the client payload.
- ///
- /// The details are omitted from responses when the error has been marked as
- /// [`redactable`](Self::redactable).
- ///
- /// # Examples
- ///
- /// ```rust
- /// # #[cfg(feature = "serde_json")]
- /// # {
- /// use masterror::{AppError, AppErrorKind};
- /// use serde_json::json;
- ///
- /// let err = AppError::new(AppErrorKind::Validation, "invalid input")
- /// .with_details_json(json!({"field": "email"}));
- /// assert!(err.details.is_some());
- /// # }
- /// ```
- #[must_use]
- #[cfg(feature = "serde_json")]
- pub fn with_details_json(mut self, details: JsonValue) -> Self {
- self.details = Some(details);
- self.mark_dirty();
- self
- }
-
- /// Serialize and attach structured details.
- ///
- /// Returns [`crate::AppError`] with [`crate::AppErrorKind::BadRequest`] if
- /// serialization fails.
- ///
- /// # Examples
- ///
- /// ```rust
- /// # #[cfg(feature = "serde_json")]
- /// # {
- /// use masterror::{AppError, AppErrorKind};
- /// use serde::Serialize;
- ///
- /// #[derive(Serialize)]
- /// struct Extra {
- /// reason: &'static str
- /// }
- ///
- /// let err = AppError::new(AppErrorKind::BadRequest, "invalid")
- /// .with_details(Extra {
- /// reason: "missing"
- /// })
- /// .expect("details should serialize");
- /// assert!(err.details.is_some());
- /// # }
- /// ```
- #[cfg(feature = "serde_json")]
- #[allow(clippy::result_large_err)]
- pub fn with_details(self, payload: T) -> crate::AppResult
- where
- T: Serialize
- {
- let details = to_value(payload).map_err(|err| Self::bad_request(err.to_string()))?;
- Ok(self.with_details_json(details))
- }
-
- /// Attach plain-text details for client payloads.
- ///
- /// The text is omitted from responses when the error is
- /// [`redactable`](Self::redactable).
- ///
- /// # Examples
- ///
- /// ```rust
- /// # #[cfg(not(feature = "serde_json"))]
- /// # {
- /// use masterror::{AppError, AppErrorKind};
- ///
- /// let err = AppError::new(AppErrorKind::Internal, "boom").with_details_text("retry later");
- /// assert!(err.details.is_some());
- /// # }
- /// ```
- #[must_use]
- #[cfg(not(feature = "serde_json"))]
- pub fn with_details_text(mut self, details: impl Into) -> Self {
- self.details = Some(details.into());
- self.mark_dirty();
- self
- }
-}
diff --git a/src/app_error/core/builder/constructors.rs b/src/app_error/core/builder/constructors.rs
new file mode 100644
index 0000000..93328bf
--- /dev/null
+++ b/src/app_error/core/builder/constructors.rs
@@ -0,0 +1,65 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Core constructors for creating `AppError` instances.
+
+use alloc::borrow::Cow;
+
+use crate::{AppErrorKind, app_error::core::error::Error};
+
+impl Error {
+ /// Create a new [`Error`] with a kind and message.
+ ///
+ /// This is equivalent to [`Error::with`], provided for API symmetry and to
+ /// keep doctests readable.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::{AppError, AppErrorKind};
+ /// let err = AppError::new(AppErrorKind::BadRequest, "invalid payload");
+ /// assert!(err.message.is_some());
+ /// ```
+ #[must_use]
+ pub fn new(kind: AppErrorKind, msg: impl Into>) -> Self {
+ Self::with(kind, msg)
+ }
+
+ /// Create an error with the given kind and message.
+ ///
+ /// Prefer named helpers (e.g. [`Error::not_found`]) where it clarifies
+ /// intent.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::{AppError, AppErrorKind};
+ /// let err = AppError::with(AppErrorKind::Validation, "bad input");
+ /// assert_eq!(err.kind, AppErrorKind::Validation);
+ /// ```
+ #[must_use]
+ pub fn with(kind: AppErrorKind, msg: impl Into>) -> Self {
+ let err = Self::new_raw(kind, Some(msg.into()));
+ err.emit_telemetry();
+ err
+ }
+
+ /// Create a message-less error with the given kind.
+ ///
+ /// Useful when the kind alone conveys sufficient information to the client.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::{AppError, AppErrorKind};
+ /// let err = AppError::bare(AppErrorKind::NotFound);
+ /// assert!(err.message.is_none());
+ /// ```
+ #[must_use]
+ pub fn bare(kind: AppErrorKind) -> Self {
+ let err = Self::new_raw(kind, None);
+ err.emit_telemetry();
+ err
+ }
+}
diff --git a/src/app_error/core/builder/context.rs b/src/app_error/core/builder/context.rs
new file mode 100644
index 0000000..654fe8d
--- /dev/null
+++ b/src/app_error/core/builder/context.rs
@@ -0,0 +1,134 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Source and context attachment methods for `AppError`.
+
+use alloc::sync::Arc;
+use core::error::Error as CoreError;
+#[cfg(feature = "backtrace")]
+use std::backtrace::Backtrace;
+
+use crate::app_error::core::{
+ error::Error,
+ types::{CapturedBacktrace, ContextAttachment}
+};
+
+impl Error {
+ /// Attach upstream diagnostics using [`with_source`](Self::with_source) or
+ /// an existing [`Arc`].
+ ///
+ /// This is the preferred alias for capturing upstream errors. It accepts
+ /// either an owned error implementing [`core::error::Error`] or a
+ /// shared [`Arc`] produced by other APIs, reusing the allocation when
+ /// possible.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// # #[cfg(feature = "std")] {
+ /// use masterror::AppError;
+ ///
+ /// let err = AppError::service("downstream degraded")
+ /// .with_context(std::io::Error::new(std::io::ErrorKind::Other, "boom"));
+ /// assert!(err.source_ref().is_some());
+ /// # }
+ /// ```
+ #[must_use]
+ pub fn with_context(self, context: impl Into) -> Self {
+ match context.into() {
+ ContextAttachment::Owned(source) => {
+ match source.downcast::>() {
+ Ok(shared) => self.with_source_arc(*shared),
+ Err(source) => self.with_source_arc(Arc::from(source))
+ }
+ }
+ ContextAttachment::Shared(source) => self.with_source_arc(source)
+ }
+ }
+
+ /// Attach a source error for diagnostics.
+ ///
+ /// Prefer [`with_context`](Self::with_context) when capturing upstream
+ /// diagnostics without additional `Arc` allocations.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// # #[cfg(feature = "std")] {
+ /// use masterror::{AppError, AppErrorKind};
+ ///
+ /// let io_err = std::io::Error::new(std::io::ErrorKind::Other, "boom");
+ /// let err = AppError::internal("boom").with_source(io_err);
+ /// assert!(err.source_ref().is_some());
+ /// # }
+ /// ```
+ #[must_use]
+ pub fn with_source(mut self, source: impl CoreError + Send + Sync + 'static) -> Self {
+ self.source = Some(Arc::new(source));
+ self.mark_dirty();
+ self
+ }
+
+ /// Attach a shared source error without cloning the underlying `Arc`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// # #[cfg(feature = "std")] {
+ /// use std::sync::Arc;
+ ///
+ /// use masterror::{AppError, AppErrorKind};
+ ///
+ /// let source = Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "boom"));
+ /// let err = AppError::internal("boom").with_source_arc(source.clone());
+ /// assert!(err.source_ref().is_some());
+ /// assert_eq!(Arc::strong_count(&source), 2);
+ /// # }
+ /// ```
+ #[must_use]
+ pub fn with_source_arc(mut self, source: Arc) -> Self {
+ self.source = Some(source);
+ self.mark_dirty();
+ self
+ }
+
+ /// Attach a captured backtrace.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// # #[cfg(feature = "backtrace")]
+ /// # {
+ /// use std::backtrace::Backtrace;
+ ///
+ /// use masterror::AppError;
+ ///
+ /// let bt = Backtrace::capture();
+ /// let err = AppError::internal("test").with_backtrace(bt);
+ /// # }
+ /// ```
+ #[must_use]
+ pub fn with_backtrace(mut self, backtrace: CapturedBacktrace) -> Self {
+ #[cfg(feature = "backtrace")]
+ {
+ self.set_backtrace_slot(Arc::new(backtrace));
+ }
+ #[cfg(not(feature = "backtrace"))]
+ {
+ self.set_backtrace_slot(backtrace);
+ }
+ self.mark_dirty();
+ self
+ }
+
+ /// Attach a shared backtrace without cloning.
+ ///
+ /// Internal method for sharing backtraces between errors.
+ #[cfg(feature = "backtrace")]
+ pub(crate) fn with_shared_backtrace(mut self, backtrace: Arc) -> Self {
+ self.set_backtrace_slot(backtrace);
+ self.mark_dirty();
+ self
+ }
+}
diff --git a/src/app_error/core/builder/details.rs b/src/app_error/core/builder/details.rs
new file mode 100644
index 0000000..1e63011
--- /dev/null
+++ b/src/app_error/core/builder/details.rs
@@ -0,0 +1,103 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Details attachment methods for `AppError`.
+
+#[cfg(not(feature = "serde_json"))]
+use alloc::string::String;
+
+#[cfg(feature = "serde_json")]
+use serde::Serialize;
+#[cfg(feature = "serde_json")]
+use serde_json::{Value as JsonValue, to_value};
+
+use crate::app_error::core::error::Error;
+
+impl Error {
+ /// Attach structured JSON details for the client payload.
+ ///
+ /// The details are omitted from responses when the error has been marked as
+ /// [`redactable`](Self::redactable).
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// # #[cfg(feature = "serde_json")]
+ /// # {
+ /// use masterror::{AppError, AppErrorKind};
+ /// use serde_json::json;
+ ///
+ /// let err = AppError::new(AppErrorKind::Validation, "invalid input")
+ /// .with_details_json(json!({"field": "email"}));
+ /// assert!(err.details.is_some());
+ /// # }
+ /// ```
+ #[must_use]
+ #[cfg(feature = "serde_json")]
+ pub fn with_details_json(mut self, details: JsonValue) -> Self {
+ self.details = Some(details);
+ self.mark_dirty();
+ self
+ }
+
+ /// Serialize and attach structured details.
+ ///
+ /// Returns [`crate::AppError`] with [`crate::AppErrorKind::BadRequest`] if
+ /// serialization fails.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// # #[cfg(feature = "serde_json")]
+ /// # {
+ /// use masterror::{AppError, AppErrorKind};
+ /// use serde::Serialize;
+ ///
+ /// #[derive(Serialize)]
+ /// struct Extra {
+ /// reason: &'static str
+ /// }
+ ///
+ /// let err = AppError::new(AppErrorKind::BadRequest, "invalid")
+ /// .with_details(Extra {
+ /// reason: "missing"
+ /// })
+ /// .expect("details should serialize");
+ /// assert!(err.details.is_some());
+ /// # }
+ /// ```
+ #[cfg(feature = "serde_json")]
+ #[allow(clippy::result_large_err)]
+ pub fn with_details(self, payload: T) -> crate::AppResult
+ where
+ T: Serialize
+ {
+ let details = to_value(payload).map_err(|err| Self::bad_request(err.to_string()))?;
+ Ok(self.with_details_json(details))
+ }
+
+ /// Attach plain-text details for client payloads.
+ ///
+ /// The text is omitted from responses when the error is
+ /// [`redactable`](Self::redactable).
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// # #[cfg(not(feature = "serde_json"))]
+ /// # {
+ /// use masterror::{AppError, AppErrorKind};
+ ///
+ /// let err = AppError::new(AppErrorKind::Internal, "boom").with_details_text("retry later");
+ /// assert!(err.details.is_some());
+ /// # }
+ /// ```
+ #[must_use]
+ #[cfg(not(feature = "serde_json"))]
+ pub fn with_details_text(mut self, details: impl Into) -> Self {
+ self.details = Some(details.into());
+ self.mark_dirty();
+ self
+ }
+}
diff --git a/src/app_error/core/builder/diagnostics.rs b/src/app_error/core/builder/diagnostics.rs
new file mode 100644
index 0000000..136388a
--- /dev/null
+++ b/src/app_error/core/builder/diagnostics.rs
@@ -0,0 +1,197 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Diagnostics builder methods for `AppError`.
+//!
+//! Provides methods to attach hints, suggestions, documentation links,
+//! and related error codes to errors.
+
+use alloc::{borrow::Cow, boxed::Box};
+
+use crate::app_error::{
+ core::error::Error,
+ diagnostics::{DiagnosticVisibility, Diagnostics, DocLink, Hint, Suggestion}
+};
+
+impl Error {
+ /// Adds a development-only hint to explain the error.
+ ///
+ /// Hints provide context about why an error occurred without necessarily
+ /// offering a solution. They are only shown in Local (development) mode.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::AppError;
+ ///
+ /// let err = AppError::not_found("User not found")
+ /// .with_hint("Check if the user ID is correct")
+ /// .with_hint("User might have been deleted");
+ /// ```
+ #[must_use]
+ pub fn with_hint(mut self, message: impl Into>) -> Self {
+ self.ensure_diagnostics().hints.push(Hint::new(message));
+ self.mark_dirty();
+ self
+ }
+
+ /// Adds a hint with custom visibility.
+ ///
+ /// Use this when a hint should be visible in Staging or Production.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::{AppError, DiagnosticVisibility};
+ ///
+ /// let err = AppError::unauthorized("Invalid token").with_hint_visible(
+ /// "Token may have expired, please login again",
+ /// DiagnosticVisibility::Public
+ /// );
+ /// ```
+ #[must_use]
+ pub fn with_hint_visible(
+ mut self,
+ message: impl Into>,
+ visibility: DiagnosticVisibility
+ ) -> Self {
+ self.ensure_diagnostics()
+ .hints
+ .push(Hint::with_visibility(message, visibility));
+ self.mark_dirty();
+ self
+ }
+
+ /// Adds an actionable suggestion to fix the error.
+ ///
+ /// Suggestions provide concrete steps users can take to resolve an error.
+ /// They are only shown in Local (development) mode by default.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::AppError;
+ ///
+ /// let err = AppError::database_with_message("Connection failed")
+ /// .with_suggestion("Check if the database server is running");
+ /// ```
+ #[must_use]
+ pub fn with_suggestion(mut self, message: impl Into>) -> Self {
+ self.ensure_diagnostics()
+ .suggestions
+ .push(Suggestion::new(message));
+ self.mark_dirty();
+ self
+ }
+
+ /// Adds a suggestion with an executable command or code snippet.
+ ///
+ /// The command is displayed in a distinct style to indicate it can be
+ /// copied and executed.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::AppError;
+ ///
+ /// let err = AppError::database_with_message("PostgreSQL connection refused")
+ /// .with_suggestion_cmd(
+ /// "Check if PostgreSQL is running",
+ /// "systemctl status postgresql"
+ /// );
+ /// ```
+ #[must_use]
+ pub fn with_suggestion_cmd(
+ mut self,
+ message: impl Into>,
+ command: impl Into>
+ ) -> Self {
+ self.ensure_diagnostics()
+ .suggestions
+ .push(Suggestion::with_command(message, command));
+ self.mark_dirty();
+ self
+ }
+
+ /// Links to documentation explaining this error.
+ ///
+ /// Documentation links are publicly visible by default, helping end users
+ /// understand and resolve errors.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::AppError;
+ ///
+ /// let err = AppError::not_found("User not found")
+ /// .with_docs("https://docs.example.com/errors/USER_NOT_FOUND");
+ /// ```
+ #[must_use]
+ pub fn with_docs(mut self, url: impl Into>) -> Self {
+ self.ensure_diagnostics().doc_link = Some(DocLink::new(url));
+ self.mark_dirty();
+ self
+ }
+
+ /// Links to documentation with a human-readable title.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::AppError;
+ ///
+ /// let err = AppError::unauthorized("Token expired").with_docs_titled(
+ /// "https://docs.example.com/auth/tokens",
+ /// "Authentication Guide"
+ /// );
+ /// ```
+ #[must_use]
+ pub fn with_docs_titled(
+ mut self,
+ url: impl Into>,
+ title: impl Into>
+ ) -> Self {
+ self.ensure_diagnostics().doc_link = Some(DocLink::with_title(url, title));
+ self.mark_dirty();
+ self
+ }
+
+ /// Adds a related error code for cross-reference.
+ ///
+ /// Related codes help users discover errors that might provide additional
+ /// context or alternative explanations.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::AppError;
+ ///
+ /// let err = AppError::database_with_message("Connection timeout")
+ /// .with_related_code("DB_POOL_EXHAUSTED")
+ /// .with_related_code("DB_AUTH_FAILED");
+ /// ```
+ #[must_use]
+ pub fn with_related_code(mut self, code: impl Into>) -> Self {
+ self.ensure_diagnostics().related_codes.push(code.into());
+ self.mark_dirty();
+ self
+ }
+
+ /// Returns a mutable reference to diagnostics, initializing if needed.
+ fn ensure_diagnostics(&mut self) -> &mut Diagnostics {
+ if self.inner.diagnostics.is_none() {
+ self.inner.diagnostics = Some(Box::new(Diagnostics::new()));
+ }
+ self.inner
+ .diagnostics
+ .as_mut()
+ .expect("diagnostics initialized above")
+ }
+
+ /// Returns diagnostics if present.
+ #[must_use]
+ pub fn diagnostics(&self) -> Option<&Diagnostics> {
+ self.inner.diagnostics.as_deref()
+ }
+}
diff --git a/src/app_error/core/builder/metadata.rs b/src/app_error/core/builder/metadata.rs
new file mode 100644
index 0000000..0995500
--- /dev/null
+++ b/src/app_error/core/builder/metadata.rs
@@ -0,0 +1,84 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Metadata attachment methods for `AppError`.
+
+use crate::{
+ FieldRedaction,
+ app_error::{
+ core::error::Error,
+ metadata::{Field, Metadata}
+ }
+};
+
+impl Error {
+ /// Attach additional metadata to the error.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::{AppError, AppErrorKind, field};
+ /// let err = AppError::new(AppErrorKind::Validation, "bad field")
+ /// .with_field(field::str("field_name", "email"));
+ /// assert!(err.metadata().get("field_name").is_some());
+ /// ```
+ #[must_use]
+ pub fn with_field(mut self, field: Field) -> Self {
+ self.metadata.insert(field);
+ self.mark_dirty();
+ self
+ }
+
+ /// Extend metadata from an iterator of fields.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::{AppError, AppErrorKind, field};
+ /// let fields = vec![field::str("key1", "value1"), field::str("key2", "value2")];
+ /// let err = AppError::new(AppErrorKind::BadRequest, "test").with_fields(fields);
+ /// assert!(err.metadata().get("key1").is_some());
+ /// ```
+ #[must_use]
+ pub fn with_fields(mut self, fields: impl IntoIterator- ) -> Self {
+ self.metadata.extend(fields);
+ self.mark_dirty();
+ self
+ }
+
+ /// Override the redaction policy for a stored metadata field.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::{AppError, AppErrorKind, FieldRedaction, field};
+ ///
+ /// let err = AppError::new(AppErrorKind::Internal, "test")
+ /// .with_field(field::str("password", "secret"))
+ /// .redact_field("password", FieldRedaction::Redact);
+ /// ```
+ #[must_use]
+ pub fn redact_field(mut self, name: &'static str, redaction: FieldRedaction) -> Self {
+ self.metadata.set_redaction(name, redaction);
+ self.mark_dirty();
+ self
+ }
+
+ /// Replace metadata entirely.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::{AppError, AppErrorKind, Metadata};
+ ///
+ /// let metadata = Metadata::new();
+ /// let err = AppError::new(AppErrorKind::Internal, "test").with_metadata(metadata);
+ /// ```
+ #[must_use]
+ pub fn with_metadata(mut self, metadata: Metadata) -> Self {
+ self.metadata = metadata;
+ self.mark_dirty();
+ self
+ }
+}
diff --git a/src/app_error/core/builder/mod.rs b/src/app_error/core/builder/mod.rs
new file mode 100644
index 0000000..229ee97
--- /dev/null
+++ b/src/app_error/core/builder/mod.rs
@@ -0,0 +1,18 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Builder pattern implementation for `AppError`.
+//!
+//! This module provides fluent builder methods for constructing errors with
+//! various attributes like metadata, source chains, and diagnostics.
+
+mod constructors;
+mod context;
+mod details;
+mod diagnostics;
+mod metadata;
+mod modifiers;
+
+#[cfg(test)]
+mod tests;
diff --git a/src/app_error/core/builder/modifiers.rs b/src/app_error/core/builder/modifiers.rs
new file mode 100644
index 0000000..aad1eaa
--- /dev/null
+++ b/src/app_error/core/builder/modifiers.rs
@@ -0,0 +1,84 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Modifier methods for `AppError` that change error properties.
+
+use alloc::string::String;
+
+use crate::{
+ AppCode, RetryAdvice,
+ app_error::core::{error::Error, types::MessageEditPolicy}
+};
+
+impl Error {
+ /// Override the machine-readable [`AppCode`].
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::{AppCode, AppError, AppErrorKind};
+ /// let err = AppError::new(AppErrorKind::BadRequest, "test").with_code(AppCode::NotFound);
+ /// assert_eq!(err.code, AppCode::NotFound);
+ /// ```
+ #[must_use]
+ pub fn with_code(mut self, code: AppCode) -> Self {
+ self.code = code;
+ self.mark_dirty();
+ self
+ }
+
+ /// Attach retry advice to the error.
+ ///
+ /// When mapped to HTTP, this becomes the `Retry-After` header.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::{AppError, AppErrorKind};
+ /// let err = AppError::new(AppErrorKind::RateLimited, "slow down").with_retry_after_secs(60);
+ /// assert_eq!(err.retry.map(|r| r.after_seconds), Some(60));
+ /// ```
+ #[must_use]
+ pub fn with_retry_after_secs(mut self, secs: u64) -> Self {
+ self.retry = Some(RetryAdvice {
+ after_seconds: secs
+ });
+ self.mark_dirty();
+ self
+ }
+
+ /// Attach a `WWW-Authenticate` challenge string.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::{AppError, AppErrorKind};
+ /// let err = AppError::new(AppErrorKind::Unauthorized, "auth required")
+ /// .with_www_authenticate("Bearer realm=\"api\"");
+ /// assert!(err.www_authenticate.is_some());
+ /// ```
+ #[must_use]
+ pub fn with_www_authenticate(mut self, value: impl Into) -> Self {
+ self.www_authenticate = Some(value.into());
+ self.mark_dirty();
+ self
+ }
+
+ /// Mark the message as redactable.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use masterror::{AppError, AppErrorKind, MessageEditPolicy};
+ ///
+ /// let err = AppError::new(AppErrorKind::Internal, "secret").redactable();
+ /// assert_eq!(err.edit_policy, MessageEditPolicy::Redact);
+ /// ```
+ #[must_use]
+ pub fn redactable(mut self) -> Self {
+ self.edit_policy = MessageEditPolicy::Redact;
+ self.mark_dirty();
+ self
+ }
+}
diff --git a/src/app_error/core/builder/tests.rs b/src/app_error/core/builder/tests.rs
new file mode 100644
index 0000000..ee976ed
--- /dev/null
+++ b/src/app_error/core/builder/tests.rs
@@ -0,0 +1,428 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Tests for builder module.
+
+use crate::{AppCode, AppError, AppErrorKind, FieldRedaction, MessageEditPolicy, Metadata, field};
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Constructors tests
+// ─────────────────────────────────────────────────────────────────────────────
+
+#[test]
+fn new_creates_error_with_message() {
+ let err = AppError::new(AppErrorKind::BadRequest, "test message");
+ assert_eq!(err.kind, AppErrorKind::BadRequest);
+ assert_eq!(err.message.as_deref(), Some("test message"));
+}
+
+#[test]
+fn new_with_owned_string() {
+ let msg = String::from("owned message");
+ let err = AppError::new(AppErrorKind::Internal, msg);
+ assert_eq!(err.message.as_deref(), Some("owned message"));
+}
+
+#[test]
+fn with_creates_error_with_message() {
+ let err = AppError::with(AppErrorKind::Validation, "validation failed");
+ assert_eq!(err.kind, AppErrorKind::Validation);
+ assert_eq!(err.message.as_deref(), Some("validation failed"));
+}
+
+#[test]
+fn bare_creates_error_without_message() {
+ let err = AppError::bare(AppErrorKind::NotFound);
+ assert_eq!(err.kind, AppErrorKind::NotFound);
+ assert!(err.message.is_none());
+}
+
+#[test]
+fn bare_sets_correct_code() {
+ let err = AppError::bare(AppErrorKind::Unauthorized);
+ assert_eq!(err.code, AppCode::Unauthorized);
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Modifiers tests
+// ─────────────────────────────────────────────────────────────────────────────
+
+#[test]
+fn with_code_overrides_default_code() {
+ let err = AppError::new(AppErrorKind::BadRequest, "test").with_code(AppCode::NotFound);
+ assert_eq!(err.code, AppCode::NotFound);
+}
+
+#[test]
+fn with_retry_after_secs_sets_retry_advice() {
+ let err = AppError::new(AppErrorKind::RateLimited, "slow down").with_retry_after_secs(60);
+ assert!(err.retry.is_some());
+ assert_eq!(err.retry.unwrap().after_seconds, 60);
+}
+
+#[test]
+fn with_retry_after_secs_zero() {
+ let err = AppError::new(AppErrorKind::RateLimited, "test").with_retry_after_secs(0);
+ assert_eq!(err.retry.unwrap().after_seconds, 0);
+}
+
+#[test]
+fn with_www_authenticate_sets_header() {
+ let err = AppError::new(AppErrorKind::Unauthorized, "auth required")
+ .with_www_authenticate("Bearer realm=\"api\"");
+ assert_eq!(
+ err.www_authenticate.as_deref(),
+ Some("Bearer realm=\"api\"")
+ );
+}
+
+#[test]
+fn with_www_authenticate_owned_string() {
+ let challenge = String::from("Basic realm=\"test\"");
+ let err = AppError::unauthorized("test").with_www_authenticate(challenge);
+ assert_eq!(
+ err.www_authenticate.as_deref(),
+ Some("Basic realm=\"test\"")
+ );
+}
+
+#[test]
+fn redactable_sets_edit_policy() {
+ let err = AppError::new(AppErrorKind::Internal, "secret").redactable();
+ assert_eq!(err.edit_policy, MessageEditPolicy::Redact);
+}
+
+#[test]
+fn redactable_can_be_chained() {
+ let err = AppError::internal("secret")
+ .redactable()
+ .with_code(AppCode::Internal);
+ assert_eq!(err.edit_policy, MessageEditPolicy::Redact);
+ assert_eq!(err.code, AppCode::Internal);
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Metadata tests
+// ─────────────────────────────────────────────────────────────────────────────
+
+#[test]
+fn with_field_adds_metadata() {
+ let err = AppError::new(AppErrorKind::Validation, "bad field")
+ .with_field(field::str("name", "email"));
+ assert!(err.metadata().get("name").is_some());
+}
+
+#[test]
+fn with_field_multiple_fields() {
+ let err = AppError::internal("test")
+ .with_field(field::str("key1", "value1"))
+ .with_field(field::u64("key2", 42));
+ assert!(err.metadata().get("key1").is_some());
+ assert!(err.metadata().get("key2").is_some());
+}
+
+#[test]
+fn with_fields_adds_multiple() {
+ let fields = vec![
+ field::str("a", "1"),
+ field::str("b", "2"),
+ field::str("c", "3"),
+ ];
+ let err = AppError::new(AppErrorKind::BadRequest, "test").with_fields(fields);
+ assert!(err.metadata().get("a").is_some());
+ assert!(err.metadata().get("b").is_some());
+ assert!(err.metadata().get("c").is_some());
+}
+
+#[test]
+fn with_fields_empty_iterator() {
+ let err = AppError::internal("test").with_fields(Vec::new());
+ assert!(err.metadata().is_empty());
+}
+
+#[test]
+fn redact_field_sets_redaction() {
+ let err = AppError::new(AppErrorKind::Internal, "test")
+ .with_field(field::str("password", "secret"))
+ .redact_field("password", FieldRedaction::Redact);
+ // Field exists but is marked for redaction
+ assert!(err.metadata().get("password").is_some());
+}
+
+#[test]
+fn redact_field_nonexistent_field() {
+ let err = AppError::internal("test").redact_field("nonexistent", FieldRedaction::Redact);
+ // Should not panic, just no-op
+ assert!(err.metadata().get("nonexistent").is_none());
+}
+
+#[test]
+fn with_metadata_replaces_all() {
+ let mut metadata = Metadata::new();
+ metadata.insert(field::str("new_key", "new_value"));
+ let err = AppError::internal("test")
+ .with_field(field::str("old_key", "old_value"))
+ .with_metadata(metadata);
+ assert!(err.metadata().get("old_key").is_none());
+ assert!(err.metadata().get("new_key").is_some());
+}
+
+#[test]
+fn with_metadata_empty() {
+ let err = AppError::internal("test")
+ .with_field(field::str("key", "value"))
+ .with_metadata(Metadata::new());
+ assert!(err.metadata().is_empty());
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Context tests
+// ─────────────────────────────────────────────────────────────────────────────
+
+#[cfg(feature = "std")]
+#[test]
+fn with_source_attaches_error() {
+ use std::io::{Error as IoError, ErrorKind};
+ let io_err = IoError::new(ErrorKind::NotFound, "file not found");
+ let err = AppError::internal("failed").with_source(io_err);
+ assert!(err.source_ref().is_some());
+}
+
+#[cfg(feature = "std")]
+#[test]
+fn with_source_arc_shares_error() {
+ use std::{io::Error as IoError, sync::Arc};
+ let source = Arc::new(IoError::other("shared error"));
+ let err = AppError::internal("test").with_source_arc(source.clone());
+ assert!(err.source_ref().is_some());
+ assert_eq!(Arc::strong_count(&source), 2);
+}
+
+#[cfg(feature = "std")]
+#[test]
+fn with_context_accepts_owned_error() {
+ use std::io::Error as IoError;
+ let io_err = IoError::other("context error");
+ let err = AppError::service("degraded").with_context(io_err);
+ assert!(err.source_ref().is_some());
+}
+
+#[cfg(feature = "std")]
+#[test]
+fn with_context_accepts_arc() {
+ use std::{io::Error as IoError, sync::Arc};
+ let source: Arc = Arc::new(IoError::other("arc error"));
+ let err = AppError::internal("test").with_context(source);
+ assert!(err.source_ref().is_some());
+}
+
+#[cfg(feature = "backtrace")]
+#[test]
+fn with_backtrace_attaches_backtrace() {
+ use std::backtrace::Backtrace;
+ let bt = Backtrace::capture();
+ let err = AppError::internal("test").with_backtrace(bt);
+ // Just ensure it doesn't panic
+ let _ = err;
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Details tests
+// ─────────────────────────────────────────────────────────────────────────────
+
+#[cfg(feature = "serde_json")]
+#[test]
+fn with_details_json_attaches_value() {
+ use serde_json::json;
+ let err = AppError::new(AppErrorKind::Validation, "invalid")
+ .with_details_json(json!({"field": "email", "reason": "invalid format"}));
+ assert!(err.details.is_some());
+}
+
+#[cfg(feature = "serde_json")]
+#[test]
+fn with_details_json_null() {
+ use serde_json::Value;
+ let err = AppError::internal("test").with_details_json(Value::Null);
+ assert!(err.details.is_some());
+}
+
+#[cfg(feature = "serde_json")]
+#[test]
+fn with_details_serializes_struct() {
+ use serde::Serialize;
+
+ #[derive(Serialize)]
+ struct Details {
+ code: u32,
+ reason: &'static str
+ }
+
+ let err = AppError::bad_request("invalid")
+ .with_details(Details {
+ code: 100,
+ reason: "missing field"
+ })
+ .expect("serialization should succeed");
+ assert!(err.details.is_some());
+}
+
+#[cfg(feature = "serde_json")]
+#[test]
+fn with_details_handles_serialization_error() {
+ use serde::Serialize;
+
+ #[derive(Serialize)]
+ struct BadStruct {
+ #[serde(serialize_with = "fail_serialize")]
+ value: u32
+ }
+
+ fn fail_serialize(_: &u32, _: S) -> Result
+ where
+ S: serde::Serializer
+ {
+ Err(serde::ser::Error::custom("intentional failure"))
+ }
+
+ let result = AppError::internal("test").with_details(BadStruct {
+ value: 0
+ });
+ assert!(result.is_err());
+}
+
+#[cfg(not(feature = "serde_json"))]
+#[test]
+fn with_details_text_attaches_string() {
+ let err = AppError::internal("test").with_details_text("additional info");
+ assert!(err.details.is_some());
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Diagnostics tests
+// ─────────────────────────────────────────────────────────────────────────────
+
+#[test]
+fn with_hint_adds_hint() {
+ let err = AppError::not_found("User not found").with_hint("Check user ID");
+ let diag = err.diagnostics().expect("diagnostics should exist");
+ assert_eq!(diag.hints.len(), 1);
+}
+
+#[test]
+fn with_hint_multiple() {
+ let err = AppError::not_found("test")
+ .with_hint("hint 1")
+ .with_hint("hint 2")
+ .with_hint("hint 3");
+ let diag = err.diagnostics().unwrap();
+ assert_eq!(diag.hints.len(), 3);
+}
+
+#[test]
+fn with_hint_visible_sets_visibility() {
+ use crate::DiagnosticVisibility;
+ let err = AppError::unauthorized("test")
+ .with_hint_visible("public hint", DiagnosticVisibility::Public);
+ let diag = err.diagnostics().unwrap();
+ assert_eq!(diag.hints[0].visibility, DiagnosticVisibility::Public);
+}
+
+#[test]
+fn with_suggestion_adds_suggestion() {
+ let err =
+ AppError::database_with_message("Connection failed").with_suggestion("Check DB server");
+ let diag = err.diagnostics().unwrap();
+ assert_eq!(diag.suggestions.len(), 1);
+}
+
+#[test]
+fn with_suggestion_cmd_includes_command() {
+ let err = AppError::database_with_message("test")
+ .with_suggestion_cmd("Check status", "systemctl status postgresql");
+ let diag = err.diagnostics().unwrap();
+ assert!(diag.suggestions[0].command.is_some());
+ assert_eq!(
+ diag.suggestions[0].command.as_deref(),
+ Some("systemctl status postgresql")
+ );
+}
+
+#[test]
+fn with_docs_sets_doc_link() {
+ let err = AppError::not_found("test").with_docs("https://docs.example.com/errors/NOT_FOUND");
+ let diag = err.diagnostics().unwrap();
+ assert!(diag.doc_link.is_some());
+ assert_eq!(
+ diag.doc_link.as_ref().unwrap().url.as_ref(),
+ "https://docs.example.com/errors/NOT_FOUND"
+ );
+}
+
+#[test]
+fn with_docs_titled_includes_title() {
+ let err = AppError::unauthorized("test")
+ .with_docs_titled("https://docs.example.com/auth", "Authentication Guide");
+ let diag = err.diagnostics().unwrap();
+ let doc = diag.doc_link.as_ref().unwrap();
+ assert_eq!(doc.title.as_deref(), Some("Authentication Guide"));
+}
+
+#[test]
+fn with_related_code_adds_code() {
+ let err = AppError::database_with_message("test")
+ .with_related_code("DB_POOL_EXHAUSTED")
+ .with_related_code("DB_AUTH_FAILED");
+ let diag = err.diagnostics().unwrap();
+ assert_eq!(diag.related_codes.len(), 2);
+}
+
+#[test]
+fn diagnostics_returns_none_when_empty() {
+ let err = AppError::internal("no diagnostics");
+ assert!(err.diagnostics().is_none());
+}
+
+#[test]
+fn diagnostics_returns_some_when_present() {
+ let err = AppError::internal("test").with_hint("a hint");
+ assert!(err.diagnostics().is_some());
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Chaining tests
+// ─────────────────────────────────────────────────────────────────────────────
+
+#[test]
+fn all_builders_can_be_chained() {
+ let err = AppError::new(AppErrorKind::Service, "service error")
+ .with_code(AppCode::Service)
+ .with_retry_after_secs(30)
+ .with_field(field::str("service", "payment"))
+ .with_hint("Check service health")
+ .with_suggestion("Retry in 30 seconds")
+ .with_docs("https://docs.example.com/errors");
+
+ assert_eq!(err.code, AppCode::Service);
+ assert!(err.retry.is_some());
+ assert!(err.metadata().get("service").is_some());
+ assert!(err.diagnostics().is_some());
+}
+
+#[cfg(feature = "std")]
+#[test]
+fn chaining_with_source_preserves_all() {
+ use std::io::Error as IoError;
+
+ let err = AppError::internal("test")
+ .with_field(field::u64("request_id", 12345))
+ .with_source(IoError::other("underlying error"))
+ .with_hint("debug hint")
+ .redactable();
+
+ assert!(err.metadata().get("request_id").is_some());
+ assert!(err.source_ref().is_some());
+ assert!(err.diagnostics().is_some());
+ assert_eq!(err.edit_policy, MessageEditPolicy::Redact);
+}
diff --git a/src/app_error/core/display.rs b/src/app_error/core/display.rs
deleted file mode 100644
index cc92573..0000000
--- a/src/app_error/core/display.rs
+++ /dev/null
@@ -1,799 +0,0 @@
-// SPDX-FileCopyrightText: 2025 RAprogramm
-//
-// SPDX-License-Identifier: MIT
-
-use alloc::string::ToString;
-use core::{
- error::Error as CoreError,
- fmt::{Formatter, Result as FmtResult},
- sync::atomic::{AtomicU8, Ordering}
-};
-
-use super::error::Error;
-use crate::{FieldRedaction, FieldValue, MessageEditPolicy};
-
-/// Display mode for error output.
-///
-/// Controls the structure and verbosity of error messages based on
-/// the deployment environment. The mode is determined by the
-/// `MASTERROR_ENV` environment variable or auto-detected based on
-/// build configuration and runtime environment.
-///
-/// # Examples
-///
-/// ```
-/// use masterror::DisplayMode;
-///
-/// let mode = DisplayMode::current();
-/// match mode {
-/// DisplayMode::Prod => println!("Production mode: JSON output"),
-/// DisplayMode::Local => println!("Local mode: Human-readable output"),
-/// DisplayMode::Staging => println!("Staging mode: JSON with context")
-/// }
-/// ```
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum DisplayMode {
- /// Production mode: lightweight JSON, minimal fields, no sensitive data.
- ///
- /// Output includes only: `kind`, `code`, `message` (if not redacted).
- /// Metadata is filtered to exclude sensitive fields.
- /// Source chain and backtrace are excluded.
- ///
- /// # Example Output
- ///
- /// ```json
- /// {"kind":"NotFound","code":"NOT_FOUND","message":"User not found"}
- /// ```
- Prod = 0,
-
- /// Development mode: human-readable, full context.
- ///
- /// Output includes: error details, full source chain, complete metadata,
- /// and backtrace (if enabled). Supports colored output when the `colored`
- /// feature is enabled and output is a TTY.
- ///
- /// # Example Output
- ///
- /// ```text
- /// Error: NotFound
- /// Code: NOT_FOUND
- /// Message: User not found
- ///
- /// Caused by: database query failed
- /// Caused by: connection timeout
- ///
- /// Context:
- /// user_id: 12345
- /// ```
- Local = 1,
-
- /// Staging mode: JSON with additional context.
- ///
- /// Output includes: `kind`, `code`, `message`, limited `source_chain`,
- /// and filtered metadata. No backtrace.
- ///
- /// # Example Output
- ///
- /// ```json
- /// {"kind":"NotFound","code":"NOT_FOUND","message":"User not found","source_chain":["database error"],"metadata":{"user_id":12345}}
- /// ```
- Staging = 2
-}
-
-impl DisplayMode {
- /// Returns the current display mode based on environment configuration.
- ///
- /// The mode is determined by checking (in order):
- /// 1. `MASTERROR_ENV` environment variable (`prod`, `local`, or `staging`)
- /// 2. Kubernetes environment detection (`KUBERNETES_SERVICE_HOST`)
- /// 3. Build configuration (`cfg!(debug_assertions)`)
- ///
- /// The result is cached for performance.
- ///
- /// # Examples
- ///
- /// ```
- /// use masterror::DisplayMode;
- ///
- /// let mode = DisplayMode::current();
- /// assert!(matches!(
- /// mode,
- /// DisplayMode::Prod | DisplayMode::Local | DisplayMode::Staging
- /// ));
- /// ```
- #[must_use]
- pub fn current() -> Self {
- static CACHED_MODE: AtomicU8 = AtomicU8::new(255);
- let cached = CACHED_MODE.load(Ordering::Relaxed);
- if cached != 255 {
- return match cached {
- 0 => Self::Prod,
- 1 => Self::Local,
- 2 => Self::Staging,
- _ => unreachable!()
- };
- }
- let mode = Self::detect();
- CACHED_MODE.store(mode as u8, Ordering::Relaxed);
- mode
- }
-
- /// Detects the appropriate display mode from environment.
- ///
- /// This is an internal helper called by [`current()`](Self::current).
- fn detect() -> Self {
- #[cfg(feature = "std")]
- {
- use std::env::var;
- if let Ok(env) = var("MASTERROR_ENV") {
- return match env.as_str() {
- "prod" | "production" => Self::Prod,
- "local" | "dev" | "development" => Self::Local,
- "staging" | "stage" => Self::Staging,
- _ => Self::detect_auto()
- };
- }
- if var("KUBERNETES_SERVICE_HOST").is_ok() {
- return Self::Prod;
- }
- }
- Self::detect_auto()
- }
-
- /// Auto-detects mode based on build configuration.
- fn detect_auto() -> Self {
- if cfg!(debug_assertions) {
- Self::Local
- } else {
- Self::Prod
- }
- }
-}
-
-#[allow(dead_code)]
-impl Error {
- /// Formats error in production mode (compact JSON).
- ///
- /// # Arguments
- ///
- /// * `f` - Formatter to write output to
- ///
- /// # Examples
- ///
- /// ```
- /// use masterror::AppError;
- ///
- /// let error = AppError::not_found("User not found");
- /// let output = format!("{}", error);
- /// // In prod mode: {"kind":"NotFound","code":"NOT_FOUND","message":"User not found"}
- /// ```
- #[cfg(not(test))]
- pub(crate) fn fmt_prod(&self, f: &mut Formatter<'_>) -> FmtResult {
- self.fmt_prod_impl(f)
- }
-
- #[cfg(test)]
- #[allow(missing_docs)]
- pub fn fmt_prod(&self, f: &mut Formatter<'_>) -> FmtResult {
- self.fmt_prod_impl(f)
- }
-
- fn fmt_prod_impl(&self, f: &mut Formatter<'_>) -> FmtResult {
- write!(f, r#"{{"kind":"{:?}","code":"{}""#, self.kind, self.code)?;
- if !matches!(self.edit_policy, MessageEditPolicy::Redact)
- && let Some(msg) = &self.message
- {
- write!(f, ",\"message\":\"")?;
- write_json_escaped(f, msg.as_ref())?;
- write!(f, "\"")?;
- }
- if !self.metadata.is_empty() {
- let has_public_fields = self
- .metadata
- .iter_with_redaction()
- .any(|(_, _, redaction)| !matches!(redaction, FieldRedaction::Redact));
- if has_public_fields {
- write!(f, r#","metadata":{{"#)?;
- let mut first = true;
- for (name, value, redaction) in self.metadata.iter_with_redaction() {
- if matches!(redaction, FieldRedaction::Redact) {
- continue;
- }
- if !first {
- write!(f, ",")?;
- }
- first = false;
- write!(f, r#""{}":"#, name)?;
- write_metadata_value(f, value)?;
- }
- write!(f, "}}")?;
- }
- }
- write!(f, "}}")
- }
-
- /// Formats error in local/development mode (human-readable).
- ///
- /// # Arguments
- ///
- /// * `f` - Formatter to write output to
- ///
- /// # Examples
- ///
- /// ```
- /// use masterror::AppError;
- ///
- /// let error = AppError::internal("Database error");
- /// let output = format!("{}", error);
- /// // In local mode: multi-line human-readable format with full context
- /// ```
- #[cfg(not(test))]
- pub(crate) fn fmt_local(&self, f: &mut Formatter<'_>) -> FmtResult {
- self.fmt_local_impl(f)
- }
-
- #[cfg(test)]
- #[allow(missing_docs)]
- pub fn fmt_local(&self, f: &mut Formatter<'_>) -> FmtResult {
- self.fmt_local_impl(f)
- }
-
- fn fmt_local_impl(&self, f: &mut Formatter<'_>) -> FmtResult {
- #[cfg(feature = "colored")]
- {
- use crate::colored::style;
- writeln!(f, "Error: {}", self.kind)?;
- writeln!(f, "Code: {}", style::error_code(self.code.to_string()))?;
- if let Some(msg) = &self.message {
- writeln!(f, "Message: {}", style::error_message(msg))?;
- }
- if let Some(source) = &self.source {
- writeln!(f)?;
- let mut current: &dyn CoreError = source.as_ref();
- let mut depth = 0;
- while depth < 10 {
- writeln!(
- f,
- " {}: {}",
- style::source_context("Caused by"),
- style::source_context(current.to_string())
- )?;
- if let Some(next) = current.source() {
- current = next;
- depth += 1;
- } else {
- break;
- }
- }
- }
- if !self.metadata.is_empty() {
- writeln!(f)?;
- writeln!(f, "Context:")?;
- for (key, value) in self.metadata.iter() {
- writeln!(f, " {}: {}", style::metadata_key(key), value)?;
- }
- }
- Ok(())
- }
- #[cfg(not(feature = "colored"))]
- {
- writeln!(f, "Error: {}", self.kind)?;
- writeln!(f, "Code: {}", self.code)?;
- if let Some(msg) = &self.message {
- writeln!(f, "Message: {}", msg)?;
- }
- if let Some(source) = &self.source {
- writeln!(f)?;
- let mut current: &dyn CoreError = source.as_ref();
- let mut depth = 0;
- while depth < 10 {
- writeln!(f, " Caused by: {}", current)?;
- if let Some(next) = current.source() {
- current = next;
- depth += 1;
- } else {
- break;
- }
- }
- }
- if !self.metadata.is_empty() {
- writeln!(f)?;
- writeln!(f, "Context:")?;
- for (key, value) in self.metadata.iter() {
- writeln!(f, " {}: {}", key, value)?;
- }
- }
- Ok(())
- }
- }
-
- /// Formats error in staging mode (JSON with context).
- ///
- /// # Arguments
- ///
- /// * `f` - Formatter to write output to
- ///
- /// # Examples
- ///
- /// ```
- /// use masterror::AppError;
- ///
- /// let error = AppError::service("Service unavailable");
- /// let output = format!("{}", error);
- /// // In staging mode: JSON with source_chain and metadata
- /// ```
- #[cfg(not(test))]
- pub(crate) fn fmt_staging(&self, f: &mut Formatter<'_>) -> FmtResult {
- self.fmt_staging_impl(f)
- }
-
- #[cfg(test)]
- #[allow(missing_docs)]
- pub fn fmt_staging(&self, f: &mut Formatter<'_>) -> FmtResult {
- self.fmt_staging_impl(f)
- }
-
- fn fmt_staging_impl(&self, f: &mut Formatter<'_>) -> FmtResult {
- write!(f, r#"{{"kind":"{:?}","code":"{}""#, self.kind, self.code)?;
- if !matches!(self.edit_policy, MessageEditPolicy::Redact)
- && let Some(msg) = &self.message
- {
- write!(f, ",\"message\":\"")?;
- write_json_escaped(f, msg.as_ref())?;
- write!(f, "\"")?;
- }
- if let Some(source) = &self.source {
- write!(f, r#","source_chain":["#)?;
- let mut current: &dyn CoreError = source.as_ref();
- let mut depth = 0;
- let mut first = true;
- while depth < 5 {
- if !first {
- write!(f, ",")?;
- }
- first = false;
- write!(f, "\"")?;
- write_json_escaped(f, ¤t.to_string())?;
- write!(f, "\"")?;
- if let Some(next) = current.source() {
- current = next;
- depth += 1;
- } else {
- break;
- }
- }
- write!(f, "]")?;
- }
- if !self.metadata.is_empty() {
- let has_public_fields = self
- .metadata
- .iter_with_redaction()
- .any(|(_, _, redaction)| !matches!(redaction, FieldRedaction::Redact));
- if has_public_fields {
- write!(f, r#","metadata":{{"#)?;
- let mut first = true;
- for (name, value, redaction) in self.metadata.iter_with_redaction() {
- if matches!(redaction, FieldRedaction::Redact) {
- continue;
- }
- if !first {
- write!(f, ",")?;
- }
- first = false;
- write!(f, r#""{}":"#, name)?;
- write_metadata_value(f, value)?;
- }
- write!(f, "}}")?;
- }
- }
- write!(f, "}}")
- }
-}
-
-/// Writes a string with JSON escaping.
-#[allow(dead_code)]
-fn write_json_escaped(f: &mut Formatter<'_>, s: &str) -> FmtResult {
- for ch in s.chars() {
- match ch {
- '"' => write!(f, "\\\"")?,
- '\\' => write!(f, "\\\\")?,
- '\n' => write!(f, "\\n")?,
- '\r' => write!(f, "\\r")?,
- '\t' => write!(f, "\\t")?,
- ch if ch.is_control() => write!(f, "\\u{:04x}", ch as u32)?,
- ch => write!(f, "{}", ch)?
- }
- }
- Ok(())
-}
-
-/// Writes a metadata field value in JSON format.
-#[allow(dead_code)]
-fn write_metadata_value(f: &mut Formatter<'_>, value: &FieldValue) -> FmtResult {
- use crate::app_error::metadata::FieldValue;
- match value {
- FieldValue::Str(s) => {
- write!(f, "\"")?;
- write_json_escaped(f, s.as_ref())?;
- write!(f, "\"")
- }
- FieldValue::I64(v) => write!(f, "{}", v),
- FieldValue::U64(v) => write!(f, "{}", v),
- FieldValue::F64(v) => {
- if v.is_finite() {
- write!(f, "{}", v)
- } else {
- write!(f, "null")
- }
- }
- FieldValue::Bool(v) => write!(f, "{}", v),
- FieldValue::Uuid(v) => write!(f, "\"{}\"", v),
- FieldValue::Duration(v) => {
- write!(
- f,
- r#"{{"secs":{},"nanos":{}}}"#,
- v.as_secs(),
- v.subsec_nanos()
- )
- }
- FieldValue::Ip(v) => write!(f, "\"{}\"", v),
- #[cfg(feature = "serde_json")]
- FieldValue::Json(v) => write!(f, "{}", v)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{AppError, field};
-
- #[test]
- fn display_mode_current_returns_valid_mode() {
- let mode = DisplayMode::current();
- assert!(matches!(
- mode,
- DisplayMode::Prod | DisplayMode::Local | DisplayMode::Staging
- ));
- }
-
- #[test]
- fn display_mode_detect_auto_returns_local_in_debug() {
- if cfg!(debug_assertions) {
- assert_eq!(DisplayMode::detect_auto(), DisplayMode::Local);
- } else {
- assert_eq!(DisplayMode::detect_auto(), DisplayMode::Prod);
- }
- }
-
- #[test]
- fn fmt_prod_outputs_json() {
- let error = AppError::not_found("User not found");
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#""kind":"NotFound""#));
- assert!(output.contains(r#""code":"NOT_FOUND""#));
- assert!(output.contains(r#""message":"User not found""#));
- }
-
- #[test]
- fn fmt_prod_excludes_redacted_message() {
- let error = AppError::internal("secret").redactable();
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(!output.contains("secret"));
- }
-
- #[test]
- fn fmt_prod_includes_metadata() {
- let error = AppError::not_found("User not found").with_field(field::u64("user_id", 12345));
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#""metadata""#));
- assert!(output.contains(r#""user_id":12345"#));
- }
-
- #[test]
- fn fmt_prod_excludes_sensitive_metadata() {
- let error = AppError::internal("Error").with_field(field::str("password", "secret"));
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(!output.contains("secret"));
- }
-
- #[test]
- fn fmt_local_outputs_human_readable() {
- let error = AppError::not_found("User not found");
- let output = format!("{}", error.fmt_local_wrapper());
- assert!(output.contains("Error:"));
- assert!(output.contains("Code: NOT_FOUND"));
- assert!(output.contains("Message: User not found"));
- }
-
- #[cfg(feature = "std")]
- #[test]
- fn fmt_local_includes_source_chain() {
- use std::io::Error as IoError;
- let io_err = IoError::other("connection failed");
- let error = AppError::internal("Database error").with_source(io_err);
- let output = format!("{}", error.fmt_local_wrapper());
- assert!(output.contains("Caused by"));
- assert!(output.contains("connection failed"));
- }
-
- #[test]
- fn fmt_staging_outputs_json_with_context() {
- let error = AppError::service("Service unavailable");
- let output = format!("{}", error.fmt_staging_wrapper());
- assert!(output.contains(r#""kind":"Service""#));
- assert!(output.contains(r#""code":"SERVICE""#));
- }
-
- #[cfg(feature = "std")]
- #[test]
- fn fmt_staging_includes_source_chain() {
- use std::io::Error as IoError;
- let io_err = IoError::other("timeout");
- let error = AppError::network("Network error").with_source(io_err);
- let output = format!("{}", error.fmt_staging_wrapper());
- assert!(output.contains(r#""source_chain""#));
- assert!(output.contains("timeout"));
- }
-
- #[test]
- fn fmt_prod_escapes_special_chars() {
- let error = AppError::internal("Line\nwith\"quotes\"");
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#"\n"#));
- assert!(output.contains(r#"\""#));
- }
-
- #[test]
- fn fmt_prod_handles_infinity_in_metadata() {
- let error = AppError::internal("Error").with_field(field::f64("ratio", f64::INFINITY));
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains("null"));
- }
-
- #[test]
- fn fmt_prod_formats_duration_metadata() {
- use core::time::Duration;
- let error = AppError::internal("Error")
- .with_field(field::duration("elapsed", Duration::from_millis(1500)));
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#""secs":1"#));
- assert!(output.contains(r#""nanos":500000000"#));
- }
-
- #[test]
- fn fmt_prod_formats_bool_metadata() {
- let error = AppError::internal("Error").with_field(field::bool("active", true));
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#""active":true"#));
- }
-
- #[cfg(feature = "std")]
- #[test]
- fn fmt_prod_formats_ip_metadata() {
- use std::net::IpAddr;
- let ip: IpAddr = "192.168.1.1".parse().unwrap();
- let error = AppError::internal("Error").with_field(field::ip("client_ip", ip));
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#""client_ip":"192.168.1.1""#));
- }
-
- #[test]
- fn fmt_prod_formats_uuid_metadata() {
- use uuid::Uuid;
- let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
- let error = AppError::internal("Error").with_field(field::uuid("request_id", uuid));
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#""request_id":"550e8400-e29b-41d4-a716-446655440000""#));
- }
-
- #[cfg(feature = "serde_json")]
- #[test]
- fn fmt_prod_formats_json_metadata() {
- let json = serde_json::json!({"nested": "value"});
- let error = AppError::internal("Error").with_field(field::json("data", json));
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#""data":"#));
- }
-
- #[test]
- fn fmt_prod_without_message() {
- let error = AppError::bare(crate::AppErrorKind::Internal);
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#""kind":"Internal""#));
- assert!(!output.contains(r#""message""#));
- }
-
- #[test]
- fn fmt_local_without_message() {
- let error = AppError::bare(crate::AppErrorKind::BadRequest);
- let output = format!("{}", error.fmt_local_wrapper());
- assert!(output.contains("Error:"));
- assert!(!output.contains("Message:"));
- }
-
- #[test]
- fn fmt_local_with_metadata() {
- let error = AppError::internal("Error")
- .with_field(field::str("key", "value"))
- .with_field(field::i64("count", -42));
- let output = format!("{}", error.fmt_local_wrapper());
- assert!(output.contains("Context:"));
- assert!(output.contains("key: value"));
- assert!(output.contains("count: -42"));
- }
-
- #[test]
- fn fmt_staging_without_message() {
- let error = AppError::bare(crate::AppErrorKind::Timeout);
- let output = format!("{}", error.fmt_staging_wrapper());
- assert!(output.contains(r#""kind":"Timeout""#));
- assert!(!output.contains(r#""message""#));
- }
-
- #[test]
- fn fmt_staging_with_metadata() {
- let error = AppError::service("Service error").with_field(field::u64("retry_count", 3));
- let output = format!("{}", error.fmt_staging_wrapper());
- assert!(output.contains(r#""metadata""#));
- assert!(output.contains(r#""retry_count":3"#));
- }
-
- #[test]
- fn fmt_staging_with_redacted_message() {
- let error = AppError::internal("sensitive data").redactable();
- let output = format!("{}", error.fmt_staging_wrapper());
- assert!(!output.contains("sensitive data"));
- }
-
- #[test]
- fn fmt_prod_escapes_control_chars() {
- let error = AppError::internal("test\x00\x1F");
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#"\u0000"#));
- assert!(output.contains(r#"\u001f"#));
- }
-
- #[test]
- fn fmt_prod_escapes_tab_and_carriage_return() {
- let error = AppError::internal("line\ttab\rreturn");
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#"\t"#));
- assert!(output.contains(r#"\r"#));
- }
-
- #[test]
- fn display_mode_current_caches_result() {
- let first = DisplayMode::current();
- let second = DisplayMode::current();
- assert_eq!(first, second);
- }
-
- #[test]
- fn display_mode_detect_auto_returns_prod_in_release() {
- if !cfg!(debug_assertions) {
- assert_eq!(DisplayMode::detect_auto(), DisplayMode::Prod);
- }
- }
-
- #[test]
- fn fmt_prod_with_multiple_metadata_fields() {
- let error = AppError::not_found("test")
- .with_field(field::str("first", "value1"))
- .with_field(field::u64("second", 42))
- .with_field(field::bool("third", true));
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#""first":"value1""#));
- assert!(output.contains(r#""second":42"#));
- assert!(output.contains(r#""third":true"#));
- }
-
- #[test]
- fn fmt_prod_escapes_backslash() {
- let error = AppError::internal("path\\to\\file");
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#"path\\to\\file"#));
- }
-
- #[test]
- fn fmt_prod_with_i64_metadata() {
- let error = AppError::internal("test").with_field(field::i64("count", -100));
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#""count":-100"#));
- }
-
- #[test]
- fn fmt_prod_with_string_metadata() {
- let error = AppError::internal("test").with_field(field::str("name", "value"));
- let output = format!("{}", error.fmt_prod_wrapper());
- assert!(output.contains(r#""name":"value""#));
- }
-
- #[cfg(feature = "colored")]
- #[test]
- fn fmt_local_with_deep_source_chain() {
- use std::io::{Error as IoError, ErrorKind};
- let io1 = IoError::new(ErrorKind::NotFound, "level 1");
- let io2 = IoError::other(io1);
- let error = AppError::internal("top").with_source(io2);
- let output = format!("{}", error.fmt_local_wrapper());
- assert!(output.contains("Caused by"));
- assert!(output.contains("level 1"));
- }
-
- #[test]
- fn fmt_staging_with_multiple_metadata_fields() {
- let error = AppError::service("error")
- .with_field(field::str("key1", "value1"))
- .with_field(field::u64("key2", 123))
- .with_field(field::bool("key3", false));
- let output = format!("{}", error.fmt_staging_wrapper());
- assert!(output.contains(r#""key1":"value1""#));
- assert!(output.contains(r#""key2":123"#));
- assert!(output.contains(r#""key3":false"#));
- }
-
- #[test]
- fn fmt_staging_with_deep_source_chain() {
- use std::io::{Error as IoError, ErrorKind};
- let io1 = IoError::new(ErrorKind::NotFound, "inner error");
- let io2 = IoError::other(io1);
- let error = AppError::service("outer").with_source(io2);
- let output = format!("{}", error.fmt_staging_wrapper());
- assert!(output.contains(r#""source_chain""#));
- assert!(output.contains("inner error"));
- }
-
- #[test]
- fn fmt_staging_with_redacted_and_public_metadata() {
- let error = AppError::internal("test")
- .with_field(field::str("public", "visible"))
- .with_field(field::str("password", "secret"));
- let output = format!("{}", error.fmt_staging_wrapper());
- assert!(output.contains(r#""public":"visible""#));
- assert!(!output.contains("secret"));
- }
-
- impl Error {
- fn fmt_prod_wrapper(&self) -> FormatterWrapper<'_> {
- FormatterWrapper {
- error: self,
- mode: FormatterMode::Prod
- }
- }
-
- fn fmt_local_wrapper(&self) -> FormatterWrapper<'_> {
- FormatterWrapper {
- error: self,
- mode: FormatterMode::Local
- }
- }
-
- fn fmt_staging_wrapper(&self) -> FormatterWrapper<'_> {
- FormatterWrapper {
- error: self,
- mode: FormatterMode::Staging
- }
- }
- }
-
- enum FormatterMode {
- Prod,
- Local,
- Staging
- }
-
- struct FormatterWrapper<'a> {
- error: &'a Error,
- mode: FormatterMode
- }
-
- impl core::fmt::Display for FormatterWrapper<'_> {
- fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
- match self.mode {
- FormatterMode::Prod => self.error.fmt_prod(f),
- FormatterMode::Local => self.error.fmt_local(f),
- FormatterMode::Staging => self.error.fmt_staging(f)
- }
- }
- }
-}
diff --git a/src/app_error/core/display/helpers.rs b/src/app_error/core/display/helpers.rs
new file mode 100644
index 0000000..e1d8827
--- /dev/null
+++ b/src/app_error/core/display/helpers.rs
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Helper functions for display formatting.
+
+use core::fmt::{Formatter, Result as FmtResult};
+
+use crate::FieldValue;
+
+#[allow(dead_code)]
+/// Writes a string with JSON escaping.
+pub(super) fn write_json_escaped(f: &mut Formatter<'_>, s: &str) -> FmtResult {
+ for ch in s.chars() {
+ match ch {
+ '"' => write!(f, "\\\"")?,
+ '\\' => write!(f, "\\\\")?,
+ '\n' => write!(f, "\\n")?,
+ '\r' => write!(f, "\\r")?,
+ '\t' => write!(f, "\\t")?,
+ ch if ch.is_control() => write!(f, "\\u{:04x}", ch as u32)?,
+ ch => write!(f, "{}", ch)?
+ }
+ }
+ Ok(())
+}
+
+#[allow(dead_code)]
+/// Writes a metadata field value in JSON format.
+pub(super) fn write_metadata_value(f: &mut Formatter<'_>, value: &FieldValue) -> FmtResult {
+ match value {
+ FieldValue::Str(s) => {
+ write!(f, "\"")?;
+ write_json_escaped(f, s.as_ref())?;
+ write!(f, "\"")
+ }
+ FieldValue::I64(v) => write!(f, "{}", v),
+ FieldValue::U64(v) => write!(f, "{}", v),
+ FieldValue::F64(v) => {
+ if v.is_finite() {
+ write!(f, "{}", v)
+ } else {
+ write!(f, "null")
+ }
+ }
+ FieldValue::Bool(v) => write!(f, "{}", v),
+ FieldValue::Uuid(v) => write!(f, "\"{}\"", v),
+ FieldValue::Duration(v) => {
+ write!(
+ f,
+ r#"{{"secs":{},"nanos":{}}}"#,
+ v.as_secs(),
+ v.subsec_nanos()
+ )
+ }
+ FieldValue::Ip(v) => write!(f, "\"{}\"", v),
+ #[cfg(feature = "serde_json")]
+ FieldValue::Json(v) => write!(f, "{}", v)
+ }
+}
diff --git a/src/app_error/core/display/local.rs b/src/app_error/core/display/local.rs
new file mode 100644
index 0000000..bdc03e6
--- /dev/null
+++ b/src/app_error/core/display/local.rs
@@ -0,0 +1,282 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Local/development mode formatting (human-readable).
+
+#[cfg(feature = "colored")]
+use alloc::string::ToString;
+use core::{
+ error::Error as CoreError,
+ fmt::{Formatter, Result as FmtResult}
+};
+
+use crate::app_error::core::error::Error;
+
+#[allow(dead_code)]
+impl Error {
+ /// Formats error in local/development mode (human-readable).
+ ///
+ /// # Arguments
+ ///
+ /// * `f` - Formatter to write output to
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use masterror::AppError;
+ ///
+ /// let error = AppError::internal("Database error");
+ /// let output = format!("{}", error);
+ /// // In local mode: multi-line human-readable format with full context
+ /// ```
+ #[cfg(not(test))]
+ pub(crate) fn fmt_local(&self, f: &mut Formatter<'_>) -> FmtResult {
+ self.fmt_local_impl(f)
+ }
+
+ #[cfg(test)]
+ #[allow(missing_docs)]
+ pub fn fmt_local(&self, f: &mut Formatter<'_>) -> FmtResult {
+ self.fmt_local_impl(f)
+ }
+
+ pub(super) fn fmt_local_impl(&self, f: &mut Formatter<'_>) -> FmtResult {
+ #[cfg(feature = "colored")]
+ {
+ self.fmt_local_colored(f)
+ }
+ #[cfg(not(feature = "colored"))]
+ {
+ self.fmt_local_plain(f)
+ }
+ }
+
+ #[cfg(feature = "colored")]
+ fn fmt_local_colored(&self, f: &mut Formatter<'_>) -> FmtResult {
+ use crate::colored::style;
+
+ writeln!(f, "Error: {}", self.kind)?;
+ writeln!(f, "Code: {}", style::error_code(self.code.to_string()))?;
+ if let Some(msg) = &self.message {
+ writeln!(f, "Message: {}", style::error_message(msg))?;
+ }
+ self.fmt_source_chain_colored(f)?;
+ self.fmt_metadata_colored(f)?;
+ self.fmt_diagnostics_colored(f)?;
+ Ok(())
+ }
+
+ #[cfg(feature = "colored")]
+ fn fmt_source_chain_colored(&self, f: &mut Formatter<'_>) -> FmtResult {
+ use crate::colored::style;
+
+ if let Some(source) = &self.source {
+ writeln!(f)?;
+ let mut current: &dyn CoreError = source.as_ref();
+ let mut depth = 0;
+ while depth < 10 {
+ writeln!(
+ f,
+ " {}: {}",
+ style::source_context("Caused by"),
+ style::source_context(current.to_string())
+ )?;
+ if let Some(next) = current.source() {
+ current = next;
+ depth += 1;
+ } else {
+ break;
+ }
+ }
+ }
+ Ok(())
+ }
+
+ #[cfg(feature = "colored")]
+ fn fmt_metadata_colored(&self, f: &mut Formatter<'_>) -> FmtResult {
+ use crate::colored::style;
+
+ if !self.metadata.is_empty() {
+ writeln!(f)?;
+ writeln!(f, "Context:")?;
+ for (key, value) in self.metadata.iter() {
+ writeln!(f, " {}: {}", style::metadata_key(key), value)?;
+ }
+ }
+ Ok(())
+ }
+
+ #[cfg(feature = "colored")]
+ fn fmt_diagnostics_colored(&self, f: &mut Formatter<'_>) -> FmtResult {
+ use crate::{app_error::diagnostics::DiagnosticVisibility, colored::style};
+
+ if let Some(diag) = &self.diagnostics {
+ let min_visibility = DiagnosticVisibility::DevOnly;
+
+ // Hints
+ let hints: alloc::vec::Vec<_> = diag.visible_hints(min_visibility).collect();
+ if !hints.is_empty() {
+ writeln!(f)?;
+ for hint in hints {
+ writeln!(
+ f,
+ " {}: {}",
+ style::hint_label("hint"),
+ style::hint_text(&hint.message)
+ )?;
+ }
+ }
+
+ // Suggestions
+ for suggestion in diag.visible_suggestions(min_visibility) {
+ writeln!(f)?;
+ write!(
+ f,
+ " {}: {}",
+ style::suggestion_label("suggestion"),
+ style::suggestion_text(&suggestion.message)
+ )?;
+ if let Some(cmd) = &suggestion.command {
+ writeln!(f)?;
+ writeln!(f, " {}", style::command(cmd))?;
+ } else {
+ writeln!(f)?;
+ }
+ }
+
+ // Documentation link
+ if let Some(doc) = diag.visible_doc_link(min_visibility) {
+ writeln!(f)?;
+ if let Some(title) = &doc.title {
+ writeln!(
+ f,
+ " {}: {} ({})",
+ style::docs_label("docs"),
+ title,
+ style::url(&doc.url)
+ )?;
+ } else {
+ writeln!(
+ f,
+ " {}: {}",
+ style::docs_label("docs"),
+ style::url(&doc.url)
+ )?;
+ }
+ }
+
+ // Related codes
+ if !diag.related_codes.is_empty() {
+ writeln!(f)?;
+ write!(f, " {}: ", style::related_label("see also"))?;
+ for (i, code) in diag.related_codes.iter().enumerate() {
+ if i > 0 {
+ write!(f, ", ")?;
+ }
+ write!(f, "{}", style::error_code(code))?;
+ }
+ writeln!(f)?;
+ }
+ }
+ Ok(())
+ }
+
+ #[cfg(not(feature = "colored"))]
+ fn fmt_local_plain(&self, f: &mut Formatter<'_>) -> FmtResult {
+ writeln!(f, "Error: {}", self.kind)?;
+ writeln!(f, "Code: {}", self.code)?;
+ if let Some(msg) = &self.message {
+ writeln!(f, "Message: {}", msg)?;
+ }
+ self.fmt_source_chain_plain(f)?;
+ self.fmt_metadata_plain(f)?;
+ self.fmt_diagnostics_plain(f)?;
+ Ok(())
+ }
+
+ #[cfg(not(feature = "colored"))]
+ fn fmt_source_chain_plain(&self, f: &mut Formatter<'_>) -> FmtResult {
+ if let Some(source) = &self.source {
+ writeln!(f)?;
+ let mut current: &dyn CoreError = source.as_ref();
+ let mut depth = 0;
+ while depth < 10 {
+ writeln!(f, " Caused by: {}", current)?;
+ if let Some(next) = current.source() {
+ current = next;
+ depth += 1;
+ } else {
+ break;
+ }
+ }
+ }
+ Ok(())
+ }
+
+ #[cfg(not(feature = "colored"))]
+ fn fmt_metadata_plain(&self, f: &mut Formatter<'_>) -> FmtResult {
+ if !self.metadata.is_empty() {
+ writeln!(f)?;
+ writeln!(f, "Context:")?;
+ for (key, value) in self.metadata.iter() {
+ writeln!(f, " {}: {}", key, value)?;
+ }
+ }
+ Ok(())
+ }
+
+ #[cfg(not(feature = "colored"))]
+ fn fmt_diagnostics_plain(&self, f: &mut Formatter<'_>) -> FmtResult {
+ use crate::app_error::diagnostics::DiagnosticVisibility;
+
+ if let Some(diag) = &self.diagnostics {
+ let min_visibility = DiagnosticVisibility::DevOnly;
+
+ // Hints
+ let hints: alloc::vec::Vec<_> = diag.visible_hints(min_visibility).collect();
+ if !hints.is_empty() {
+ writeln!(f)?;
+ for hint in hints {
+ writeln!(f, " hint: {}", hint.message)?;
+ }
+ }
+
+ // Suggestions
+ for suggestion in diag.visible_suggestions(min_visibility) {
+ writeln!(f)?;
+ write!(f, " suggestion: {}", suggestion.message)?;
+ if let Some(cmd) = &suggestion.command {
+ writeln!(f)?;
+ writeln!(f, " {}", cmd)?;
+ } else {
+ writeln!(f)?;
+ }
+ }
+
+ // Documentation link
+ if let Some(doc) = diag.visible_doc_link(min_visibility) {
+ writeln!(f)?;
+ if let Some(title) = &doc.title {
+ writeln!(f, " docs: {} ({})", title, doc.url)?;
+ } else {
+ writeln!(f, " docs: {}", doc.url)?;
+ }
+ }
+
+ // Related codes
+ if !diag.related_codes.is_empty() {
+ writeln!(f)?;
+ write!(f, " see also: ")?;
+ for (i, code) in diag.related_codes.iter().enumerate() {
+ if i > 0 {
+ write!(f, ", ")?;
+ }
+ write!(f, "{}", code)?;
+ }
+ writeln!(f)?;
+ }
+ }
+ Ok(())
+ }
+}
diff --git a/src/app_error/core/display/mod.rs b/src/app_error/core/display/mod.rs
new file mode 100644
index 0000000..77c2dc1
--- /dev/null
+++ b/src/app_error/core/display/mod.rs
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Display formatting for `AppError`.
+//!
+//! Provides environment-aware display modes: production (compact JSON),
+//! local (human-readable), and staging (JSON with context).
+
+mod helpers;
+mod local;
+mod mode;
+mod prod;
+mod staging;
+
+#[cfg(test)]
+mod tests;
+
+pub use mode::DisplayMode;
diff --git a/src/app_error/core/display/mode.rs b/src/app_error/core/display/mode.rs
new file mode 100644
index 0000000..dd88b49
--- /dev/null
+++ b/src/app_error/core/display/mode.rs
@@ -0,0 +1,145 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Display mode detection and configuration.
+
+use core::sync::atomic::{AtomicU8, Ordering};
+
+/// Display mode for error output.
+///
+/// Controls the structure and verbosity of error messages based on
+/// the deployment environment. The mode is determined by the
+/// `MASTERROR_ENV` environment variable or auto-detected based on
+/// build configuration and runtime environment.
+///
+/// # Examples
+///
+/// ```
+/// use masterror::DisplayMode;
+///
+/// let mode = DisplayMode::current();
+/// match mode {
+/// DisplayMode::Prod => println!("Production mode: JSON output"),
+/// DisplayMode::Local => println!("Local mode: Human-readable output"),
+/// DisplayMode::Staging => println!("Staging mode: JSON with context")
+/// }
+/// ```
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum DisplayMode {
+ /// Production mode: lightweight JSON, minimal fields, no sensitive data.
+ ///
+ /// Output includes only: `kind`, `code`, `message` (if not redacted).
+ /// Metadata is filtered to exclude sensitive fields.
+ /// Source chain and backtrace are excluded.
+ ///
+ /// # Example Output
+ ///
+ /// ```json
+ /// {"kind":"NotFound","code":"NOT_FOUND","message":"User not found"}
+ /// ```
+ Prod = 0,
+
+ /// Development mode: human-readable, full context.
+ ///
+ /// Output includes: error details, full source chain, complete metadata,
+ /// and backtrace (if enabled). Supports colored output when the `colored`
+ /// feature is enabled and output is a TTY.
+ ///
+ /// # Example Output
+ ///
+ /// ```text
+ /// Error: NotFound
+ /// Code: NOT_FOUND
+ /// Message: User not found
+ ///
+ /// Caused by: database query failed
+ /// Caused by: connection timeout
+ ///
+ /// Context:
+ /// user_id: 12345
+ /// ```
+ Local = 1,
+
+ /// Staging mode: JSON with additional context.
+ ///
+ /// Output includes: `kind`, `code`, `message`, limited `source_chain`,
+ /// and filtered metadata. No backtrace.
+ ///
+ /// # Example Output
+ ///
+ /// ```json
+ /// {"kind":"NotFound","code":"NOT_FOUND","message":"User not found","source_chain":["database error"],"metadata":{"user_id":12345}}
+ /// ```
+ Staging = 2
+}
+
+impl DisplayMode {
+ /// Returns the current display mode based on environment configuration.
+ ///
+ /// The mode is determined by checking (in order):
+ /// 1. `MASTERROR_ENV` environment variable (`prod`, `local`, or `staging`)
+ /// 2. Kubernetes environment detection (`KUBERNETES_SERVICE_HOST`)
+ /// 3. Build configuration (`cfg!(debug_assertions)`)
+ ///
+ /// The result is cached for performance.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use masterror::DisplayMode;
+ ///
+ /// let mode = DisplayMode::current();
+ /// assert!(matches!(
+ /// mode,
+ /// DisplayMode::Prod | DisplayMode::Local | DisplayMode::Staging
+ /// ));
+ /// ```
+ #[must_use]
+ pub fn current() -> Self {
+ static CACHED_MODE: AtomicU8 = AtomicU8::new(255);
+ let cached = CACHED_MODE.load(Ordering::Relaxed);
+ if cached != 255 {
+ return match cached {
+ 0 => Self::Prod,
+ 1 => Self::Local,
+ 2 => Self::Staging,
+ _ => unreachable!()
+ };
+ }
+ let mode = Self::detect();
+ CACHED_MODE.store(mode as u8, Ordering::Relaxed);
+ mode
+ }
+
+ /// Detects the appropriate display mode from environment.
+ ///
+ /// This is an internal helper called by [`current()`](Self::current).
+ pub(crate) fn detect() -> Self {
+ #[cfg(feature = "std")]
+ {
+ use std::env::var;
+ if let Ok(env) = var("MASTERROR_ENV") {
+ return match env.as_str() {
+ "prod" | "production" => Self::Prod,
+ "local" | "dev" | "development" => Self::Local,
+ "staging" | "stage" => Self::Staging,
+ _ => Self::detect_auto()
+ };
+ }
+ if var("KUBERNETES_SERVICE_HOST").is_ok() {
+ return Self::Prod;
+ }
+ }
+ Self::detect_auto()
+ }
+
+ /// Auto-detects mode based on build configuration.
+ pub(crate) fn detect_auto() -> Self {
+ if cfg!(debug_assertions) {
+ Self::Local
+ } else {
+ Self::Prod
+ }
+ }
+}
diff --git a/src/app_error/core/display/prod.rs b/src/app_error/core/display/prod.rs
new file mode 100644
index 0000000..768505a
--- /dev/null
+++ b/src/app_error/core/display/prod.rs
@@ -0,0 +1,73 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Production mode formatting (compact JSON).
+
+use core::fmt::{Formatter, Result as FmtResult};
+
+use super::helpers::{write_json_escaped, write_metadata_value};
+use crate::{FieldRedaction, MessageEditPolicy, app_error::core::error::Error};
+
+#[allow(dead_code)]
+impl Error {
+ /// Formats error in production mode (compact JSON).
+ ///
+ /// # Arguments
+ ///
+ /// * `f` - Formatter to write output to
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use masterror::AppError;
+ ///
+ /// let error = AppError::not_found("User not found");
+ /// let output = format!("{}", error);
+ /// // In prod mode: {"kind":"NotFound","code":"NOT_FOUND","message":"User not found"}
+ /// ```
+ #[cfg(not(test))]
+ pub(crate) fn fmt_prod(&self, f: &mut Formatter<'_>) -> FmtResult {
+ self.fmt_prod_impl(f)
+ }
+
+ #[cfg(test)]
+ #[allow(missing_docs)]
+ pub fn fmt_prod(&self, f: &mut Formatter<'_>) -> FmtResult {
+ self.fmt_prod_impl(f)
+ }
+
+ pub(super) fn fmt_prod_impl(&self, f: &mut Formatter<'_>) -> FmtResult {
+ write!(f, r#"{{"kind":"{:?}","code":"{}""#, self.kind, self.code)?;
+ if !matches!(self.edit_policy, MessageEditPolicy::Redact)
+ && let Some(msg) = &self.message
+ {
+ write!(f, ",\"message\":\"")?;
+ write_json_escaped(f, msg.as_ref())?;
+ write!(f, "\"")?;
+ }
+ if !self.metadata.is_empty() {
+ let has_public_fields = self
+ .metadata
+ .iter_with_redaction()
+ .any(|(_, _, redaction)| !matches!(redaction, FieldRedaction::Redact));
+ if has_public_fields {
+ write!(f, r#","metadata":{{"#)?;
+ let mut first = true;
+ for (name, value, redaction) in self.metadata.iter_with_redaction() {
+ if matches!(redaction, FieldRedaction::Redact) {
+ continue;
+ }
+ if !first {
+ write!(f, ",")?;
+ }
+ first = false;
+ write!(f, r#""{}":"#, name)?;
+ write_metadata_value(f, value)?;
+ }
+ write!(f, "}}")?;
+ }
+ }
+ write!(f, "}}")
+ }
+}
diff --git a/src/app_error/core/display/staging.rs b/src/app_error/core/display/staging.rs
new file mode 100644
index 0000000..0975f2c
--- /dev/null
+++ b/src/app_error/core/display/staging.rs
@@ -0,0 +1,109 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Staging mode formatting (JSON with context).
+
+use alloc::string::ToString;
+use core::{
+ error::Error as CoreError,
+ fmt::{Formatter, Result as FmtResult}
+};
+
+use super::helpers::{write_json_escaped, write_metadata_value};
+use crate::{FieldRedaction, MessageEditPolicy, app_error::core::error::Error};
+
+#[allow(dead_code)]
+impl Error {
+ /// Formats error in staging mode (JSON with context).
+ ///
+ /// # Arguments
+ ///
+ /// * `f` - Formatter to write output to
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use masterror::AppError;
+ ///
+ /// let error = AppError::service("Service unavailable");
+ /// let output = format!("{}", error);
+ /// // In staging mode: JSON with source_chain and metadata
+ /// ```
+ #[cfg(not(test))]
+ pub(crate) fn fmt_staging(&self, f: &mut Formatter<'_>) -> FmtResult {
+ self.fmt_staging_impl(f)
+ }
+
+ #[cfg(test)]
+ #[allow(missing_docs)]
+ pub fn fmt_staging(&self, f: &mut Formatter<'_>) -> FmtResult {
+ self.fmt_staging_impl(f)
+ }
+
+ pub(super) fn fmt_staging_impl(&self, f: &mut Formatter<'_>) -> FmtResult {
+ write!(f, r#"{{"kind":"{:?}","code":"{}""#, self.kind, self.code)?;
+ if !matches!(self.edit_policy, MessageEditPolicy::Redact)
+ && let Some(msg) = &self.message
+ {
+ write!(f, ",\"message\":\"")?;
+ write_json_escaped(f, msg.as_ref())?;
+ write!(f, "\"")?;
+ }
+ self.fmt_staging_source_chain(f)?;
+ self.fmt_staging_metadata(f)?;
+ write!(f, "}}")
+ }
+
+ fn fmt_staging_source_chain(&self, f: &mut Formatter<'_>) -> FmtResult {
+ if let Some(source) = &self.source {
+ write!(f, r#","source_chain":["#)?;
+ let mut current: &dyn CoreError = source.as_ref();
+ let mut depth = 0;
+ let mut first = true;
+ while depth < 5 {
+ if !first {
+ write!(f, ",")?;
+ }
+ first = false;
+ write!(f, "\"")?;
+ write_json_escaped(f, ¤t.to_string())?;
+ write!(f, "\"")?;
+ if let Some(next) = current.source() {
+ current = next;
+ depth += 1;
+ } else {
+ break;
+ }
+ }
+ write!(f, "]")?;
+ }
+ Ok(())
+ }
+
+ fn fmt_staging_metadata(&self, f: &mut Formatter<'_>) -> FmtResult {
+ if !self.metadata.is_empty() {
+ let has_public_fields = self
+ .metadata
+ .iter_with_redaction()
+ .any(|(_, _, redaction)| !matches!(redaction, FieldRedaction::Redact));
+ if has_public_fields {
+ write!(f, r#","metadata":{{"#)?;
+ let mut first = true;
+ for (name, value, redaction) in self.metadata.iter_with_redaction() {
+ if matches!(redaction, FieldRedaction::Redact) {
+ continue;
+ }
+ if !first {
+ write!(f, ",")?;
+ }
+ first = false;
+ write!(f, r#""{}":"#, name)?;
+ write_metadata_value(f, value)?;
+ }
+ write!(f, "}}")?;
+ }
+ }
+ Ok(())
+ }
+}
diff --git a/src/app_error/core/display/tests.rs b/src/app_error/core/display/tests.rs
new file mode 100644
index 0000000..128bd5b
--- /dev/null
+++ b/src/app_error/core/display/tests.rs
@@ -0,0 +1,451 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Tests for display module.
+
+use core::fmt::{Formatter, Result as FmtResult};
+
+use super::DisplayMode;
+use crate::{AppError, field};
+
+// ─────────────────────────────────────────────────────────────────────────────
+// DisplayMode tests
+// ─────────────────────────────────────────────────────────────────────────────
+
+#[test]
+fn display_mode_current_returns_valid_mode() {
+ let mode = DisplayMode::current();
+ assert!(matches!(
+ mode,
+ DisplayMode::Prod | DisplayMode::Local | DisplayMode::Staging
+ ));
+}
+
+#[test]
+fn display_mode_detect_auto_returns_local_in_debug() {
+ if cfg!(debug_assertions) {
+ assert_eq!(DisplayMode::detect_auto(), DisplayMode::Local);
+ } else {
+ assert_eq!(DisplayMode::detect_auto(), DisplayMode::Prod);
+ }
+}
+
+#[test]
+fn display_mode_current_caches_result() {
+ let first = DisplayMode::current();
+ let second = DisplayMode::current();
+ assert_eq!(first, second);
+}
+
+#[test]
+fn display_mode_detect_auto_returns_prod_in_release() {
+ if !cfg!(debug_assertions) {
+ assert_eq!(DisplayMode::detect_auto(), DisplayMode::Prod);
+ }
+}
+
+// Note: Environment-based mode detection tests (MASTERROR_ENV,
+// KUBERNETES_SERVICE_HOST) cannot be tested without unsafe env var manipulation
+// which is forbidden by #![deny(unsafe_code)]. These code paths are tested via
+// integration tests and manual verification.
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Production format tests
+// ─────────────────────────────────────────────────────────────────────────────
+
+#[test]
+fn fmt_prod_outputs_json() {
+ let error = AppError::not_found("User not found");
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#""kind":"NotFound""#));
+ assert!(output.contains(r#""code":"NOT_FOUND""#));
+ assert!(output.contains(r#""message":"User not found""#));
+}
+
+#[test]
+fn fmt_prod_excludes_redacted_message() {
+ let error = AppError::internal("secret").redactable();
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(!output.contains("secret"));
+}
+
+#[test]
+fn fmt_prod_includes_metadata() {
+ let error = AppError::not_found("User not found").with_field(field::u64("user_id", 12345));
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#""metadata""#));
+ assert!(output.contains(r#""user_id":12345"#));
+}
+
+#[test]
+fn fmt_prod_excludes_sensitive_metadata() {
+ let error = AppError::internal("Error").with_field(field::str("password", "secret"));
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(!output.contains("secret"));
+}
+
+#[test]
+fn fmt_prod_escapes_special_chars() {
+ let error = AppError::internal("Line\nwith\"quotes\"");
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#"\n"#));
+ assert!(output.contains(r#"\""#));
+}
+
+#[test]
+fn fmt_prod_handles_infinity_in_metadata() {
+ let error = AppError::internal("Error").with_field(field::f64("ratio", f64::INFINITY));
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains("null"));
+}
+
+#[test]
+fn fmt_prod_formats_duration_metadata() {
+ use core::time::Duration;
+ let error = AppError::internal("Error")
+ .with_field(field::duration("elapsed", Duration::from_millis(1500)));
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#""secs":1"#));
+ assert!(output.contains(r#""nanos":500000000"#));
+}
+
+#[test]
+fn fmt_prod_formats_bool_metadata() {
+ let error = AppError::internal("Error").with_field(field::bool("active", true));
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#""active":true"#));
+}
+
+#[cfg(feature = "std")]
+#[test]
+fn fmt_prod_formats_ip_metadata() {
+ use std::net::IpAddr;
+ let ip: IpAddr = "192.168.1.1".parse().unwrap();
+ let error = AppError::internal("Error").with_field(field::ip("client_ip", ip));
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#""client_ip":"192.168.1.1""#));
+}
+
+#[test]
+fn fmt_prod_formats_uuid_metadata() {
+ use uuid::Uuid;
+ let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
+ let error = AppError::internal("Error").with_field(field::uuid("request_id", uuid));
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#""request_id":"550e8400-e29b-41d4-a716-446655440000""#));
+}
+
+#[cfg(feature = "serde_json")]
+#[test]
+fn fmt_prod_formats_json_metadata() {
+ let json = serde_json::json!({"nested": "value"});
+ let error = AppError::internal("Error").with_field(field::json("data", json));
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#""data":"#));
+}
+
+#[test]
+fn fmt_prod_without_message() {
+ let error = AppError::bare(crate::AppErrorKind::Internal);
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#""kind":"Internal""#));
+ assert!(!output.contains(r#""message""#));
+}
+
+#[test]
+fn fmt_prod_with_multiple_metadata_fields() {
+ let error = AppError::not_found("test")
+ .with_field(field::str("first", "value1"))
+ .with_field(field::u64("second", 42))
+ .with_field(field::bool("third", true));
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#""first":"value1""#));
+ assert!(output.contains(r#""second":42"#));
+ assert!(output.contains(r#""third":true"#));
+}
+
+#[test]
+fn fmt_prod_escapes_backslash() {
+ let error = AppError::internal("path\\to\\file");
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#"path\\to\\file"#));
+}
+
+#[test]
+fn fmt_prod_with_i64_metadata() {
+ let error = AppError::internal("test").with_field(field::i64("count", -100));
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#""count":-100"#));
+}
+
+#[test]
+fn fmt_prod_with_string_metadata() {
+ let error = AppError::internal("test").with_field(field::str("name", "value"));
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#""name":"value""#));
+}
+
+#[test]
+fn fmt_prod_escapes_control_chars() {
+ let error = AppError::internal("test\x00\x1F");
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#"\u0000"#));
+ assert!(output.contains(r#"\u001f"#));
+}
+
+#[test]
+fn fmt_prod_escapes_tab_and_carriage_return() {
+ let error = AppError::internal("line\ttab\rreturn");
+ let output = format!("{}", error.fmt_prod_wrapper());
+ assert!(output.contains(r#"\t"#));
+ assert!(output.contains(r#"\r"#));
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Local format tests
+// ─────────────────────────────────────────────────────────────────────────────
+
+#[test]
+fn fmt_local_outputs_human_readable() {
+ let error = AppError::not_found("User not found");
+ let output = format!("{}", error.fmt_local_wrapper());
+ assert!(output.contains("Error:"));
+ assert!(output.contains("Code: NOT_FOUND") || output.contains("Code:"));
+ assert!(output.contains("Message: User not found") || output.contains("Message:"));
+}
+
+#[cfg(feature = "std")]
+#[test]
+fn fmt_local_includes_source_chain() {
+ use std::io::Error as IoError;
+ let io_err = IoError::other("connection failed");
+ let error = AppError::internal("Database error").with_source(io_err);
+ let output = format!("{}", error.fmt_local_wrapper());
+ assert!(output.contains("Caused by"));
+ assert!(output.contains("connection failed"));
+}
+
+#[test]
+fn fmt_local_without_message() {
+ let error = AppError::bare(crate::AppErrorKind::BadRequest);
+ let output = format!("{}", error.fmt_local_wrapper());
+ assert!(output.contains("Error:"));
+ assert!(!output.contains("Message:"));
+}
+
+#[test]
+fn fmt_local_with_metadata() {
+ let error = AppError::internal("Error")
+ .with_field(field::str("key", "value"))
+ .with_field(field::i64("count", -42));
+ let output = format!("{}", error.fmt_local_wrapper());
+ assert!(output.contains("Context:"));
+ assert!(output.contains("key"));
+ assert!(output.contains("value") || output.contains("-42"));
+}
+
+#[cfg(feature = "colored")]
+#[test]
+fn fmt_local_with_deep_source_chain() {
+ use std::io::{Error as IoError, ErrorKind};
+ let io1 = IoError::new(ErrorKind::NotFound, "level 1");
+ let io2 = IoError::other(io1);
+ let error = AppError::internal("top").with_source(io2);
+ let output = format!("{}", error.fmt_local_wrapper());
+ assert!(output.contains("Caused by"));
+ assert!(output.contains("level 1"));
+}
+
+#[test]
+fn fmt_local_with_hints() {
+ let error = AppError::not_found("Resource missing")
+ .with_hint("Check the resource ID")
+ .with_hint("Verify permissions");
+ let output = format!("{}", error.fmt_local_wrapper());
+ assert!(output.contains("hint"));
+ assert!(output.contains("Check the resource ID"));
+}
+
+#[test]
+fn fmt_local_with_suggestions() {
+ let error = AppError::service("Service unavailable")
+ .with_suggestion("Retry the request")
+ .with_suggestion_cmd("Check service status", "systemctl status myservice");
+ let output = format!("{}", error.fmt_local_wrapper());
+ assert!(output.contains("suggestion"));
+ assert!(output.contains("Retry the request"));
+ assert!(output.contains("systemctl status myservice"));
+}
+
+#[test]
+fn fmt_local_with_doc_link() {
+ let error = AppError::unauthorized("Authentication required")
+ .with_docs("https://docs.example.com/auth");
+ let output = format!("{}", error.fmt_local_wrapper());
+ assert!(output.contains("docs"));
+ assert!(output.contains("https://docs.example.com/auth"));
+}
+
+#[test]
+fn fmt_local_with_doc_link_titled() {
+ let error = AppError::forbidden("Access denied")
+ .with_docs_titled("https://docs.example.com/rbac", "RBAC Guide");
+ let output = format!("{}", error.fmt_local_wrapper());
+ assert!(output.contains("docs"));
+ assert!(output.contains("RBAC Guide"));
+}
+
+#[test]
+fn fmt_local_with_related_codes() {
+ let error = AppError::database_with_message("Connection failed")
+ .with_related_code("DB_POOL_EXHAUSTED")
+ .with_related_code("DB_TIMEOUT");
+ let output = format!("{}", error.fmt_local_wrapper());
+ assert!(output.contains("see also"));
+ assert!(output.contains("DB_POOL_EXHAUSTED"));
+}
+
+#[test]
+fn fmt_local_with_all_diagnostics() {
+ let error = AppError::internal("Critical failure")
+ .with_hint("Check logs for details")
+ .with_suggestion_cmd("View logs", "journalctl -u myapp")
+ .with_docs_titled("https://docs.example.com/troubleshoot", "Troubleshooting")
+ .with_related_code("ERR_STARTUP_FAILED");
+ let output = format!("{}", error.fmt_local_wrapper());
+ assert!(output.contains("hint"));
+ assert!(output.contains("suggestion"));
+ assert!(output.contains("docs"));
+ assert!(output.contains("see also"));
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Staging format tests
+// ─────────────────────────────────────────────────────────────────────────────
+
+#[test]
+fn fmt_staging_outputs_json_with_context() {
+ let error = AppError::service("Service unavailable");
+ let output = format!("{}", error.fmt_staging_wrapper());
+ assert!(output.contains(r#""kind":"Service""#));
+ assert!(output.contains(r#""code":"SERVICE""#));
+}
+
+#[cfg(feature = "std")]
+#[test]
+fn fmt_staging_includes_source_chain() {
+ use std::io::Error as IoError;
+ let io_err = IoError::other("timeout");
+ let error = AppError::network("Network error").with_source(io_err);
+ let output = format!("{}", error.fmt_staging_wrapper());
+ assert!(output.contains(r#""source_chain""#));
+ assert!(output.contains("timeout"));
+}
+
+#[test]
+fn fmt_staging_without_message() {
+ let error = AppError::bare(crate::AppErrorKind::Timeout);
+ let output = format!("{}", error.fmt_staging_wrapper());
+ assert!(output.contains(r#""kind":"Timeout""#));
+ assert!(!output.contains(r#""message""#));
+}
+
+#[test]
+fn fmt_staging_with_metadata() {
+ let error = AppError::service("Service error").with_field(field::u64("retry_count", 3));
+ let output = format!("{}", error.fmt_staging_wrapper());
+ assert!(output.contains(r#""metadata""#));
+ assert!(output.contains(r#""retry_count":3"#));
+}
+
+#[test]
+fn fmt_staging_with_redacted_message() {
+ let error = AppError::internal("sensitive data").redactable();
+ let output = format!("{}", error.fmt_staging_wrapper());
+ assert!(!output.contains("sensitive data"));
+}
+
+#[test]
+fn fmt_staging_with_multiple_metadata_fields() {
+ let error = AppError::service("error")
+ .with_field(field::str("key1", "value1"))
+ .with_field(field::u64("key2", 123))
+ .with_field(field::bool("key3", false));
+ let output = format!("{}", error.fmt_staging_wrapper());
+ assert!(output.contains(r#""key1":"value1""#));
+ assert!(output.contains(r#""key2":123"#));
+ assert!(output.contains(r#""key3":false"#));
+}
+
+#[cfg(feature = "std")]
+#[test]
+fn fmt_staging_with_deep_source_chain() {
+ use std::io::{Error as IoError, ErrorKind};
+ let io1 = IoError::new(ErrorKind::NotFound, "inner error");
+ let io2 = IoError::other(io1);
+ let error = AppError::service("outer").with_source(io2);
+ let output = format!("{}", error.fmt_staging_wrapper());
+ assert!(output.contains(r#""source_chain""#));
+ assert!(output.contains("inner error"));
+}
+
+#[test]
+fn fmt_staging_with_redacted_and_public_metadata() {
+ let error = AppError::internal("test")
+ .with_field(field::str("public", "visible"))
+ .with_field(field::str("password", "secret"));
+ let output = format!("{}", error.fmt_staging_wrapper());
+ assert!(output.contains(r#""public":"visible""#));
+ assert!(!output.contains("secret"));
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Helper wrapper for testing
+// ─────────────────────────────────────────────────────────────────────────────
+
+use crate::app_error::core::error::Error;
+
+impl Error {
+ fn fmt_prod_wrapper(&self) -> FormatterWrapper<'_> {
+ FormatterWrapper {
+ error: self,
+ mode: FormatterMode::Prod
+ }
+ }
+
+ fn fmt_local_wrapper(&self) -> FormatterWrapper<'_> {
+ FormatterWrapper {
+ error: self,
+ mode: FormatterMode::Local
+ }
+ }
+
+ fn fmt_staging_wrapper(&self) -> FormatterWrapper<'_> {
+ FormatterWrapper {
+ error: self,
+ mode: FormatterMode::Staging
+ }
+ }
+}
+
+enum FormatterMode {
+ Prod,
+ Local,
+ Staging
+}
+
+struct FormatterWrapper<'a> {
+ error: &'a Error,
+ mode: FormatterMode
+}
+
+impl core::fmt::Display for FormatterWrapper<'_> {
+ fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
+ match self.mode {
+ FormatterMode::Prod => self.error.fmt_prod(f),
+ FormatterMode::Local => self.error.fmt_local(f),
+ FormatterMode::Staging => self.error.fmt_staging(f)
+ }
+ }
+}
diff --git a/src/app_error/core/error.rs b/src/app_error/core/error.rs
index 9bb9564..9e4c106 100644
--- a/src/app_error/core/error.rs
+++ b/src/app_error/core/error.rs
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2025 RAprogramm
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
//
// SPDX-License-Identifier: MIT
@@ -18,7 +18,10 @@ use serde_json::Value as JsonValue;
#[cfg(not(feature = "backtrace"))]
use super::types::CapturedBacktrace;
use super::types::MessageEditPolicy;
-use crate::{AppCode, AppErrorKind, RetryAdvice, app_error::metadata::Metadata};
+use crate::{
+ AppCode, AppErrorKind, RetryAdvice,
+ app_error::{diagnostics::Diagnostics, metadata::Metadata}
+};
/// Internal representation of error state.
///
@@ -49,6 +52,10 @@ pub struct ErrorInner {
#[cfg(not(feature = "serde_json"))]
pub details: Option,
pub source: Option>,
+ /// Diagnostic information (hints, suggestions, documentation).
+ ///
+ /// Stored as `Option>` for zero-cost when not used.
+ pub diagnostics: Option>,
#[cfg(feature = "backtrace")]
pub backtrace: Option>,
#[cfg(feature = "backtrace")]
@@ -194,6 +201,7 @@ impl Error {
www_authenticate: None,
details: None,
source: None,
+ diagnostics: None,
#[cfg(feature = "backtrace")]
backtrace: None,
#[cfg(feature = "backtrace")]
diff --git a/src/app_error/diagnostics.rs b/src/app_error/diagnostics.rs
new file mode 100644
index 0000000..69a31d1
--- /dev/null
+++ b/src/app_error/diagnostics.rs
@@ -0,0 +1,509 @@
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
+//
+// SPDX-License-Identifier: MIT
+
+//! Diagnostic information for enhanced error reporting.
+//!
+//! This module provides rich diagnostic capabilities for errors, inspired by
+//! compiler diagnostics (rustc, miette) but designed for runtime application
+//! errors.
+//!
+//! # Features
+//!
+//! - **Hints**: Contextual advice explaining why an error occurred
+//! - **Suggestions**: Actionable fixes with optional commands/code snippets
+//! - **Documentation links**: URLs to detailed explanations
+//! - **Related codes**: Cross-references to related error codes
+//! - **Visibility control**: Per-item visibility for dev/staging/prod
+//! environments
+//!
+//! # Example
+//!
+//! ```rust
+//! use masterror::{AppError, DiagnosticVisibility};
+//!
+//! let err = AppError::not_found("User not found")
+//! .with_hint("Check if the user ID is correct")
+//! .with_hint("User might have been deleted")
+//! .with_suggestion_cmd("List all users to verify", "curl -X GET /api/users")
+//! .with_docs("https://docs.example.com/errors/USER_NOT_FOUND");
+//! ```
+//!
+//! # Display Modes
+//!
+//! Diagnostics are filtered based on [`DisplayMode`](crate::DisplayMode):
+//!
+//! | Visibility | Local | Staging | Prod |
+//! |------------|-------|---------|------|
+//! | `DevOnly` | ✅ | ❌ | ❌ |
+//! | `Internal` | ✅ | ✅ | ❌ |
+//! | `Public` | ✅ | ✅ | ✅ |
+
+use alloc::borrow::Cow;
+
+use super::inline_vec::InlineVec;
+
+/// Visibility of diagnostic information across environments.
+///
+/// Controls where hints, suggestions, and other diagnostic information
+/// are displayed based on the deployment environment.
+///
+/// # Ordering
+///
+/// Variants are ordered from most restrictive to least restrictive:
+/// `DevOnly < Internal < Public`. This enables filtering with simple
+/// comparisons.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
+#[repr(u8)]
+pub enum DiagnosticVisibility {
+ /// Shown only in Local mode (development).
+ ///
+ /// Use for internal debugging hints that may expose implementation
+ /// details or sensitive information.
+ #[default]
+ DevOnly = 0,
+
+ /// Shown in Local and Staging environments.
+ ///
+ /// Use for hints that help with testing and debugging but should
+ /// not reach production users.
+ Internal = 1,
+
+ /// Shown everywhere including Production.
+ ///
+ /// Use for user-facing hints and documentation links that help
+ /// end users understand and resolve errors.
+ Public = 2
+}
+
+/// A single hint providing context about an error.
+///
+/// Hints explain why an error might have occurred without necessarily
+/// providing a fix. They help developers and users understand the
+/// error context.
+///
+/// # Example
+///
+/// ```rust
+/// use masterror::diagnostics::{DiagnosticVisibility, Hint};
+///
+/// let hint = Hint {
+/// message: "Database connection pool may be exhausted".into(),
+/// visibility: DiagnosticVisibility::Internal
+/// };
+/// ```
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Hint {
+ /// The hint message.
+ pub message: Cow<'static, str>,
+
+ /// Where this hint should be displayed.
+ pub visibility: DiagnosticVisibility
+}
+
+impl Hint {
+ /// Creates a new hint with default (DevOnly) visibility.
+ #[must_use]
+ pub fn new(message: impl Into>) -> Self {
+ Self {
+ message: message.into(),
+ visibility: DiagnosticVisibility::DevOnly
+ }
+ }
+
+ /// Creates a new hint with specified visibility.
+ #[must_use]
+ pub fn with_visibility(
+ message: impl Into>,
+ visibility: DiagnosticVisibility
+ ) -> Self {
+ Self {
+ message: message.into(),
+ visibility
+ }
+ }
+
+ /// Creates a public hint visible in all environments.
+ #[must_use]
+ pub fn public(message: impl Into>) -> Self {
+ Self::with_visibility(message, DiagnosticVisibility::Public)
+ }
+
+ /// Creates an internal hint visible in Local and Staging.
+ #[must_use]
+ pub fn internal(message: impl Into>) -> Self {
+ Self::with_visibility(message, DiagnosticVisibility::Internal)
+ }
+}
+
+/// An actionable suggestion to fix an error.
+///
+/// Suggestions provide concrete steps users can take to resolve an error,
+/// optionally including a command or code snippet.
+///
+/// # Example
+///
+/// ```rust
+/// use masterror::diagnostics::{DiagnosticVisibility, Suggestion};
+///
+/// let suggestion = Suggestion {
+/// message: "Check if PostgreSQL is running".into(),
+/// command: Some("systemctl status postgresql".into()),
+/// visibility: DiagnosticVisibility::DevOnly
+/// };
+/// ```
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Suggestion {
+ /// Human-readable description of the suggestion.
+ pub message: Cow<'static, str>,
+
+ /// Optional command or code snippet.
+ ///
+ /// When present, displayed in a distinct style (monospace, highlighted)
+ /// to indicate it can be copied and executed.
+ pub command: Option>,
+
+ /// Where this suggestion should be displayed.
+ pub visibility: DiagnosticVisibility
+}
+
+impl Suggestion {
+ /// Creates a new suggestion without a command.
+ #[must_use]
+ pub fn new(message: impl Into>) -> Self {
+ Self {
+ message: message.into(),
+ command: None,
+ visibility: DiagnosticVisibility::DevOnly
+ }
+ }
+
+ /// Creates a new suggestion with a command.
+ #[must_use]
+ pub fn with_command(
+ message: impl Into>,
+ command: impl Into>
+ ) -> Self {
+ Self {
+ message: message.into(),
+ command: Some(command.into()),
+ visibility: DiagnosticVisibility::DevOnly
+ }
+ }
+
+ /// Sets the visibility for this suggestion.
+ #[must_use]
+ pub fn visibility(mut self, visibility: DiagnosticVisibility) -> Self {
+ self.visibility = visibility;
+ self
+ }
+}
+
+/// A link to documentation explaining the error.
+///
+/// Documentation links provide detailed explanations and context that
+/// don't fit in hint messages. They typically point to error catalogs,
+/// API documentation, or troubleshooting guides.
+///
+/// # Example
+///
+/// ```rust
+/// use masterror::diagnostics::{DiagnosticVisibility, DocLink};
+///
+/// let doc = DocLink {
+/// url: "https://docs.example.com/errors/E001".into(),
+/// title: Some("Connection Errors".into()),
+/// visibility: DiagnosticVisibility::Public
+/// };
+/// ```
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DocLink {
+ /// URL to documentation.
+ pub url: Cow<'static, str>,
+
+ /// Optional human-readable title for the link.
+ pub title: Option>,
+
+ /// Where this link should be displayed.
+ ///
+ /// Documentation links are typically `Public` since they help
+ /// end users understand errors.
+ pub visibility: DiagnosticVisibility
+}
+
+impl DocLink {
+ /// Creates a new documentation link with public visibility.
+ #[must_use]
+ pub fn new(url: impl Into>) -> Self {
+ Self {
+ url: url.into(),
+ title: None,
+ visibility: DiagnosticVisibility::Public
+ }
+ }
+
+ /// Creates a new documentation link with a title.
+ #[must_use]
+ pub fn with_title(
+ url: impl Into>,
+ title: impl Into>
+ ) -> Self {
+ Self {
+ url: url.into(),
+ title: Some(title.into()),
+ visibility: DiagnosticVisibility::Public
+ }
+ }
+
+ /// Sets the visibility for this link.
+ #[must_use]
+ pub fn visibility(mut self, visibility: DiagnosticVisibility) -> Self {
+ self.visibility = visibility;
+ self
+ }
+}
+
+/// Complete diagnostic information for an error.
+///
+/// This structure collects all diagnostic information associated with an
+/// error. It is stored in `Option>` to ensure zero cost
+/// when diagnostics are not used.
+///
+/// # Memory Layout
+///
+/// The structure uses `InlineVec` for hints and suggestions, which stores
+/// up to 4 elements inline without heap allocation. This optimizes for the
+/// common case of 1-2 hints/suggestions per error.
+///
+/// # Example
+///
+/// ```rust
+/// use masterror::diagnostics::{Diagnostics, DocLink, Hint, Suggestion};
+///
+/// let mut diag = Diagnostics::new();
+/// diag.hints.push(Hint::new("Check configuration"));
+/// diag.suggestions.push(Suggestion::with_command(
+/// "Restart the service",
+/// "systemctl restart myapp"
+/// ));
+/// diag.doc_link = Some(DocLink::new("https://docs.example.com/errors"));
+/// ```
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
+pub struct Diagnostics {
+ /// Contextual hints explaining the error.
+ ///
+ /// Hints provide context without necessarily offering a solution.
+ /// Multiple hints can be attached to explain different aspects.
+ pub hints: InlineVec,
+
+ /// Actionable suggestions to fix the error.
+ ///
+ /// Suggestions provide concrete steps to resolve the error.
+ /// Usually 0-2 suggestions are most helpful.
+ pub suggestions: InlineVec,
+
+ /// Link to detailed documentation.
+ ///
+ /// Only one documentation link is supported per error to avoid
+ /// overwhelming users with choices.
+ pub doc_link: Option,
+
+ /// Related error codes for cross-reference.
+ ///
+ /// Helps users discover related errors that might provide additional
+ /// context or alternative explanations.
+ pub related_codes: InlineVec>
+}
+
+impl Diagnostics {
+ /// Creates an empty diagnostics container.
+ #[must_use]
+ pub const fn new() -> Self {
+ Self {
+ hints: InlineVec::new(),
+ suggestions: InlineVec::new(),
+ doc_link: None,
+ related_codes: InlineVec::new()
+ }
+ }
+
+ /// Returns `true` if no diagnostic information is present.
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ self.hints.is_empty()
+ && self.suggestions.is_empty()
+ && self.doc_link.is_none()
+ && self.related_codes.is_empty()
+ }
+
+ /// Returns `true` if any diagnostic information has the given visibility
+ /// or higher.
+ #[must_use]
+ pub fn has_visible_content(&self, min_visibility: DiagnosticVisibility) -> bool {
+ self.hints.iter().any(|h| h.visibility >= min_visibility)
+ || self
+ .suggestions
+ .iter()
+ .any(|s| s.visibility >= min_visibility)
+ || self
+ .doc_link
+ .as_ref()
+ .is_some_and(|d| d.visibility >= min_visibility)
+ }
+
+ /// Returns an iterator over hints visible at the given level.
+ pub fn visible_hints(
+ &self,
+ min_visibility: DiagnosticVisibility
+ ) -> impl Iterator- {
+ self.hints
+ .iter()
+ .filter(move |h| h.visibility >= min_visibility)
+ }
+
+ /// Returns an iterator over suggestions visible at the given level.
+ pub fn visible_suggestions(
+ &self,
+ min_visibility: DiagnosticVisibility
+ ) -> impl Iterator
- {
+ self.suggestions
+ .iter()
+ .filter(move |s| s.visibility >= min_visibility)
+ }
+
+ /// Returns the documentation link if visible at the given level.
+ #[must_use]
+ pub fn visible_doc_link(&self, min_visibility: DiagnosticVisibility) -> Option<&DocLink> {
+ self.doc_link
+ .as_ref()
+ .filter(|d| d.visibility >= min_visibility)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn visibility_ordering() {
+ assert!(DiagnosticVisibility::DevOnly < DiagnosticVisibility::Internal);
+ assert!(DiagnosticVisibility::Internal < DiagnosticVisibility::Public);
+ }
+
+ #[test]
+ fn hint_constructors() {
+ let hint = Hint::new("test");
+ assert_eq!(hint.visibility, DiagnosticVisibility::DevOnly);
+
+ let public = Hint::public("test");
+ assert_eq!(public.visibility, DiagnosticVisibility::Public);
+
+ let internal = Hint::internal("test");
+ assert_eq!(internal.visibility, DiagnosticVisibility::Internal);
+ }
+
+ #[test]
+ fn suggestion_constructors() {
+ let suggestion = Suggestion::new("do this");
+ assert!(suggestion.command.is_none());
+
+ let with_cmd = Suggestion::with_command("do this", "some command");
+ assert_eq!(with_cmd.command.as_deref(), Some("some command"));
+ }
+
+ #[test]
+ fn doc_link_constructors() {
+ let link = DocLink::new("https://example.com");
+ assert!(link.title.is_none());
+ assert_eq!(link.visibility, DiagnosticVisibility::Public);
+
+ let titled = DocLink::with_title("https://example.com", "Example");
+ assert_eq!(titled.title.as_deref(), Some("Example"));
+ }
+
+ #[test]
+ fn diagnostics_is_empty() {
+ let diag = Diagnostics::new();
+ assert!(diag.is_empty());
+
+ let mut diag = Diagnostics::new();
+ diag.hints.push(Hint::new("test"));
+ assert!(!diag.is_empty());
+ }
+
+ #[test]
+ fn diagnostics_visibility_filtering() {
+ let mut diag = Diagnostics::new();
+ diag.hints.push(Hint::new("dev hint"));
+ diag.hints.push(Hint::internal("internal hint"));
+ diag.hints.push(Hint::public("public hint"));
+
+ // DevOnly level sees all
+ let dev_hints: Vec<_> = diag.visible_hints(DiagnosticVisibility::DevOnly).collect();
+ assert_eq!(dev_hints.len(), 3);
+
+ // Internal level sees internal + public
+ let internal_hints: Vec<_> = diag.visible_hints(DiagnosticVisibility::Internal).collect();
+ assert_eq!(internal_hints.len(), 2);
+
+ // Public level sees only public
+ let public_hints: Vec<_> = diag.visible_hints(DiagnosticVisibility::Public).collect();
+ assert_eq!(public_hints.len(), 1);
+ assert_eq!(public_hints[0].message, "public hint");
+ }
+
+ #[test]
+ fn diagnostics_has_visible_content() {
+ let mut diag = Diagnostics::new();
+ assert!(!diag.has_visible_content(DiagnosticVisibility::DevOnly));
+
+ diag.hints.push(Hint::new("dev only"));
+ assert!(diag.has_visible_content(DiagnosticVisibility::DevOnly));
+ assert!(!diag.has_visible_content(DiagnosticVisibility::Internal));
+ assert!(!diag.has_visible_content(DiagnosticVisibility::Public));
+
+ diag.suggestions
+ .push(Suggestion::new("fix it").visibility(DiagnosticVisibility::Public));
+ assert!(diag.has_visible_content(DiagnosticVisibility::Public));
+ }
+
+ #[test]
+ fn diagnostics_doc_link_visibility() {
+ let mut diag = Diagnostics::new();
+ diag.doc_link = Some(DocLink::new("https://example.com"));
+
+ assert!(
+ diag.visible_doc_link(DiagnosticVisibility::Public)
+ .is_some()
+ );
+ assert!(
+ diag.visible_doc_link(DiagnosticVisibility::Internal)
+ .is_some()
+ );
+ assert!(
+ diag.visible_doc_link(DiagnosticVisibility::DevOnly)
+ .is_some()
+ );
+
+ // Change to internal visibility
+ diag.doc_link =
+ Some(DocLink::new("https://example.com").visibility(DiagnosticVisibility::Internal));
+ assert!(
+ diag.visible_doc_link(DiagnosticVisibility::Internal)
+ .is_some()
+ );
+ assert!(
+ diag.visible_doc_link(DiagnosticVisibility::Public)
+ .is_none()
+ );
+ }
+
+ #[test]
+ fn cow_static_str() {
+ let hint = Hint::new("static string");
+ assert!(matches!(hint.message, Cow::Borrowed(_)));
+
+ let hint = Hint::new(alloc::string::String::from("owned string"));
+ assert!(matches!(hint.message, Cow::Owned(_)));
+ }
+}
diff --git a/src/app_error/inline_vec.rs b/src/app_error/inline_vec.rs
index d24e6df..6b6dbb2 100644
--- a/src/app_error/inline_vec.rs
+++ b/src/app_error/inline_vec.rs
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2025 RAprogramm
+// SPDX-FileCopyrightText: 2025-2026 RAprogramm
//
// SPDX-License-Identifier: MIT
@@ -37,12 +37,12 @@ const INLINE_CAPACITY: usize = 4;
/// assert_eq!(vec.len(), 2);
/// assert!(vec.is_inline()); // Still on stack
/// ```
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InlineVec {
storage: Storage
}
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
enum Storage {
/// Inline storage for 0-4 elements using fixed arrays.
///
@@ -533,4 +533,381 @@ mod tests {
}
assert_eq!(vec[0], 10);
}
+
+ #[test]
+ fn test_get_three_elements() {
+ let mut vec: InlineVec = InlineVec::new();
+ vec.push(1);
+ vec.push(2);
+ vec.push(3);
+ assert_eq!(vec.get(0), Some(&1));
+ assert_eq!(vec.get(1), Some(&2));
+ assert_eq!(vec.get(2), Some(&3));
+ assert_eq!(vec.get(3), None);
+ }
+
+ #[test]
+ fn test_get_four_elements() {
+ let mut vec: InlineVec = InlineVec::new();
+ vec.push(1);
+ vec.push(2);
+ vec.push(3);
+ vec.push(4);
+ assert_eq!(vec.get(0), Some(&1));
+ assert_eq!(vec.get(3), Some(&4));
+ assert_eq!(vec.get(4), None);
+ }
+
+ #[test]
+ fn test_get_heap() {
+ let mut vec: InlineVec = InlineVec::new();
+ for i in 1..=6 {
+ vec.push(i);
+ }
+ assert!(!vec.is_inline());
+ assert_eq!(vec.get(0), Some(&1));
+ assert_eq!(vec.get(5), Some(&6));
+ assert_eq!(vec.get(6), None);
+ }
+
+ #[test]
+ fn test_get_mut_three_elements() {
+ let mut vec: InlineVec = InlineVec::new();
+ vec.push(1);
+ vec.push(2);
+ vec.push(3);
+ *vec.get_mut(2).unwrap() = 30;
+ assert_eq!(vec[2], 30);
+ assert!(vec.get_mut(3).is_none());
+ }
+
+ #[test]
+ fn test_get_mut_four_elements() {
+ let mut vec: InlineVec = InlineVec::new();
+ vec.push(1);
+ vec.push(2);
+ vec.push(3);
+ vec.push(4);
+ *vec.get_mut(3).unwrap() = 40;
+ assert_eq!(vec[3], 40);
+ assert!(vec.get_mut(4).is_none());
+ }
+
+ #[test]
+ fn test_get_mut_heap() {
+ let mut vec: InlineVec = InlineVec::new();
+ for i in 1..=6 {
+ vec.push(i);
+ }
+ *vec.get_mut(5).unwrap() = 60;
+ assert_eq!(vec[5], 60);
+ assert!(vec.get_mut(6).is_none());
+ }
+
+ #[test]
+ fn test_insert_empty() {
+ let mut vec: InlineVec = InlineVec::new();
+ vec.insert(0, 42);
+ assert_eq!(&*vec, &[42]);
+ }
+
+ #[test]
+ fn test_insert_three_all_positions() {
+ let mut vec: InlineVec = InlineVec::new();
+ vec.push(1);
+ vec.push(3);
+ vec.insert(1, 2);
+ assert_eq!(&*vec, &[1, 2, 3]);
+
+ let mut vec2: InlineVec