# 领域模型：7 个核心对象 → 14 张表

方案 §5.3 列了 7 个核心领域对象。当前 SQLAlchemy 元数据包含 14 张表。
额外表用于事件溯源、恢复动作、资源锁、失败实例与审计；它们不是新的顶层产品概念，
而是核心对象的行为记录和读模型。

## 对象 → 表

| 方案对象 | 表 | 补充说明 |
|---|---|---|
| DUT | `duts` | 序列号是防错盘的第一信号 |
| Test Host | `test_hosts` + `oob_controllers` | 带外控制器独立成表：它必须能在主机死后独活 |
| Firmware Artifact | `firmware_artifacts` | 哈希 + 签名 + 适用型号，缺一不可审计 |
| Workflow | `workflows` | 存 spec 原文 + spec_hash，改一个字就是新版本 |
| Run | `runs` + `run_steps` + `run_events` + `recovery_actions` | 见下 |
| Failure Signature | `failure_signatures` + `run_failures` | 签名是聚类锚点，失败实例挂在它下面 |
| Evidence Bundle | `evidence_bundles` | manifest 记录完整率，缺字段要能查出来 |
| —（新增） | `resource_locks` | DUT/主机独占，防并发踩踏 |
| —（新增） | `audit_log` | 谁在什么时候用哪个模板做了什么 |

## Run 为什么拆成四张表

`runs` 只存**当前投影**（状态、进度、结论）。真相在 `run_events`。

```
run_events  (append-only, 唯一真相)
    │  projection
    ▼
runs / run_steps / recovery_actions  (可重建的读模型)
```

任何时刻都能靠重放 `run_events` 重建 `runs` 的状态——这是"事件溯源"落地的含义，
也是统一时间线、审计、自动复现三个功能的共同地基。

**代价**：写路径必须走 `events/recorder.py`。仓储层故意**没有**
`update_run_state()` 这种 API。想改状态？记一条事件，投影自己会跟上。

## 关键字段的设计理由

### `duts.allow_destructive` + `duts.serial`

破坏性动作（Format / Sanitize / 刷写）的三重校验：序列号匹配、
`allow_destructive=True`、设备路径不是系统盘。三个信号缺一个就拒绝执行。
误盘一次 = PoC 判定失败（方案 §9.3），所以这个字段不给 API 直接改，
只能走带审批记录的资产管理接口。

### `runs.env_fingerprint`（JSON，不可变快照）

任务开始时冻结：主板 / BIOS / OS / 内核 / 驱动 / Agent 版本 / 工具版本 / DUT 固件。
A/B 对比的前提是环境相同——固件之外任何一项变了，对比结论就不成立。
签名哈希也吃这个指纹，所以"换了台机器复现不出来"能被自动识别为不同签名。

### `runs.unattended_completion` + `runs.human_touches`

这两个字段是**销售武器**，不是技术指标。UCR（无人值守完成率）和人工触碰次数
是客户签字确认 ROI 的凭据（方案 §9.3、报告 Phase 2 现场指标）。
从第一行代码就记，不要等到要卖了才补。

### `run_events.seq`（每个 run 内单调递增）

时间戳会因为主机断电、时钟漂移、带外控制器与主机时钟不同步而乱序。
`seq` 由控制平面单点分配，保证时间线可重放。带外事件与 Agent 事件
进同一条序列——双通道观测的交叉校验靠它。

### `failure_signatures.hash`（8 要素）

```
hash(workflow_step, normalized_error_codes, log_templates, host_state,
     dut_enumeration_state, data_integrity_state, recovery_outcome,
     environment_fingerprint)
```

只用错误码会把"掉盘"和"脚本超时"归成一类，聚类就废了。
`log_templates` 是日志模板化后的结果（数字/路径/时间被替换为占位符），
不是原始日志——否则每条日志都是新签名。

### `run_failures.failure_class`

`DUT_DEFECT` / `INFRA_FAILURE` / `SCRIPT_FAILURE` / `DATA_INTEGRITY` / `UNKNOWN`。

**`INFRA_FAILURE` 必须与 `DUT_DEFECT` 分开统计**（方案 §4.3）。
把网络断了、磁盘满了算成 SSD 缺陷，客户第一周就不信任这个系统了。
基础设施误报率 <5% 是 MVP 硬指标。

## 状态字段一览

| 表 | 字段 | 取值 |
|---|---|---|
| `runs` | `state` | QUEUED / PREFLIGHT / RUNNING / RECOVERING / PAUSED / FROZEN / COMPLETED / ABORTED / REJECTED |
| `runs` | `verdict` | PASS / FAIL / INCONCLUSIVE / null |
| `run_steps` | `state` | PENDING / DISPATCHED / RUNNING / SUCCEEDED / FAILED / TIMED_OUT / SKIPPED / CANCELLED |
| `test_hosts` | `status` | ONLINE / DEGRADED / OFFLINE / UNKNOWN |
| `duts` | `status` | IDLE / IN_USE / QUARANTINED / MISSING |
| `recovery_actions` | `outcome` | RECOVERED / FAILED / ESCALATED / FROZEN |

详见 [state-machine.md](state-machine.md)。
