﻿# TCG 契约 · 四诚实 + 7 硬约束 + 用例格式

> 三方共享的唯一契约：`test-case-generation-generator`(照此写用例) / `test-case-generation-validator`(照此读、判语义) / `tools/validate.ts`(照此扫结构)。
> **用例格式 = 现有真实产出的格式**(`前置条件 / 动作 / 预期结果 / 测试点`),不另造字段;删 `scenarios.json`,证据就是「规范写法本身」活在 `test_case.md` 里。
> 朴素话:谁写谁不能自己判对;复核者只核它留下的东西、不重写一遍。
>
> **这份文件是唯一定义处,四处指同一份。** 改这里的字段或枚举,必须同步 `validate.ts` 顶部那份「代码侧镜像」正则;`generator` 与 `validator` 只「指向」本文件、不另抄一遍。三方任何一处与本文件不一致,以本文件为准。

---

## 1. 四种诚实(每条一句话 + 一个真实微例)

> 这是整套契约的「为什么」。下面 §2 的 7 条硬约束、§3 的字段格式,都是为了把这四件事**写成机器和人都能逐条核对的样子**。

| 诚实 | 一句话 | 微例(真实格式) |
|---|---|---|
| **有源** | 每条用例都对得上 SPEC 的某个场景;需求没说却补的判断,要能说出依据,不许冒充。 | `Scenario 1-1` 对到 `## 编号映射表` 里的 `SPEC-01 新建歌单`;挂一个不存在的功能 → 不合格。 |
| **周延** | 需求被「或/边界/异常」打开的每一支都要有去向:出用例(可标 `[推导]`),或写一条带原因的 `[SKIP]`;不许凭空蒸发。**持久化/重启验证只认字面**:场景显式写了「重启」才出重启用例,**没写就不加**、绝不靠「持久/应保留」语义联想自造。 | 「名称过长」→ 派一条 `输入 150 字符` 的用例;漏了就不合格。 |
| **可循** | 动作落到具体对象,把「差不多/某个页面/随便一个」逼成「就是它」,不懂需求的人也能照做。 | `点击「新建歌单」按钮` 具体 → OK;`随便点个菜单` 含糊 → 不合格。 |
| **可证** | 每个测试点(TP)都是机器能在某一刻看出真假的状态;不许「运行正常/不崩溃」这种永远成立、等于没说的话。 | `TP: 弹出 toast「歌单名称不得为空」` 可证;`功能正常` 是恒真空话,语义校验拒绝(程序只查非空)。 |

