# OrionError 与稳定身份

本文解释当前实现里的四个概念：

- `DomainReason`
- `OrionError`
- `ErrorCode`
- `ErrorIdentityProvider`

核心结论先放在前面：

- `DomainReason` 解决“这个 reason 能不能作为 `StructError<R>` 的运行时语义载体”
- `OrionError` 解决“这个 reason 能不能低成本地同时获得显示文案、兼容数值码和稳定身份”
- `ErrorCode` 是兼容数值码
- `ErrorIdentityProvider` 才是对外稳定协议的机器主键来源

## 1. 当前 trait 约束

当前 `DomainReason` 很薄，只要求：

```rust
pub trait DomainReason: PartialEq + Display {}
```

它只负责说明：

- `StructError<R>` 的 reason 类型是显式的
- reason 本身有可显示文本

`DomainReason` 本身不包含：

- 稳定 code
- category
- exposure 决策
- 对外协议投影

## 2. `OrionError` 做了什么

推荐写法：

```rust
use derive_more::From;
use orion_error::{OrionError, UvsReason};

#[derive(Debug, Clone, PartialEq, From, OrionError)]
enum AppReason {
    #[orion_error(identity = "biz.invalid_request")]
    InvalidRequest,
    #[orion_error(transparent)]
    Uvs(UvsReason),
}
```

`OrionError` 会同时生成：

- `Display`
- `DomainReason`
- `ErrorCode`
- `ErrorIdentityProvider`

因此它是当前定义领域 reason 的推荐入口。

## 3. `ErrorCode` 与 `ErrorIdentityProvider` 的区别

### 3.1 `ErrorCode`

`ErrorCode::error_code()` 返回兼容数值码：

- `UvsReason::system_error()` 是 `201`
- `OrionError` 未显式声明 `code = ...` 时默认是 `500`

它的定位是：

- 兼容旧系统
- 兼容已有数值码测试
- 保留传统集成接口

### 3.2 `ErrorIdentityProvider`

`ErrorIdentityProvider` 提供两个稳定协议字段：

- `stable_code()`
- `error_category()`

例如：

- `sys.io_error`
- `biz.invalid_request`
- `logic.internal_invariant_broken`

它们用于：

- 稳定断言
- exposure 决策
- HTTP / CLI / log / RPC 投影
- 指标、聚合、告警、跨服务协作

如果系统要依赖稳定机器主键，应该依赖这里，而不是 `Display` 文案。

## 4. `identity` 注解规则

### 4.1 最常用写法

```rust
#[derive(Debug, Clone, PartialEq, OrionError)]
enum AppReason {
    #[orion_error(identity = "biz.order_not_found")]
    OrderNotFound,
}
```

这会产生：

- stable code：`biz.order_not_found`
- 默认显示文案：`order not found`
- category：`Biz`
- 默认兼容数值码：`500`

### 4.2 显式指定 message / code / category

```rust
#[derive(Debug, Clone, PartialEq, OrionError)]
enum AppReason {
    #[orion_error(
        message = "storage temporarily unavailable",
        code = 2000,
        identity = "sys.storage_unavailable",
        category = Sys
    )]
    StorageUnavailable,
}
```

这里：

- `message` 控制 `Display`
- `code` 控制兼容数值码
- `identity` 控制 stable code
- `category` 可显式覆盖，也可由 identity 前缀推导

### 4.3 `transparent`

```rust
#[derive(Debug, Clone, PartialEq, OrionError)]
enum AppReason {
    #[orion_error(transparent)]
    Uvs(UvsReason),
}
```

`transparent` 会把以下能力委托给内部单字段类型：

- `Display`
- `ErrorCode`
- `ErrorIdentityProvider`

这适合保留一个 `Uvs(UvsReason)` 兜底通道。

## 5. 为什么不能只靠 `Display`

`Display` 适合给人看，不适合当协议主键。

原因：

- 文案可能优化
- 文案可能国际化
- 文案可能带动态值
- 不同调用方很难稳定依赖自然语言文本

例如：

- `read config failed`
- `failed to read config`
- `读取配置失败`

这些都可能描述同一类错误，但它们不适合做稳定协议主键。

相比之下：

- `sys.io_error`
- `biz.order_not_found`

才适合作为跨边界约定。

## 6. `UvsReason` 的角色

`UvsReason` 是内置的通用错误分类，已经实现：

- `DomainReason`
- `ErrorCode`
- `ErrorIdentityProvider`

这意味着它可以直接：

- 作为 `StructError<UvsReason>` 的 reason
- 作为 `#[orion_error(transparent)]` 的底层 reason
- 进入稳定身份和协议投影路径

例如：

- `UvsReason::system_error()` -> `sys.io_error`
- `UvsReason::network_error()` -> `sys.network_error`
- `UvsReason::core_conf()` -> `conf.core_invalid`
- `UvsReason::logic_error()` -> `logic.internal_invariant_broken`

## 7. 什么时候必须提供稳定身份

下面这些情况不要只停在 `DomainReason`：

- 要使用 `identity_snapshot()`
- 要用 `assert_err_identity(...)`
- 要输出 HTTP / CLI / log / RPC 响应
- 要做统一 exposure 决策
- 要把错误接入指标、聚合、告警
- 要形成跨 crate 或跨服务的错误契约

这时 reason 应该实现 `ErrorIdentityProvider`，而最省事的做法通常就是 derive `OrionError`。

## 8. 推荐模式

推荐让领域 reason 只有少量真正稳定的业务语义，并保留一个通用兜底分支：

```rust
use derive_more::From;
use orion_error::{OrionError, UvsReason};

#[derive(Debug, Clone, PartialEq, From, OrionError)]
enum OrderReason {
    #[orion_error(identity = "biz.order_not_found")]
    OrderNotFound,
    #[orion_error(identity = "biz.insufficient_funds")]
    InsufficientFunds,
    #[orion_error(transparent)]
    Uvs(UvsReason),
}
```

这样做的好处是：

- 业务错误有稳定领域语义
- 通用系统错误不需要重复造轮子
- HTTP / RPC / log 等投影可以直接复用 `UvsReason` 的稳定身份

## 9. 设计建议

设计 stable code 时，建议遵守：

- 不把动态值写进 code
- 不把实现细节写进 code
- code 尽量表达语义，不表达调用点
- category 保持粗粒度

推荐：

- `biz.invalid_request`
- `biz.order_not_found`
- `conf.feature_invalid`
- `sys.io_error`

不推荐：

- `read_config_failed_for_user_42`
- `primary_db_timeout_us_east_1`
- `error_1`
