# 工作流规范：SOP → 可执行状态机

> **当前状态**：这是待实现的发布与物化规范。数据库中已经保存一份符合该结构的
> 演示工作流，但 `WorkflowSpec` 完整 schema 校验、YAML 发布器与 `planner.py`
> 尚未落地；当前 API 不接受任意工作流上传。

工作流必须是**确定性的结构化配置**，不是让大模型自由生成命令（方案 §6.3、§8.1）。
AI 可以把自然语言 SOP 草拟成下面这份 YAML，但**必须过 schema 校验 + 人工发布**才能执行。

目标实现位置：`engine/planner.py`（物化）、`schemas.py::WorkflowSpec`（校验）
与 `workflows/fw-ab-regression.yaml`（版本化示例）。

## Schema

```yaml
key: fw-ab-regression          # 全局唯一，改 key = 新工作流
name: 固件 A/B 回归
version: 3                     # 每次发布 +1；spec_hash 变了但 version 没变 → 拒绝发布
danger_level: high             # none | low | high —— high 需要审批记录
source_sop: "客户X_固件回归SOP_v2.1（脱敏）"

params:                        # 运行时参数，带类型与默认值
  loops:      {type: int,    default: 100, min: 1, max: 10000}
  dut_serial: {type: string,  required: true}
  host:       {type: string,  required: true}
  fw_a:       {type: string,  required: true}   # firmware_artifact id
  fw_b:       {type: string,  required: true}

resources:                     # 独占锁，PREFLIGHT 阶段获取，终态释放
  - {type: dut,       ref: params.dut_serial, exclusive: true}
  - {type: test_host, ref: params.host,       exclusive: true}

policy:
  heartbeat_timeout_s: 30
  max_recovery_per_loop: 3     # 单轮循环内恢复超过 3 次 → FREEZE
  max_recovery_total: 20
  on_data_integrity_failure: freeze   # 硬编码值，写在这里只是为了显式

steps:
  - key: preflight
    type: precheck
    adapter: safety

  - key: flash-fw-a
    type: command
    adapter: nvme_cli
    template: nvme_fw_download_commit   # 只能引用已注册的签名模板
    params: {firmware: "{{ params.fw_a }}", slot: 1, action: 3}
    danger: high
    timeout_s: 300
    idempotent: false                   # 默认值，写出来是为了提醒：挂了不自动重跑
    checkpoint: true

  - key: regression-loop
    type: loop
    count: "{{ params.loops }}"
    body:
      - key: workload
        type: command
        adapter: fio
        template: fio_seq_rw
        timeout_s: 600
        retry: 1
        idempotent: true
      - key: enumeration-check
        type: device_check
        adapter: nvme_cli
        expect: {enumerated: true, link_speed_min: "8GT/s"}
      - key: integrity-check
        type: command
        adapter: shell
        template: sha256_readback
        on_fail: freeze                 # 覆盖默认处置
    checkpoint: true                    # 每轮结束落检查点

  - key: report
    type: report
    adapter: builtin
```

## 步骤类型（MVP 六种）

| type | 语义 | 备注 |
|---|---|---|
| `command` | 执行签名命令模板 | 唯一能碰危险动作的类型 |
| `device_check` | 设备探针断言（枚举/固件版本/链路速率/SMART） | 不改变设备状态 |
| `wait` | 等待固定时长或条件 | 用于温度稳定、上电间隔 |
| `loop` | 固定次数循环，body 是步骤列表 | 支持嵌套一层 |
| `precheck` | 安全门禁 | 每个工作流的第一步都应该是它 |
| `report` | 生成报告与 Evidence Bundle | 内置适配器 |

企业版才加的：`http`、`parallel`、`subflow`、`approval`、`instrument`。
这一波故意不做——`planner.py` 里遇到未知 type 直接拒绝发布，不静默跳过。

## 模板变量

只支持 `{{ params.X }}` 和 `{{ loop.index }}` 两种插值，**不支持表达式求值**。
理由：能求值就能注入。需要计算的场景写进适配器，不写进 YAML。

## 版本与 Git

工作流 YAML 进 `workflows/` 目录、进 Git。发布时控制平面记录：

```
workflows.spec       原文
workflows.spec_hash  sha256(规范化后的 spec)
workflows.version    发布号
```

`spec_hash` 变了而 `version` 没变 → 拒绝发布。理由：A/B 对比的结论必须能追溯到
**具体哪一版流程**，否则"上周跑的和这周跑的是不是同一个流程"就说不清了。

## `on_fail` 的可选处置

| 值 | 行为 |
|---|---|
| `retry`（默认，受 `retry:` 次数约束） | 重试该步骤 |
| `fail_run` | 整个 Run 判 FAIL 并结束 |
| `continue` | 记失败，继续下一步（用于非关键采集步骤） |
| `freeze` | 立刻冻结现场，停止一切覆盖性动作 |
| `recover` | 进入恢复阶梯 |

数据完整性相关的步骤**只允许** `freeze`，planner 校验时强制。