> 闭环:生成时**用例就写成可逐条核对的样子**(具体动作 + 二值 TP + 对得上场景);做不到的,**把「做不到」写成一条带原因的 `[SKIP]`**(下表 #5),不假装做到。

---

## 2. 7 条硬约束(生成约束 | 机械核)

> 注:原「成本·一次一意图」(只装必需料 + 防串味)**不是成品用例的属性、无法在成品上核**,已降为 generator 的**生成纪律**(见 `agents/...-generator.md` S2 + 防串味),不再列为硬约束。所以这里**共 7 条硬约束**,不是 8 条。

> 左列 = generator 必满足;右列 = validator / validate.ts 怎么查(只查存在/计数/二值/枚举,不做语义判断)。
> 右列分两半:`prog:` = `validate.ts` 程序能机械查的部分(纯格式/结构,**方案 A**);`validator:` = 需要读懂意思才能判的部分,归语义校验。**程序那半永远不碰语义**,这是不可越界的红线。

| # | 硬约束 | 生成约束(generator 必满足) | 机械核(validate.ts 程序 / validator 语义) |
|---|---|---|---|
| 1 | **有源·场景映射** | 每个 `### Scenario N-M` 对应 SPEC 某场景,记进 `## 编号映射表`(功能↔SPEC↔REQ);改造/推导的另记 `## 场景来源映射`(用例↔spec场景↔delta),delta 里写明推导类型、触发依据和变化点。 | prog:① `## 编号映射表` 这张表存在且非空;② 映射表里引用的每个 SPEC 编号在 `--spec` 指的 SPEC 文件里**真存在**(外键校验,挂空号即 FAIL)。**S4 机械核不核「用例编号↔映射表行」方向**——`Scenario N-M` 的编号 N 是否能在映射表找到对应行,由 S5 validator 语义核。validator:**来源相关性 + 预期忠实性**——该用例是否真测它对应的场景(非牵强挂靠)、且预期结果忠实于该场景所述结果(测对场景但断言写错也算不合格)。 |
| 2 | **周延·场景覆盖** | 每个 SPEC 场景至少出 1 条 Scenario 或 1 条 `[SKIP]`;场景内「或/边界/异常」额外派生的标 `[推导]`。 | prog:① 从 SPEC 抓出 `{SPEC 场景全集}`(每个 `## 场景N`);② 从 test_case.md 抓出 `{已覆盖场景}`(被任意 Scenario 或 SKIP 记录覆盖的);③ 两者作集合差,**差集非空(漏一个场景)即 FAIL**,报告里列出漏了哪些。附:每条 `[推导]` 用例应在 `## 场景来源映射` 或 review_notes 里有账本项,否则记疑点。validator:**语义完整性**——同义/隐含有没有漏派生(完备性红线外、标注不保证)。 |
| 3 | **可循·动作具体** | `- 动作：` 每步用具体对象名(「X」按钮 / 具体页面),不含糊;"随机选一首 / 点第N首"可用——只要该 TP 不在乎是哪一首(在乎则须先「记下选中项」供 TP 引用);依赖动态文案/纯序位的要带前提态说明。 | prog(**方案 A**):只查 `- 动作` 字段**非空**(纯格式,不读内容)。validator:**动作可循性 + 跨态稳定性**——含不含糊(随便点❌ vs 随机选一首✅)、随机/序位是否要绑定 identity、反模式锚点(纯序位/动态文案漂移);ui_elements 在则软比对(不命中只记疑点)。 |
| 4 | **可证·TP 非平凡** | 每条 Scenario 有非空 `- 预期结果：` 和 `- 测试点：`;每个 `TP-K` 是具体二值状态。 | prog(**方案 A**):只查 `- 预期结果` 与 `- 测试点` 字段**非空**(纯格式,不读内容)。validator:**oracle 充分性 + TP 非平凡**——TP 是不是恒真空话(运行正常/不崩溃/功能正常)、断言够不够强。 |
| 5 | **兜底·SKIP 规范** | 做不到的写 `[SKIP: <reason>]`,**仍要写前置条件 + 非空预期**(声明应有结果 + 交还对象)。 | prog:① `[SKIP: <reason>]` 里的 `reason` 必须 ∈ SKIP 原因枚举(见 §3 末);② SKIP 用例的 `- 预期结果` 仍非空,且不许是「无/略/N.A.」这类占位。validator:**双向兜 SKIP**——挡「本该 SKIP 却写成假用例」,也挡「本可测却赖账标 SKIP」。 |
| 6 | **去重·base 撞车不丢** | 跨场景重复:**派生用例**可丢(记账);某场景**唯一(base)用例**撞车**不丢**、标红进 review_notes。 | prog:只认**逐字精确相同**的用例 = 精确重复(派生丢+记 / base 标红保留);并复核「去重后每场景仍 ≥1 用例」。**签名构成(唯一定义,validate.ts 实现)**:`前置条件(各条件正文按序) + 动作 + 预期结果 + 测试点集合(排序比较、顺序无关)`,各段仅折叠空白、数据值原样保留;任一段不同即不判重复。**程序绝不抹平数据差异**(`100` 与 `101`、`歌单A` 与 `歌单B` 的具体值原样进签名)。validator:**「动作一样、数据不同算不算同种」(100 vs 101、歌单A vs B)由 AI 判**——数据等价归语义侧,程序不抹平。 |
| 7 | **前置·标注合法** | `- 前置条件：` 下每条 `- 条件K:` 带分诊标注。 | prog:每条 `- 条件K:` 末尾括号里的标注必须 ∈ 标注枚举 { AutoTest 自动处理 / 见前置用例 / 特殊测试数据 / 已下沉到测试步骤 };不在枚举内即 FAIL。 |

---

## 2.5 实现偏差 / 桩实现 / 待建依赖(spec 夹带「现状」账时怎么办)

按 hmos-spec-generate 的规则(Principle 6),spec **不得含任何 Android 相关/设计/代码描述**,所以 spec 正常**不带源码行号**;但它允许**状态标注**。若 spec 仍夹带 `> [偏差]` 标注、「## 与本用例相关的偏差」「## 前置待建依赖」这类**无代码行号**段落 —— 意思是「规格写了,但当前 build 是个桩 / 没落地 / 数据源恒返回空 / 不持久化」—— 按**背景知识**处理。**这是「产品没造出来」,不是「测试手段够不着」。** 关键判断:**断言本身原则上看不看得见**(不看当前 build):

- **断言本身看得见**(歌单数 1→2、新建项在不在顶部、列表里有没有这条)——哪怕当前桩进不到那个状态:**这就是一条正常用例,不写 SKIP**。照应有行为正常写(动作 + 干净「应有」预期 + 二值 TP);要什么起始状态,**在前置条件里写明白**(标注 `见前置用例` / `特殊测试数据`)。当前 build 是桩、跑了会失败 / 前置可能构造不出 —— 那是**跑的人的判断**:愿意构造就构造,不想构造就把这条删了。**TCG 不替他做这个决定、不挂 SKIP。**
- **断言本身根本看不见**(逐帧高亮、一闪而过的 loading、纯白盒埋点)——跟落没落地无关:才用 `[SKIP: 不可观测]` / `[SKIP: 白盒]`。

两条铁规矩(无论上面走哪支):

1. **预期结果只写「应有」行为,保持干净**:不写「现状……」、不写源码文件名/行号、不写「交还研发/查 DB」。这些是给人看的账,不是断言。
2. **偏差的现状(无代码行号)作为知悉项进 review_notes 非阻塞区**(给研发/复核当背景:「这块当前是桩,跑了会失败」),test_case.md 字段里一个字都不留。**源码文件名/行号(`*.ets:行号`)绝不进 test_case.md 任何字段**——白盒证据不进黑盒产物;万一 spec 异常夹带了行号,也只进 review_notes 背景。validate.ts 机械扫(`code_ref_in_field`)。

> 一句话:spec 告诉你「这块是桩」,你照**应有行为出一条正常用例**,起始状态在**前置条件**里写清楚,桩的现状当**背景**记进 review_notes —— 至于这条值不值得跑、要不要构造前置,**交给跑的人删或留**。**别把它挂成 SKIP,更别把现状/代码塞进预期。**(原 `前置不可达` 这档 SKIP 已删:它和「前置条件写明要构造什么」重复了。)

---

## 3. 用例格式(在 test_case.md 里长什么样 · 即现有真实格式)

> generator 这样写、validator 这样读、validate.ts 这样扫。**四诚实靠这套规范写法本身编码,不另加字段。**
> **全角符号是格式的一部分,不可换成半角**:字段冒号一律用全角「：」;名称/文案一律用全角书名号「」;前置标注与 TP 的步骤标记一律用全角括号（）。**动作步骤之间用 ` -> ` 分隔**(半角箭头,两侧各一个空格)。validate.ts 的正则按这些字面来扫,写错符号会扫不出 → 被判结构缺失。
> **前置用例(pre_test_case.md)是另一档格式**:它不用 `### Scenario` 字段块,而是「`## 段N: <段名>` + 一行操作链」;格式与资源复用原则见 §4。test_case.md 里标「（见前置用例）」的累积态前提,其准备序列外置到 pre_test_case.md,按 §4 写。

### 3.1 文件骨架 + 几条真实用例(摘自「新建歌单」SPEC-01,示意;app 名占位用「被测应用」)

```markdown
# 新建歌单

**说明：用例入口第一步均为打开 被测应用，执行前需先满足前置条件**

## 编号映射表                          <!-- 有源(#1):Scenario ↔ SPEC ↔ REQ -->
| 功能名称 | SPEC 编号 | REQ 编号 |
|---------|-----------|----------|
| 新建歌单 | SPEC-01   | REQ      |

## Scenario List

### Scenario 1-1: 新建歌单输入空白名称点击确定时提示名称不得为空并关闭对话框 [P0]
- 前置条件：
  - 条件1: 已安装 被测应用 并授予存储权限（AutoTest 自动处理）   <!-- #7 标注 ∈ 枚举 -->
- 动作：打开 被测应用 -> 进入歌单页面 -> 点击更多菜单 -> 点击「新建歌单」按钮 -> 输入空白名称 -> 点击确定按钮   <!-- 可循(#3):每步具体,步骤间用 ` -> ` 分隔 -->
- 预期结果：弹出 toast「歌单名称不得为空」，且新建歌单对话框关闭
- 测试点：                                                       <!-- 可证(#4):逐条二值 -->
  - TP-1: 弹出 toast「歌单名称不得为空」
  - TP-2: 新建歌单对话框关闭

### Scenario 1-2: 新建歌单输入超过最大长度名称点击确定时提示名称过长 [P0] [推导]   <!-- 周延(#2):边界派生标 [推导] -->
- 前置条件：
  - 条件1: 已安装 被测应用 并授予存储权限（AutoTest 自动处理）
- 动作：打开 被测应用 -> 进入歌单页面 -> 点击更多菜单 -> 点击「新建歌单」按钮 -> 输入长度为 150 个字符的名称 -> 点击确定按钮
- 预期结果：弹出 toast「歌单名称过长」，且新建歌单对话框关闭
- 测试点：
  - TP-1: 弹出 toast「歌单名称过长」

### Scenario 1-3: 新建有效歌单后歌单页首位显示且重启后仍在 [P0]
- 前置条件：
  - 条件1: 已安装 被测应用 并授予存储权限（AutoTest 自动处理）
- 动作：打开 被测应用 -> 进入歌单页面 -> 点击更多菜单 -> 点击「新建歌单」按钮 -> 输入有效名称「测试歌单-{随机后缀}」 -> 点击确定按钮
- 预期结果：（步骤6后）歌单页列表第一位显示新建歌单「测试歌单-{随机后缀}」；（重启应用后）该歌单仍在列表中   <!-- 本场景标题【显式写了】「重启后仍在」→ 才出重启验证;重启=冷启动(杀进程重开),非切后台/重装。场景没写「重启」就不加这一支 -->
- 测试点：
  - TP-1（步骤6后）: 歌单页列表第一位显示新建歌单「测试歌单-{随机后缀}」
  - TP-2（重启应用后）: 重启应用后该歌单仍可见
```

读这段要点(给生成者/复核者看):
- 用例头 `### Scenario N-M: <标题> [优先级] [推导]? [SKIP]?` 一行写完;`N` 对应 SPEC 编号、`M` 是该 SPEC 内的流水号。
- `（步骤N后）` / `（重启应用后）` 写在 `- 预期结果：` 的总述里、也可写在对应 `TP-K` 的标记里(形态 `TP-K（步骤N后）:`);两处都用全角括号。
- Scenario 1-3 这种持久化验证**只有场景标题里字面写了「重启后仍在」才出**;重启 = 冷启动(杀进程重开),不是切后台、不是重装。场景没写「重启」就**绝不**靠「持久/应保留」自造这一支。
- 分支若不单开、而折叠进别的用例覆盖(如一组「存在→显示实值」字段共现一次观测):**必须**在目标用例落一条断言该分支的 TP(可用合并 TP)+ 在 `## 场景来源映射` 写 `去向=fold:N-M#TP-k` 指向它;**禁只在散文写「折叠进 X」而不落 TP**(空白折叠)。详见 §3.3 末「折叠记账」。

### 3.2 一条 SKIP 记录(做不到也要留,#5 + 覆盖 #2)

```markdown
### Scenario 1-7: 点击确定瞬时 loading 指示 [P1] [SKIP: 不可观测]   <!-- 原因唯一写在用例头 -->
- 前置条件：
  - 条件1: 已安装 被测应用 并授予存储权限（AutoTest 自动处理）
- 动作：打开 被测应用 -> 进入歌单页面 -> 点击更多菜单 -> 点击「新建歌单」按钮 -> 输入有效名称 -> 点击确定按钮
- 预期结果：（应有）点击确定后瞬时出现 loading 指示；当前无法稳定观测该帧，交还人工/帧级抓取   <!-- SKIP 仍写非空「应有」预期 -->
```

读这段要点:
- SKIP 原因**只写在用例头** `[SKIP: <reason>]` 里一处,`reason` 必须 ∈ SKIP 原因枚举(见末尾)。
- SKIP 仍要写 `- 前置条件：` 和**非空**的 `- 预期结果：`——预期写「（应有）……」声明本该出现的结果,可缀一句极简交还语(人工/帧级抓取等);不许写「无/略/N.A.」。
- **预期里禁写「现状……」描述、源码文件名/行号、待建依赖清单**(见 §2.5):那是给人看的账,进 review_notes 非阻塞区当背景,不进预期。`桩/数据源恒空/不持久化` 导致跑不起来的 **不写 SKIP** —— 它是**正常用例**,起始状态在前置条件里写明,跑的人决定构造还是删(见 §2.5)。`不可观测` 只留给「断言本身看不见」的。
- SKIP 用例**不强制写 `- 测试点：`**(既然测不了);它靠用例头的 `[SKIP]` 标记被计入 §2 #2 的「已覆盖场景」,不算静默蒸发。

### 3.3 字段清单(closed vocabulary;字段标记由 validate.ts 按字面扫,**两张「黑名单」改为语义校验参照、程序不扫**——见末尾;示例字段必须全在此表)

> 这张表是字段的**唯一权威清单**:§3.1 / §3.2 出现的每个字段都在此表里,validate.ts 要扫的每个标记也都对应此表的一行。三处(本表 ↔ 例子 ↔ validate.ts 正则)逐字一致。

| 字段 | 含义/诚实 | 谁写 | 谁扫 | 硬/软 |
|---|---|---|---|---|
| `### Scenario N-M: <标题> [P0\|P1\|P2] [推导]? [SKIP: <reason>]?` | 用例头 + 优先级 + 派生/跳过标记 | generator | gen(自律)+ validator(语义核) | 硬 |
| `## 编号映射表`(功能\|SPEC\|REQ) / `## 场景来源映射`(用例\|spec场景\|delta) | 有源(#1);`[推导]` 的 delta 应写 `类型=<推导类型>; 触发=...; 变化=...`;折叠分支另带 `去向=fold:N-M#TP-k`(见下「折叠记账」) | generator | prog(存在+编号一致+SPEC 外键 + `去向=fold:` 指针真伪)+ validator(相关性、推导类型是否合适、折叠 TP 够不够格) | 硬+软 |
| `- 前置条件：` + `- 条件K: …（<标注>）` | 前置分诊(#7) | generator | prog(标注 ∈ 枚举) | 硬 |

> **映射表外键两种形式都认**(实跑校准):「SPEC 编号」列填 spec 里真实存在的外键——spec-generate 产物用 `场景X`(如「场景五」),旧式 spec 用 `SPEC-NN`(如 SPEC-01)。validate.ts 两种都做存在性核验,且 `场景X` 与周延 #2 的 `## 场景X` 同源。§3.1 例子用旧式 `SPEC-01`(SaltPlayer 风格);hometrans 的 spec 则是 `场景X`。
| `- 动作：打开 X -> … -> …` | 可循(#3) | generator | prog(非空)+ validator(含糊/可循/跨态) | 硬+软 |
| `- 预期结果：…（步骤N后）/（重启应用后）…` | 可证总述(#4) | generator | prog(非空) | 硬 |
| `- 测试点：` + `- TP-K（步骤N后）?: <具体二值>` | 可证逐条(#4) | generator | prog(非空)+ validator(恒真/充分性) | 硬+软 |

字段写法细则(给 generator;validate.ts 按这些字面扫):
- `### Scenario N-M:` 后接标题,标题后的 `[...]` 串依次可含:优先级 `[P0]`/`[P1]`/`[P2]`(必有一个)、`[推导]`(派生才有)、`[SKIP: <reason>]`(跳过才有)。
- `[推导]` 不是“边界/错误”的同义词,而是“非 base、从同一 SPEC 场景内部派生”的来源标记。`## 场景来源映射` 的 delta 写作 `类型=<下表类型>; 触发=...; 变化=...`。**分支若不单开、而是折叠进别的用例覆盖**,delta 改写 `去向=fold:N-M#TP-k`,见本节末「折叠记账(`去向=fold:`)与禁空白折叠」。

#### 推导类型枚举与处理

以下类型名称是 `[推导]` 与已裁剪分支的**闭集**。generator 按触发条件和可观察结果选择类型;validator 独立核对分类是否成立,**不得新增类型名称**。

**判定优先级(消歧)** —— 一条分支可能同时像多类;按下述规则定**唯一**类型,避免 generator 与 validator 分类分歧空烧修复预算:
- **结果有别 > 仅数据有别**:各取值产生**不同可见结果/文案/控件状态** → `条件输出/决策表`(每个结果一条,不得静默合并);各取值**行为相同、仅数据不同**(排序项、开关值、菜单项) → `参数化枚举`。两者同时像时判**更强**的 `条件输出/决策表`。
- **入口维度**:纯"多个入口进同一能力"、进入后流程与结果一致 → `多入口`;若不同入口后结果不同,升级为 `条件输出/决策表`。
- **系统中断 vs 重进**:`切后台/回前台`、`横屏/旋转`、`来电` 等系统级中断 → `中断/生命周期`;`离开并重新进入页面后状态应保留` → `持久化/重进页面`;冷启动**仅当 SPEC 明写“重启”**才用(归 `持久化/重进页面`)。

| 推导类型 | 适用条件 | 处理方式 |
|---|---|---|
| `边界/错误输入` | 空值、纯空白、最大长度、越界、非法格式 | 通常生成独立用例;动作写明具体输入值 |
| `空态/无数据` | 无条目、空列表、无搜索结果、全新安装状态 | 生成独立用例或 SKIP;若依赖执行顺序,记入 review_notes 阻塞区 |
| `多入口` | `或/或者/也可以从` 等多个入口进入同一能力 | 流程/结果不同则升 `条件输出/决策表`;相同则抽样 1-2 个入口并记录其余 |
| `条件输出/决策表` | 不同条件产生不同文案、控件状态或结果 | 每个可见结果生成一条用例,不得静默合并 |
| `参数化枚举` | 同一操作覆盖多个排序方式、开关、菜单项或数据值,**行为相同仅数据不同** | 保留代表值,在 review_notes 记录裁剪项和理由 |
| `幂等/去重` | 重复添加、重复成员、撤销后重做等不应重复生效的行为 | 固定可观察基线,断言计数或内容保持稳定 |
| `批量/数量变化` | 多选、批量添加/移除、计数从 N 变为 M | 固定选中对象,断言可观察的 N→M 变化 |
| `状态同步/跨页联动` | 一个页面的变更应反映到另一页面或弹窗 | 至少验证一个变更入口和一个展示位置 |
| `持久化/重进页面` | 离开并重新进入页面后状态应保留 | 执行离开和重进;仅当 SPEC 明写“重启”时使用冷启动 |
| `中断/生命周期` | 切后台/回前台、横屏/旋转、来电等系统级中断 | 执行中断并恢复,断言状态/播放位置/输入正确保持 |

> `不可观测/SKIP` **不是一种产出用例的推导类型,而是一个处置(disposition)**:派生点没有稳定的黑盒观察出口时,不伪造可执行用例——改写 `[SKIP]`(reason 取 §5 的 **SKIP 原因枚举**,不另立名称)或交还 review_notes。它与推导类型正交:一条派生分支可以「归某推导类型 + 被判不可观测 → 转 SKIP」。

同一类型下存在多个分支时,每个分支仍须记录具体触发和变化。未展开的分支须在 review_notes 中记录类型和裁剪理由。

#### 折叠记账(`去向=fold:`)与禁空白折叠

有些分支不单开用例、也不 SKIP,而是**把可观测结果并进另一条用例的 TP**(最典型:一组「存在→显示实值」字段共现于同一弹窗,一次观测即覆盖)。这类折叠**必须记账、必须落 TP**,不许只在散文里写一句「折叠进 X」就算数——那是**空白支票**:覆盖静默流失、谁也证伪不了。规矩五条:

**(a) `去向=` 字段进 delta**。`## 场景来源映射` 的 delta 除 `类型=; 触发=; 变化=` 外,可带一个 `去向=<枚举>`:
- `own` —— 这个分支单开成了本行用例(**默认,可省**)。
- `fold:N-M#TP-k` —— 被 Scenario N-M 的第 k 条 TP 覆盖。
- `prune:<理由>` —— 判为与某条冗余、故意丢弃(理由另记 review_notes)。
- (`skip:<原因>` **短期不作机械核**;SKIP 仍只写在用例头 `[SKIP:原因]`。)

**被折叠的分支没有自己的用例 id**,所以它的账本行 `用例` 列填 `（折叠）<分支简名>` 记号(如 `（折叠）会话标识存在`),delta 写 `去向=fold:N-M#TP-k; 触发=「<SPEC 原句>」`。validate.ts 见到 `去向=fold:` 就**决定性核** N-M 真存在、且真有第 k 条 TP(规则 `fold_target_scenario_missing` / `fold_target_tp_missing`);TP-k **语义上够不够格**覆盖该字段,归 validator。

**(b) 禁空白折叠(硬规则)**:任何「把某 SPEC 分支的可观测结果并入另一条用例」的折叠,**必须**(a)在目标用例落一条**断言该分支**的 TP,(b)在账本写 `去向=fold:N-M#TP-k` 指向它。**两者缺一即空白折叠**:指针指向不存在的用例/TP → validate.ts `cases` FAIL;指针在但 TP 没真点到该字段 → validator complete·FAIL。

**禁散文折叠字样**:`review_notes.md` / test_case.md 任何字段**不得**出现 `折叠进` / `折叠入` / `folded into` —— 折叠只准活在账本的 `去向=fold:` 行。validate.ts `md` 子命令机械扫这三个精确字样,命中即 `prose_fold_claim` FAIL。(「折叠面板/折叠列表」等正常 UI 语境不含「进/入/into」,不误伤。)

**(c) 合并 TP 出口(规定省力写法)**:一条 TP 可对**多个共现字段**做「非空/显示真实值」断言,作为「存在→显示实值」簇的省力写法——覆盖而不膨胀。规范例:
```
- TP-6（步骤4后）: SALT CORE 分区「音频会话标识」显示非0整数、「DSP浮点支持」「初始化采样率」「使用系统媒体解码器」各显示非空真实值
```
被它覆盖的每个「存在」臂,各在账本记一行 `去向=fold:N-M#TP-6`。**按分区自然拆,不设数字上界**:合并 TP 跨 ≥3 个功能分区时拆成 2-3 条(各 2-4 字段),保诊断性(下游按 TP 粒度报告,合取失败要能定位坏字段)。拆不拆是**生成规范**,由 validator 判(跨 ≥3 分区 → repair 拆),validate.ts **不核字段数**。

**(d) `own` 行不变量**:`## 场景来源映射` 里 `用例` 列**凡非 `（折叠）` 前缀,必须是真实存在的 Scenario id**(`own` 默认可省仍成立)。防把折叠行错标成 own。

**(e) 边界声明(老实话,别误读为全量覆盖)**:本机制只核**已写进账本的** `fold` 指针是否指向真实存在的 TP;**不核 SPEC 是否每个分支都进了账本**(那需要上游分支 id,属中期)。即:能挡「声称折叠却查无 TP」,挡不住「某分支压根没记账」——后者归 validator 尽力标「不保证」+ 中期分支 id。
- `- 条件K:` 的 K 从 1 起编号;标注写在该行**末尾的全角括号** `（<标注>）` 里。**该标注全角括号必须是整行最后一个字符,其后不得再接任何文字/分号/第二个括号**——validate 取「行末最后一个全角括号」当标注,行尾若再缀并列说明(如 `（见前置用例）；…由动作现建（fresh）`)会被误判为非法或缺失标注。一条前置只放一个 `（<标注>）` 于行尾;补充说明并入括号前的正文,或移到 review_notes。
- `- 动作：` 一行写完,步骤之间用 ` -> `(半角箭头两侧各一空格)分隔。
- `- TP-K` 的 K 从 1 起编号;若该 TP 限定在某步骤后或重启后判定,用 `TP-K（步骤N后）:` 这种带全角括号的形态,括号可省(不限定时直接 `TP-K:`)。

- **标注枚举(#7)**：`AutoTest 自动处理` / `见前置用例` / `特殊测试数据` / `已下沉到测试步骤`
- **SKIP 原因枚举(#5)**：`跨应用` / `不可观测` / `白盒` / `故障注入` / `上下文超窗` / `输入缺失`
  - **没有「前置不可达」**(已删,见 §2.5):需要构造起始状态的(含桩/待建依赖挡住的),不写 SKIP —— 写成**正常用例**,起始状态在前置条件里写明白(标注 `见前置用例` / `特殊测试数据`),跑的人决定构造还是删。
  - `不可观测` 只给「断言本身根本看不见」(逐帧/瞬时/白盒出口);不是「当前 build 进不到那个状态」。
- **源码引用(`*.ets:行号` 等)禁入任何字段(§2.5)**:白盒证据不进黑盒用例;现状与代码引用作背景进 review_notes。validate.ts 机械扫 `code_ref_in_field`。
- **恒真黑名单(#4,语义校验参照·非程序扫)**：`运行正常` / `不崩溃` / `无异常` / `正常显示` / `功能正常` —— 给 validator 当反例判 TP 恒不恒真;**方案 A 后程序不扫**(程序只查 TP 非空)。
- **疑似含糊词(#3,语义校验参照·非程序扫)**：`随便` / `差不多` / `某个` / `任一` 等 —— 给 validator 结合 TP 判可循性,**非自动拒**:"随机选一首 / 点第N首"在 TP 不在乎 identity 时合法(见 §2 #3)。**方案 A 后程序不扫**。

> 为什么这两张黑名单不进程序:照词表硬判会误杀"随机选一首 / 点第N首"这类合法采样——是否合法取决于「该 TP 在不在乎是哪一首」,得读懂语义。所以恒真/含糊整体移交 validator,程序只留「非空 + 结构 + 枚举 + 外键 + 计数」这些不用读懂意思就能查的部分(方案 A)。

## 4. 前置用例段体格式 + 资源复用原则

> `pre_test_case.md` 与 test_case.md 不同档:它不是 `### Scenario` 字段块,而是「段 + 单行操作链」。这一节是 pre_test_case.md 的**唯一权威格式定义**;generator 写它、hmos-integration-test skill 切它、validator 读它(输入 `pre-test-case-path`)、validate.ts 的 `md` 子命令扫它,四处指这一节。改这里必须同步 validate.ts 顶部 `PRE_SEG_HEADER` 旁的镜像正则(见 §4 末「代码镜像」提示)。

### 前置用例段体格式(pre_test_case.md 的唯一权威格式)

前置用例(`pre_test_case.md`)用于把 test_case.md 中标注「（见前置用例）」的**应用内累积态前提**外置成一套可先行执行的准备序列。每次测试前会重新卸载并重装 HAP,应用处于**全新安装**状态(无歌曲、无歌单、空库)。

下游 `hmos-integration-test` skill 按段切片注入 `testcases.json`,由 `batch_runner.js` **按段逐条执行**;单条 case 执行超时 `CASE_TIMEOUT_MS = 10*60*1000`(10 分钟)。段间应用会 force-stop 重启,但**数据持久**(写入持久存储的歌曲/歌单/成员关系跨段保留)。因此前置用例必须按语义阶段拆成多段、单段不超时。

**段体格式(逐字遵守)**:

```markdown
## 段1: <段名>
打开 {app} -> 操作1 -> 操作2 -> ... -> 期望结果：<UI 可见状态>

## 段2: <段名>
打开 {app} -> ... -> 期望结果：<UI 可见状态>
```

**硬规则**:

1. 每段 = **恰好两行**:第一行 `## 段N: <段名>`(N 从 1 起、按执行顺序);紧跟一行**单行操作链**。
2. 操作链**必须以「打开」起步**(每段是独立 AutoTest 任务,段间 app 被 force-stop,须自行重启 app)。
3. 操作链**单行**,步骤之间用 ` -> ` 连接(半角箭头,两侧各一个空格——与 contract §3 动作链同一约定)。
4. 操作链**以行内全角「期望结果：」收尾**,冒号后只写**本段产生的 UI 可观测状态**(歌曲页里有哪几首、歌单列表里有哪几条、某歌单歌曲数为几)。
5. 每段步骤数 **≤ 15**(含「打开」与「期望结果：」),保证单段执行 ≤ 5min;超 15 步拆成下一段。
6. **段名不含「前置」字样**(避开 validate 对「前置条件」字面的禁用),简洁体现产出(段名可带测试用途、给人看):「扫描 N 首测试歌曲」「创建基线歌单」「给基线歌单灌入成员」。
7. 歌单名、歌曲名一律用**具体且中性**的名称(歌单「测试歌单1」、歌曲「歌曲A」),不写「某个歌单 / 任一歌曲」;且**不把测试用途/角色编进数据名**(见 §4「资源命名口径」)。
8. **禁** bullet 字段:不许出现 `- 用途：`、`- 应有起始态：`、`- 准备步骤：`、`- 准备方式：`、`- 达成态：`、`- 适用：` 等任何 `- 字段：` 形态的行。段体只有「段标题行 + 单行操作链」两类行。
9. **禁** frontmatter(开头 `---` 块)、**禁**「## 说明」段、**禁**在段体里写执行顺序例外/特殊说明——这些写进 `review_notes.md` 的**阻塞区**(单一人工伴随件,不另起 manual-intervention.md)。
10. 全角符号是格式的一部分:书名号「」、全角冒号「：」与 contract §3 一致。
11. 若只需 1 段,仍按 `## 段1:` 输出,保持解析一致。

**用途映射不进段体**:某段被哪些用例引用(用途/映射)如需保留,放 `review_notes.md` 的非阻塞区,**不进 pre_test_case.md 段体**。

### 前置操作链生成

`pre_test_case.md` 根据当前 SPEC、适用参考资料与 UI 信息组装准备序列。只写这些资料中能得到的操作,不补充没有出现的弹窗、页面或中间步骤。每段按以下规则生成:

1. **取材顺序**:优先使用 SPEC 中明写的操作、适用于当前 App/版本的 references-dir 资料,以及当前页的 `ui_elements.elements[]`。不从其他 App 类推,不使用硬编码的通用操作模板。
2. **页面内对齐**:使用 ui_elements 元素时维护 `current_page`;同名元素只能在它所属的页面上作为操作依据。存在匹配 `flows[]` 边时,按该边进入目标页。
3. **按现有信息书写**:元素有匹配 outgoing flow 时继续写目标页;没有时不写任何未出现的后续操作。
4. **段尾目标态来自前置需求**:行内 `期望结果：` 写消费用例的累积态前提/SPEC 要求的可见目标状态;不为了证明该结果而反向增加中间 UI 步骤。
5. **UI 信息不影响前置分诊**:页面/flow/结果捕获不存在时,普通应用内累积态仍标为 `（见前置用例）` 并生成操作链。只有应用外文件、超大批量预置等真正需外部构造的数据才标 `（特殊测试数据）`。

### 前置资源复用原则(三类分诊)

**核心目标**:前置用例不是「逐用例一段」,而是把**非破坏性、可累积、可被多个用例只读引用**的前置态收敛成一套**累积式共享夹具**;只有会污染计数的写操作才各自隔离。段越少,自测越快、越稳。

> **资源命名口径(铁规矩):会被真机输入的数据名一律中性。** 歌单/歌曲这种**会被当成真实数据输入 app、并展示给执行人**的名字,只用**中性、可区分**的名:歌单用「测试歌单1」「测试歌单2」…,歌曲用「歌曲A」「歌曲B」…。**绝不把测试用途/角色/状态/进入方式编进数据名**——禁「去重歌单」「添加目标歌单」「长按目标歌单」「基线只读歌单」「空基线歌单」这类(它们会被真机原样输入、给人看,像内部标签、让人误解,复用时还会变假)。**测试用途只写在 pre_test_case 段名与用例描述里**(给人看的上下文、不被输入):段名可写「创建去重起点歌单」,但段里实际建的歌单名是中性的「测试歌单N」;用例的「(见前置用例)」按这个中性名逐字引用。下文各类别里出现的「基线只读歌单 / 空基线歌单 / 去重起点」等都是**角色叫法**,落到数据名时一律换成中性名。

> **跨批同名归一由 S6 负责。** generator 逐批累计、批 2+ 不重读批 1,不同批可能各自用同一中性名却内容不同(如两批都用「测试歌单1」成员不同);段是独立 AutoTest 任务、持久化数据会带过,**同名不同内容会在真机上互相污染**。故 generator 只需保证**本批内**命名中性可区分;跨批同名冲突由 S6 探测改名并同步 test_case.md 逐字引用(见 SKILL.md S6 步骤 3「全局 fixture 命名归一」)。

#### 类别一 · 非破坏性资源 → 累积式共享夹具(尽量复用)

判据:该前置态是**只读引用**、或**一次建成后多用例共享**、且后续用例**不会改变它的计数/成员**。典型:

- **扫描入库的歌曲**(歌曲库一旦扫好,所有用例只读引用,不会有人删它)。
- **基线只读歌单**(中性名如「测试歌单1」含固定 N 首,被多条「读取内容 / 排序 / 多选浏览」用例只读引用;「基线只读」是角色叫法、不是数据名)。
- **空基线歌单**(供「空内容页」一类只读用例引用)。

做法:把这些建成一套**按序累积**的共享夹具——段1 扫歌、段2 建好全部基线歌单、段3 给只读歌单灌满成员;段间 force-stop 重启但**数据持久**,后段在前段的累积态上继续。多个用例共享同一套夹具,**不为每个用例单开一段**。

> 关键:**只有当一个用例不改变该夹具的计数/成员时,它才能复用该夹具。** 一旦某用例会往某基线歌单(如「测试歌单1」)加歌或删歌,它就**不能**复用该歌单(会污染后续读它的用例),改走类别二。

> **只读基线按「状态」去重、不按「角色」去重(防重复造夹具)**:同一个只读态(空歌单 / 含 N 首的歌单)在共享夹具里**只建一份**;另一条只读用例需要同样的态时,**只读复用已有那一份、不再新建第二个**。判断只看「态」、不看它原本给哪类用例用——一个一直空着的基线歌单,既能当「排序基线」也能当「空内容页基线」,别因角色叫法不同就又建一个空歌单(这正是「按角色造资源」的浪费)。**安全前提(必须全满足才可复用)**:被复用的是**类别一只读基线**,且**全程没有任何用例会改它的计数/成员**——类别二已保证 mutation 永远走 fresh、绝不碰基线,故基线终身只读,这条复用蹭的就是这个不变量,无需对后续批次做额外分析。**红线**:**绝不复用任何会被某用例改动的歌单**(那是类别二的 fresh 歌单);**拿不准某歌单后续会不会被改 → 当它会被改,按类别二单建 fresh**(宁可多建,不可污染)。

> **断言写法(复用夹具的代价 → 用相对数字)**:复用共享夹具的用例,凡涉及总数/计数的断言**一律用相对数字、不锁绝对总数**——夹具会建好几个歌单,「列表恰好 N 个歌单」这种绝对总数会被污染、不稳定。改写成相对形态:**相对位置**(「歌单乙在歌单甲之上」即可证明倒序,不必数总数)、或**记录基线再断言迁移量**(动作里加「记下当前歌单数 N」步 → 断言「N→N+1」)。这与 §3.1「基线 N 必须确定且可观测:优先用前置固定的值,否则在动作里加『查看并记录当前数量』步」一脉。绝对总数只在**隔离环境**(为某用例单开、不与共享夹具共存的独占夹具)里才写。

#### 类别二 · 会改计数/会删除的 mutation 用例 → fresh 命名歌单(不复用)

判据:用例的**断言本身**是一次写库迁移(歌单歌曲数 `0→1`、`0→3`、`3→2`、再次添加保持 `1`、新建后数量 `N→N+1`、取消后保持 `N`)。这类用例会改变目标歌单的计数。

做法:**每条这种用例用它自己的 fresh 中性命名歌单**(随用例新建、名字唯一**且中性**,如「测试歌单1」「测试歌单2」「测试歌单3」——**用途靠段名区分、不进数据名**,见上「资源命名口径」),**不去复用共享夹具的基线歌单**。fresh 歌单怎么来:

- 若用例动作本身就含「新建歌单」(场景六类、对话框内新建)→ fresh 歌单**在用例动作里现建**,前置只需保证「已扫描歌曲」这一非破坏性夹具,**不为它单开前置段**。
- 若用例需要一个「已含 K 首的歌单作为 mutation 起点」(去重起点歌曲数=1、移除起点歌曲数=3)→ 给它**单独建一个 fresh 命名歌单并灌好起始成员**,作为独立前置段,**不与基线只读歌单合用**。

> 为什么不复用:mutation 用例若复用基线只读歌单,会把基线的计数改脏,后续只读引用基线的用例(类别一)就会看到错的数,造成**计数污染 + 用例间隐式依赖**。fresh 歌单天然隔离:每条 mutation 用自己的歌单,谁也不踩谁。

#### 类别三 · 真冲突态 / 空态 → 不进 pre_test_case,写执行顺序例外

判据:该前置态与共享夹具**互斥不可共存**(典型:「空库/无任何歌单」的空态用例,要求库里**一个歌单都没有**——这与「已建好基线歌单」的夹具直接冲突)。

做法:

- **空态用例**(歌单列表空态、空库)→ **不单开「清空所有歌单」段**。理由:**全新安装本就是空库**,空态用例在「未执行任何前置段、刚装好」时天然满足。把它的执行顺序例外写进 `review_notes.md` **阻塞区**的「## 特殊执行顺序」小节:「Scenario X-Y(空态)须在执行任何前置段之前、于全新安装态运行」。**绝不**在 pre_test_case 里塞一段「逐个删除歌单直到为空」(那是反向破坏,既慢又脆)。
- 任何「某用例需在前置序列之前/之中的特定时点运行」的例外,同样写进 `review_notes.md` **阻塞区**(单一人工伴随件,不另起 manual-intervention.md),**不进 pre_test_case.md**。

#### 收敛后的产物形态

- `pre_test_case.md`:类别一的累积式共享夹具(少数几段)+ 类别二中「需预置起始成员的 fresh 歌单」的独立段。
- `review_notes.md` **阻塞区**:类别三的执行顺序例外(空态用例须在全新安装态先跑)。**(单一人工伴随件,不另起 manual-intervention.md)**
- `review_notes.md` **非阻塞区**:段↔用例的用途映射(哪段被哪些用例引用)。
- mutation 用例在 `test_case.md` 里**用自己动作现建 fresh 歌单**或引用「类别二独立段」的 fresh 歌单名。

> **代码镜像**:本节「每段首行以『打开』起步 + 行内含全角『期望结果：』+ 单行 + 步骤间 ` -> `」由 `tools/validate.ts` 的 `check_pre_testcase_md` 机械核(fail rule `pre_seg_bad_format`);改本节规则必须同步该函数与顶部 `PRE_SEG_*` 正则。

---

> **零 CS 术语守则(全 skeleton 适用)**:不用学术黑话——禁 certifying generator / proof-carrying / Hoare / ⊕ / ⊥ / where-provenance / demand-paging / TCB / IPOG;已统一改写:`见证/证书`→「证据」、`TypedGap`→「SKIP 记录」、`认证式/certifying`→「生成与复核分离」、validate.ts 子命令 `witness`→`cases`。用「前置条件/动作/预期结果/测试点」这套真实字段,且四处(契约 / generator / validator / validate.ts)逐字一致。
