# Product Mockup Conventions（Path A — Figma）

> 任何 AI 工具在 TVU 产品 Figma 文件里画 UX mockup 时**必须遵循的硬约束**。
> 项目级真源；不要在对话或工具私有 memory 里复刻规则。
>
> **作用域**：在 TVU 产品 Figma 文件（如 `Micro-Apps-20250923` 等）画产品页面 mockup。**不**作用于设计系统库本身的迭代。

---

## 🤖 AI 读取指引（按需加载，避免全量吞）

本文件 2600+ 行，**不要起手就全量 Read**。按下表只读起手必读段，具体 M-rule jump to 时再读：

| 起手必读（任务前先吃这些） | 行段 |
|---|---|
| 本文件顶部 intro + §作用域 + §必读链路 + §真源 / 优先级 | §0–§2 |
| §Convention Priority Hierarchy + §Task Entry Modes + §Migrated Rules Pointer Index | §3–§5 |
| **§M48 Startup Rule-Coverage Self-Check（起手第一产出物，所有任务）** — 先列「本任务命中的 locked spec + active M-rule」清单再开建 | [`design-process.md` §M48](./design-process.md) |
| **§M49 Design Quality Contract（设计质量合同，所有产品设计任务）** — 先列 Baseline / Delta / Semantic / Feedback UI Inventory / UX / Simplicity / Content，再生成 deliverable | [`design-process.md` §M49](./design-process.md) |
| §M0 Phase 0 Element-to-Component Mapping（**所有任务前置**） | §M0 |
| **§M-DISCIPLINE Rule Execution（起手协议后必跑，所有任务）** — TodoWrite 把「读到的规则」物化成 checklist，防"读了不执行" | §M-DISCIPLINE |
| **§M-LIFECYCLE Mockup Edit Lifecycle Gate（任何 create/update/delete 任务）** — 操作前(I5/I6 probe)·后(conformance 机检)·走查 三时点必过 | §M-LIFECYCLE |

| 触发后再读（按场景 jump，不预读） | 触发条件 |
|---|---|
| §M1 Top bar 公共基建 | 涉及产品顶栏时 |
| §M-COLOR (§C1/§C2/§C3) | 要填颜色 / icon fill / state color 时 |
| §M-INTEGRITY (§I1/§I2/§I3/§I4/§I5/§I6) | **任何编辑 confirmed 后（M-DISCIPLINE.SYNC）** / mockup 完成自检 / sibling 布局 / section 重叠 / children wrap / element parent 归属 / 替换既有节点前 diff(I5) / **create·move·place 前 probe 目标位防叠加(I6)** 时 |
| [`figma-technical-reference.md`](./figma-technical-reference.md) **Q15**（SECTION 子节点 x/y = section-relative 非画布绝对；`child_abs = section.x + child.x`，赋目标坐标前先减 section 原点、落点必 probe `absoluteBoundingBox` 验证，禁信 `.x/.y`）| **往 Section 内 create / move / 定位任何节点 / 设其坐标**时（高频复发坑，2026-06-04 立后已多次重踩）|
| §M23 + §M23.6 + §M33 | 要画 UX 交付注释 / 流程图 / annotation 字符串时 |
| §M23.10 + §M23.11 | 起草 UX 交付卡（Why / Changes / Data Contract / Interaction / Acceptance）时 |
| §M23 〈canonical UX 卡结构〉（Why/Changes/Data Contract/Interaction/Acceptance + Section Header 正向模板）| **起草 / 更新 UX 交付卡结构**时（含"写/更新 UX 交付说明"措辞）—— 禁自创临时段 |
| §M23.14 双语堆叠行距（组内紧 / 组间松 / 段间最松 三档差异化 lineHeight + itemSpacing）| **UX 卡 / PRD 卡正文出现 ZH 译文堆在 EN 行下方（A-下 / A-逐行 / 多语堆叠）时**——统一行距会让卡拥挤、EN/ZH 不成组 |
| §M23.15 卡内模块段名层级（section heading = cyan `#33A4FD` + Medium 大号，与正文拉开）| **起草 / 更新 PRD 卡 · UX 交付卡、卡内含模块段名（Source/Background/Why/Changes…）时** |
| §M23.16 规则优先于范本（防继承 pre-规则范本债务）| **开建 PRD / UX 卡前照抄最近同类范本时**——范本若 pre-规则须先用规则真源校验，勿照抄 lineHeight / itemSpacing / 层级 |
| §M23.12 + §M23.13（卡 placement 紧邻 mockup + sizing 竖向 Auto Layout hug 高度 + 标准宽）| **放置 / 设定 UX·PRD 卡的位置与尺寸**时 |
| §M23.7 + §M23.9（state-label = what+why+invariants + state-preview 元素放产品 frame 外、连线指回触发元素）| **写状态标注 / 把边界态预览元素放到产品 frame 外**时 |
| §M23.8（Jira requirement annotation 必带 setRangeHyperlink）+ §M23.8.1（PRD 需求来源段列全 ticket + 全部可点击 + Slack 外链，create/update 都遵守）| **加 Jira annotation / FB-xxxx 编号 / 写改 PRD 需求来源段**时 |
| [`design-process.md`](./design-process.md) Pre-Phase 0 **Step B**（PRD 6 段固定结构：需求来源 / 背景 / 现状 / 功能需求 / 验收 / 优先级排期）| **起草 / 更新 PRD** 时（含"按设计系统更新 PRD"这类局部更新措辞）|
| §M24 | "已有 code 还原效果图"场景前置 |
| §M29 | 涉及表格 cell（copyable / empty placeholder）时 |
| §M31 | 决定 Auto Layout / Slot / Boolean / Absolute 时 |
| §M32 (含 M30 icon 扩展) | 要 instance 任何 product UI 元素前 |
| §M32.2 自建产品 UI 件起手三件套 | **自建 自定义卡 / 浮层 / 面板（非库直用 instance）起手**时 |
| §M35 affordance 搜索 | 要找 chevron / sort / close 等"基本语义元素"前 |
| §M43 + §M43.2 | 涉及受限容器（cell / chip / tag）+ 调列宽 / 改 cell 内容时 |
| §M46 | 迭代既有 mockup（clone 上期 page 改本期）起手前必读 |
| §M47 | 改任何 text 节点的 fontName 前 |
| §M49.1 库样式/变量 Fidelity + §C5 语义色优先 | 建节点要绑任何视觉属性 token（颜色 / 间距 / 圆角 / 字体 / 阴影）时 |
| §M49.2 + §M49.3 + §M49.4（字段级 parity 表 / 反馈控件同源 inventory / 卡片强调预算）| **重构·拆分组件 · 出现 ≥2 反馈类控件 · 评估卡片强调预算·视觉权重**时 |
| §M-FONT | 设 / 改 text 节点 fontSize（须落 textStyle grid）时 |
| §M-TXT-ICON-AUDIT | 放任何 icon / 文字符号（防 Unicode glyph 充数）时 |
| §M-LIBRARY-HYGIENE | 新建 / 命名 component（`_` / `.` 前缀语义）时 |
| §M38 | 做 selected / active row·item 指示（3-layer rail）时 |
| §M41 | 做删除 / 破坏性确认弹窗时 |
| §M42 + §M42.2 + §M42.3 | clone / 复制 节点或 frame 时（§M42.3：迭代既有物**优先就地改**、禁旁路克隆搭样板制造重复集）|
| §M44 | 写 UI label / 字段名（防暴露后端名）时 |
| §M45 | 建 / 命名 Figma page 时 |
| §M36 / §M37 | 同一组合跨 frame·page 重复 ≥2（M36 promote 提案）/ 有参考图·截图输入（M37 probe）时 |
| §M39 / §M40 | 抽组件 variant 文本属性（M39）/ 用 #N 序号 vs 语义命名（M40）时 |
| §legacy stub M27 / M28 / M30 / M34 | 仅为外部链接兼容；优先读 umbrella |

**红线**：跳过"起手必读段"直接进 M-rule = 协议违反（漏 M0 Phase 0 是历史最高频回归源）。

---

## 必读链路（起手按顺序读）

| 文件 | 内容 | 必读时机 |
|---|---|---|
| [`design-process.md`](./design-process.md) | 通用 process 规则（M22 / Pre-Phase 0 / Phase 0 / M11 / M14 / M15 / M16 / M21 / M6）| **所有任务** |
| [`domain-tvu.md`](./domain-tvu.md) | TVU 业务规则（M3 / M4 / M5 / M7 / M8 / M9）| **所有任务** |
| [`figma-technical-reference.md`](./figma-technical-reference.md) | Figma API quirks（Q1-Q4）| Path A 实现时 |
| [`tools/figma-quirks.md`](./tools/figma-quirks.md) | Figma quirks 快查表 | Path A 实现时 |
| 本文件 | Path A 专属：Figma 真源 / 组件优先级 / M0 / M1 / M10 / M23 | **所有 Path A 任务** |

---

## 真源 / 优先级（先读完这段再动手）

### Figma 库源文件

| 维度 | 值 |
|---|---|
| TVU 设计系统库源文件 fileKey | `YbsPRUVmNdsbN40NNwh1Gn` |
| Published library 名 | `TVU UX Design System` |
| Library key | `lk-057f6ba0f771bfa7f63a6a197502999462c2974f995488b381708ca5faadb7f9f6675e04aa442b3a5ffb31732150a7f5aa8ca3102073ab210edc684022fc6c21` |
| 源文件登记位置 | [`docs/site-review-manifest.json`](../site-review-manifest.json) `figmaFileKey` 字段 |
| 组件名 → code 映射 | [`figma-data/figma-to-code-mapping.json`](../../figma-data/figma-to-code-mapping.json) |

**任何 mockup 任务起步必须先 `cat docs/site-review-manifest.json | jq .figmaFileKey` 拿到真源 key，再做 Figma 库探索**——不要靠扫产品文件 instance 反推库归属。

### 组件取数优先级（默认顺序）

**0. 第 0 步必查：[Figma Component Catalog](./figma-component-catalog.md)**
任何 mockup 任务起手第一件事是 grep 这个 catalog。它已 bootstrap 了库的全部 49 个 component_set + 647 icon 的骨架，**已实证过的组件还附带 Primary use / Extension scenarios / Don't use**。70% 的"library 缺件/找不到"判断在这里就有答案。

1. **catalog 没命中 → published `TVU UX Design System` library**（用 library key 过滤搜索 + import sample 看 variants，结果回填 catalog）
2. **库里也没有 → file-local 组件集**（产品文件内的 component_set，少数特殊场景用，如 `APP Icons`）
3. **最后：自画 + 标 🟡 入库候选**（必须前两条都验证后无果，且把候选写回 catalog 让下次免坑）

**默认假设：library 已覆盖大多数通用组件**（Button / Input / Badge / Tooltip / Top bar / Notification / Switch / Tab / Drop down List / Form Item / Pagination / Slider 等——具体清单见 catalog）。除非实证证明无对应物，否则不允许跳到第 2 / 第 3 条。

### 库归属验证机制

- ❌ 错误做法：扫某个既有 page 的 instance 来选 key——产品文件里可能同时存在多代库（旧 `TVU UX Library` + 新 `TVU UX Design System`），盲选会撞旧库。
- ✅ 正确做法：`search_design_system` 时通过 `includeLibraryKeys: ["lk-057f6ba0..."]` 显式过滤；返回结果中检查 `libraryName: "TVU UX Design System"`。

---

## Convention Priority Hierarchy

→ [`design-process.md §Convention Priority Hierarchy`](./design-process.md#convention-priority-hierarchy)（完整优先级表 P0-P3 + 冲突解决规则）

---

## Task Entry Modes

AI 接到任务后**先 classify** 再决定走哪条路径：

| ID | 用户表达 | 流程 |
|---|---|---|
| **US-1** Greenfield 产品 | "基于 TVU 设计一个 X 产品 mockup，功能大概 ABC..." | Pre-Phase 0 → M0 → 画 frame |
| **US-2** Greenfield 单 frame | "基于 TVU 给 X 产品画 dashboard frame" | Pre-Phase 0（单 frame 版）→ M0 → 画 |
| **US-3** Existing product 增/改/删 | "MicroApps Console mockup 加/改/删任意 frame 或元素" | Read handoff → Pre-Phase 0（仅变化部分）→ M0 → 画（仅 update 变化部分）|
| **US-4** Figma frame → code mirror | "把这个 frame 翻成代码" | **不归本 conventions**（走 [`code-conventions.md`](./code-conventions.md) US-4 路径）|
| **US-5** 小调整（1-2 element）| "改下按钮位置" | 跳过 Pre-Phase 0 + M0 → 直接改 |
| **US-6** Review / Audit | "帮我审 X frame 是否符合 TVU 规范" | 跳过 Pre-Phase 0 + M0 → 对照规则逐条审 → 产 audit report |
| **US-7** Redesign legacy product | "给 X 老产品出 redesign 提案 / 重新设计 X" | 见下方 US-7 子流程（审 → gap → 提案 → 用户挑路径 → 才进 US-1/US-3） |

Classification 不确定时 → AI **主动问用户**哪种入口。

### US-7 子流程 — Redesign Existing Legacy Product

**入口条件**：用户对**已存在的产品**（线上 / 老 Figma / 老代码）要出 **redesign 提案**——还**不知道改什么**，需先审完再决定路径。不同于 US-3（已知改什么、直接动 Figma）和 US-6（只审、不出 redesign 方向）。

**4 阶段，强制顺序，每阶段 STOP 等用户拍板：**

#### Phase A — 输入收集

用户至少提供一项：截图 / 线上 URL / 源码路径。同时拿到：产品名 + 目标受众 + 重设计动机（业务驱动 / 视觉债 / DS 对齐 / 功能升级）。动机不清 → STOP 问用户，不能凭空假设。

#### Phase B — 现状审计

复用 **US-6 规则 + M30-M36 library-first lookup + M-TXT-ICON-AUDIT**，不重新发明。产出 3 层清单：

| 层 | 拆解粒度 | 必标字段 |
|---|---|---|
| 页面 | route / view 级 | 用途、目标用户、主要交互 |
| Section | 页面内功能区块 | 用途、含哪些 component |
| Component | 原子元素 | 当前实现方式 + 是否已有 TVU canonical 对应（grep `src/canonical/` + `figma-component-catalog.md`）+ 用到的图标（核 `dist/icons/svg/`） |

#### Phase C — Gap 分析

按 component 输出三类清单：

- ✅ **可直接换 TVU canonical** — component key + variant 直接列出
- ⚠️ **TVU 已有近似但需扩 variant** — 触发 backlog entry 提议（grep 全部已用 ID 挑最大 +1），不直接改 canonical
- ❌ **TVU 没有、需新增 pattern** — 走 AGENTS.md §reusable pattern 入库流程，**用户拍板**才进库；提案阶段先标记不实做

**跨产品复用扫描**（US-7 专属，US-6 不做）：如果 redesign 多个产品并行，⚠️ / ❌ 清单需跨产品比对，相同 pattern 合并为一个 backlog entry，避免重复入库。

#### Phase D — Redesign 提案

按页面产出 before/after 对照 + 改动清单 + 风险点。**硬约束：**

- **不动 Figma 库文件**（[figma-write-scope](memory)）
- **不写代码**
- 产物落 `docs/internal/_prompts/redesign-<product>-<date>.md` 或对话，不污染 canonical 真源
- before/after 仅文字描述 + 引用现有 Figma component key，不画新 mockup
- 等用户挑路径后**才进 US-1**（greenfield 新画）**或 US-3**（增量改既有 mockup）

**违例 signal（命中立即 STOP）：**

- "顺手把 ⚠️ canonical 扩了" → 错，必须先入 backlog 待用户拍板
- "提案阶段已经在 Figma 起画 frame" → 错，提案阶段产物是文字
- "凭印象列 canonical 对应" → 错，必须 grep 实证（参考 [feedback_baseline-before-plan](memory) 同源教训）

---

## Migrated Rules — Pointer Index

通用 process rules → [`design-process.md`](./design-process.md)：M22 / Pre-Phase 0 / Stage 0.5 / M21 / Library-First + Evidence Discipline（M2）/ M6 / M11 / M14 / M15 / M16 / Lazy Reference Loading

TVU 业务规则 → [`domain-tvu.md`](./domain-tvu.md)：M3 / M4 / M5 / M7 / M8 / M9

---

## Hard Rules（Path A 专属）

### M0 — Phase 0：Element-to-Component Mapping（前置硬规则）

多 frame 或 **≥ 3 个 element** 的 mockup 任务**必须**在画之前先产出 mapping table。trivial 单元素调整可省。

**Scope (2026-05-14 clarification)**：以下都算"多 frame / ≥3 element"，**不算 trivial，必须做 M0 mapping**：
- 主交付 frame（如 1920×1080 完整页面）
- **Variant state mini-frames**（Empty / Loading / Sort-active / dropdown / popover / state group 内嵌 mini）
- Annotation 内嵌的 UI（state group `_flow-state-N` 的 UI frame、condition label group 等）
- 任何包含 ≥3 个 iconographic / chip / badge / button 元素的次级 frame

**违例 history**: 2026-05-14 Plan B Row 4 6 个 variant minis（Empty B/C/D + Loading + Sort-active + App Picker）当成"次要快速摆"完全跳 M0 mapping → 4 个 M2 违例 (App icons placeholder rect / Sidebar Badge 手画 / Sort ↑ TEXT / chevron Unicode)。Mini-frames 不是 trivial。

#### Lookup 序列（按序）

1. **grep [`figma-component-catalog.md`](./figma-component-catalog.md)** — 第一优先；每个 element 至少 2-3 个同义词 / casing 变体试（M15 防"一次 negative 即定论"）
2. **catalog 命中 ✅ verified** → 直接采用 entry 的 Primary use / Extension
3. **catalog 命中 🟡 skeleton** → 标记需 import sample instance 验 variants
4. **catalog 全 miss** → `search_design_system` + import sample，仍**先把全部 element 的 mapping 拉完再开始画**

#### 输出格式（mandatory table）

| Element | Figma component_set (key) | Variants | Status | catalog 索引 |
|---|---|---|---|---|
| top bar | `Top bar` (`918b928e...`) | Tag=AfterLogin/BeforeLogin | ✅ verified | catalog §Verified |
| status badge | `Badge` (`4db5246d...`) | Type/Color/Tag | ✅ verified | catalog §Verified |
| confirmation modal | `prompt message` (待查) | pop-confirm variant | 🟡 skeleton → 需 import 验 | catalog §Skeleton |
| APP icon | `APP Icons` (file-local, `fefd6aac...`) | Tag × Color | ⚠️ file-local | §已知 file-local |

#### 处理流程

1. AI 产出整张 table
2. 发给用户校验（一次性决策 gap 处理 / 验证 🟡 是否进 import 流程）
3. 用户确认后才开始 import + 画 frame
4. 画的过程中若 🟡 skeleton 实证后升级为 ✅ → **回填 catalog**（不是 nice-to-have，是责任）
5. 新发现 element 没在 table 里 → **回到 Phase 0** 补一行，**禁止**现场判断"library 缺件"

#### 图标与字体的源头锁定（M48 前移，2026-06-08 新增）

M48 的 Glyph / 字体自查是**交付前** gate；本步把同两类决策**前移到 mapping 阶段**，从源头杜绝"边建边手搓 glyph / 默认 Inter"——建完再扫等于返工。

- **图标 affordance 必须当场定 component key**：mapping 表里凡 affordance 属"图标 / 箭头 / 状态符号 / 删除 / 增删 / chevron"等的 element，**必须当场 grep catalog / search library 填上真 component key**（配合 M35 affordance trace），**不允许留 TBD / 空 / "建时再定"**——留空 = 建时图省事敲 Unicode glyph 的温床（M48 §icons 违例的高发路径，2026-06-08 V4-2285/2286 实证）。
- **字体起手即锁**：mapping 起手声明本任务**注释层字体 = EN `Roboto` / ZH `Noto Sans SC`**（M23 §字体规范），产品本体按库 instance / file-level baseline。注释卡建卡时即 `loadFontAsync` + `setRangeFontName`，不依赖 `createText()` 的 Inter 默认。

> **三道防线同治图标/字体两类高频复发病灶**：**M0 源头列定 → 主建按表 → M48 交付前 audit gate**（[`scripts/audit-mockup-typography-icon.mjs`](../../scripts/audit-mockup-typography-icon.mjs)，接进 `pnpm audit:consumer-mockup`）。交付前综合单闸跑 `pnpm audit:mockup-conformance --file <fileKey>`（聚合全 5 个 mockup audit，含原 orphan 的 library-origin；场景 2 加 `--non-blocking`）。

#### Why

M2 / M15 反复踩坑根因是"边画边判断 library 缺件"。Phase 0 把所有判断前置到一张 table，gap 一次暴露，决策一次到位。

---

### M1 — TVU Top bar 是公共基建，不允许自画

任何 TVU 产品页面的 top bar **必须直接 instance** Figma library `Top bar` 组件（来自 `TVU UX Design System` library）。

- Library: `TVU UX Design System` (key `lk-057f6ba0f771bfa7f63a6a197502999462c2974f995488b381708ca5faadb7f9f6675e04aa442b3a5ffb31732150a7f5aa8ca3102073ab210edc684022fc6c21`)
- Component set: `Top bar` (key `918b928e4541f1573de5c98c32b408af663d7f2f`)
- Variants: `Tag=After Login`（默认, 1920×56）/ `Tag=Before Login`（1920×64）
- 关键 properties：`Show Menu` / `Show Search box`（BOOLEAN）/ `Menu` `Right_content`（SLOT）

⚠️ 文件里还有同名旧版 `Top bar`（来自 `TVU UX Library`，key `2f36c51ae9fcdfd450f3151c654c9931c9069d96`），**不要用**。验证库归属：`search_design_system` 调用时通过 `includeLibraryKeys: ["lk-057f6ba0..."]` 过滤；不要用 `findAll(INSTANCE)` 扫描既有页面（那些页面可能用旧库建的）。

- **不允许**自画 top bar
- **不允许**修改 top bar 结构（菜单数量 / 时区 / Tokens / Network Delay 等只能开关显示）
- **不允许**把 page-level 元素塞进 top bar（计数器 / 过滤项 / 批量操作等放 top bar 下方）

⚠️ **产品标题不属于 page-level 元素**——填进 top bar 自带的 product-name slot（默认 `Product Name`，主动覆写为实际产品名）。不要在 top bar 下方再加只写 title 的 page-header strip——冗余。

**Why:** TVU 全产品视觉一致性靠 top bar 统一。AI 自画会破坏跨产品体验。

---

### M-COLOR — Color Token Discipline（umbrella，覆盖原 M10 / M25 / M26）

凡是给 mockup 元素填色（fill / stroke / text color）时，必须按 token 规范取用。三个 sub-clauses 同源——都是"违反 token 系统语义"——所以合并为一组以便 audit 一次跑完。

**Legacy ID 映射**：M10 → §C1（icon fill），M25 → §C2（state color），M26 → §C3（bg vs text grey）。

**自动化 audit**：`scripts/audit-mockup-colors.mjs`（一次扫完 3 个 sub-clauses）。

> **US-3（existing product iteration）专用前置步骤**：在写任何颜色常量 / Color Variable 前，必须先完成 **M21.2 — Feature Iteration Color Contract**（见 [`design-process.md`](./design-process.md)）——从 sibling frame 实测提取色值，禁止默认套用 TVU DS token 值（两者可能 intentionally diverge）。M-COLOR 的 C1/C2/C3 在 Color Contract 建立后继续适用。
>
> **元层 reference**：M21.2 是 [`../meta-rules.md` §触发器 O](../meta-rules.md) "Onboarding 知识 ≠ 实际产品状态（实测优先）" 的颜色专项落地。同一元规则下还有 §Sprint 收尾 Self-Audit 协议（AGENTS.md）、触发器 I/J/G 等子规则。

---

#### C1 — Brand / Token color paint 必须绑 Color Variable，不写 hex literal（原 M10；2026-05-26 扩 stroke + text + frame fill）

**适用范围**（**任何** brand-color / token-driven 视觉应用，**不限 icon path**）：

| 视觉应用 | 正确做法 | 错误做法 |
|---|---|---|
| **Icon path fill** — 单色非品牌图标 | `path.fill = Variable("Color Type/Icon/Default")` | `path.fill = "#dbdbdb"` literal |
| **Icon path fill** — 单色品牌图标 | 绑 `UX/Brand/Brand` / `UX/Red/Default` etc. | hex literal |
| **Icon path fill** — 状态色多色图标 | 各 path 分别绑语义 Color Variable | 全 path 一律 hex |
| **Frame / shape stroke**（如 selected row 的 4px LEFT accent rail）| `frame.strokes = [{ paint绑 UX/Brand/Brand }]` | `frame.strokes = [{ color: {r,g,b} 无 boundVariables }]` |
| **Frame / shape fill**（如 selected row 的 dark green bg）| `frame.fills = [{ paint绑 UX/Brand/Match,Hover }]` | hex literal |
| **Text fill**（如品牌色强调文字、Syncing state pill 文字）| `text.fills = [{ paint绑 UX/Brand/Brand or Color Type/Text/* }]` | hex literal |
| **第三方 logo / app icon** | hex literal 可接受（封装在 catalog SVG 内部）| 加到 design system token 里 |

**核心命题：任何 brand 色 / token 色的视觉应用都不允许 raw hex paint，全部必须绑 Color Variable。** Icon 是子集，**不是全集**。

**Why**：
1. figma SVG export API 把 Color Variable 引用解析为 literal hex（pipeline 限制）。Variable 绑定语义在 figma 内部保留，未来 token 升级 / theme 切换自动同步；hex literal 不会同步。
2. **跨 paint type 的一致性**：fill 绑 var 而 stroke 漏绑（或反之）= 视觉 split brain：theme 切换时一边变一边不变。
3. **Clone 风险放大**：raw hex 在 clone 后变 N 倍 bug（详见 **M42 Clone Gate**）。

**Code 端 fallback**：`figma-sync/export-icons.mjs` 的 `transformSvgCurrentColor` 把 `#dbdbdb` 转 `currentColor`；消费组件容器设 `color: var(--icon-default)` 实现 cascade。详见 [`translation/divergences.md`](../../src/design-system/translation/divergences.md) §"Figma SVG export pipeline limitation"。

**落实方法**：
1. 编辑既有 figma library icon / element 时，所有 brand-color paint（fill / stroke / text）改用 Variable 绑定（不用 hex）
2. 画新 element / icon 时直接用 Variable 绑定
3. import / paste element 后**自检**：所有 brand-color paint 是否 hex？是 → fix 绑 Variable
4. AI 生成 figma 效果图时，instance 继承 library 设置即可——**不要**在 instance 上 override 成 literal hex
5. **Instance / clone 后 paint 绑定自检**：任何 `createInstance()` / `node.clone()` 调用后，立即 probe 所有 brand-color paint 的 `boundVariables?.color`——若为空或 raw SOLID → 二步法绑对应 token。批量场景每个均需检查，不因"library 默认已绑"而跳过。
6. **写 paint object 前先检查源**：若 paint 复用自现有 paint object（如 `const stroke = sourceNode.strokes`），先 probe `boundVariables` 是否完整——源缺失则**先修源再 clone**，不要把 bug 传播到 N 个 instance（详见 **M42 Clone Gate**）。

**实证 — 2026-05-26 Video Sync Phase B**：本规则原仅约束 icon path fill。Selected row 的 4px LEFT BRAND stroke 在 M0/M1 原版用 raw `{0.184, 0.71, 0.306}` 无 boundVariables → clone 12 variants 时全部继承 → walkthrough audit 才捕获 → 全绑 `UX/Brand/Brand` (`3010:102`)。根因：C1 原 wording "Icon path fill" 不覆盖 frame stroke，**是规则 wording 盲区不是 AI 失误**。本次扩 stroke + frame fill + text fill 覆盖即修复盲区。

---

#### C2 — State Color Discipline (hover ≠ brand)（原 M25）

TVU library 为状态色提供专门 token 套件，**不可省略**：

| State | Brand | Red | Blue | Orange |
|---|---|---|---|---|
| Default / Active | `UX/Brand/Brand` | `UX/Red/Default` | `UX/Blue/Default` | `UX/Orange/Default` |
| **Hover** | `UX/Brand/Hover` | `UX/Red/Hover 1` | `UX/Blue/Hover 1` | `UX/Orange/Hover 1` |
| Disabled | `UX/Brand/Disable` | — | — | — |
| Icon-specific active/hover | `Color Type/Icon/Active & Hover` | （同左） | （同左） | （同左） |
| Grey button hover | `Color Type/Background/Hover Grey Button` + `Color Type/Text/Hover Grey button` | — | — | — |

**规则**：
- **Active state** = primary token（pure brand）— 表达"现在选中的"身份
- **Hover state** = `*/Hover` variant — 比 active 略浅；提示"可点击"
- **Disabled** = `*/Disable` — 灰化但保留品牌指代
- 当 interactive element 是 **brand-colored** 时，hover bg **必须**用 brand-hover，不可用 Layer_3 生灰（后者是"中性 hover"，只对非品牌色 element 用）

**Why**：用 `Brand/Brand` 给 hover state 会让 hover 视觉等同 active，破坏 "click affordance vs selected state" 的区分。Layer_3 生灰则丢失品牌色暗示——用户看不到这是个 brand action。

---

#### C3 — BG vs Text Grey Discipline（原 M26）

TVU library 的灰色 token 分两套：

| 用途 | 命名空间 | 示例 |
|---|---|---|
| **Background fills**（FRAME_FILL / SHAPE_FILL）| `Color Type/Background/Layer_{1,2,3,4}` + `Top Bar` | Layer_1 canvas / Layer_2 card / Layer_3 chip / Layer_4 inactive bar |
| **Text / Border foreground**（TEXT_FILL / STROKE）| `UX/Grey/grey-{2..9}` + `Color Type/Text/*` + `Color Type/Line/*` | grey-7 tip text / grey-9 disabled text / Line/Deep Divider |

**禁用规则**：
- ❌ 把 `UX/Grey/grey-N` 用作 frame/shape fill（grey-7/8/9 是 text color，scope 没必要给 FRAME_FILL）
- ❌ 把 `Color Type/Text/*` 用作 background fill
- ❌ 把 `Color Type/Background/Layer_N` 用作文字色

**正例 vs 反例**：

| 元素 | ✅ 正确 | ❌ 反例 |
|---|---|---|
| Status bar inactive segment | `Color Type/Background/Layer_4` | `UX/Grey/grey-7` |
| Card bg | `Color Type/Background/Layer_2` (or hex if not published) | `UX/Grey/grey-9` |
| Tips text | `Color Type/Text/Tips` | `UX/Grey/grey-7` |
| Sidebar hover bg (brand element) | `UX/Brand/Hover @ low alpha` | `Color Type/Background/Layer_3` |

**Why**：Token scope 是设计意图的硬约束。`UX/Grey/grey-7` 的 scope 是 ALL_SCOPES（向后兼容），但它的 hex `#7B7B7B` 视觉上是中等灰，作为 BG 太亮、作为 text 比 tips 暗——错位使用会让浅色文字消失或深色 BG 过亮。

**Why（深层）**：Layer 系列灰是按视觉层级递增（layer_1 最暗 / layer_4 最亮 elevated），符合 BG 堆叠语义。Grey-N 系列是文本/边框灰度阶梯。混用是把"颜色"和"语义"切了。

---

#### C4 — Brand color vs M23 annotation accent 不能混用（2026-06-01 新增）

TVU 视觉系统里 **brand green `#33ab4f`** 与 **M23 annotation accent cyan `#33A4FD`** 是两套独立色彩归属，严禁混用：

| 色 | hex | 用途归属 | 何时用 |
|---|---|---|---|
| **Brand green** | `#33ab4f` (`UX/Brand/Brand`) | 产品 UI 的 active state / brand identity | Selected radio dot / 已连 SSID / brand button / 主操作 affordance |
| **M23 cyan accent** | `#33A4FD` | UX 交付注释卡的 accent | M23 卡 4px 左 stroke / 状态标签标题色 / **流程线（起点圆点边框 + 主干 + 终点箭头）** / 条件标签文字 / spec card title |
| **Tips gray** | **新库** `#A6ADB8` (`Color Type/Text/Tips`) / **legacy 文件** `#999999`（无 token，hex literal 但属文件 file-level convention 合法）| 次要提示文字 | `* hint`类提示行 / 注释说明次要信息 / inline 括号内 description |

**反例 — 2026-06-01 V4-1865**：
- F5 Mode=WiFi redirect note 用 brand green 染（`#33ab4f`）→ 用户抓"提示文字为什么用品牌色"。修法：改 tips gray 与 baseline `* Enable to connect only to this WiFi network.` 同源。
- M23 spec card title 用 brand green → 应是 cyan `#33A4FD`。修法：所有 M23 卡 section title fill 绑 cyan token / paint。

**反例 — 2026-06-02 V4-1865 v6**：F5 hint 第一次改 tips gray 用了**新库 `#A6ADB8` + fontSize 14**，但 Config-T 是 legacy file，baseline hint 实际是 **`#999999` + fontSize 12**（fontSize 也不对）。用户抓"提示文案颜色应参考项目中已有 `#999`"。修法：legacy file 起手用 `findAll` probe sibling hint text 实测色 + 字号，不套新库 token。**file-level convention precedes umbrella default** — 本表 hex 是新库默认，legacy 文件以自身 baseline 为准。

**Why**：
1. **brand color overuse 稀释品牌信号**——active state 该突出时反而被一片 brand 海淹没
2. **M23 accent ≠ 品牌色**——M23 卡是"附注层"视觉契约，与产品 UI 视觉是隔离 layer (M33 已规定)；用 brand 色破隔离
3. **hint 文字用 brand 是反 UX**——hint 应低视觉权重；brand 色高对比反喧宾夺主

**Acceptance**：
- mockup product UI 区出现 cyan `#33A4FD` 任意应用 → 违例（除非 token 明文允许）
- M23 card / **流程线任意部位（起点圆点边框 / 主干 / 终点箭头）** / 条件标签出现 brand green `#33ab4f` → 违例（流程线统一 cyan `#33A4FD`；唯一非蓝处是起点圆点填充 `#FFFFFF` 白衬底，非品牌色）
- "提示 hint" / "次要说明" 类文字色 ≠ tips gray → 违例

---

#### C5 — Semantic-color-first（语义色优先于 grey 原语）（2026-06-12 新增）

绑颜色时**先按元素语义角色选 `Color Type/*` 语义变量；`UX/Grey/grey-N` 原语仅在没有对应语义角色时 fallback**。

| 元素角色 | ✅ 首选语义变量（Figma 实查名） | code token | ❌ 别直接绑原语 |
|---|---|---|---|
| 文字（标题/正文/提示/占位/禁用）| `Color Type/Text/{Heading & Button, Text_1, Text_2, Tips, Placeholder & Button, Disable}` | `--text-*` | `UX/Grey/grey-N` |
| 图标 | `Color Type/Icon/{Active & Hover, Default, Placeholder, Disable}` | `--icon-*` | `UX/Grey/grey-N` |
| 背景/卡/分层 | `Color Type/Background/{Layer_1..4, Top Bar}` | `--bg-*` | `UX/Grey/grey-N` |
| 分隔线/边框 | `Color Type/Line/{Popup Border, Deep Divider, Light Divider}` | `--line-*` | `UX/Grey/grey-N` |

**Why**：实查证实语义变量本身就 alias 到 grey 原语（`Color Type/Text/Tips` → `grey-6` #9e9e9e）——所以**绑原语像素看起来对、但丢了语义角色**：角色→灰阶映射一旦在库里调整，绑原语的节点不跟随、绑语义的自动跟随。这是 design-token 间接层的全部意义。

**实证 — 2026-06-12**：owner 发现执行器画 mockup 优先绑 `UX/Grey/grey-N` 而非自定义的 Text/Icon/Background/Line 语义色。根因：§C3 "Text/Border foreground" 把 `UX/Grey/grey-{2..9}` 列为合法 foreground 命名空间，留了绑原语口子。本 §C5 收紧为"语义角色优先、原语仅 fallback"，与 §C3 的 scope 纪律并行（C3 管"别把 bg-grey 当 text 用"，C5 管"有语义角色就别绑原语"）。

**Acceptance**：text/icon/bg/line 角色节点绑了 `UX/Grey/grey-N`、而存在对应 `Color Type/*` 语义变量 → 违例（改绑语义变量）。机检见 D16 verifier（`audit-mockup-binding-fidelity`）。

---

#### M-COLOR 共用 Acceptance

- 一次 probe 跑全：`pnpm audit:mockup:colors -- --file <fileKey> --node <nodeId>`（脚本扫 §C1 / §C2 / §C3 三条；§C5 语义优先由 D16 verifier 机检）
- 任一 sub-clause 违例 → handoff doc 必含 fix log（"colors fixed: [§C1 N nodes, §C2 N nodes, §C3 N nodes, §C5 N nodes]"）
- Legacy ID 引用兼容：M10 / M25 / M26 anchor 在文末"Legacy ID Map"段维护重定向

---

### M23 — UX 交付注释：多状态交互流程图格式

当一个 feature 存在多个明显不同的交互状态（如 Default / Loading / Applied），**必须**用标准化的多状态流程图格式来交付 UX 说明，而不是单一静态 frame。

#### 何时触发

- 功能有 ≥2 个用户可感知的交互状态
- 存在状态转换触发条件（用户操作 / 系统响应）
- 开发需要明确的状态边界和数据变化说明

#### 结构层级（5 层）

| 层 | 节点类型 | 命名规范 | 说明 |
|---|---|---|---|
| 1. Section header | FRAME | `"UX · [Feature] — Interaction States"` | 横跨所有 state frame，full-width |
| 2. State group | GROUP | `"_flow-state-N"` | 每个状态一个 GROUP，包含 label chip + UI frame |
| 3. Label chip | FRAME（GROUP 子节点）| 无固定命名 | 在 UI frame 正上方 8px，显示状态名 + 说明 |
| 4. 流程箭头 + 条件标签 | VECTOR + GROUP | 箭头: `"Vector"`；条件: `"_"` | 页面级节点，不放进 state group |
| 5. 规则卡 | FRAME | `"UX · [Feature] — Rules"` | 放在最后一个 state group 右侧 |

#### 主题无关配色系统（硬规则）

所有注释元素必须使用**显式填充容器**——禁止文字直接浮在 canvas 上。

| 元素 | Fill | Text color | 原则 |
|---|---|---|---|
| Header chip / Label chip / 规则卡背景 | `#2B2D42`（深海军蓝）| `#FFFFFF` | 深底略亮可读；浅底深色 chip 突出为 annotation overlay |
| 状态标签标题行 `[State Name]:` | — | `#33A4FD`（蓝色） | 与箭头颜色一致；深/浅背景对比度均达标 |
| 状态说明文字（次要信息）| — | `#A6ADB8`（浅灰） | 降噪，不抢夺标题视觉权重 |
| 流程连线起点圆点 | `#FFFFFF` | — | ⌀6px 圆点，**白色实心填充 + 2px 蓝色边框 `#33A4FD`**，标在主干起点 → 流程一眼定向「从哪到哪」，不靠读者猜（白填充比空心环在小尺寸下更利落不糊；2026-06-17 由空心环改白填充圆点）|
| 流程箭头 stroke | — | `#33A4FD` | 2px stroke，主干从起点圆点右缘起笔；终点手动绘制箭头三角（`strokeEndCap` 在 VECTOR 不支持）|
| 条件标签文字 | — | `#33A4FD` | 背景 `#2B2D42`，放在箭头正下方 |
| 规则卡左边框 | 4px `#33A4FD` left stroke | — | `strokeLeftWeight=4`，其余三边 = 0，`strokeAlign='INSIDE'`（偶数 stroke 便于像素对齐，2026-05-26 由 3 改为 4）|

**Why**：依赖 canvas 背景色（白字 = 假设深底 / 黑字 = 假设浅底）会在主题切换时翻车。`#2B2D42` container 在深/浅 canvas 上均有明确边界，是主题无关的最小可读单元。

#### Figma canvas 层级硬规则

**条件标签必须用 GROUP，不能用 FRAME。**

FRAME 节点在 Figma canvas 上显示名称标签，会干扰流程图阅读。条件标签（arrow 下方小 chip）必须用 GROUP 实现：

```js
// ✅ 正确：RECTANGLE（背景）+ TEXT，打 GROUP
const rect = figma.createRectangle();  // navy fill, cornerRadius=4
const t = figma.createText();          // blue text
const grp = figma.group([rect, t], page);
grp.name = '_';  // 最短名，canvas 上不显眼

// ⚠️ 关键：RECTANGLE 必须在 index 0（下层），TEXT 在 index 1（上层）
// figma.group([rect, t], page) 中最后一项渲染在最上方
// 若顺序错误，rect 会遮住 text → 文字消失
grp.insertChild(0, rect);  // 确认 rect 在底部
```

**State group 同理**：每个「label chip + UI frame」对打一个 GROUP（`_flow-state-N`）。GROUP 无 canvas 名称标签，8px gap 可以紧贴，不会与 Figma UI 的 frame 名称标签冲突。

→ Figma FRAME vs GROUP canvas 标签行为详见 [`figma-technical-reference.md Q5`](./figma-technical-reference.md#q5--frame-vs-group：canvas-名称标签行为差异)

#### 流程连线画法（起点圆点 + 终点箭头）

流程连线 = **起点实心圆点 ● + 终点箭头 ▶**（`●————▶`），让用户一眼看出从哪到哪、不靠猜方向。线身按 §M23.6 走正交折线：同行/同列即单段直线，跨行用折线（直线还是折线按拓扑需要选，见 M23.6 几何模板）：

- **起点圆点**：`figma.createEllipse()`，⌀6px，`fills=[白色 #FFFFFF]` + 2px stroke `#33A4FD`（蓝色边框），垂直居中对齐主干（白填充比空心环在小尺寸下更利落、不糊）
- **主干 + 终点三角**：VECTOR，从圆点右缘 +2px 起笔；`strokeEndCap` 在 VECTOR 不支持，三角手动画进 `vectorPaths.data`
- **打 GROUP**：圆点 + 箭头 group 成一条连线（沿用本节注释元素 GROUP 范式），reconnect_map 存 GROUP id

```js
// 主干 + 三角（局部坐标，相对 vec.x/y）；起点圆环 + GROUP 见 Q6 完整示例
const len = x2 - (x1 + 8), AH = 8;  // 主干从圆点右缘（⌀6 + 2 间隙）起笔
vec.vectorPaths = [{ windingRule:'EVENODD',
  data: `M 0 ${AH} L ${len-AH} ${AH} M ${len-AH*2} 0 L ${len} ${AH} L ${len-AH*2} ${AH*2}` }];
```

→ 起点圆点 + 箭头 + GROUP 的完整画法详见 [`figma-technical-reference.md Q6`](./figma-technical-reference.md#q6--vector-节点不支持-strokeendcap，箭头必须手动画)

#### Reconnect map（快速重连）

每次画完流程图，**必须**把 arrow 位置信息存入 sharedPluginData，下次"重新连线"时直接读取，无需重探坐标：

```js
page.setSharedPluginData('ux_annotation', '[feature]_reconnect_map', JSON.stringify({
  arrows: [
    { id:'<groupId>', label:'state-1 → state-2',   // 圆点 + 箭头 GROUP 的 id
      x, y, w, h,
      // 两端都锚在语义元素 bbox（非卡边）；fromCanvas/tipCanvas 供 M23.6 (A) 两端审计
      from:{ frameId:'<id>', triggerElemId:'<btn/cell/icon id>', edge:'right', midY }, fromCanvas:{ x, y },
      to:  { frameId:'<id>', targetElemId:'<id>',                edge:'left',  midY }, tipCanvas:{ x, y } },
  ],
  stateGroups: [{ id, label:'state-N', chipId, frameId }],
  headerChipId, ruleCardId,
}));
// 读取：page.getSharedPluginData('ux_annotation', '[feature]_reconnect_map')
```

#### 反例（必须避免）

| ❌ 错误做法 | 原因 |
|---|---|
| 条件标签用 FRAME | canvas 上显示 "Frame" 名称标签，视觉干扰 |
| 文字直接浮在 canvas 上 | 颜色依赖主题背景 → 主题切换后不可读 |
| `strokeEndCap = 'TRIANGLE_ARROW'` 用在 VECTOR | VECTOR 不支持此属性，silent fail |
| GROUP 子节点顺序：TEXT 在前 RECTANGLE 在后 | RECTANGLE 渲染在最上层，遮住文字 |
| vectorPaths data 用绝对坐标 | data 是局部坐标（相对 vec.x/y），用绝对坐标会导致路径位置翻倍偏移 |
| 流程连线起点用裸线头（无起点标记）| 读者要靠箭头反推起点，方向读不出来；`●————▶` 起点实心圆点才一眼定向 |

#### 双语支持（中英文必须同时提供）

所有 UX 注释文案**必须同时提供中文和英文**，排版按元素类型采用不同方式：

| 元素 | 排版方式 | 说明 |
|---|---|---|
| **State label chip** / **Header chip** 各行 | **内联式（Layout A-右）**：英文 + `  `（2 space）+ 中文，同一文本节点（ZH 弱化样式见下方〈字体规范〉〈颜色规范〉）| 单行 label / 标题，EN 短，中文直接跟在右侧 |
| **条件标签 chip** | **内联式（Layout A-下）**：英文一行 + 中文换行另起一行，同缩进（ZH 弱化样式见下方〈字体规范〉〈颜色规范〉）| 元素宽度有限，中文另起一行跟随 |
| **多行 bullet 列表 / 多行公式 / 含 `\n` 的结构化正文（UX delivery 注解卡 / spec 表）** | **内联式（Layout A-逐行）**：按 EN 行 / bullet 拆分，每行末尾 + `  `（2 space）+ 对应 ZH + `\n` 进下一行（ZH 弱化样式见下方〈字体规范〉〈颜色规范〉）| EN 已按 `\n` 或 `•` 自然分行；逐行 ZH 比 Layout B 上下分节更易 1:1 对应 |
| **规则卡** | **分节式（Layout B）**：英文完整段落 → 分隔线（1px `#3A3D55`） → 中文完整段落（`规则说明：`为标题） | EN 是一大块 prose（无 `\n` / 无 `•`）且 ≥ 3 行；逐行内联放不下时退到此层 |

**布局选择决策树（A-右 vs A-下 vs A-逐行 vs B）**：

```
文本行数 ≥ 3 行 或 spec 级正文？
  └─ YES → EN 已按 \n / • 自然分行 且 每行 EN+ZH 能塞进容器宽度？
        └─ YES → Layout A-逐行（line-by-line interleaved）
        └─ NO  → Layout B（分节式上下分）
  └─ NO  → 该行英文字符 ≤ 50？ 且 chip 宽度充足？
        └─ YES → Layout A-右（中文直接内联到右侧，2-space 分隔）
        └─ NO  → Layout A-下（中文另起一行，同缩进）
```

> **行距与分组**：A-下 / A-逐行 / 多语堆叠的交付卡 ZH 译文堆在 EN 下方时，必须按 **§M23.14（双语堆叠排版的行距与分组）** 做"组内紧 / 组间松 / 段间最松"三档差异化间距——否则统一行距会让卡拥挤、EN/ZH 不成组。

**技术实现（use_figma）**：
- Layout A-右：`setRangeFills` / `setRangeFontName` / `setRangeFontSize` 在同一 text node 的不同字符区间分别设样式
- Layout A-下：text node 内插入 `\n` + ZH 行，同样用 range API 设中文样式
- Layout A-逐行：按 `\n` split EN 为行数组，每行配对 ZH，重建 characters，再对每个 ZH range 套样式：
  ```js
  const enLines = node.characters.split('\n');
  const zhLines = [/* 对应 zh per line, 空字符串跳过 */];
  const newChars = enLines.map((en, i) => zhLines[i] ? `${en}  ${zhLines[i]}` : en).join('\n');
  node.characters = newChars;
  // 然后游走 cursor 算出每个 ZH range [zhStart, zhEnd]，套 setRangeFontName/Size/Fills (opacity 0.45)
  // 关键：先用 setRangeFontName/Size/Fills 对 (0, newChars.length) reset 为 EN 样式（清掉前一版本残留），
  //       再对每个 ZH range 套 ZH 样式，避免 setCharacters 后 trailing 残留样式污染
  ```
- Layout B：追加独立 text node（`规则说明：` 标题 + 6 bullets）+ 1px 分隔 RECTANGLE
- **ZH 弱化**：对所有 `Noto Sans SC` 字符区间，setRangeFills 时加 `opacity: 0.45`；EN opacity 保持 1.0（默认）。EN 用什么底色 ZH 就用相同底色 + 半透明，无需记特殊颜色值

**视觉优先级原则（最重要）**：
> **英文优先，中文视觉弱化**。习惯读英文的工程师只看英文，中文不应干扰视线；习惯读中文的成员虽然中文弱化但仍可识别。双语不等于双权重——中文永远是附注，不是并列。

**逐行内联 > 上下分节（在能放下的前提下）**：
> Layout B 上下分节把工程师阅读路径切两段——先读完整段 EN，再回头读整段 ZH，对照费眼睛。Layout A-逐行 让每行 EN/ZH 同视框，1:1 对照零阅读跳跃。但前提是 EN 已自然分行（`\n` 或 `•`）且每行 EN+ZH 能塞进容器宽度；EN 是一大块 prose（无 `\n` 无 `•`）或单行太长塞不下时仍走 Layout B。

**字体规范（双语 ZH 字体/字号唯一真源）**：
- 英文：`Roboto`（**对齐设计系统真源**——`src/tokens/variables.css` §Text Style tokens 7 个 Text Styles + `figma-data/raw/design-tokens.tokens.json` typography.roboto 均为 Roboto family；style 用 `Regular` / `Medium` / `Bold`）
- 中文：`Noto Sans SC`（Figma 可用简中字体；style 用 `Regular` / `Medium`；`PingFang SC` 在此环境不可用，故 ZH 用 Noto Sans SC 作环境替代）
- 中文字号：比同层英文小 **2px**（如英文 10px → 中文 8px；宁小勿同）
- **适用范围**：**所有 Figma 上的说明文字**（UX 交付卡 / PRD card / spec / state-note / 注解 / Change Summary / Interaction Spec / Baseline 等），**无特殊说明默认遵此规则**。产品 mockup 本体文案另按其库 instance / file-level baseline（见 §C4 "file-level convention precedes umbrella default"）。
- **加载机制**：设 fontName 前 `await figma.loadFontAsync({ family: 'Roboto', style })`；Roboto 在 TVU Figma 环境**已 live 实证可用**（2026-06-08 `use_figma` 实跑：`listAvailableFontsAsync` 列出 36 个 Roboto 字重，`loadFontAsync` Regular/Medium/Bold 全 OK；`Noto Sans SC` Regular 同 OK）。**若 `loadFontAsync` 真的抛错才 fallback 到 Inter，且必须显式上报用户**（不静默退回）——见 §M47。

> **Why（2026-06-08 回流）**：注解层字体此前用 `Inter`（Figma 新建文本节点默认值），与产品真源 `Roboto` 不一致 → 同一 Figma 文件里注解层和产品层两套英文字体，设计 mockup 时容易在产品节点误用 Inter / 在注解误用别的字体。统一成 **EN Roboto / ZH Noto Sans SC** 后全文件单一字体体系，消灭跨层字体误用面，且注解层字体 = 产品设计系统真源。用户 2026-06-08 指出「中文 Noto Sans SC、英文 Roboto 更合适，所有 Figma 说明文字无特殊说明默认遵此」。（注：早前 §M47 把 Roboto 与 Helvetica 一起归为「编辑环境不可用」是 2026-05-28 TPC-628 Helvetica 事故的过度归并，与 design-tokens + 实际渲染 Roboto:Bold 的硬证据冲突，本次一并校正。）

**颜色规范（双语 ZH 配色唯一真源 — 其余各处只引用本节，不复述数值）**：
- 英文主文：`#FFFFFF`（白）或 `#33A4FD`（accent，视层级）
- 中文次级：**与对应英文同色 + opacity 0.45**（不另设独立灰 hex）— 靠透明度弱化，保持与英文的同色关联，中文不抢视觉权重
- ⚠️ 中文必须与英文**同色**，仅靠 **opacity 0.45 + 字号小 2px** 弱化；禁止与英文同 opacity 同字号 → 否则视觉上文字量翻倍，注释难以快速扫读

> **校正（2026-06-04）**：历史表述「中文用浅灰 `#A6ADB8`」与本规范「同色 + opacity 0.45」相互矛盾，已统一为后者——中文恒用对应英文**同色**、靠 **opacity 0.45 + 字号小 2px** 弱化，**不另设独立灰 hex**（`#A6ADB8` 仅作 EN 描述文字基础色，见上方主题无关配色表）。实证：2026-06-04 Graphics Insertion「1 Layer per line」用户手册流，AI 照旧表述给中文上了独立灰色，用户再次指出"中文颜色应是英文颜色的透明度"。

**反例**：
- ❌ 只写英文（工程师无法判断中文产品文案）
- ❌ 只写中文（国际团队协作障碍）
- ❌ 多行 body 未按 `\n` / `•` 拆 line 就整块内联（EN 全段 + 2-space + ZH 全段）→ ZH 接在 EN 末尾继续 wrap，工程师看不到一一对应（用 Layout A-逐行 替代）
- ❌ state label 用分节式 → chip 高度膨胀，流程图显得松散
- ❌ 直接新增 child text node 到 chip（会导致 auto-layout 撑高）→ 用 range API 改写同一节点
- ❌ 中文与英文同 opacity 同字号（看起来文字量很多，失去弱化效果）—— 弱化靠 opacity 0.45 + 小 2px，而非把中文换成另一种灰色
- ❌ 只靠字号差（1px）区分 EN/ZH —— 当 EN 本身已经是灰色时，1px 肉眼不可见；必须配合 opacity 0.45

##### 描述性表格也必双语（区分"描述 prose"cell vs "数据中性"cell）— 2026-06-09 新增

注释层的**对比表 / 描述性表格**（如方案 A vs B 对比卡、特性对照、spec 表）每个 cell 同样落 §双语——**但按 cell 内容性质区分**，不是整列一刀切：

| cell 类型 | 双语？ | 例 |
|---|---|---|
| **描述 prose**（解释 / 优劣 / 行为描述 / 维度名）| **必双语**（EN 主 + ZH 同色 opacity 0.45 弱化）| "统一弹窗，少跳转" / "Splits balance & seats into 2 surfaces" / 列首维度名「钱包归属」|
| **数据中性**（纯数字 / 金额 / SKU / 专名 / node-id / 代码标识 / token 名）| **EN-only 可**（无语义翻译价值，翻了反噪）| `1,300 tokens/mo` / `$77` / `BM-1047` / `5399:2046` / `UX/Brand/Brand` |

判定口径：**这个 cell 翻成中文对读者有信息增量吗？** 有（描述 / 解释类）→ 双语；纯数据 / 标识符（翻了等于没翻或更乱）→ EN-only。**同一表里维度名列双语、数值列 EN-only 是正常的**——按 cell 判，不按列判。

**实证 — 2026-06-09 BM-1047 V3 Design Spec 对比卡**：V3 拆分 vs V2 统一对比表的**描述性 cell**（方案优劣 / 维度名）漏了 ZH 弱化，只剩数值 cell 是 EN——但描述 cell 恰恰最该双语。补描述 cell 双语后合规。区分"数据中性 vs 描述 prose"即本条回流。

#### 验收标准

- [ ] 深色 canvas 上：所有注释文字可读，颜色无依赖画布背景
- [ ] 浅色 canvas 上：navy chip 作为 overlay 突出可识别（无需截图验证，配色规则保证）
- [ ] Figma canvas 上无多余 "Frame" 标签（条件标签是 GROUP）
- [ ] Arrow ID 和坐标已存入 sharedPluginData（可通过 `getSharedPluginData` 验证）
- [ ] Header chip 和规则卡有语义化名称（非默认 "Frame"）
- [ ] 所有注释文案同时提供中英文，排版符合内联式（A-右 / A-下 / A-逐行）/ 分节式（B）分类规范
- [ ] 多行 body 走 A-逐行 时：每行 EN 后紧跟对应 ZH 同视框（无 ZH 整块甩末尾的反模式）
- [ ] 所有 Noto Sans SC 字符区间的 fill opacity ≈ 0.45（probe 脚本可验证，无需截图）

#### 验证策略（优先用 probe，截图最后手段）

| 验证目标 | 推荐方式 | 截图？ |
|---|---|---|
| ZH opacity 是否为 0.45 | `getRangeFills` 返回 JSON，检查 `opacity` 字段 | ❌ |
| EN 颜色是否正确（白/蓝/灰） | `getRangeFills` 检查 `color` | ❌ |
| 节点类型（GROUP vs FRAME） | `get_metadata` 或 `node.type` | ❌ |
| 位置 / 间距 / 宽高 | probe 脚本返回 x/y/w/h | ❌ |
| sharedPluginData 是否写入 | `getSharedPluginData` 返回字符串验证 | ❌ |
| 视觉布局（间距感、层叠、对齐感） | 截图 | ✅ 仅此类 |
| 用户最终拍板前确认 | 截图 | ✅ 仅此类 |

**probe 模板**（批量验证 ZH 弱化，一次 call 覆盖所有 text node）：

```js
// 检查所有 annotation text 的 ZH range opacity
function checkZHOpacity(node, expected = 0.45) {
  const len = node.characters.length;
  const fails = [];
  let i = 0;
  while (i < len) {
    const font = node.getRangeFontName(i, i + 1);
    if (font && font.family === 'Noto Sans SC') {
      let j = i + 1;
      while (j < len && node.getRangeFontName(j, j+1)?.family === 'Noto Sans SC') j++;
      const fill = node.getRangeFills(i, j)[0];
      const op = fill?.opacity ?? 1.0;
      if (Math.abs(op - expected) > 0.05) fails.push({ range: [i,j], opacity: op });
      i = j;
    } else { i++; }
  }
  return { pass: fails.length === 0, fails };
}
```

**Why**：截图消耗 Token 约为 probe 脚本的 10–20 倍；所有可量化属性（颜色、opacity、位置、类型）均可用 `use_figma` 返回 JSON 验证，截图只留给"眼睛才能判断"的视觉感受类问题。

**Why**：UX 交付注释本质是给工程师读的文档；如果注释本身因主题/缩放/层级问题无法清晰呈现，文档价值归零。双语确保跨语言团队无歧义对齐。

#### M23 — UX 交付卡 Canonical 结构（正向模板）

> ⚠️ **起草/更新 UX 卡必照此结构，禁自创 What/Menu/Navigation 等临时段。**

UX 交付卡 = 一组 **navy chip 卡**（背景 `#2B2D42`，title fill cyan `#33A4FD`），固定 ≤ 6 张、固定顺序、固定语义。**这是唯一合法卡结构**：

| # | 卡 | 内容 | 必填 |
|---|---|---|---|
| 1 | **Section Header** | `UX · [Feature] — [States/Rules]` | 必 |
| 2 | **Why** | 1-3 句驱动力：为什么做这个改动（中英）| 必 |
| 3 | **Changes** | 本期改动 list（只列本次，不堆历史/roadmap）| 必 |
| 4 | **Data Contract** | 字段 / 数据契约（来源、类型、空态、边界）| 数据相关时必 |
| 5 | **Interaction** | 交互细节（触发、状态转移、反馈）| 有交互时必 |
| 6 | **Acceptance** | 验收 checkbox（可勾选、可测）| 必 |

**硬约束**：
- 段名、顺序、语义照上表，**不得自创**（如 What / Menu / Navigation / Notes 等临时段 = 违例）。
- 语言纪律见 §M23.10（禁 regex/pseudocode，白话决策树）；scope 纪律见 §M23.11（只放本期、不堆 Persona/out-of-scope/roadmap）。**本段管结构，.10 管语言，.11 管取舍——三者配套。**
- 卡视觉 = M23 navy chip（§配色表 + §M23.6 同色系 cyan accent）。

**实证 2026-06-18**：AI 被要求"更新 UX 交付说明"时写成 What/Menu/Navigation 临时结构——根因是 6 卡 schema 此前埋在 §M23.11 scope 纪律里、无正向模板、无关键词路由。本子段即把结构提为一等公民（修 W2）。

#### M23.12 — Spatial Proximity（UX 卡 / PRD 卡必须紧邻其描述的 mockup）— 2026-06-01 新增

UX 交付说明卡（M23 canonical 卡）和 PRD 卡的 **layout 位置必须与其描述的 mockup frame 在视觉上紧邻**。"紧邻"定义：阅读时一屏内可同看到卡 + mockup，且不需横/纵向滚动跳跃。

**Acceptance**：
- per-section UX 卡 → 同 row 右侧（或同 column 下方），距对应 mockup 行最近的边 ≤ 200px
- PRD 卡 → 整页顶部，width 跨所有 mockup column 或紧邻 Jira widget
- 不允许把所有 spec/PRD 内容堆成"页面底部一个大卡"，与上方 mockup 视觉断开

**反例 — 2026-06-01 V4-1865 v2**：M23 卡（1600×2294）单独放 row 3（y=2810），距 row 1 mockups（y=0..1240）2570px 垂直距，工程师阅读时必须滚动 + 失去对应。修法 v4：拆为 1 个 PRD 卡 (top y=-1100 with Jira widget side-by-side) + 2 个 per-section UX 卡 (WiFi UX 卡 右 row 1 / Hotspot UX 卡 右 row 2)，每卡与其描述的 mockup 行水平同 Y。

**Why**：M23 的核心价值是给 dev/QA 提供"对 mockup 的注解"。注解距 mockup > 一屏 = 阅读时强迫上下文切换，1:1 对应失效。Layout 决策必须把"deliverable 与 mockup 的空间关系"当一等约束。

#### M23.13 — UX delivery card sizing：默认竖向 Auto Layout（hug 高度）+ 标准宽度（2026-06-05 新增）

所有 **UX 交付卡**（注释卡 / 规则卡 / state-note / Change Summary / Interaction Spec / Baseline 等）默认必须：

- **竖向 Auto Layout**：`layoutMode='VERTICAL'` + `primaryAxisSizingMode='AUTO'`（**hug 高度**，随内容自适应） + `counterAxisSizingMode='FIXED'`（宽度锁定）。
- **宽度规则**（承接 [M23.12](#m2312--spatial-proximityux-卡--prd-卡必须紧邻其描述的-mockup-2026-06-01-新增) 的"注解紧贴 mockup · 1:1 对应"）：卡宽 = **其所注释的 mockup 元素/帧的宽度**，卡放在该元素正下方/旁、左对齐，1:1 贴合；注释整页/整个流程的 summary 卡 = 跨内容带的**统一宽度**。同一交付集合内卡宽一致，不逐卡手调。高度永远 hug（不设固定高度）。
- 子 TEXT：`textAutoResize='HEIGHT'` + `layoutSizingHorizontal='FILL'` + `layoutSizingVertical='HUG'`；用 padding 内缩（左 padding 让出 4/10px 品牌左 stroke）。

**Why**：固定高度 = 内容多则截断、内容少则大片空白，且后续改文案不重排。Auto Layout hug 让卡永远贴合内容；统一宽度避免同组卡参差。

**反例**：`note.resize(W, fixedH)`；或创建时按内容定高一次、后续改 `characters` 不重算 → 同一组卡截断与空白并存。

**Acceptance**：
- 任一 UX 交付卡**不得**手设固定高度；必 `primaryAxisSizingMode='AUTO'`
- 同一交付集合内卡宽一致（标准值）
- 改卡内文案后无需手动 resize 卡高

**实证 — 2026-06-05 Graphics Insertion**：state-note 与 spec 卡用固定高度，编辑文案后有的截断有的空；宽度也逐卡手调参差 → 用户抓"高度没自适应 / 宽度也该默认"。改竖向 Auto Layout hug + 标准宽度即修。

#### M23.14 — 双语堆叠排版的行距与分组（line/paragraph spacing，M23 §双语 extension）— 2026-06-18 新增

§双语 现行只规定了字体 / 字号 / 颜色 / opacity / 布局选择（A-右 / A-下 / A-逐行 / B），**未规定行距与分组**。当 ZH 译文以统一行距堆在 EN 行下方（A-下 / A-逐行 / 多语堆叠的交付卡 / PRD 卡正文）时，`EN→其 ZH` 的间距 = `对→对` 的间距 = `段→段` 的间距 → Gestalt 邻近性失效，「一句 EN + 它的 ZH」不成组，注释读成行数翻倍的密集文字墙（用户感知 = "拥挤"）。本规则补这个洞。

**核心原则：差异化间距制造分组（不是整体放大）。** 三档间距必须**可视递增**，让「EN + 其 ZH」读成一个单元、单元之间、段落之间有递增的呼吸空间：

| 层级 | 相对间距 | 默认值（按本卡 EN 字号 `S` 比例锚定，不写死任意 px）| 机制 |
|---|---|---|
| **同对内**：EN ↔ 其 ZH 译文（intra-pair）| **最紧** | ZH 行 lineHeight ≈ `ZH字号 × 1.15`（ZH 贴住其 EN）| range API：给 ZH range 设紧 lineHeight |
| **对与对之间**：同段内 bullet ↔ bullet（inter-pair）| **中** | ≈ `S × 0.6`（EN 14px → ~8px，吸附 spacing scale）| EN 行 lineHeight 加大到 `S × 1.8` 让每个 EN 自带"与上一对之间"的间隔；或 per-bullet Auto Layout `itemSpacing` |
| **段与段之间**：Source / Background / Requirements 块（inter-section）| **最大** | ≈ `S × 1.2–1.5`（~16–24px）| section 竖向 Auto Layout `itemSpacing`；或 section 标题行 paddingTop |

**数值锚定**：不写死绝对 px——按卡内 EN 字号 `S` 比例 + 吸附既有 spacing scale（vertical rhythm 真源见本文件 §textStyle grid「11px off-grid 破 baseline」段 + [`binding-source-map.json`](../../figma-data/normalized/binding-source-map.json) 可绑 Number Var）。同一交付集合内三档值一致，不逐卡手调。

**机制选择（按 artifact 类型分流，与 §双语 既有约束不冲突）**：

| artifact | 机制 |
|---|---|
| **大卡**（UX 交付卡 / PRD 卡，M23.13 默认竖向 Auto Layout）| **首选 per-section 一个 text node + range lineHeight**——复用 §双语 已有的 ZH range cursor-walk（line ~545 / probe line ~622），在走 ZH range 设 opacity 的同时给 ZH range 设紧 lineHeight、EN range 设松 lineHeight；section 之间用 Auto Layout `itemSpacing`。per-bullet「一个 bullet = 一个 text node + Auto Layout itemSpacing」是等效替代 |
| **小 chip**（state label / header chip，单行）| 维持 §双语 既有约束：单节点 range API、**不新增 child node**（避免撑高 auto-layout，见 §双语 反例）。chip 单行无分组问题，本规则不适用 |

**Why**：差异化三档间距是 Gestalt 邻近性的直接应用——分组靠"组内紧、组间松"，而非靠分隔线 / 加粗 / 换色（那些会增加视觉噪声，违 §双语「中文永远是附注不抢权重」）。整体放大行距只会让卡更长且仍不分组，是治标。

**Acceptance**：
- [ ] `EN→其 ZH` 间距 < `对→对` 间距 < `段→段` 间距（三档**可视递增**）
- [ ] 扫读时「EN + 其 ZH」一眼成组，能分清哪条 ZH 属于哪条 EN
- [ ] lineHeight / itemSpacing 按 EN 字号比例或吸附 spacing scale，无任意 off-grid px
- [ ] **机器闸**：`pnpm audit:mockup-bilingual-spacing --file <key> [--node <id>]`（已挂进 `audit:mockup-conformance` G4）—— 多对堆叠单节点 EN 行 lineHeight ≥ ZH 行 lineHeight × 1.3（无差异 = uniform/auto = 拥挤 → FAIL）。**per-bullet + Auto Layout itemSpacing 机制不机器扫**（脚本会 log skipped 数，留本表上一档人工 自查），无需截图
- [ ] 不靠分隔线 / 换色 / 加粗 制造分组（仅靠间距）

**实证 — 2026-06-18 V4-1864 + V4-1360 Per-SIM Roaming + Access-Tech PRD 卡**：每条 EN 行下加 ZH 译文后用统一行距，三档间距无差异 → 卡显拥挤、ZH 与 EN 不成组。用户："加了中文翻译在下面，所以显得比较拥挤……把这个规则制定一下"。本规则即此回流。

> **范本债务标注（2026-06-23 V4-1827）**：催生本规则的 V4-1864 PRD 卡（建于 0608，pre-M23.14）**自身从未按本规则修正**，仍是统一行距。2026-06-23 画 V4-1827 NDI PRD 卡时 AI 照抄该范本 → 继承拥挤间距 + meta 行混入 + 段名无层级，用户连续纠正。**已按本规则修正 V4-1864 PRD 卡**；并新增 §M23.16 防再次继承 pre-规则范本债务。**勿再照抄 pre-规则范本的可量化属性（lineHeight / itemSpacing / 层级），以规则值为准。**

#### M23.15 — 卡内模块段名层级（section heading：cyan + 大号，与正文拉开）— 2026-06-23 新增

PRD 卡 / UX 交付卡内的**模块段名**（PRD 的 Source / Background / Current State / Requirements / Acceptance / Priority & Timeline；UX 卡的 Why / Changes / Interaction / … 等章节名）必须与正文形成**可视层级**——不得与正文同字号同色，否则段名淹没在正文里、模块入口不清，整卡读成无分段的文字流。

| 文本角色 | 样式（EN / ZH） |
|---|---|
| 卡大标题（card title）| Roboto Bold 18 + 白 `#FFFFFF` / ZH 16 同色 opacity 0.45 |
| 卡副标题 · meta 行 | Roboto Medium 12 + cyan `#33A4FD` |
| **模块段名（section heading）**| **Roboto Medium 15 + cyan `#33A4FD`** / ZH 13 同色 opacity 0.45 |
| 正文（body bullet）| Roboto Regular 13 + 白 `#FFFFFF` / ZH 11 同色 opacity 0.45 |

- 段名 cyan 呼应 M23 卡 title cyan（`#33A4FD`）；比正文大 **2px**（13→15）+ 换 **Medium** 字重 + 换色，三重区分。
- ZH 段名照 §双语：与 EN 段名**同色** cyan + opacity 0.45 + 字号小 2px（15→13）。
- 行距照 §M23.14：段名行 lineHeight 偏松（制造段间呼吸），其 ZH 紧贴。

**Why**：段名是模块入口，dev/QA 扫读靠它定位。段名与正文同样式 = 失去信息架构层级。

**反例 — 2026-06-23 V4-1827 NDI PRD 卡 v1**：段名（Source/Background…）照抄 V4-1864 范本用白色 Regular 13，与正文无区分，用户问"为什么模块标题没用蓝色大号"。改 cyan Medium 15 后层级清晰。

**Acceptance**：
- [ ] 卡内模块段名 = cyan `#33A4FD` + Roboto Medium + 字号 > 正文（probe `getStyledTextSegments` 验 fill / size）
- [ ] ZH 段名同 cyan + opacity 0.45 + 小 2px

#### M23.16 — 规则优先于范本：防继承 pre-规则范本债务（meta · cross-cutting）— 2026-06-23 新增

`figma-use`「inspect the file, match what's there」要求对齐现状——但**范本 ≠ 规则真源**。当最近的同类范本建于某规则回流**之前**，照抄它的可量化属性会把违规债务一并继承。

**反模式**：开建 PRD / UX 卡前 dump 最近同类范本的 lineHeight / itemSpacing / 层级当"范式"直接照抄，**未先用规则真源（M23 系列 + §双语）校验范本**。范本若 pre-规则（如 V4-1864 pre-M23.14），即继承拥挤间距 / meta 混入 / 段名无层级等已知违规。

**正解（开建 UX / PRD 卡前必做）**：
1. **先 jump-read 规则真源**（M23 + §双语 + M23.14 + M23.15）建立"该长什么样"的标准；范本仅用于对齐**视觉 token**（颜色 / 结构 / 组件 / 命名），不照抄有规则管的可量化属性。
2. **有规则管的属性用规则值**（lineHeight / itemSpacing / 段名层级 / 双语 opacity）；范本实测值与规则冲突时**以规则为准**。
3. **建完卡立即跑机器闸** `pnpm audit:mockup-bilingual-spacing --file <key>`（及 `audit:mockup-conformance`），不等用户指出。

**实证 — 2026-06-23 V4-1827 NDI PRD 卡**：AI 照抄 V4-1864（pre-M23.14）范本 → 继承拥挤间距 + meta 行 + 段名无层级，用户连续 3 轮纠正（间距 / 段名色 / meta + 超链接）。根因：范本驱动而非规则驱动，未在开建前用规则校验范本。

**Acceptance**：
- [ ] 开建前 M48 rule-coverage self-check 含"范本 vs 规则真源校验"一项
- [ ] 建完卡跑 bilingual-spacing 机器闸通过后才进 Gate

#### M23.8 — Jira requirement annotation 必带超链接（M23 extension）— 2026-05-27 新增

任何 mockup 加 `Jira requirement` component instance 时，**Issue key text node（"FB-xxxx"）必须 `setRangeHyperlink` 指向真实 Jira URL**，不能只写文字。

**触发**：mockup 含 jira annotation / FB-xxxx 编号 / 需求关联 → 强制。

##### `Jira requirement` 组件 node 地址（per-file 登记，免每次猜测）— 2026-06-03 新增

`Jira requirement` 是 **file-local 组件（每个产品文件各有一份、不发布）**→ `importComponentByKeyAsync` 跨文件会 fail（"Component with key not found"）。**正确做法：在目标文件内 clone 一个现成 instance，改 title text + issue-key text + `setRangeHyperlink`。** 各文件已知 key / 可 clone 的 instance：

| 产品文件 | fileKey | component key | 可 clone 的现成 instance |
|---|---|---|---|
| Config-T (Local UI) | `rJJjWWs51n2iFOlCIC7aYG` | `a6a34623f80b763077b53d7c31ecce046d498c2d` | （V4-1865 等页内同源 instance）|
| Touch Screen v7.7/8.0 | `0054ib0nLmt27bC3QlGDl7` | `e8604a08a31c8c17cb8aec35d7692e88965485e7`（变体 `Priority=High`；本地 mainComponent `5315:9604`）| `5356:4431`(V4-1542) / `5315:19564`(V4-1535) / `5392:17702`(V4-1543) |

> 新产品文件首次用时：用 `findAll(INSTANCE, name~/jira|requirement/)` 在已有 Jira 关联 page 上找一个 instance，clone 它；并把该文件 key + instance 追加到本表。组件结构：1 个 title text（大号）+ 1 个 issue-key text（带 hyperlink）。

**实现**：
```js
issueKeyText.setRangeHyperlink(0, issueKeyText.characters.length, {
  type: "URL",
  value: "https://tvunetworks.atlassian.net/browse/FB-9398"
});
```

**Why**：mockup 是给 PM / dev / QA 跨角色共读的，"FB-9398" 纯文字 reader 还得手动复制到 Jira 搜——hyperlink 一步直达，且 Figma annotation 是单向只读不可编辑，hyperlink 一次设对永久受益。

**反例**：2026-05-27 FB-9398 mockup v1 加了 jira component 但只改 text 没设 hyperlink → 用户抓"JIRA 的链接没带上去"。

**Acceptance**：`probe → issueKeyText.getRangeHyperlink(0, len)` 返回 `{ type: 'URL', value: '<jira url>' }` 非 null。

##### M23.8.1 — 需求来源链接完整性 + 全部可点击（PRD「需求来源/Source」段 + 任意 deliverable，create/update 都遵守）— 2026-06-23 新增

M23.8 主条只管"加 `Jira requirement` 组件时其 issue-key 必带 hyperlink"。本子条把范围扩到 **PRD「需求来源 / Source」段 + 交付物内任意 ticket / 外链引用**：

1. **列全需求链**：需求来源须枚举**所有相关 ticket**——客户 / Epic（如 `SEC-*`）+ 功能请求（`FB-*`）+ 开发任务（`V4-*` / `BM-*` 等），不能只写开发任务号。一个功能常跨多个 Jira（`FB` 提需求 → `V4` 拆开发 → `SEC` 客户来源），漏任一个 = reader 看不到完整溯源。来源以 Jira issuelinks（implements / relates to / parent）为准核全。**遍历须到二度**：命名 task 的直接 issuelinks 常只有它实现的 FR；FR 上的 `relates to` bug（客户现场缺陷等）**不在命名 task 的直接 links 里**——须再对该 FR `getJiraIssue` 取其 issuelinks 捞全，否则二度关联 ticket 漏列。
2. **每个引用都可点击**：所有 ticket ID **必须 `setRangeHyperlink` 指向真实 Jira URL**（`https://tvunetworks.atlassian.net/browse/<ID>`），禁纯文本 ID。建议同时把 ID range 染 accent cyan `#33A4FD` 作"可点"视觉提示。
3. **Slack 等外链同样收录可点**：若需求从 Slack 线程 / 其它外部源解码，相关链接也须放进需求来源并 `setRangeHyperlink`。
4. **create 与 update 都遵守**：新建 PRD 时列全；后续需求**新增关联 Jira / Slack 时，update 必须回补**到需求来源——不是只在初版做一次。

**触发**：写 / 改任何含需求溯源的 PRD / 交付卡时强制。

**实现**：
```js
// 对来源行每个 ticket ID：
const i = textNode.characters.indexOf("FB-9586");
textNode.setRangeHyperlink(i, i + 7, { type: "URL", value: "https://tvunetworks.atlassian.net/browse/FB-9586" });
textNode.setRangeFills(i, i + 7, [{ type: "SOLID", color: { r: 0.2, g: 0.643, b: 0.992 } }]); // cyan 可点提示
```

**Why**：PRD 是 PM / dev / QA 跨角色溯源入口。只写开发任务号（`V4-*`）会丢掉"为什么做"（`FB` 功能请求 / `SEC` 客户 Epic）；纯文本 ID 要手动复制去 Jira 搜，hyperlink 一步直达。

**反例实证**：2026-06-23 V4-2285/2286 Embedded Audio PRD —— 需求来源只写 V4-2285/V4-2286，漏 FB-9586 / FB-9587（功能请求）+ SEC-669（客户 Epic），且全是纯文本不可点击。用户抓"需求缺 FB JIRA + PRD 链接为什么不可点击"→ 回补全链 + 每个 ID setRangeHyperlink + cyan 高亮。

**第二次复发实证**：2026-06-23 V4-2259（同 file `0054ib`）—— PRD 需求来源初版只写 V4-2259（开发 task），漏 FB-8926（FR，V4-2259 implements 它）+ FB-9619（二度 bug，`relates to` FB-8926、**不在 V4-2259 直接 issuelinks 里**）+ 父 Epic V4-1676，且纯文本不可点。用户三次追问（"需求来源全了吗 / 相关 Jira 都带上了吗 / FB-9619 为什么没关联上"）才补全。**两次复发 = activation 层反复失败**——PRD 起草/重建（尤其委派 subagent 执行时）须把本条写进执行 spec 并遍历到二度关联，不能只靠"读了 Step B"。

**Acceptance**：需求来源段列全需求链上所有相关 ticket（Jira issuelinks 核对无遗漏）；`probe → getRangeHyperlink(idStart, idEnd)` 对每个 ID 返回非 null URL；交付物内无纯文本 ticket ID；外链（Slack 等）同样带 hyperlink。

---

### M24 — Code→Token Mapping Table（仅 "已有 code 还原效果图" 场景前置硬规则）

**触发条件（严格 AND）**：
1. 任务表达明确含 "根据已有 code 还原效果图" / "match the running implementation" / "把 vue-app 翻成 figma" / "参考 demo HTML" 等措辞，**或**用户主动指向 source file 作 visual truth；**且**
2. 该 source 直接定义了视觉 token / CSS variables / 颜色常量（不是只引用别人发布的 token）。**source 的形式不限**：
   - 多文件项目：`.vue` / `.tsx` / `.jsx` / `.svelte` + 独立 `.css` / `.scss` / `tokens.css`
   - **单 HTML 文件 demo**：`<style>` 内联 CSS（含 `:root { --xxx: ... }` 块）/ `<script>` 内联 JS 常量
   - Tailwind config / theme.ts / tokens.json / Storybook theme
   - 任何形式只要能找到"定义视觉常量 → 后面被组件引用"的代码段

> ⚠️ **不只看文件后缀**——`.html` 单文件 demo 里 `<style>:root { --brand: #2fb54e; --brand-hover: #41c760; }` 与多文件项目的 `tokens.css` 等价，**同样触发 M24**。本次实战实证：`microapps-console-planb.html` 单文件 + `vue-app/src/assets/tokens.css` 多文件项目两种形式都是有效 source。

**不触发**（避免误用）：
- **Greenfield mockup**（US-1 / US-2）— 无 code，从 spec/brief 起 → 跳过此规则，按常规 Phase 0 走
- **纯 UX 探索 / brainstorm 阶段** — 还没确定方向 → 跳过
- **US-3 增量但无 code spec** — 只在既有 Figma 上加新元素 → 走 M21 sibling visual contract，不走 M24
- **改 mockup 视觉但 code 不存在** — 设计先行场景 → 不触发
- **spec.md 提到颜色但无 source code 引用** — 走常规 catalog grep + library search

**问题**：当 code 存在时，spec 文档 + code 里出现的 CSS 变量名（如 `--brand-hover` / `--bg-layer4` / `--status-inactive`）是**工程师的术语**，不是 Figma 库的 Variable 名（`UX/Brand/Hover` / `Color Type/Background/Layer_4`）。**AI 凭直觉把 "inactive grey" 映射到 `UX/Grey/grey-7` 是典型踩坑**——grey-7 是 TEXT 色不是 BG 色。

**触发后必做**：动手 Phase 0 之前先产 `Code→Token mapping table`：

| Source 引用 | 出现位置 | TVU library variable | Key | 用途 |
|---|---|---|---|---|
| `--brand` | sidebar active row | `UX/Brand/Brand` | `ea8c2383...` | active state primary |
| `--brand-hover` | sidebar hover | `UX/Brand/Hover` | `eab4ef3b...` | **hover state** — 严禁用 `UX/Brand/Brand` |
| `--bg-layer1` | canvas | `Color Type/Background/Layer_1` | `1c7389d2...` | page bg |
| `--bg-layer2` | card bg | （library 未发布，hex `#1f1f1f`） | — | 标记 🟡 入库候选 |
| `--bg-layer3` | row hover / chip bg | `Color Type/Background/Layer_3` | `93476370...` | elevated bg |
| `--bg-layer4` | inactive status segment / elevated header | `Color Type/Background/Layer_4` | `f576bb4f...` | inactive **fill**（不是 text） |
| `--text-tips` | secondary text | `Color Type/Text/Tips` | `b3b5983a...` | TEXT only — 禁止做 fill |
| `--brand-bg` (rgba 0.18) | translucent brand chip | — | — | library 未发布，用 child Rectangle + `node.opacity=0.18`（per Q3 嵌套 instance edge case） |

**Why**：spec 里写 `--status-inactive: var(--text-tips)` 时是说"pill TEXT 用 tips"，不是"任何叫 inactive 的元素都用 tips"。**Status BAR 的 inactive 段是 BG fill，必须用 Layer_N，不是 Text 系列 Grey**。

**反例实证**（MicroApps Console Plan B 2026-05-14）：
- Sidebar Hover 误用 `Layer_3`（生灰），缺品牌色 hover affordance → 修正为 `UX/Brand/Hover @ 0.10`
- 状态条 inactive 段误用 `UX/Grey/grey-7 #7B7B7B`（text color）→ 修正为 `Color Type/Background/Layer_4`
- 都因为没产前置 mapping table，凭"哪个 grey 看着对就用哪个"踩坑

**判断流程**：
1. 任务描述里有 "已有 code" / "running impl" / "翻成 figma" / "match the code" / "参考 demo" 类字样吗？→ 是 → 检查 (2)
2. 真有 source 可读吗（多文件项目 OR 单 HTML demo 含 `<style>:root` token 块 OR Tailwind/theme 配置 等任意形式）？→ 是 → **M24 触发**，产 mapping table
3. 否则 → **不触发**，按常规 Phase 0 + library catalog grep + sibling visual contract（M21）走

**自检反例**（如下情况误触发 M24 就是过度套规则）：
- "画一个 dashboard mockup" → greenfield，不触发
- "Plan A 已有 Figma，画 Plan B 对比方案" → 无 source code，走 M21
- "改下 button 颜色" → US-5 小调整，跳所有前置
- "设计师让我画 onboarding 流程" → 无 code，走 Pre-Phase 0 + Phase 0

---

### M-INTEGRITY — Mockup Layout Integrity（umbrella，覆盖原 M27 / M28 / M34）

凡是改 Section / Frame 布局或大小后，必须按 3 个 sub-clauses verify 完整性。同源——都是"layout 完整性的不同视角"——合并为一组以便 audit 一次跑完。

**Legacy ID 映射**：M27 → §I1（sibling layout consistency），M28 → §I2（sibling section overlap），M34 → §I3（children bbox wrap）。

**自动化 audit**：`scripts/audit-mockup-integrity.mjs`（一次扫完 3 个 sub-clauses）。

---

#### I1 — Sibling Plan/Version Layout Consistency（原 M27）

任何 product Figma 文件里同时存在多个 Plan / version section（如 `Plan A: ...` + `Plan B: ...` / `v1 ...` + `v2 ...` / **`BEFORE` + `AFTER`**）时，**新 section 的 layout 风格必须 mirror 已存在的 sibling**。

> **与 BEFORE/AFTER 规则的接续**：[`design-process.md` 步骤①](./design-process.md) 要求 BEFORE 与 AFTER **分属不同 Section**；本 I1 接着要求这两个对照 section **布局互相 mirror**（便于并排 review）。两条规则配套——"分开" + "对齐"——缺一不可。

**Mandatory probe (起手协议)**：开始画前必须执行：

```js
// 1. 找同 page 所有 SECTION
const sections = [];
for (let i = 0; i < page.children.length; i++) {
  try { const c = page.children[i]; if (c.type === 'SECTION') sections.push(c); } catch (e) {}
}
// 2. 找名字含 "Plan" / version 类的 sibling
const sibling = sections.find(s => /Plan [A-Z]|v\d|版本|BEFORE|AFTER/i.test(s.name) && s.id !== currentSection.id);
// 3. 走查 sibling 的 layout：frames 排列 / annotations 摆放 / connector 风格
```

**Layout style 要 mirror 的维度**：
- Frames 排列（横向 row / 纵向 stack / N×M grid）
- Pair 配对方式（按 view / 按 persona / 按 state）
- Annotations 摆放位置（4 边围绕 / 单侧列 / 散布）
- Connector 形态（短直线 / L 形 / 长对角）
- Section 长宽比（≈ 2:1 / ≈ 1:2）

**不 mirror 的情况**：用户**明确**说"这次跟 Plan A 风格不一样，因为..."。否则一律 mirror。

**违例 history**：2026-05-14 Plan B 没 probe Plan A 就画**纵向 stack** (3280×9806)；Plan A 实际是 **横向 3×2 pair** (11829×5130)。三轮 retrofit (v1 → v2 → v3) 才纠正。

##### I1.1 — Section 含 10+ frames 时推荐 2-col × N-row reading-order grid（2026-05-26 新增）

当 spec section 含 **10 个或以上**主 frame（state mockup / variant mockup / 流程帧）时，默认 layout 模式：

| 维度 | 推荐值 | Why |
|---|---|---|
| Grid 形态 | **2 cols × N rows** | 自然 left-to-right top-to-bottom 阅读顺序 |
| Col 1 x | `section_left + 100` (page padding) | 留 section 左边距 |
| Col 2 x | `col1_x + frame_w + 200` (gap) | 200px col-gap 视觉清晰 |
| Row pitch | `frame_h + 400` | 含 state-label 居中 + 上下 breathing；少于 400 时 state-label 上下挤压 |
| 阅读顺序 | M1, M2 / M3, M4 / ... 按编号 | reader 按编号顺序在 2-col 中找帧 |
| State-label 位置 | 每 frame 正上方 `frame.y - label.h - 50` | 50px gap 让 label 不贴 frame |

**反例**：
- 5-col × 2-row：宽度 ~10000+，浏览器需横向滚动，破坏 reading flow
- Scatter（无 grid）：reader 不知阅读顺序，找帧靠 search
- Pitch 1280（gap=200）：state-label 上下仅 ~47px 喘息，挤压感强

**适用范围 + 例外**：
- 适用：spec mockup section 含 10+ frame 时
- 例外：< 10 frame 不强制 grid（可用 1-row pair 或 cross-layout）
- 例外：流程图 / annotation 注释 section 不适用（M23 family 另有规则）

**Code 端镜像**：无（属 figma 文件 spatial layout 规则，code 端无对应）。

**实证 — 2026-05-26 Video Sync Phase B Step 3**：11 frames 初版用 pitch 1280 (gap=200) → state-label 与上下 frame 仅 47px → 用户抓"间距不够大，UX 交付说明离上下太近，需要界限更清晰" → 改 pitch 1480 (gap=400) 后舒适。

---

#### I2 — Sibling Section Overlap Probe（section.resize 后 mandatory）（原 M28）

调用 `section.resize()` / `section.resizeWithoutConstraints()` 改 section bounds 后，**必须** probe 同 page 所有其他 SECTION 检查 bounds 不重叠。

```js
// mandatory after any section.resize
function checkOverlap(target, page) {
  const overlaps = [];
  for (let i = 0; i < page.children.length; i++) {
    try {
      const s = page.children[i];
      if (s.type !== 'SECTION' || s.id === target.id) continue;
      const overlap = !(s.x + s.width <= target.x || s.x >= target.x + target.width ||
                       s.y + s.height <= target.y || s.y >= target.y + target.height);
      if (overlap) overlaps.push({ id: s.id, name: s.name });
    } catch (e) {}
  }
  return overlaps;
}
// 若 overlaps.length > 0：STOP，调小 target section 或移 sibling
```

**Why**：Section 扩大后视觉上可能覆盖邻居 section 内容；Figma 不会拒绝重叠 section（不像 frame 的 absolute pos），但视觉效果错乱。Local Components / Reference / Draft section 都是常见邻居。

**违例 history**：2026-05-14 Plan B v2 纵向 stack resize 到 3280×9806（canvas y=8389-18195），压住 sibling `MicroApps PlanB — Local Components`（y=15463-17663）。重叠 ~2200 px 才被用户抓出，retrofit v3 才发现 + 修。

---

#### I3 — Section / Frame children-bbox wrap audit（原 M34）

任何 SECTION 或非-auto-layout FRAME 内塞了 **会自动撑大的子节点**（auto-layout 子 frame、text node with `textAutoResize='HEIGHT'` 等）后，**必须** probe 子节点最终 bbox 并按需把容器撑到包住所有子。

**Why**

- **Section 不是 layout 容器** —— 是组织容器，children 完全自由 positioned + 容器**不会** auto-hug
- **非-auto-layout frame 同理** —— 只有 `layoutMode !== 'NONE'` 的 frame 才会随子内容自动调整大小
- Auto-layout 子节点（`primaryAxisSizingMode='AUTO'`）的高度只在 append 后 / 子内容变化后才确定，初始 `resize(w, h)` 给的值会被覆盖

**必跑 probe**：

```js
const children = container.children.map(c => ({
  id: c.id, name: c.name,
  right: c.x + c.width, bottom: c.y + c.height,
}));
const maxRight = Math.max(...children.map(c => c.right));
const maxBottom = Math.max(...children.map(c => c.bottom));
const overflow = children.filter(c =>
  c.right > container.width || c.bottom > container.height ||
  c.x < 0 || c.y < 0
);
if (overflow.length > 0) {
  container.resizeWithoutConstraints(
    Math.max(container.width, maxRight + 40),
    Math.max(container.height, maxBottom + 40)
  );
}
return { overflowCount: overflow.length, overflow };
```

**触发时机**：

- `container.appendChild(autoLayoutChild)` 后
- `text.characters = '...'`（如果该 text 在 auto-layout 子里）后
- `child.itemSpacing` / `child.padding*` 变化后
- 任何 M23 注释 chip / 规则卡 内容改完后

**违例 history**：2026-05-18 Touch-Screen v8 — 4-source flow rules card 加双语规则文本后撑高到 651（auto-layout `primaryAxisSizingMode='AUTO'`），section 只 1500 高 → 底部 251 px rules card 内容跑到 section 外。fix: `section.resizeWithoutConstraints(1740, cardBottom + 40)`。**根因**：以为 section 是 frame-like 容器会自动 hug children。

---

#### I4 — Element Parent Discipline（2026-05-28 新增）

任何添加到 mockup 的视觉元素（NEW badge / 提示 chip / 装饰矩形 / 状态指示器 / annotation marker 等）**必须 `appendChild` 到所属 product mockup frame**——不允许直接 `appendChild` 到 page 层成为孤儿元素。

#### 触发场景

| 元素类型 | 正确 parent | 错误 parent |
|---|---|---|
| 列头 NEW badge / 标签 chip | 所属 table header instance 的容器 frame | page |
| 行高亮 / row marker | 所属 row 的 frame | page |
| Inline 提示 chip（hover hint / "click me"）| 所属 trigger element 的 frame | page |
| 状态指示器（spinner / error dot） | 所属 input / cell frame | page |

#### 例外（允许 page 级）

仅 **M23.9 state-preview elements** 是例外——dropdown / popover / context menu 等 overlay 状态预览必须放在产品 frame 外，且必有 M23.6 connector 指回 trigger（此例外的反例：放 page 层但无 connector → 违 M23.9）。

#### 反例

| ❌ | 原因 |
|---|---|
| `newPage.appendChild(badge)` 放到列头上方 | mockup 移动 / 复制时 badge 留在原位置，关系断裂 |
| 装饰元素 absoluteBoundingBox 与 product frame 无 spatial 关联 | reader 不知道这个 chip 属于哪里 |
| Page-level orphan element 用绝对坐标硬贴在 mockup 上 | 后续 mockup 平移 → orphan 留在原 (x, y)，立刻错位 |

#### Acceptance

- [ ] 所有 mockup 装饰元素的 `parent` 是某个 product frame / cell instance，不是 PageNode
- [ ] 唯一允许 page 级 parent 的视觉元素是 M23.9 state-preview（且必有 connector）

#### 实证

**2026-05-28 TPC-628**：给 T list "Platform" / "Dial Version" 两个新列加 NEW badge 时直接 `newPage.appendChild(badge)`，badge 用绝对坐标 (colInst.absoluteBoundingBox.x, ...) 浮在列头上方。用户抓"页面上还叠加了 New Badge 这两个 Frame，不知道是什么"——badge 飘在 mockup 外、跟产品脱节、reader 不知道这两个浮 chip 是什么。**根因**：M-INTEGRITY 原本只覆盖 layout 大小完整性，没规定 element parent 归属。本规则即此回流。

---

#### M-INTEGRITY §I5 — 替换既有节点前先 diff（2026-06-17 新增）

覆盖 / swap / clear / 整体重建任何**既有节点**前，**先读其当前状态 + 用户近期改动**，做最小差量替换，不可大刀阔斧整体替换而**丢失用户手动改动**。新建"页面 / 流程态"前先确认它挂在哪条**既有 UX flow** 上，不建 off-flow 孤儿物料。

- **Acceptance**：动既有节点前 handoff/wrap-up 能答"它原本是什么 + 用户最近改了什么 + 我只改了 delta"；新页面能答"它在 flow 的哪一步、从哪个入口进"。
- **实证 — 2026-06-17 BM-1047 V3**：(1) 把下拉按钮整体换 DS 组件时 clear 了 action-buttons，**清掉了用户手动加的 Token 图标**（用户："我加好的你又去掉了"）；(2) 自建 Purchase 弹窗放在 off-flow 的 POPUPS lane，而真正的购买页是流程内的 Case1——"你说的那个页面没在我们的 UX 流程上"。

#### M-INTEGRITY §I6 — create / move / place 前先 probe 目标坐标区（防叠加）（2026-06-17 新增）

新建 / 移动 / 放置任何节点到指定坐标**前**，**先 probe 目标坐标区 bbox 是否已有内容**（与现有兄弟节点求交）——确认为空才放，否则显式 reflow 让位。**不可按"算出来的坐标"直接 place 而不查现状**。

- **与 §I2 区别（关键）**：§I2 是 `section.resize` **后**的 overlap 兜底（仅 SECTION↔SECTION）；§I6 是 place / create **前**的预防 probe——**前置 gate，不是事后 QA**。两者方向相反、都要跑。
- **机器后置兜底**：§I6 是 L1 自律前置 probe，本身无机器拦截；其**未防住的叠加结果**由 `pnpm audit:mockup-overlap --file <key> --node <id>` 写后机检（扫 SECTION / CANVAS 直接 block 子节点两两求交，不下钻产品 frame 内部层叠 → 零误报）。已挂进 `audit:mockup-conformance`。I6 防、overlap audit 抓，配套。
- **Acceptance**：任何 create/move 到具体坐标的操作，handoff/wrap-up 能答"放之前 probe 了目标区、确认空 / 或 reflow 了谁"；放置后立即跑 `audit:mockup-overlap`（+ §I2）求交复检。
- **实证 — 2026-06-17 BM-1047 V3**：反复按算出的坐标直接放置导致叠加——M23 触发图例放 y1316 压上一张 `DD·role·Member` 卡（健康列非空，AI 误判为空需移位）；新建角色态卡未带健康卡领头、行不对齐。用户："创建的时候没检查当前位置是否有内容，直接放置，导致总是出现图标叠加的问题"。根因：place 前不 probe 目标区。

#### M-INTEGRITY 共用 Acceptance

- 一次 probe 跑全：`pnpm audit:mockup:integrity -- --file <fileKey> --page <pageId>`（脚本扫 §I1 / §I2 / §I3 / §I4 四条）
- §I5（替换前 diff）/ §I6（place 前 probe）是 L1 自律前置 gate，**整套 conformance 跑 `pnpm audit:mockup-conformance --file <key> --node <id>` 含 §I6 的机器后置兜底 `audit-mockup-overlap`**（block 级叠加）
- handoff doc 必含 "Integrity audit: I1=pass / I2=overlap-gap N px / I3=overflow-count 0 / I4=orphan-count 0 / overlap(block)=0"
- Legacy ID 引用兼容：M27 / M28 / M34 anchor 在文末"Legacy ID Map"段维护重定向

---

## §M-LIFECYCLE — Mockup Edit Lifecycle Gate（创建/更新/删除：前·后·走查 三时点检查）— 2026-06-18 新增

> **为什么单立一条**：本项目反复出现「规则已存在但没在对的时点触发」的 silent 失败（STATUS 实证：6 process gap 中 5 个同源此模式；I5/I6/M23.6 probe 都"写了没跑"）。维度有没有规则不是瓶颈，**有没有在 create/update/delete 的前、后、走查三个时点真跑**才是。本闸把已有规则钉到三时点，不重造规则、只锁触发。

**任何对 TVU 产品 Figma 的 create / update / delete 操作，三时点都必须过：**

### ① BEFORE（操作前 — 预防）

| 操作 | 前置检查 | 真源 |
|---|---|---|
| **create / move / place** | 先 probe 目标坐标区 bbox 与现有兄弟求交，确认为空才放、否则显式 reflow 让位；新建卡带 row 领头对齐 | §I6 |
| **update（改既有节点）** | 先 live 重读目标节点当前状态 + 用户近期手改，做最小 delta；不信记忆 / 旧截图 | §I5 + [[feedback_external-claims-verify-on-move]] |
| **delete** | 删前看内容确认确是要删的（不只看 name）；查有无 connector / reference 指向它，连 connector 一起删 / 重连，不留 orphan 线 | §I5 + §M23.6 |

### ② AFTER（每个写操作批次后 — 即时机检，不拖到交付末尾）

- 跑 `pnpm audit:mockup-conformance --file <key> --node <id>`，`--node` 覆盖**本批次触碰的所有节点（不止新建）**。含 integrity(I1-I4) + colors + typography-icon + library-origin + binding-fidelity + **bilingual-spacing(M23.14)** + **overlap(I6 兜底)**。
- 连线几何另跑 `use_figma` `checkConnectorOrthogonality`（§M23.6；REST 不可机检 centerline）。
- 任一非 0 exit / 违例 → **当场修，不 silent pass**（declared≠consumed 反模式）。

### ③ WALKTHROUGH（design-walkthrough 走查时 — 兜底全扫）

- 三轴固定层全跑（[`design-walkthrough` skill](../../skills/design-walkthrough/SKILL.md) §执行协议 step 8 keystone）；机械维度**贴机检 pass/fail 原文、禁目测打 ✅/N/A**；机检 `--node` 覆盖**全 session 触碰节点**。
- 机械维度全集（跑 conformance 一次覆盖）：integrity I1-I4 · colors · typography-icon · library-origin · binding-fidelity(D16) · **overlap(block 叠加)** · **bilingual-spacing(M23.14)**；连线正交另跑 M23.6 probe；位置紧邻(M23.12) 仍自查。

**Enforcement 现实（诚实标注）**：Figma 写操作本身无机器可拦（AGENTS 硬规则 #1，L1）。三层强制：
- **① BEFORE**：L1 自律 + `UserPromptSubmit` hook（`detect-figma-task.sh`）起手注入协议。
- **② AFTER**：`PostToolUse` hook（`post-figma-write.sh`，挂 `use_figma` 写工具）每次 Figma 写后注入「报 done 前必跑 conformance」提醒 —— 把"忘了跑写后机检"从「AI 要记得」变成「harness 每次写后都提醒」。机检本身（conformance / overlap / bilingual / connector probe）是确定性写后判。
- **③ WALKTHROUGH**：design-walkthrough 三轴兜底全扫。

**仍消不掉的边界（诚实）**：PostToolUse hook 只能**提醒**、不能**替跑** audit（要 token + nodeId），也不能保证 `--node` 范围覆盖全部触碰节点；动手**前**的实时拦截 Figma 不给接口。即"前置防不住 → 后置 hook 逼机检 → 机检确定性判"三层是此架构上限，无法做到「不可能违反」。

---

### M27 — Sibling Plan/Version Layout Consistency

> **⚠️ Merged into [M-INTEGRITY §I1](#m-integrity--mockup-layout-integrityumbrella覆盖原-m27--m28--m34)**. This anchor is preserved for legacy reference.

---

### M28 — Sibling Section Overlap Probe

> **⚠️ Merged into [M-INTEGRITY §I2](#m-integrity--mockup-layout-integrityumbrella覆盖原-m27--m28--m34)**. This anchor is preserved for legacy reference.

---

### M29 — Table Cell Content Pattern（Copyable / Empty Placeholder）

带数据 table 的 cell（典型如 Event Name / Object ID / URL 列）有 2 种内容形态，**视觉规则不同**：

**Form 1 · Copyable（有值）**：

| 维度 | 规则 |
|---|---|
| Text | `textAutoResize = 'WIDTH_AND_HEIGHT'`（hug content），text fill 默认 token (`Text/Text_1` 或类似) |
| Action icon | TVU `icon/Edit/Copy` instance，紧跟 text：`icon.x = text.x + text.width + 4`（gap 4px） |
| Icon fill | bind to `Color Type/Icon/Default` (key `e8580f8f...`) via Q2 two-step |
| Icon size | 12×12 typical for table 行高 15-20，更大行可用 14×14 |

**Form 2 · Empty Placeholder（无值，显示 "—" / "-" / "–"）**：

| 维度 | 规则 |
|---|---|
| Text | "—" em dash (US-201C dash 优先)，text fill bind to `Color Type/Text/Placeholder & Button` (key `1fe2a119...`) |
| Action icon | **不要** copy/action icon — empty 值不可复制操作 |
| Detection | text.characters.trim() ∈ {"—", "-", "–"} → 走 Form 2 |

**生成时自动判断**：

```js
const EMPTY = new Set(['—', '-', '–']);
function applyCellPattern(cell, copyIconComp) {
  const text = cell.children.find(c => c.type === 'TEXT');
  if (EMPTY.has(text.characters.trim())) {
    // Form 2: bind placeholder color, no icon
    text.fills = bindToVar(text.fills, textPlaceholderVar);
    cell.children.filter(c => /Copy/.test(c.name)).forEach(c => c.remove());
  } else {
    // Form 1: hug + icon + bind Icon/Default
    text.textAutoResize = 'WIDTH_AND_HEIGHT';
    const icon = copyIconComp.createInstance(); /* ... */
    icon.x = text.x + text.width + 4;
  }
}
```

**Why**：Empty 占位若用同色 + Copy icon，dev 会写 copy logic 处理 "—"，QA 测出 bug；用户看见 "—" 但不知道是否可点击。Placeholder 色 + 无 icon = "this is intentionally empty, not copyable" 的视觉契约。

**违例 history**：2026-05-14 Plan B F2/F4 session table 一开始 44 cells 一律加 Copy icon，"—" cells 也加。用户指出后删了 6 处 empty cell 的 icon + 改 text 为 Placeholder 色。

**dev-side spec (text overflow)**：长 URL/ID 用 **center-ellipsis** 截断（如 `udp://237.0...0:1234`），保留协议前缀 + 尾段，确保 Copy icon 不被遮挡。Figma TEXT 无 native center-ellipsis，dev 走 CSS / JS 实现。

---

### M23.6 — Connector Endpoint Anchoring（两端锚定）+ Frame Content Avoidance（M23 extension）

M23 主体规则定义了 connector 形态（VECTOR + 双语 label + reconnect map）。本 sub-rule 补 **几何精度** + **路由避让** 两条 acceptance：

**(A) Connector Endpoint Anchoring（两端都锚在语义元素 bbox 上，非卡边）**：connector 的**两端都必须锚在它声明连接的语义元素 `absoluteBoundingBox` 上**（各容差 ≤ AH 即 8px），**不是锚在卡片 / frame / 容器的边**：

- **起点圆点 ●** 锚在 **source / 触发元素**（用户点击 / hover 的那个 button / cell / icon）的 bbox 边缘 —— **从触发按钮起笔，不是从卡边起笔**
- **终点箭头 tip** 落在 **target 元素** 的 bbox 内

Reconnect map 里每条 arrow 必含 `fromCanvas: {x, y}`（起点圆点位）+ `tipCanvas: {x, y}`（终点位）两个字段供审计。

**❌ 高频违例（recurring，2026-06-17 回流）**：spine 画成「卡边到卡边」的简易直线（card-edge → card-edge LINE），起点没锚在触发按钮上。这条**反复出现**——记住：spine 不是「两张卡之间的连线」，是「**触发元素 → target 元素**」的连线；起点必须 probe 触发元素 bbox 从它的边缘起笔。

**Audit script**（post-design wrap-up 必跑 —— 两端各查一次）：

```js
const within = (pt, bbox) =>                       // ±8px 容差
  pt.x >= bbox.x - 8 && pt.x <= bbox.x + bbox.width + 8 &&
  pt.y >= bbox.y - 8 && pt.y <= bbox.y + bbox.height + 8;

for (const arrow of reconnectMap.arrows) {
  // 终点：tip 落在 target 元素 bbox（原 (A)）
  const targetElem = await probeElem(arrow.to);    // by subElement path
  if (!within(arrow.tipCanvas, targetElem.absoluteBoundingBox))
    console.error(`${arrow.label} tip 不在 TARGET bbox 内`);
  // 起点：圆点锚在触发元素 bbox（非卡边）—— 高频违例，必查
  const triggerElem = await probeElem(arrow.from);
  if (!within(arrow.fromCanvas, triggerElem.absoluteBoundingBox))
    console.error(`${arrow.label} 起点圆点未锚在触发元素上（是不是从卡边起笔了？）`);
}
```

**(B) Frame Content Avoidance**：connector 的中间路径段（非起点非终点的 horizontal / vertical 段）**不应穿越** target frame 的内部 UI 内容区。允许穿越 frame margin / gap，禁止穿越 sub-header / table rows / cards 等可见内容。

**(C) Orthogonal segments only — 禁止斜线 / 自由曲线**（2026-05-27 补）：

所有 M23 connector **必须用正交折线（horizontal / vertical 段组合）**。**禁止**：
- 单段斜线（如 `M x0 y0 L x1 y1`，两端点不在同一 x 或同一 y）
- 自由曲线 / 贝塞尔（不可控、与其它 UI 风格冲突）
- 45° 拐角斜短段

**为什么**：UX 交付注释要"工程感 + 一眼看清指向"。斜线视觉上像草稿，且当 connector 数量 ≥ 3 时彼此交叉走向乱（折线可对齐 grid，斜线不能）。整套 UX 注释统一折线 = 视觉一致 + 可读性 + 与 product UI 90° 风格契合。

**几何模板**（按 source → target 相对位置选）：

| 拓扑 | 段数 | 路径模板（local coords） |
|---|---|---|
| **同行 same y** (`|y0 - y1| < 4`) | 1 | `M x0 y0 L x1 y0` (纯横线) |
| **同列 same x** (`|x0 - x1| < 4`) | 1 | `M x0 y0 L x0 y1` (纯竖线) |
| **跨行 + target 在 source 右下方**（典型：cell → 右侧 annotation panel） | 2 seg L (横-竖) | `M x0 y0 L mx y0 L mx y1`，`mx` = source 与 target 之间的中段 x（默认 `(x0 + x1) / 2`） |
| **跨行 + 反向**（target 在左下） | 2 seg L (竖-横) | `M x0 y0 L x0 my L x1 my` |
| **U-routing**（target_x 在 frame 内部，需绕到 frame 外） | 3 seg | 走 frame 上方 / 下方 gap 段 + 两端两段竖 dip |

**箭头 tip**：在最后一段末端用 M23 arrow helper（vectorPaths data 里手动画 `M tip-AH⋅u L tip L tip-AH⋅u(同 perpendicular)`）。

**Audit**：probe 每条 vector 的 `vectorPaths[0].data`，按 `L` 分段。任一段若两端点 `Δx > 4 && Δy > 4`（即斜线）→ 标 ❌ M23.6 (C) 违例。

**实证**：2026-05-27 FB-9398 Environment dropdown 连接线，AI v1 画了单段斜线 (`M startX startY L endX endY`)，用户抓"连线要用折线，不要一个笔直的先，不美观。同 UX 交付说明用的折线"。本 sub-clause 即此回流。

**Routing 决策树**：
- target 在同行 same x band → 直 horizontal/vertical line
- target 跨行但 target_x 在 source 之间空白区 → 2-seg L
- target 跨行 + target_x 在 frame 内部（如 sub-header 按钮 x=1589 F local）→ **3-seg U-routing** 绕到 frame 外（above / below frame in row gap）再 dip 进 frame

**违例 history**：
- 2026-05-14 Plan B v3：#6 → AutoRefresh tip 偏 58px（误用 TopBar y 而非 Sub-header y）/ #8-F5 + #8-F6 tip 偏 68-89px（误用 element topMid 而非 leftMid/rightMid）→ v3.1 fix
- 2026-05-14 Plan B v3：#5 → F3/F4 button 用 L-v-first 横段穿 F3 sub-header / F4 filter bar 内容 → v3.1 改 U-3seg 绕到 F3 顶上方 + F3/F4 gap

**Acceptance**：handoff doc 必含 connector accuracy audit table，**每条 arrow 两端各一行**：起点（start dot canvas / 触发元素 bbox / delta / pass-fail）+ 终点（tip canvas / target bbox / delta / pass-fail）。

**合并探针（paste-ready，post-op + 走查必跑 — 属 §Mockup Edit Lifecycle Gate）**：连线几何**只能用 `use_figma` 实时探**（Figma REST 只给 stroke 外轮廓、不给 centerline `vectorPaths`，箭头三角会假阳 → 无法 REST 机检）。下面一段自包含：正交性（C）不依赖 reconnect map、永远可跑；锚定（A）在有 reconnect map 时附加跑。

```js
// 在目标 section node 上跑：检查所有 VECTOR connector 的正交性（C）。
// 返回斜线违例清单；空 = pass。anchoring(A) 见上方 within() 片段，有 reconnectMap 时加跑。
function checkConnectorOrthogonality(section) {
  const bad = [];
  const vectors = [];
  (function walk(n){ if(n.type==='VECTOR') vectors.push(n); (n.children||[]).forEach(walk); })(section);
  for (const v of vectors) {
    const data = v.vectorPaths?.[0]?.data; if (!data) continue;
    // 解析 M/L 点序列（local coords）
    const pts = [...data.matchAll(/[ML]\s*(-?[\d.]+)[ ,]+(-?[\d.]+)/g)].map(m => ({x:+m[1], y:+m[2]}));
    for (let i = 1; i < pts.length; i++) {
      const dx = Math.abs(pts[i].x - pts[i-1].x), dy = Math.abs(pts[i].y - pts[i-1].y);
      if (dx > 4 && dy > 4) { bad.push({ id: v.id, name: v.name, seg: [pts[i-1], pts[i]] }); break; }
    }
  }
  return { pass: bad.length === 0, bad };
}
```

> ⚠️ 箭头 tip 的两段（`M tip-AH·u L tip L tip-AH·u`）本身是斜的，是合法箭头不是违例——上面按 `vectorPaths[0]`（spine 第一路径）取段、不取 arrow helper 路径，已规避；若 spine 与 arrow 合在同一 path 里，需先按子路径切分再判。

---

### M23.9 — State-preview elements 放产品 frame 外，连线指回触发元素（M23 / M23.6 extension）— 2026-05-27 新增

任何**用来展示交互状态**的 mockup 元素——**dropdown 选项列表 / popover / context menu / inline edit dialog preview / hover tooltip spec / focus state overlay** ——若它在真实运行时是**临时浮层**（点击后才出现 / hover 后才出现），那在 mockup 里**必须**：

1. **放在产品 frame 边上**（外部），不放在 frame 内部覆盖产品 UI
2. **用 M23.6 VECTOR 连接线**指向触发元素（即用户点击 / hover 的那个 cell / icon / button）
3. 连接线遵守 M23.6 (A) tip accuracy + (B) frame content avoidance（不穿越产品 UI）

#### 触发场景（典型）

| 状态预览 | 触发元素 | mockup 落位 |
|---|---|---|
| Dropdown 选项列表 | table cell hover-link / select widget | frame 右侧 / 上下方空白区 |
| Context menu / 右键菜单 | row / icon | frame 边上靠近 trigger row |
| Hover tooltip spec | cell / button | 紧邻 trigger element（tooltip 本身已是 reveal 容器，可在 frame 内 — 例外见下） |
| Inline edit dialog preview | "Edit" link / pencil icon | frame 右侧 |
| Focus state overlay | input | frame 边上对应 input 同 y |
| **Confirm / delete-confirm popover**（如 "Remove on-air layer?"）| 触发它的 destructive button（Remove / Delete 等）| **frame 外**靠近该 button + 连线 + 配 1 行 UX 说明（"二次确认：×× 操作前弹出"）|

#### 例外（允许放 frame 内）

- **真实 tooltip hover preview**：tooltip 本身就是单元格 hover 的产物，落位贴近 cell 可演示真实行为 — 但 dropdown / menu / dialog 这类**遮挡较大**的状态预览不属此例外
- **inline auto-expand 内容**（如 row hover 展开 sub-row）：本身就是 inline 真实形态，不算 overlay

#### 反例

| ❌ | 原因 |
|---|---|
| Dropdown 直接放 cell 下方覆盖下一行 | 遮挡产品 UI 真实内容，walkthrough reader 看不清下一行原貌 |
| 状态预览没连线 → reader 不知道它属于哪个 trigger | 注释失锚，跨 frame 阅读断裂 |
| 用红框 / 箭头注解但连接线穿过其它 sub-header / table rows | 违 M23.6 (B) Frame Content Avoidance |
| 把 dropdown 放 frame 外但没连线，靠 "目测就近" | 跨大间距时 reader 误判属于哪个 cell |

#### Acceptance

- [ ] state-preview 元素 absoluteBoundingBox 与产品 frame absoluteBoundingBox 无重叠
- [ ] state-preview 与 trigger element 之间有 1 条 VECTOR connector（遵守 M23 + M23.6）
- [ ] connector tip 落在 trigger element bbox 内（M23.6 (A)）
- [ ] connector 路径不穿越 frame 内部 sub-header / table rows / cards 内容（M23.6 (B)）
- [ ] state-preview 元素有独立 frame name（如 `Dropdown preview — Environment edit`），不与产品 UI 同名

#### 实证

**2026-05-27 FB-9398**：Environment 编辑下拉演示，AI v1 直接放 frame 内单元格下方 → 遮挡后续 row → 用户手动拖到 frame 外 + 连接线 → AI 后续操作又把它 reparent 回 frame 内"对齐 cell 下方"→ 用户抓"我刚刚改好了，又被你移掉了，无语了"。根因：M23.6 已规定 connector routing 避让，但**没明确 state-preview 元素本身必须在 frame 外**，AI 在重新 parent 时按"视觉就近"错判。本规则即此回流，把"state-preview 元素落位"也纳入硬约束。

**2026-06-04 Graphics Insertion MC-44（重复违例）**：Remove-confirm popover 直接叠在 PGM preview 上，歪理"popover 本来就盖内容"。但交付物身份要求放 frame 外 + 连线。用户原话"这个我记得也提醒过"。**根因**：confirm/popover 虽已在本节（§M23.9）触发表列举，但 AI 把"运行态 popover 盖内容"误当合理。强化：trigger 表显式加 "Confirm/delete-confirm popover" 行；**任何 destructive 二次确认弹窗 = state-preview，必 frame 外 + 连线 + UX 说明**，无例外。

#### Why（深层）

Mockup 的双重身份：(1) **运行时视觉真实**——dropdown 真实就开在 cell 下方；(2) **交付物可读性**——reader 在静态画面上要看清产品默认态 + 状态预览**同时**。两个身份在静态 mockup 里冲突，**M23.9 优先选 (2)**：state-preview 移到 frame 外，用连接线明确触发关系。运行时真实由代码 / Prototype mode 还原。

---

### M23.10 — UX Card Audience-Aware Language（M23 extension）— 2026-05-28 新增

M23 UX 交付卡（Why / Changes / Data Contract / Interaction / Acceptance）的读者是 **PM / QA / dev**，不只是工程师。卡内文案**默认用白话决策树**，**禁止**直接塞 regex / pseudocode / 长 code block 当主要表达载体。

#### 触发场景

任何 M23 UX 交付卡里写"显示逻辑"、"解析规则"、"取值规则"时。

#### 默认写法（白话决策树 / If-Then 句式）

```
有 RELEASE_STR → Version = "Build" 前段，Build = "Build" 后数字
无 RELEASE_STR 但有 VER=a.b.c.d → Version 显示完整 "a.b.c.d"，Build = "d"
两个都没 → 两列都 "—"
```

#### 允许的 code 出现位置

| 场景 | 允许 |
|---|---|
| **样本 payload 原文**（dev 需复制粘贴）| ✅ 单独 sample 段，标注"原始字段示例" |
| **正则 / 字段名引用**（必须精确的字符）| ✅ 行内 backtick `\`RELEASE_STR\`` / `\`D="RPS One:"\`` |
| **完整算法实现** | ❌ 不放 M23 卡，放 `code-conventions.md` 或 engineering spec |
| **pseudocode 大块** | ❌ 改成白话决策树 |

#### 反例

| ❌ M23 卡里出现 | ✅ 改成 |
|---|---|
| `parts.slice(0, -1).join(".")` | `Version 显示完整 VER 字符串` |
| `/^(.+?)\s*Build(\d+)/i` 单独一行 | `Version 取 "Build" 前段；Build 取 "Build" 后数字` |
| 8 行 JS 代码块描述显示逻辑 | 3 行 If-Then 决策树 |

#### Acceptance

- [ ] M23 卡里没有 code block（除"样本 payload"段允许 1 个）
- [ ] 显示 / 取值 / 渲染规则用白话 If-Then 表达
- [ ] 字段名 / 字面量值用行内 backtick 引用

#### 实证

**2026-05-28 TPC-628**：起手 Data Contract 卡用了 `Parse RELEASE_STR regex: /^(.+?)\s*Build(\d+)/i  → group1=Version, group2=Build` 和 `Parse VER fallback: split by ".";  Version = parts.slice(0,-1).join(".");  Build = last part`。用户抓"不要写代码，只需要讲清楚显示逻辑就可以了"。**根因**：起草卡片时按 dev 视角写，没考虑 PM/QA 阅读门槛。本规则即此回流。

---

### M23.11 — UX Card Scope Discipline（M23 extension）— 2026-05-28 新增

M23 UX 交付卡**只放与本期改动直接相关的内容**。通用 context（Persona simulation / out-of-scope 声明 / 设计动机辩护 / 跨产品对比 / 未来 roadmap）**不入 mockup annotation 卡**——这些内容放 design report / handoff doc / Slack thread。

#### 触发条件

任何 M23 卡起草时，每条 bullet 必过 3 问：

1. **本期改动相关吗**？（变更本身 / 影响范围 / 验收点 / 后端契约）— 否 → 删
2. **PM / QA / dev 读完会执行 / 验证 / 实现什么**？无明确动作 → 多半是 context 类，删
3. **同样信息已经在其他 artifact 里有真源**？（如 Persona 在 design report、out-of-scope 在 ticket）— 是 → 删，不重复

#### 反例

| ❌ M23 卡里出现 | 应该去哪 |
|---|---|
| Persona F2 simulation（用户角色 + 反向场景） | design report / handoff doc |
| "Touchscreen-side display tracked separately by device firmware team (out of TPC scope)" | Jira ticket scope 字段 / Slack 同步 |
| "Why this matters: …" 长段背景论证 | Why 卡保留 1-3 行即可，更长论证去 design report |
| 跨产品对比 / 历史决策回顾 | decisions log |
| 未来 roadmap（"下一期会做 X"） | backlog / next-iteration spec |
| **Open questions / TBD / 待 PM 拍板 / "需 ask Danner" 等未决项** | Jira 评论 / Slack thread / 1:1 邮件——**禁止入交付卡**。详见 [`design-process.md` § No Open Questions in Deliverables](./design-process.md#no-open-questions-in-deliverables2026-06-02-新增) |

#### 允许内容

- **Why**：1-3 句话讲本期改动驱动力（bug / 新需求 / 数据契约升级）
- **Changes**：本期具体改动 enumerable list
- **Data Contract**：本期 mockup 涉及字段的数据契约
- **Interaction**：本期 mockup 涉及交互细节
- **Acceptance**：本期改动的可验收 checkbox list

#### Acceptance

- [ ] 每条 bullet 过 3 问筛掉 tangential
- [ ] 卡数量 ≤ 6（Section Header + Why + Changes + Data Contract + Interaction + Acceptance）—— Persona / Out-of-scope / Future plans 不入卡

> **卡结构正向模板见 [§M23 — UX 交付卡 Canonical 结构](#m23--ux-交付卡-canonical-结构正向模板)**；本 §M23.11 只管 scope 取舍，不再重复结构定义。

- [ ] 同 session 里的 in-progress design report / handoff doc 是 tangential 内容的去处

#### 实证

**2026-05-28 TPC-628**：起手按 FB-9398 模板加了 7 张卡，含 "Persona Simulation F2-late"（4 句话讲 Support / Field ops / Sales 三角色 + 反向场景）和 Acceptance 卡里 "Touchscreen-side display tracked separately by device firmware team (out of TPC scope)" 这种 scope-clarifying 句子。用户抓"其他跟这次改动无关的内容不需要显示在 UX 交付说明上，减少开发、PM、测试阅读的信息内容"。**根因**：起草卡时按"做得完整"堆砌通用 context，没区分本期 vs 通用。本规则即此回流。

---

### M23.7 — State-label = what + why + invariants（M23 / M33 extension，2026-05-26）

Spec mockup frame 上方的 **state-label**（每个 frame 一份的描述 sticky / frame，与 M23 流程注释不同 artifact）**必须**含三类信息：

| 信息类 | 内容 | 反例（仅 what）| 正例（what+why+invariants） |
|---|---|---|---|
| **What** | frame 在展示什么（基本场景描述） | `M5 · 2 sessions accordion` | `M5 · 2 Sessions · #1 selected expanded · default accordion` |
| **Why** | why this state matters（业务场景 / user intent / 决策 context） | （缺失） | `Demonstrates rule: only currently-selected row expanded by default. Common when user added a session and switched focus.` |
| **Invariants** | rules being demonstrated（哪些规则在此 frame 体现） | （缺失） | `Rules in play: accordion default (M3), selected 3-layer signal (M38), Inactive-Idle subtype (M10).` |

**Why（state-label 必须 = what+why+invariants）**：
- 只写 what = reader 知道画了什么但不知 design intent
- 加上 why = reader 能从 label 自带 design rationale，未来 review / dev handoff 不用反查 PRD
- 加上 invariants = reader 知道这帧"在演示哪条规则"，规则 audit 时可反向追踪

**与 M23 / M33 关系**：
- M23 管 **多状态交互流程图**（connector + arrow + bilingual label）—— 跨 frame 的关系
- M33 管 **annotation vs product UI 语言隔离** —— annotation 文案 discipline
- M23.7 管 **每 frame 的 state-label** —— 单 frame 的内容描述。三者协作：M23 画关系、M33 管语言、M23.7 管单帧文案完整度

**Acceptance:**
- 每 frame state-label 含 what / why / invariants 三段，不允许只 1-2 段
- 业务场景 frame（如 bein sports case）必显式提及业务名 / persona / 用例
- Invariants 段必显式列规则编号（M-XX），不允许只口语描述

**实证**（2026-05-26 Video Sync Phase B）：M-MultiExp state-label 含 "bein sports case (Slot1↔Slot2 + Slot3↔Slot4)" 业务场景 + math validation "2 expanded @200 + 0 collapsed = 400px well within 575 budget" + 规则引用 → 这才是 state-label 该写的样子。其它 frame 早期 state-label 缺 invariants 引用，仅 what 描述 → walkthrough 补完。

---

### M30 — Icon Component Mandatory Library Lookup

> **⚠️ Merged into [M32](#m32--library-component-firstm30-扩展所有-product-ui-元素)** as the icon sub-clause (M32 §icons). M32 is the umbrella for all product UI elements (icons + buttons + chips + inputs + ...); M30 was the icon-specific subset and is now absorbed.
>
> The icon-specific synonym table + "no Unicode arrow" prohibitions + 2026-05-14 Plan B 实证 are preserved in M32 §icons + carried by M35 (Affordance-Category Search) which generalizes synonym expansion.
>
> This anchor is preserved for legacy reference.

---

### M31 — Layout function decision tree（按场景选 Auto Layout / Slot / Boolean / Absolute）

> **↔ Code 端镜像**：[`code-conventions.md` R14](./code-conventions.md)（R14 = M31 mirror，改此条必同步对面）。

任何 product UI 或注释 container 创建 / 修改时，**必须按场景选最合适的 Figma 布局机制**，绝对定位仅在有充足理由时使用。"先 auto-layout，找不到合适机制再退到 absolute" 是默认顺序。

#### 决策树

| 场景 | 推荐机制 | 理由 |
|---|---|---|
| 内容长度可能变（文案 / item 数量 / padding 调整） | **Auto Layout** | 改内容自动重排，不需要重算坐标 |
| 同位置 swap 不同子组件（icon / text / icon+text 切换） | **Slot (Instance Swap property)** | 母组件保持布局，instance 只换内容 |
| 子元素 "显示 / 隐藏" 是状态属性（loading / empty / error / 可选 badge） | **Boolean property** | 单组件多状态，dev 端 prop 切换；避免组件爆炸 |
| 子元素位置随别的元素动（自适应 wrap / 对齐 baseline / 居中） | **Auto Layout + counter-axis align + itemSpacing** | 不是手算 x/y |
| 一次性 overlay 浮层（dialog / popover backdrop / canvas annotation arrow） | **Absolute 允许**（但 *必须* 备注理由） | 浮层没 layout 语义；用 `layoutPositioning='ABSOLUTE'` 嵌进 auto-layout parent，或直接 free x/y |
| Stand-alone annotation（流程图箭头、状态 chip overlay） | **Absolute 允许** | annotation 类元素跨 frame，没参与产品 UI layout |
| **Page-level fixed viewport 容器**（如 1920×1080 product mockup 顶层 frame） | **Absolute 允许，无需理由** | 顶层 frame 是"画板/视口"语义，children（top bar / body / floating elements）按设计稿绝对坐标摆放属正常用法。Auto-layout 适用于 viewport **内部** body 区，不适用 viewport 本身 |

#### 反例

| ❌ 错误做法 | 后果 |
|---|---|
| dialog box 内 title / body / button row 各自 x/y 绝对定位 | 改 title 字数 / 加按钮 / 调 padding → 全部要重算坐标，且看上去仍然"对齐"实际是巧合 |
| sidebar field row 用绝对定位 | 加一行字段就要平移下面所有行；改字段顺序要重排所有 y |
| "复制粘贴 3 个一模一样的 frame" 做按钮组 / list rows | 任一文案变长要手动改其他 frame；spacing 失控 |
| product UI 用了 absolute 但 handoff doc 不写理由 | 后续维护者无法判断是"故意的浮层"还是"图省事没用 auto-layout"|

#### Acceptance

- 任何 dialog box / sidebar / footer button row / 列表 row / chip → **必须 Auto Layout**（`layoutMode !== 'NONE'`）
- 任何 product UI 用绝对定位 → handoff doc 必须写明理由（"overlay on backdrop" / "anchor to canvas annotation" 等）
- probe 脚本：`product_frame.findAll(n => n.type === 'FRAME' && n.layoutMode === 'NONE').filter(non-overlay && non-viewport && non-library-internal)` 命中数应 = 0；audit 必须排除 (a) 顶层 page-level viewport frame、(b) overlay/annotation 类、(c) library instance 内部 frame

#### 实证

- **2026-05-18** Touch-Screen v8 — 4-source flow M1 dialog v1：dialog box + buttons row 全用 absolute x/y。改按钮文字 / 加 padding / 切按钮组件都要重算坐标。v2 重做：dialog (VERTICAL auto-layout, padding 24/20/24/24, gap 12) + buttons-row (HORIZONTAL auto-layout, `primaryAxisAlignItems='MAX'`, gap 12)，内容长度变化全部自动重排。

---

### M-LIBRARY-HYGIENE — 临时 / 调研 component 必须 `_` / `.` 前缀（不污染 publish 与 sync）

任何**不应被 publish 也不该被 sync pipeline 拉到 code 侧**的 component（调研草稿 / 内部参考 / 临时示意 / 已废弃保留视觉）—— Figma name 必须以 **`_` 或 `.`** 开头。

| 用途 | 命名 |
|---|---|
| 临时调研 / WIP / draft | `_Draft/<name>` 或 `_Research/<name>` |
| 内部参考 / 不交付 | `_internal/<name>` |
| 已废弃但保留视觉 | `_archived/<name>` / `_deprecated/<name>` |

**双层 honor**：Figma 原生自动排除 publish + sync pipeline (`extract.mjs`) 源头 skip 不写 raw/。完整说明见 [`AGENTS.md` §Figma 命名约定](../../AGENTS.md)。

**反模式**：放进 `_Research` page 但 component name 没加 `_` 前缀 → sync 仍拉、cleanup 标 "非生产页 待删"、Sisyphus 循环（2026-05-27 实证，56 file 反复出入）。**name = source of truth**，page name 只辅助 designer 视觉分组。

---

### M32 — Library Component-First（M30 扩展：所有 product UI 元素）

> **↔ Code 端镜像**：[`code-conventions.md` R15](./code-conventions.md)（R15 = M32 mirror，改此条必同步对面）。

M30 把"按库取用"约束限定在 icon。**所有 product UI 元素**（button / chip / badge / input / dropdown / list row / dialog action 按钮）同样适用——先 lookup component 再考虑自画。

#### 优先级（与 M30 对齐）

| 优先级 | 来源 | 触发动作 |
|---|---|---|
| **1** | 当前 Figma 文件内 `COMPONENT` / `COMPONENT_SET`（含 variants） | `findAll(n => (n.type==='COMPONENT_SET'\|\|n.type==='COMPONENT') && /<role>/i.test(n.name))` |
| **2** | 已发布 Team Library 组件 | `importComponentByKeyAsync(key)`（key 由用户提供或从 instance.mainComponent.key 反查）|
| **3（最后手段）** | 自画 + 🟡 标注库候补 | 仅 **1 / 2 均 verified miss** 时；handoff doc 必含 "TBD: replace with `<component>` from library" |

#### 同义词扫描（按 role 类别）

| 想画 | 必须穷尽的关键词 |
|---|---|
| 按钮（dialog action / footer action / sidebar primary） | `button`、`btn`、`LCD Button`、`home button`、`primary`、`secondary`、`back` |
| 输入框 | `input`、`text field`、`textbox`、`form` |
| List item / row | `list item`、`row`、`menu item`、`option`、`cell` |
| Toggle / Switch | `switch`、`toggle` |
| Card / Panel | `card`、`panel`、`tile`、`container` |

#### 反例

| ❌ 错误做法 | 后果 |
|---|---|
| 画 gradient rectangle + 文字当 "Stop all" button | dev 拿不到 variant 的 hover / disabled / pressed state；改组件颜色时这一处不会跟着变 |
| dialog 两个按钮用自画 frame，不用 `LCD Button` variants | 按钮尺寸 / 圆角 / 字号 / spacing 偏离规范；和单路画面里的按钮视觉不一致 |
| 一次 `findAll` 没结果就降级自画 | 同义词没穷尽 / 团队库没尝试 import / 没问用户要 key |
| 用 instance + 改内部 fill 颜色 "扮成" 别的 variant | variant 是 component 的合法 state，不要用 paint 改 "伪装"——直接 `setProperties({type: '<variant>'})` |

#### Acceptance

- handoff doc 必含一行 "Buttons used: `<component name>` variants \[\<list\>\]"
- 任何 product UI 里出现"渐变填充矩形 + 文字" 且不是 `INSTANCE` → 触发 flag
- 自画必备 escalation 痕迹：handoff doc 写 "`<component>` lookup miss across local + team library; user provided no key → flagged as TBD"

#### 实证

- **2026-05-18** Touch-Screen v8 M1 dialog v1：Skip / Configure 两个按钮用自画 gradient rectangle，没用 `LCD Button` 的 `Type=Back/Status=Normal` + `Type=Primary/Status=Normal` variant。v2 重做用 instance + override text，自动对齐设备风格。**根因同 M30**：lookup 步骤跳过 / 同义词没穷尽。

#### M32.1 — Component Source + Fidelity Verification（取组件三连验，M32 子条）

M32 优先级表 row 2 写"key 由用户提供或**从 instance.mainComponent.key 反查**"——这条在**产品文件**里是陷阱：产品文件混着多个库的 instance（Producer `PP`、其它产品库等），从那儿抓 key 反查 → 你 import 的是**外库 / 错语义**组件。取任何 icon/组件 **必须过三连验**：

1. **来源（Source）**：key 必须来自**指定库**（`site-review-manifest.json` 的 `figmaFileKey`）——经 `search_design_system`(指定库) 或 catalog 已验证 key 取得。**禁止**直接复用产品文件里某 instance 的 `mainComponent.key` 而不验证其发布库 = 指定库。
2. **语义（Semantic）**：同名/近名组件并存时（如 `icon/PP/play` = Start 按钮动作 vs `icon/Video/Play` = 视频预览 play）按 **catalog 记录的语义**选，不抓第一个名字命中的。
3. **保真（Fidelity / 色）**：**swap / instance 一个组件 ≠ 自动拿到对的颜色**。组件的 fill 常绑在内部**可见 vector** 上的 token；换组件后要**复制参考节点该可见 vector 的 fill（含 bound token）**，并 probe 确认 `可见 vector.fills[0].boundVariables.color` 与参考一致——不是改 instance 顶层、也不是肉眼"差不多"。

**Acceptance**：
- handoff / wrap-up 必能回答：每个 import 的组件 key 来自指定库（不是产品文件 harvest）
- 跑 `scripts/audit-mockup-library-origin.mjs` → flag mainComponent 不在指定库/catalog 的 instance（如 `icon/PP/*` 误入消费产品）
- 换组件后 probe 可见 vector 的 fill token，与参考节点逐一对齐

**实证 — 2026-06-05 Graphics Insertion 无信号 PGM play 占位**：(1) 从产品文件 instance `4105:138` harvest `icon/PP/play`(Start 动作) 当视频 play 占位——错源 + 错语义；(2) 改对组件 `icon/Video/Play` 后，**仍用其默认 token `3010:91`(浅灰) 而非参考 S0 的 `4600:1`(#595959)** → 图标色错，用户**连提 3 次**。三连验任一缺失都会翻车；本规则即此回流。

#### M32.2 — 自建产品 UI 件「起手三件套」（自定义卡 / 浮层 / 面板的强制起手勾选）— 2026-06-09 新增

**触发**：你要建的是一个**自定义产品 UI 件**——卡片 / 浮层 popover / 面板 / 自定 modal 内容，即**没有**直接 instance 一个库组件、要自己拼 `frame + text + rect` 的场景。直接 instance 库组件不触发本条（走 M32 主条）。

**根因**：返工几乎都来自"图快自建、跳过设计系统的真组件 / 真 token / 真措辞"。三件套把这三个隐性检查外化为**起手强制勾选**（呼应 [`design-process.md` M48](./design-process.md#m48--startup-rule-coverage-self-check起手强制-gate-2026-06-05-新增) 起手 self-check）：

| 腿 | 起手勾什么 | 真源 |
|---|---|---|
| ① **真组件** | 件里每个可复用子件（按钮 / 输入 / badge / 顶栏 / 分割线 …）先 lookup 库组件再考虑自拼；top bar 永远库 instance | M1 / M32 / M32.1 |
| ② **绑 token** | 自建件的所有 fill / stroke / text 颜色绑 Color Variable，不写 raw RGB / hex | [M-COLOR §C1](#c1--brand--token-color-paint-必须绑-color-variable不写-hex-literal原-m102026-05-26-扩-stroke--text--frame-fill) |
| ③ **复用措辞** | 件里文案**复用页面 / PRD / sibling 已有措辞**，不自造新的冗长 copy；同概念不漂移同义词 | 本条（新）+ [`design-process.md` §Canonical Terms](./design-process.md#canonical-terms-discipline2026-06-02-新增) |

第 ③ 腿是本条**唯一真新增**（①② 各已有真源，这里只是把三者收进**同一张起手清单**，治"自建时分别漏掉"的元根因）：自建件最容易在文案上"图省事自己写一段"，结果与页面既有措辞、PRD 用词、sibling 卡措辞都不一致——读者以为是不同的东西。**先去页面 / PRD / sibling 找现成措辞抄过来**，没有现成的才新写，且要短、与既有风格一致。

**Acceptance**：
- 起手 deliverable（M48 清单）含一行："本任务自建件 [列出] → 三件套 ①②③ 各怎么落"
- 自建件交付前：跑 M-COLOR 色 audit（②）+ grep 件内文案 vs 页面 / PRD（③）+ 确认无"该用库组件却自拼"的子件（①）
- **三件套 ①② 的"勾选"必附机检 gate 输出（keystone，2026-06-17）**：① 真组件 → `audit-mockup-library-origin` pass（含验 key 来自指定库，见 [M32.1](#m321--component-source--fidelity-verification取组件三连验m32-子条)）；② 绑 token → `audit-mockup-binding-fidelity` / `audit-mockup-colors` pass。**自我声明"已勾"不算数**——无 gate 输出视为未做。实证：BM-1047 V3 三件套被"心里勾了"却全踩（顶栏自画、硬编码 RGB、措辞自造），起手 self-check 没接机检 = 纸面合规。

**实证 — 2026-06-09 BM-1047 V3 split-wallet（一次踩满三腿）**：① 顶栏自画 token chip 不用库 Top bar（用户称"很严重"）；② 5 个浮层背景 / 文字硬编码 RGB 不绑 Color Variable；③ 席位面板措辞自造一长串，不复用 Home `Purchase Subscriptions` 组件 + PRD 现成措辞。三处同一根因 = 自建时跳过设计系统起手检查 → 本条把检查外化为起手三件套。

---

### M33 — Annotation vs Product UI 语言隔离（M23 扩展）

> **↔ Code 端镜像**：[`code-conventions.md` R16](./code-conventions.md)（R16 = M33 mirror，改此条必同步对面）。

M23 双语硬规则**仅作用于 UX 注释层**（navy chips / 规则卡 / condition labels / header chip 等 annotation 元素）。**产品 UI 层**（设备界面内真实出现的画面 / dialog body / button label / tooltip / menu item）走**设备语言**单语。

#### 边界判定

| 元素归属 | 语言策略 |
|---|---|
| 注释 chip / 规则卡 / annotation overlay（在 frame 外，浮在 canvas 上 / 作为 annotation 节点） | **双语**（EN 主 + ZH 弱化；字体/配色见 M23 §字体规范 / §颜色规范）|
| 产品 frame **内**的 title / body / button label / tooltip / menu item / inline help | **单语**（设备语言；TVU 终端通常 = EN）|
| 产品 frame **上方**的 state label chip（M23 第 3 层结构） | **双语**（属注释层，挂在 frame 外）|
| 在产品 frame **里**叠加的 ZH 翻译 sticky note / overlay text | ❌ **不合规**——属错位 annotation，违反"注释不进 frame"原则 |

#### 反例

| ❌ 错误做法 | 后果 |
|---|---|
| dialog title 写 `2 channels have no Receiver  2 路通道未选择 Receiver` | dev 实现时不知道哪句是设备 UI；产品 UI 视觉拥挤；i18n key 命名歧义 |
| button label `Skip 跳过` 混排 | 按钮宽度被 ZH 撑大；和设备其他按钮视觉不齐 |
| 用注释字体（Noto Sans SC opacity 0.45）写产品 UI | 视觉风格 ≠ 设备实际渲染（设备根本没有 Noto Sans SC 字体可用） |

#### Acceptance

- 任何 product frame（识别：name 命中设备 mockup 模式如 `S<N>` / `M<N>` / 设备屏幕 frame） → probe 所有 TEXT 节点 `fontName.family`，**不应**出现 `Noto Sans SC`
- 注释 chip 内 ZH range opacity 仍走 0.45（M23 verbatim 不变）
- handoff doc 必含一行 "Product UI language: \<device locale, e.g. en-US\>"

#### 实证

- **2026-05-18** Touch-Screen v8 — 4-source flow M1 dialog v1：title / body / 按钮全部双语 → 用户拍板"产品 UI 不需要双语"。v2 strip 掉所有 ZH，仅注释层（state label chip + rules card）保留双语。**根因**：把 M23（针对注释）的双语规则错误外推到产品 UI 层，没区分"注释 annotation"和"设备真实画面"两个层级。

---

### M34 — Section / Frame children-bbox wrap audit

> **⚠️ Merged into [M-INTEGRITY §I3](#m-integrity--mockup-layout-integrityumbrella覆盖原-m27--m28--m34)**. This anchor is preserved for legacy reference.

---

### M35 — Affordance-Category Search Discipline（思考产物外化为强制 trace）

> **↔ Code 端镜像**：[`code-conventions.md` R13](./code-conventions.md)（R13 = M35 mirror，改此条必同步对面）。

任何要新增 / 借用一个 **"基础语义元素"**（方向指示、排序、关闭、添加、checkmark、loading、warning 等）**之前**，必须按 affordance category 显式分类 + 视觉词汇扫描 + 跨上下文复用判断，**思考过程外化为 3 行 mandatory trace**。

#### Why（核心问题）

M30 / M32 / §"组件取数优先级" 规定了 **WHERE 去搜**，但没规定 **HOW 思考再搜**：

- AI 默认搜索是 "exact name regex"（搜 `link` / `dropdown` 等功能词）→ miss 一切只按形状 / 类别命名的资产（如本地 `down` / `up` instance, library `Arrow/Sorting`, `icon/Arrow/Previous` 等）
- 一旦 name 没命中 → 默认 fall back 自画 / Unicode，没有"逛"图层 / "扫"视觉词汇的本能
- 候选若在文件里别处已使用（如 pagination chevron）→ AI 倾向认为"用途不同不能借"，错失设计系统视觉一致性

**人类设计师**遇到"我要个 dropdown 小三角"时，本能扫"这文件里**所有上下方向的三角**"——pagination 的 chevron、sort 的 indicator、even 任何方向箭头都是候选；只要几何形状对，**复用优先**。

规则若只说"搜得彻底点"无法约束 AI 行为（vibe）；规则若强制 **AI 把分类思考输出成 deliverable 产物**，AI 不写就完不成 task，思考被外化为可 audit / 可拦截的 trace。

#### Step 1 · 显式分类（必须 1 句话）

> "我要画的是【方向 / 操作 / 状态】类指示，affordance category = **`<category>`**，意图是 '\<purpose\>'"

| 反例 | 正例 |
|---|---|
| "我要个 dropdown arrow" | "我要个**向下方向指示**，affordance = `directional-vertical`，意图是 '点击展开列表'" |
| "需要个 link 图标" | "我要个**链接 / 共享指示**，affordance = `link-share`，意图是 '标记此字段值 mirror 到他处'" |
| "画个 close 按钮" | "我要个**取消 / 关闭**指示，affordance = `dismissive`，意图是 '点击移除当前 layer'" |

#### Step 2 · 视觉词汇扫描（按形状词 / 类别词，不只名字词）

按 affordance category 查**固定同义词矩阵**——AI 必须扫这些 keyword 的 OR 命中集，**不预筛 "用途是否吻合"**：

| Affordance | 必扫关键词（OR 命中即候选） |
|---|---|
| `directional-vertical` | `up`, `down`, `chevron`, `arrow`, `triangle`, `caret`, `expand`, `collapse`, `sort`, `pagination`, `more` |
| `directional-horizontal` | `left`, `right`, `back`, `forward`, `previous`, `next`, `chevron`, `breadcrumb` |
| `sort` | `sort`, `sorting`, `order`, `ascending`, `descending`, `up`, `down`, `arrow` |
| `dismissive` | `close`, `cancel`, `delete`, `remove`, `x`, `clear` |
| `additive` | `add`, `plus`, `create`, `new` |
| `status-positive` | `success`, `done`, `check`, `selected`, `confirm`, `tick` |
| `status-warning` | `warning`, `alert`, `caution` |
| `status-negative` | `error`, `fail`, `stop`, `block` |
| `link-share` | `link`, `chain`, `connect`, `share`, `external`, `open` |
| `loading` | `loading`, `spinner`, `progress`, `wait` |

扫描范围（按 §"组件取数优先级"已有 4 层）：
- 0: Figma Component Catalog grep
- 1: Published library via MCP `search_design_system(includeLibraryKeys: ["lk-057f6ba0..."])`
- 2: 当前文件 COMPONENT / COMPONENT_SET / INSTANCE 的 `mainComponent.name`
- 2.5: 当前文件**未组件化 LAYER**（VECTOR / BOOLEAN_OPERATION / GROUP 命名带关键词的）

候选集 = 所有命中节点；**不预筛**。

#### Step 3 · 跨上下文复用判定

候选集得到后**默认 reuse**，除非以下"硬阻断"理由之一：

| ✅ 构成"不 reuse"理由 | ❌ 不构成 |
|---|---|
| 几何形状不匹配（要单向三角，候选是双箭头）| "它原本是 pagination / sort 用的" |
| 描边粗细 / 内边距 / 比例在新场景渲染破图 | "它当前在另一个 frame 被用" |
| 候选只有大尺寸（24px+），新场景要 8-12px 且无可缩放 svg path | "名字看上去和我场景不一样" |
| 颜色 token 绑死且不可改（极少见，多数库组件颜色可 override） | "意图描述不一样" |

→ 设计系统一致性 = **视觉词汇复用**，不是"语义恰好对齐"。pagination chevron 用作 dropdown indicator 完全合规。

#### Step 4 · Unicode / 自画黑名单（自查触发词，命中即 STOP）

| 黑名单 | STOP 后回到 |
|---|---|
| 想用 `▲ ▼ ◀ ▶ ↑ ↓ ← → ‹ › ✓ ✗` 任一 Unicode | Step 1 |
| 想 `figma.createVector()` 画三角 / 箭头 / 复合形 | Step 1 |
| 想用 `[D]` `<` `>` `*` 字符当占位 | Step 1 |
| 想用 emoji `🔗 📎 ✅` 等当 product UI 图标 | Step 1（emoji 用 annotation 内允许，product UI 不允许） |

只有 Step 1+2+3 verified miss **且** 用户授权 → 才允许进入自画路径（同 M30 priority 3 既有条款 + backlog 候补）。

#### Acceptance

每次 affordance 类元素创建前，handoff doc / annotation / commit message **必含 3 行 trace**：

```
1. Affordance: <category> (intent: <purpose>)
2. Vocabulary scan: [<candidate1 id+name>, <candidate2>, ...]
3. Chosen: <which + why>  // OR: None match → custom + backlog BRIDGE-MOCKUP-NNN
```

probe 命令：

```sh
grep -E "Affordance:|Vocabulary scan:|Chosen:" handoff-*.md
# 任何 mockup deliverable 加 affordance 元素后应有 3 行匹配
```

#### 与 M30 / M32 关系

- M30 / M32 = **search infrastructure rules**（WHERE）
- M35 = **search cognition rule**（HOW 思考再 WHERE）
- 三条联用：M35 先强制分类 → M30/M32 决定按 priority 在哪一层 invoke 搜索
- M35 不取代 M30 priority 1 "必须 MCP search_design_system 不能只 findAll"——只是把 query 的关键词从功能词扩展到 affordance 同义词矩阵

#### 实证

- **2026-05-18** Touch-Screen v8 — S6 dropdown indicator 我用 Unicode `▼` / 自画 vector triangle；文件里 `down` / `up` instance（pagination 用）几何形完全合用，名字直白但我没换搜词。**根因**：搜 `dropdown` `chevron` `arrow` 错过 `down` / `up` 这种纯方向命名；即使搜到也下意识认为"是 pagination 用的不能借"。M35 强制 Step 1 分类（`directional-vertical`）+ Step 2 同义词扫描（含 `up` / `down` / `pagination`）+ Step 3 默认复用 → 这次 miss 不会再发生。
- **2026-05-18** Touch-Screen v8 — link 图标搜索：我用 `findAll` regex `/link/i.test(name)` → 命中只有 `starlink` 干扰项 → fall back 自画 placeholder。**根因 1**：跳过 priority 1 published library MCP search（违 M30）；**根因 2**：query 用功能词 `link` 而非 affordance 同义词集 `[link, chain, connect, share, external, open]`（M35 缺）。M35 + M30 priority 1 双修才完整。

---

### M36 — Multi-page module reuse → file-local component → TVU library promotion audit

**任何 UI module 出现在 N ≥ 2 个 frame/page** 时：

1. **第一次**出现 → 自由发挥（一次性可不抽组件）
2. **第二次**出现前 → **停下**，把 module 抽为 **file-local component**（在产品文件的 `MicroApps Monitor — Local Components` 这类 section 内，命名 `MicroApp <Product> <Module>`）；后续所有出现都 instance 这个 file-local component
3. **第三次**出现后 → **停下问用户**：是否值得 promote 到 TVU library？若 yes → 写 promotion proposal（candidate name / variant axes / current usage list / value to other apps）给 TVU 库 maintainer

**Why**：本规则缺失时常见反模式——每帧 clone 一遍 module，改一处不同步，library compliance 漂移；用户事后才发现"为什么这里不一样"。

**Acceptance**：
- mockup 起手 / 写代码起手第 2 次实现某 UI module 前必跑 reuse check
- file-local component 命名遵守 `MicroApp <Product> <Module>` pattern（如 `MicroApp Video Sync Session Row`, `MicroApp Console State Pill`）
- handoff doc 必含 "reused file-local components" 段，列每个 module 的 instance count + 是否已抽

**反例**：本规则缺失时常见——session row、PP strip、Stream A/B column、`+ Add session` placeholder、Design Rules card 等模式重复出现在多帧，每帧独立 clone，未抽组件。

**实证**：
- **2026-05-20** Video Sync Multi-Session v1-v7：session row 模式在 M0/M1/M2/M3 + Plan B 共 5 帧重复，每帧独立 clone 而非 instance 同一个 file-local component。State Pill 是个反向实证——既存的 `MicroApps Monitor — Local Components` 里有 `State Pill` (3261:1480)，但 v0-v6 都自画 Badge → 用户抓 "为什么不用既存 State Pill" → v7 swap。**根因**：起手没扫产品文件 Local Components section 列举可复用组件，凭印象自画。

**Code 端镜像**：[`code-conventions.md` R17](./code-conventions.md#r17--multi-page-module-reuse 同步)。Mockup + Code 双向同源。

---

### M37 — Reference-image probe before design

**新功能需求迭代起手必做**：用户给的 reference image / 链接 / 既有页面（如 Plan B baseline / sibling Micro App）**必先 probe 提取 layout/style contract**，再进 Phase 0 / 开始设计：

#### Probe 输出（mandatory 5-row table）

| 维度 | 从 reference 提取 |
|---|---|
| **Layout 分栏** | 主区域划分：LEFT/RIGHT?  上/下？ 单栏？ 各栏宽度? |
| **Body content 模式** | 列表 / 卡片网格 / 单 detail / 多 stream 预览? |
| **Component patterns** | 现有 reference 用了哪些库组件？file-local 组件？哪些自画？ |
| **Interaction affordances** | 哪些元素 clickable？哪些 read-only？state 切换方式（click / hover / toggle）？ |
| **Spacing / padding** | page padding / panel gap / row 高度等具体数字 |

只有 5 行 table 填完才能进 Phase 0。

#### Why

凭印象造 = 必返工。本规则强制把"看 reference"从隐性快读升级为显性结构化 deliverable。

#### Acceptance
- handoff doc 必含 "Reference probe" 段（5-row table）
- Phase 0 mapping 中每个 element 必标注是来自 reference 复用 / 还是新增创造
- 用户在 review 时能从 probe table 反查"我给的 reference 是哪条被忽略了"

#### 实证
- **2026-05-20** Video Sync Multi-Session v0：我没先 probe Plan B Default + Standard Conversion 的统一 layout 模式，直接造了一个 sub-header strip（Filter chips + Sort + New Sync），用户抓"不存在于统一模式"→ v3 重做。**根因**：起手快读 reference 没结构化产出，把"凭印象记得 Plan B 长这样"当成 contract。

**Code 端镜像**：[`code-conventions.md` R18](./code-conventions.md#r18--reference-image-probe 同步)。同源约束。

---

### M38 — Selected indicator: 3-layer signal (left accent + fill + chrome) — 2026-05-26 扩第 3 层

任何**列表 / row / tab / card 的 selected 状态**视觉指示统一为 **3 层组合**（缺一不可）：

- **Layer 1 · 4px solid BRAND 左边 accent rail**（border-left）—— **必从 TVU Spacing token 取尺寸**，对齐 `--sp-xxs: 4px`（mockup 端用 `strokeLeftWeight = 4`），**必绑 `UX/Brand/Brand` Variable**（详见 M-COLOR C1）
- **Layer 2 · Layer_3 / 微亮 bg fill**（vs 未选中的透明 / Layer_2）—— **必绑 fill Variable**（如 `UX/Brand/Match,Hover` 或 `Color Type/Background/Layer_3`），不写 hex
- **Layer 3 · Selection-driven action chrome**（**新**，2026-05-26）—— 仅 selected row 才显示的 action affordance，如 `Remove` 控件、`Edit` 入口、上下文 menu 三点。**默认放 row 右端**，与 collapsed/expanded 状态无关；non-selected row **不渲染**这层 chrome
- **No** top / right / bottom border
- **No** box-shadow / no glow / 不改变 text 色（rail 和 fill 是双视觉信号 + chrome 是行为信号，足够了）

**为什么 3 层是 mandatory**：单一视觉信号在某些条件下不够稳：

| 视觉条件 | 单层失效场景 |
|---|---|
| Dark theme dim 屏幕 | rail 可能模糊；需要 fill 兜底 |
| 高密度 list（多 row 紧挨）| fill 不易 spot row-level 边界；rail 提供"这一行"边界 |
| 容器内仅 1 行 | rail 看不出选中；需要 chrome 提示"这是当前选中的 actionable row" |

3 层叠加保证在任何视觉条件下 user 都能 spot selected + 进入下一步行动。

> **Why 4px (not 3px)**：TVU Spacing token 集 `{xxs:4, xs:8, s:12, m:16, l:24, xl:32, xxl:40, xxxl:56}` 不含 3px。任何不在 token 集内的尺寸都是 off-token magic number，破坏跨产品视觉 grid 一致性。Selected indicator 这种重复出现 N 处的视觉 primitive 尤其必须 token-aligned。

#### 反例

| ❌ | 后果 |
|---|---|
| 4-side BRAND border (4px left + 1px top/right/bottom) | 看起来像 modal/card 不像 selected 行 |
| 仅改 text 色（不加 accent rail） | 信息密度低时辨识度不足 |
| 仅 rail（无 fill 无 chrome） | dark theme dim 屏幕下 rail 看不清 |
| 仅 fill（无 rail 无 chrome） | 多 row 紧挨时 fill 易混到相邻 row，缺 row 边界感 |
| **Selected row 无 selection-driven action chrome** | user 不知"选中后能干什么"，缺行动入口（2026-05-26 新增） |
| 不同选中元素用不同样式（1-side / 2-side / 3-side / 4-side 混用） | 跨页面视觉不一致 |
| **rail 宽度用 3px** 等 off-token magic number | 不在 Spacing token 集内 → 跨产品视觉 grid 不一致；改 token 时此处不会跟随 |
| 用 BLUE accent rail | 跟 brand color 冲突；BLUE 留给 annotation 层 / link / info |
| **3 层任一缺失** | M38 不达标（2026-05-26 新增） |

#### Acceptance
- 全产品所有 selected 列表项视觉一致（**3 层全齐**：1-side left accent **4px** BRAND + Layer_3 bg + selection-driven chrome）
- rail 宽度 **必从 Spacing token 取**——mockup 端用 `strokeLeftWeight = 4` 对齐 `--sp-xxs`；任何 off-token 数值（3 / 5 / 6 等）视为违规
- **rail + fill 均必绑 Color Variable**（M-COLOR C1）；raw hex 视为违规
- **第 3 层 chrome 必须在 selected 时显示、unselected 时隐藏**（不允许 always-on 占位）
- annotation 层用 BLUE，product UI selected 用 BRAND，两色 scope 不混

#### 实证
- **2026-05-20** Video Sync Multi-Session v3-v6：v3 用 BLUE 4-side wrapper + v6 不同帧 mixed 2-side / 3-side / 4-side → 用户抓"selected 边线 1/2/3 边不一致" + "BLUE 应改 BRAND" → v7 全部 unify 为 1-side BRAND 左 rail。
- **2026-05-20** Video Sync Multi-Session v8（同 session 后续）：rail 沿用 3px 初版数值 → 用户抓"3px 不在设计系统 Token 变量中，应该尽量使用 Token 变量的 Size" → 5 处 rail (M0/M1/M2/M3/M4) 改 4px（`--sp-xxs`），M38 同步增加"token-aligned size only"约束 + 反例条 + Acceptance 一条。
- **2026-05-26** Video Sync Phase B componentization：12 variants 全部含 rail + fill，但 walkthrough 发现 selected variants 的 remove-control（selection-driven chrome）当成"附加 affordance"而非"selected 视觉一部分" → 规则没把 chrome 列入 selected 视觉契约 → 本次 M38 扩第 3 层 chrome 为 mandatory。Rail + fill + chrome 三者均需 selection-driven（unselected 不渲染 chrome）。

**Code 端镜像**：[`code-conventions.md` R19](./code-conventions.md#r19--selected-indicator-css 同步)。CSS 实现：`border-left: var(--sp-xxs) solid var(--brand); background: var(--layer-3);` + selection-driven action chrome 通过 `[data-selected="true"] .row-actions { display: flex }` 切换。**禁** `box-shadow` / 4-side border / variant text color / off-token 数值（如 `3px` / `5px`）/ chrome always-on。

---

### M-FONT — TVU textStyles grid（fontSize 不可 off-grid）

任何自建 TEXT 节点的 `fontSize` 必从 **TVU textStyles 集合** {12, 14, 16, 18, 20, 22, 24} 中选。**禁** 用 10 / 11 / 13 / 15 / 17 等 off-grid 尺寸。

#### Why

TVU textStyles 是设计系统的 vertical rhythm 基础——所有 line-height / margin / 组件高度都按这个 grid 算。用 11px 等 off-grid 尺寸会破坏跨组件的 baseline alignment。

#### Acceptance
- post-build 必跑扫描：`frame.findAll(n => n.type === 'TEXT' && [10,11,13,15,17,19,21].includes(n.fontSize))` 命中数应 = 0
- 命中即视为违规，必须 round 到最近 grid 尺寸（10→12 / 11→12 / 13→14 / 15→14 / 17→18）
- 推荐用 setTextStyleIdAsync 绑定具体 textStyle 而非赤裸 fontSize

#### 实证
- **2026-05-20** Video Sync Multi-Session v4：33 个自建 TEXT fontSize 10/11/13 off-grid → 用户抓"字体也要用设计规范的字体" → 批量 round 到 12/14。

---

### M-TXT-ICON-AUDIT — Unicode-as-icon swap audit (post-build mandatory)

build mockup 后 wrap-up 协议必跑：扫描 TEXT 节点 `characters` 是否含 **Unicode-icon-charset** = `{→ ← ↑ ↓ ▲ ▼ ✓ ✗ ▾ ▸ ⚠ ⓘ ★ ❤ ⊙ ⊕ ⊖ ⊗ ⊘ ⇄ ⇅ ⤴ ⤵}`。命中即视为 **library-icon swap 候选**——必须替换为 TVU library icon 实例（按 M14.1 affordance category 选对的 icon master）。

#### Audit 脚本

```js
const UNICODE_ICONS = /[→←↑↓▲▼✓✗▾▸⚠ⓘ★❤⊙⊕⊖⊗⊘⇄⇅⤴⤵]/;
const hits = frame.findAll(n => n.type === 'TEXT' && UNICODE_ICONS.test(n.characters));
// 命中即 review；每条命中要么换 library icon，要么文字方式表达（不留 Unicode）
```

#### Why

text-as-icon 是 AI mockup 最常见的"顺手而过"漏洞——即使在同 session 内已经做了 Badge/Button/Tab 等 library swap，仍会漏掉 inline 小 arrow/checkmark/chevron。本规则强制 post-build audit 作为最后一道 gate。

#### Acceptance
- wrap-up 协议跑 audit 脚本，hits=0 才能 claim done
- handoff doc 必含 "icon audit" 段（hits 总数 / swapped 数 / 残留原因）

#### 实证
- **2026-05-14** MicroApps Console V4 Unicode `‹/›` chevron → V5 改 `icon/Arrow/Previous`（M14.1 实证）
- **2026-05-20** Video Sync Multi-Session 同款坑：route `→` / trend `↓ ↑` / sort `↓` / DONE `✓` 都漏掉 → 用户抓"图标还是用文本直接画" → 18 处 swap

---

## 已知 file-local 组件（产品文件内）

对应 Figma 文件：`Micro-Apps-20250923`（fileKey `DtZcMkhNy6qh6jbQQnhreQ`）。

| 组件名 | nodeId / key | 变体维度 | 备注 |
|---|---|---|---|
| `APP Icons` | id `3:14402`, key `fefd6aacdbb0a6e1acaf4106446e831fe3c5b8ff` | `Tag` × `Color` | 18 个 variant；**已知缺**：Color Correction / Graphics Insertion / Test Pattern Generator 变体——需要时**补到既有 set**，不要新建组件。 |

> 这类 file-local 组件遵循 M2，**优先使用**而不是自画。

---

## 历史背景

各 M-rule 对应历史复盘见 [`retrospection/`](./retrospection/)。

- **2026-04-30** Session Dashboard v1 → M1–M6
- **2026-05-06** Session Dashboard v2 review → M2 acceptance criteria 补强 + M7/M8/M9
- **2026-05-09** MicroApps mockup → M11–M20（M17-M20 已迁 `figma-technical-reference.md`；M12/M13 合并进 M11）
- **2026-05-11** SaaS Dashboard chart → M22 + M14 范围扩展 + M21 mandatory probe
- **2026-05-12** Architecture 重构（Steps 1-5）→ 通用规则迁 `design-process.md` / TVU 业务规则迁 `domain-tvu.md` / Figma quirks 迁 `figma-technical-reference.md` + `tools/figma-quirks.md`
- **2026-05-12** SaaS Dashboard Date Range Picker UX 交付注释实战 → M23（多状态交互流程图格式 + 主题无关配色 + GROUP vs FRAME + reconnect map）
- **2026-05-14** MicroApps Console Plan B Figma 实战 → M24（Code→Token mapping table 起手前置）+ M25（State Color Discipline，hover ≠ brand）+ M26（BG vs Text Grey Discipline）；Q7 嵌套 instance paint.opacity 失效（迁 `figma-technical-reference.md`）。**3 个 M-rule 同源：AI 把 "inactive grey" 凭直觉映射到 `UX/Grey/grey-7`（错的），把 sidebar hover 用 `Layer_3`（也错的），都因起手没产 Code→Token mapping table。共同根因是"凭看上去像哪个 token 就用哪个"，解药是把映射步骤显式化、前置化、产物化**。
- **2026-05-18** MicroApps Console Plan B 规则回流 → **M30**（图标强制从库取用：优先级表 + 禁止 Unicode 替代 + 同义词策略）；**M10 item 5**（product mockup icon 实例化后 fill 绑定，M2.1）；Q8 已回流 `figma-technical-reference.md`（HORIZONTAL auto-layout insertChild）。根因：V3 Sort TEXT "↑" + V4-V5 Unicode chevron 两个违例同源——跳过 icon library lookup 直接用文字占位，换词搜索即可命中。
- **2026-05-18** Touch-Screen v8 — 4-source Receiver flow 规则回流 → **M31**（Layout function decision tree：Auto Layout 默认，absolute 必备理由）+ **M32**（M30 的扩展：所有 product UI 元素 library-first，不止 icon）+ **M33**（M23 边界澄清：双语只对注释层；产品 UI 层单语）+ **M34**（M28 兄弟：Section/Frame children-bbox wrap audit，section 不会 auto-hug）+ **M35**（思考产物外化：affordance category 分类 + 视觉词汇扫描 + 跨上下文复用 3 行强制 trace）。**5 个 M-rule 同源根因**：M1 dialog v1 实现时把 (a) 产品 UI 当注释做了双语、(b) 没找 LCD Button 自画 gradient rectangle、(c) dialog 内布局全用 absolute x/y、(d) rules card auto-layout 撑大后没补 section.resize、(e) link 图标 search 用功能词没用 affordance 同义词矩阵 + 跳过 published library MCP search 5 个问题一起出现。共同主线是"在 product UI 层套用了 annotation 层的范式 / 跳过库 lookup / 跳过 layout 决策 / 思考过程没外化为产物"。**M30-M34 描述 WHERE 搜，M35 强制 HOW 思考再搜——三层联用才覆盖完。** code 侧同步镜像：**R13**（M35 mirror）+ **R14**（M31 mirror）+ **R15**（M32 mirror）+ **R16**（M33 mirror）一并 commit；用户主动要求 "不是所有的都得等实证再约束，早发现早解决"——双向规则同源消除 mockup ↔ code 之间的 anti-pattern 漂移。

---

### M39 — Variant text property abstraction（component build 必备）— 2026-05-26 新增

建 component variant 时，若 variant 内的某文本字段（title / subtitle / slot / delta / timing / status 等）在**预期 instance** 之间需要**不同值**，**必须**定义对应的 **component property (TEXT type)**——**禁止**靠 instance-level text override + (隐式) detach 完成内容差异化。

**阈值**：单 variant 同字段在 deliverable 中预期出现 **≥ 3 个不同值** = 必须抽 property。≤ 2 个不同值可暂用 inline override，但需 ledger 记录。

| 路径 | 适用 | 维护成本 |
|---|---|---|
| ✅ **Component property (TEXT)** | 任何 ≥3 instances 需不同文本的字段 | O(1)：master 加 property，instance 改一个属性值 |
| ❌ **Instance text override** | 仅 1-2 处偶发 | O(N)：每 instance 单独 find + override；detach 后失去 master 同步 |

**Why:**
1. Instance override 单次便宜，规模化（10+ instances × 5 字段）维护成本指数增长
2. Override 一旦 detach → 失去 master 更新优势 → master 升级时这批不跟随
3. Component property 是 figma 给的 first-class 机制，绕过 = 规避机制

**反例**（2026-05-26 Video Sync Phase B）：12 variants 文本（title / subtitle / slot-A / slot-B / delta / timing）全 inline override，10 个 frame 创建 instance 时挨个 `findOne('TEXT')` + 改 characters。下次扩 frame 数 / 改文案模板都要重写 N 处。**正解**应是 master 加 7 个 TEXT property（title / subtitle / slotA / slotB / deltaValue / deltaUnit / timestamp），instance 上仅设 property 值。

**Acceptance:**
- 建 variant 起手必估算"此 variant 各文本字段在 deliverable 中预期不同值数"
- ≥ 3 → 必抽 component property + ledger 注明（不抽则视为违规）
- ≤ 2 → inline override 可，但 ledger 注明"future-promote 候选"
- Instance 创建后**只通过 setProperties() 改文本**，不再 findOne + characters 直改

**Code 端镜像**：无（component property 是 figma 专属机制）。

---

### M40 — `#N` list item 标号必须文档化 sequence vs name semantic — 2026-05-26 新增

任何 list item 用 `#N` 数字标号（如 `Sync session #1`, `Slot #3`, `Stream #2`）时，**必须**在 design / spec / PRD 中**显式声明**它的语义类型：

| 语义类型 | 删除后行为 | 适用 |
|---|---|---|
| **Sequence (位置序列)** | Renumber: `[#1, #2, #3, #4]` − `#2` → `[#1, #2, #3]`（原 #3 → 现 #2） | Fungible items（如 sync session、generic slot、ordering 不重要的）|
| **Name (稳定名称)** | Gap: `[#1, #2, #3, #4]` − `#2` → `[#1, #3, #4]`（保留 gap） | Identifiable items（如 slot binding to hardware port、channel mapping 有外部引用）|

**默认推荐**：
- Fungible items → **sequence**
- Identifiable items → **name**

**Why**: 歧义会让开发实现错；用户对 remove 后 UX 变化预期 unclear → 真实交互时 surprise user。Spec 不文档化 = 开发自由发挥 = 50% 翻车。

**Acceptance**：
- Spec mockup 含 `#N` list item 必在 Design Rules card / state-label 显式声明语义类型
- Sequence 类型在 Remove confirmation flow 后必显示 renumbered list（mockup 必画一帧 after-remove，参 M16.1）
- 跨 spec 文档 / PRD / 实现代码三处文档 ID 语义必一致，不允许 spec sequence + 实现 name

**实证**（2026-05-26 Video Sync Phase B）：原 spec 无显式声明 `Sync session #N` 是 sequence 还是 name → walkthrough 时澄清为 sequence → M10 "After Remove · Renumbered" frame 新增 + Design Rules card §13 加 renumber 规则。

**Code 端镜像**：[`code-conventions.md` R20](./code-conventions.md#r20--list-item-id-semantic 同步) 候选。后端 API / 前端 state model 必须按 spec 声明实现（sequence = renumber on splice / name = id 保留）。

---

### M41 — Destructive confirm modal: 全屏 scrim + 居中单 dialog — 2026-05-26 新增

删除 / 危险操作 confirm dialog 真实用户态**必须** = **全屏 scrim + 居中单个 dialog**。

**Scrim 标准样式**（user-validated 2026-05-26 V）：
- Fill: `{r:0, g:0, b:0}` opacity **0.2**（不 0.5-0.6，避免黑过重；后景应仍可识别）
- Effect: `BACKGROUND_BLUR` **radius 8**（毛玻璃 / frosted glass 进一步柔化后景）
- 覆盖整个 viewport / frame（mockup 端 = `1920×1080` 整 frame；code 端 = `position: fixed; inset: 0`）

**Dialog 标准**：
- 必用 **TVU `Notification` component instance**（`component_set 4482:1197` master variant `4482:1204`，theme=dark, status=pop confirm）
- 推荐尺寸 **480×182**（小而聚焦），**绝不超过 720×320**
- 居中绝对定位（mockup 端 `(frame_w-480)/2`, `(frame_h-182)/2`；code 端 `top: 50%; left: 50%; transform: translate(-50%, -50%)`）

**关键区分**：
- **Mockup artifact**（design 工作区 / variants 区）：可能 2-3 个 dialog variant 并排放显示状态变化（如 "OFF session" / "ON session" / hover state）—— **这是 design tool 用法**
- **真实用户态**：永远 **1 dialog + 全屏遮罩** —— **这是 user 实际看到的**

**禁止**：
- ❌ 把 multi-variant artifact 直接当 user 态 ship 进 spec mockup user-flow frame
- ❌ Scrim opacity ≥ 0.5（视觉过重，dark-theme console 与后景对比失衡）
- ❌ Scrim 漏 `BACKGROUND_BLUR`（纯透明色块缺质感）
- ❌ Dialog detach 后自建（应保持 TVU library 连接，遵守 M32 library-first）

**Acceptance**:
- Spec mockup 含 destructive confirm flow 必有专门一帧画 modal-overlay 态（M16.1 default+override 中的 override）
- 该帧 scrim 数值精确（20% black + blur 8）
- Dialog 必是 TVU `Notification` instance，非 detach 自建
- Multi-variant 区如有展示，必标 "artifact / variants reference"，与 user-flow frame 分隔

**实证**（2026-05-26 Video Sync Phase B M9 重做）：
- v1 用 `M-Remove-Dialog source` artifact (1580×474 含 2 dialog 并排) 当 M9 overlay → 用户指出"实际只会有一个，应模拟用户真实看到的效果，只显示一个确认框，背景应该是个遮罩层来显示"
- v2 改为单 V2 backdrop 720×320 + 60% 黑 scrim → 用户进一步调整 scrim opacity 60→20、加 BACKGROUND_BLUR radius 8、dialog 换 TVU `Notification` 480×182 → 最终视感为本规则标准

**Code 端镜像**：[`code-conventions.md` R21](./code-conventions.md#r21--destructive-modal-pattern 候选)。CSS 实现：`backdrop-filter: blur(8px); background: rgba(0,0,0,0.2);` + TVU `Notification` 组件 instance。

---

### M45 — Figma Page 命名规范（Jira 关联型 mockup）— 2026-05-27 新增

任何与 Jira 需求关联的 mockup page，**page name 必须**满足三段式：

```
< FB-xxxx > <Title in EN> YYYYMMDD
```

#### 强制字段

| 字段 | 规则 | 示例 |
|---|---|---|
| `< FB-xxxx >` | Jira issue key 用尖括号包裹，前后留空格 | `< FB-9398 >` |
| `<Title in EN>` | 一句英文摘要，与 Jira summary 对齐（不强制 1:1，可缩短） | `DC Status Visibility on Global Home` → 进 page 名时可简化为 `Environment Visibility on Global Home` |
| `YYYYMMDD` | 创建日期 8 位数字，无分隔符 | `20260527` |

#### 触发

- 在 TVU 产品 Figma 文件**新建** page 用于装某个 Jira 需求的 mockup → 必须按此命名
- **修改既有 page 的内容** to host 新需求 → 必须 rename page 到此格式（不允许保留 `Page 41` 这种 Figma 默认名）

#### 反模式

| ❌ | 原因 |
|---|---|
| `Page 41` | Figma 默认名，无法搜索 / 无版本信息 |
| `FB-9398 DC Status` | 缺尖括号 + 缺日期 + 缺 EN title |
| `< FB-9398 > 设备 DC 状态` | Title 用了中文 → 跨语言团队检索困难（Figma 搜索框 EN 优先） |
| `< FB-9398 > Environment Visibility` | 缺日期 → 多版本 mockup 时分不清新旧 |

#### Why

- **可检索**：Figma 文件 page 数量很快上百，没有 FB-id 前缀只能靠模糊搜索
- **可关联**：FB-id 直连 Jira，PM / dev / QA 跨工具可追溯
- **版本对照**：日期后缀让"v1 / v2 / 同主题多次迭代"在同一 file 内可共存（旧 page 不删 / 不改名，新 page 加新日期）
- **跨语言**：Title 用 EN 保证 Figma global team 搜索一致

#### 实证

**2026-05-27 FB-9398**：AI 在 `Page 41` 直接画 mockup 未 rename，用户抓"页面的 Page 名称也没有修改"。改为 `< FB-9398 > DC Status Visibility on Global Home 20260527`（与既有 page 如 `< FB-9249 > Disable editing the PID 20260207` 对齐）。

#### Acceptance

- [ ] page name 三段式齐全
- [ ] FB-id 真实有效（gh / jira API 可查）
- [ ] 与同 file 内其它 page 命名风格一致（probe `figma.root.children.map(p=>p.name)` 抽样检查）

---

### M44 — UI Label 不暴露后端服务名 / 内部代号 — 2026-05-27 新增

任何用户可见的 UI 文案（column header / button label / chip text / tooltip title / menu item / breadcrumb / page title）**禁止**直接使用后端服务名、内部代号、数据库字段名、缩写未注释名等"用户视角看不懂或会误解"的词。

#### 触发场景

| 反模式 | 用户视角 | 正确做法 |
|---|---|---|
| 列名 `DC` | "DC = Data Center? Disconnect? DC Comics?" | `Environment`（DC 是 Device Configuration 服务的内部缩写，用户不应感知后端服务） |
| 按钮 `Trigger Webhook` | "trigger 啥？webhook 又是啥？" | `Sync Now` 或业务动词 |
| 字段 `srv_id` / `device.dcId` 直接显示 | 数据库字段直露 | `Service ID` / `Device Environment` |
| Tab `RPS One` 与 `RPS Two` | 1/2 是内部代号 | 业务名词如 `Cellular RPS` / `Wired RPS` |

#### 决策 checklist（mockup 评审 + PRD 评审同步用）

1. 这个 label 是否含**缩写**？→ 缩写是否对**目标 persona** 显而易见？不是 → 必须扩写或换业务词
2. 这个 label 是否含**内部团队/服务名**（如 DC / RPS / Cap / MicroApp）？→ 用户角色（PM / 运维 / 客户）是否在日常工作中接触这些服务？不接触 → 必须换业务词
3. 这个 label 是否是**数据库字段**直露（snake_case / camelCase）？→ 永远换成 Title Case 业务名

#### Why

UI label = 用户与产品的契约。后端服务名是"内部工程语言"，把它直露给用户：
- 用户认知负担 ↑（解码内部代号）
- 重构风险 ↑（后端服务改名 → UI 也得改）
- 跨团队沟通歧义 ↑（"DC bug" 到底是 Device Config 还是 Data Center？）

正确做法是建立**领域语言（domain language）翻译层**：UI 文案用业务词 → mapping table 维护到具体后端字段。

#### 实证

**2026-05-27 FB-9398**：Mockup v1 直接把列名命名为 "DC"（后端服务 Device Configuration 的缩写）。用户抓："DC 应该显示为 Environment（From DC）或者更好的措辞，DC 指的是一个修改设备配置的服务"。v2 改为 `Environment`，并在 UX 交付说明中明确"字段来源 DC 服务但用户侧不暴露服务名"。

#### Acceptance

- [ ] mockup 所有 user-facing label 过一遍 checklist 3 问
- [ ] PRD 在"关键元素 Key Elements"段为每个新 label 显式声明 EN/ZH 业务名 + 数据来源后端字段（M22 acknowledgment 内联）
- [ ] 后端服务名 / 内部代号 / 数据库字段名仅出现在 dev-only annotation（M33 注释 vs 产品 UI 语言隔离）

#### 配对规则

- [M33](#m33--annotation-vs-product-ui-语言隔离m23-扩展)：注释里允许写"DC 服务"，但产品 UI 必须用 Environment
- [M22](./design-process.md#m22--user-design-intent-acknowledgmentprefix-gate)：用户 design ask 含 label 命名 → ack 阶段就要确认业务词

---

### M43 — Constrained-Cell Text Overflow: wrap vs ellipsis + hover-reveal — 2026-05-27 新增

> **↔ Code 端镜像**：[`code-conventions.md` R23](./code-conventions.md)（R23 = M43 mirror，改此条必同步对面）。

任何**宽度固定 / 高度受限**的容器（典型：table cell、card title、tag、tooltip body、property panel value、breadcrumb segment）若内容**有可能超过容器尺寸**，mockup 必须**显式给出 3 个状态**：(a) 正常 in-bound、(b) overflow + 截断、(c) hover-reveal 完整内容。**禁止只画 happy path 短文本** —— 由 dev 现场拍脑袋决定截断行为是 M43 主要违例形态。

#### 触发条件

**强制触发**（任一命中）：
- 容器是 fixed-width（table cell / chip / tag / fixed column / tooltip 宽限）且内容来自后端（长度不可控）
- 容器有 `maxLines` / `height` 约束且内容可能多行
- 字段语义允许「短/长」两种形态（如 region 名 "EU" vs "EU-West-Frankfurt-2"，company 名 "TVU" vs 长公司全名）

**跳过**：
- 容器宽度 hug 内容（无约束 → 无 overflow 风险）
- 内容枚举封闭且最长值 < 容器宽度（如 status 仅 4 个固定值，每个 < 容器宽）

#### 截断策略决策树（高度规则驱动）

```
容器允许多行？（`maxLines > 1` 或 `height = auto`）
  ├─ YES → 多行 wrap，到 maxLines 上限后 tail-ellipsis（…）
  │        若 hover 时仍有未显示内容 → 触发 tooltip
  └─ NO  → 单行 ellipsis
           ├─ 内容含 ID/URL/path（前后均有信息价值） → center-ellipsis（M29 dev-side spec）
           └─ 内容是自然语言 / name / label → tail-ellipsis（...）
           hover 时 → 必须触发 tooltip 显示完整内容
```

**默认 maxLines**：

| 容器类型 | maxLines | Why |
|---|---|---|
| Table row（dense / 行高 36px）| 1 | 行高固定，wrap 会破坏整表节奏 |
| Table row（comfortable / 行高 ≥ 56px）| 2 | 行高允许 2 行不破坏对齐 |
| Card title | 2 | 标题信息密度高，2 行可接受 |
| Card subtitle / metadata | 1 | 次级信息 ellipsis 即可 |
| Tag / Chip | 1 | 固定高度，必须单行 |
| Tooltip body | 自由（不截断） | tooltip 本身是 reveal 容器 |
| Breadcrumb segment | 1 | path 用 center-ellipsis 保留首尾 |

#### Mockup 端硬规则

| 维度 | 规则 |
|---|---|
| 状态完整性 | mockup 必须包含 (a) 短文本正常态 / (b) 长文本截断态 / (c) hover tooltip 三个 frame 或一组 M23 多状态流程图 |
| 截断符号 | 单行 tail `...`（3 dot ASCII 或 Unicode `…` U+2026，任选一种全局统一）；center-ellipsis 形如 `udp://237.0...0:1234` |
| Tooltip 容器 | 沿用 TVU library `tooltip/default` instance（M32 Library-First）；禁止自画 RECTANGLE + TEXT 拼装 |
| Tooltip 触发标识 | hover 时鼠标 cursor 变 `default`（不变 `pointer`，因 tooltip 是被动 reveal 不是 action） |
| 数据准备 | mockup sample data 至少包含 1 条「显著超长」记录（≥ 容器宽 × 1.5），证明截断状态可视化 |

#### dev 端镜像（→ `code-conventions.md` R23）

| 维度 | 规则 |
|---|---|
| CSS pattern (single line) | `white-space: nowrap; overflow: hidden; text-overflow: ellipsis;` |
| CSS pattern (multi line) | `display: -webkit-box; -webkit-line-clamp: <N>; -webkit-box-orient: vertical; overflow: hidden;` |
| Center-ellipsis | JS 拆字符串保留前缀 + `...` + 尾段（CSS 无 native 支持） |
| Truncation detection | `el.scrollWidth > el.clientWidth`（单行）或 `el.scrollHeight > el.clientHeight`（多行）→ 仅截断时挂 tooltip，避免无谓 tooltip 噪音 |
| Tooltip 内容 | full text 原值；含富文本/换行时保留语义（用 `\n` 或 `<br>`） |
| a11y | tooltip 必有 `aria-describedby` 关联；截断元素 `title` 属性 fallback（无 JS / 触屏环境） |

#### UX 端镜像（→ `role-ux` skill）

- PRD 描述新字段时必声明该字段**长度分布**（典型值 / p95 / 最长理论值）+ **截断行为是否影响业务**（如 IP/MAC/ID 不能截断需要完整复制 → 走 M29 Form 1 hug + copy icon，不走 M43 ellipsis）
- 用户测试场景必涵盖「长文本 record」case，不只跑 happy path
- Persona simulation（F2-late）走查时必检查长文本下的 5 stage journey（avoidance: 用户因看不全而切窗 / 误判）

#### Acceptance

- [ ] mockup 含「正常 / 截断 / hover tooltip」3 状态（或在 M23 多状态流程图中显式标注）
- [ ] sample data 含 ≥ 1 条超长记录证明截断态可见
- [ ] 行高 / maxLines 决策有文档化依据（默认表 OR 显式 override + 理由）
- [ ] tooltip 用 library `tooltip/default`，未自画
- [ ] dev 实现含 `scrollWidth > clientWidth` truncation detection（避免无谓 tooltip）
- [ ] a11y：`aria-describedby` + `title` fallback 双保险

#### M43.2 — Column Width Self-Audit（2026-05-28 新增）

调列宽 / 改 cell 内容 / 列重排 / 加新列后，**必须 zoom-in 截图扫每一列**，不能只截整表缩略图自检"看起来 OK"。

**触发场景**：

- 调整任何列宽（变宽 / 变窄 / 重新分配 budget）
- 改 cell 内容（含批量 update 5+ cell）
- 列重命名 / 重排
- 加 / 删列

**每列 zoom-in 必验 3 项**：

1. **不溢出**：cell 内容（文本 / button group / status badge）不超过列宽边界，没有 truncation 异常（含 ellipsis 没生效、固定 width 文本溢出邻列等）
2. **不换行（除非有意 wrap）**：header label 单行（如 "IS+/ISX" 不能挤到 2 行）；body cell 按 M43 决策树（默认 36px 行高单行 ellipsis）
3. **多 element cell 内部间距**：action button group（Edit / Delete / Status）/ status badge with dot / link icon group 等含**多元素 cell**，必须验各元素间距充足（不挤到一起）+ 末尾元素不切边

**典型遗漏**：

| 遗漏点 | 表现 |
|---|---|
| Operation 列含 3 个 action link | 列宽算了 link 文字 char 数，没算 link 之间的 12-16px gap → 3 link 视觉拥挤或末 link 切边 |
| Status 列含 dot + label | 算了 label 字宽没算 dot + dot-label gap |
| 列重命名后 header label 变长 | 新 label 超出原列宽 → header 换行 |
| Cell 数据从 enum 短值升级为后端真实值 | 短值时 OK，真实值时溢出 |

**Audit 流程**：

```
1. 截全表概览（确认列数 + 行数 + 整体节奏）
2. 列循环 zoom-in（每列单独截图 → 验上述 3 项）
3. 多元素 cell 单独 zoom（Operation / Status 等）
4. 截 header 行单独看（验 label 无换行）
5. 截 1 条长文本 sample row（验 M43 ellipsis + tooltip 三状态）
```

**反例**：

| ❌ | 原因 |
|---|---|
| 调完列宽只截了整表（1920px 渲染到 800px 缩略图）就交付 | 缩略图看不清单列内容是否溢出 |
| Self-check 只 verify "数据落到正确列" | 漏了"内容在列宽内是否完整可读" |
| 没专门验 Operation / Status 等多元素 cell | 这类 cell 间距问题最容易溢出 |

#### M43.2 Acceptance

- [ ] handoff 截图含每列 zoom-in（或显式声明哪些列做了 audit + audit 结果）
- [ ] Multi-element cell（action group / status badge / icon group）单独 zoom 验证
- [ ] Header 行单独截图确认无 label 换行
- [ ] 列宽变更后立即跑 audit，不放到最后批量

#### 实证

**2026-05-28 TPC-628**：rebudget 列宽时把 Operation 列从 150px 砍到 90px 给 Extended Info 让位，截图自检只看了"数据落对位"，没 verify "Edit Delete Status" 三按钮能否在 90px 内塞下。用户抓"Operation 这列的内容已经溢出了，为什么自检的时候没有发现"。补救：Operation 还原到 130px、IS+/ISX 58 → 70（header 不换行）。**根因**：M43 原本只管"长文本截断 3 状态"，没覆盖"调列宽后每列宽度是否容得下当前内容"。本规则即此回流补强。

#### 反例

| ❌ 错误做法 | 原因 |
|---|---|
| mockup 只画短文本「Global / APAC」就交付 | dev 收到「EU-West-Frankfurt-2」时无据可依，自由发挥 |
| 单行 ellipsis 但无 tooltip | 用户永远看不到完整值，等同信息丢失 |
| 所有截断都挂 tooltip（不检测是否真的截断） | hover 短文本也弹 tooltip，视觉噪音 + a11y 干扰 |
| 用自画 RECTANGLE + TEXT 实现 tooltip | M32 Library-First 违例；样式会随主题切换翻车 |
| wrap 行数无上限 | 一条超长记录把整行撑到 5 行，破坏 table 节奏 |
| 把 ID / IP 等可复制字段强行 ellipsis | M29 Form 1 要求 hug + copy icon，M43 不适用于 copyable 信息字段 |

#### Why（深层）

**信息密度 vs 完整性的张力**。表格 / 卡片 / chip 这类信息密集容器，必须在「一屏看到更多记录」与「单条记录信息不丢」之间权衡。截断 = 牺牲完整性换密度；hover-reveal = 用交互成本（鼠标移动 + 等待）赎回完整性。**M43 把这个权衡显式化**：mockup 必须设计 3 状态、dev 必须做 truncation detection、UX 必须验证长文本场景，否则 happy-path bug 会一路漏到生产 —— 用户看不到 "EU-West-Frankfurt-2" 完整值但又没有 hover 提示，业务上会按 "EU-West..." 误判区域。

#### 实证

**2026-05-27 FB-9398 Environment column**：T list / R list 新增 Environment 列时 sample data 仅含 4-7 字符短值（"Global" / "APAC" / "US-East"），用户提出「内容显示不下应换行或省略号 + hover tooltip 显示完整」时才意识到 mockup 漏了长文本 case。本规则即此实证回流。

#### Legacy ID 关联

- **M29** dev-side spec 中已提及 `center-ellipsis` for URL/ID，M43 将其升级为 umbrella 覆盖所有受限容器，并补全 wrap / maxLines / hover tooltip 决策树
- **M32 Library-First**：tooltip 必走 library instance，不允许自画 → 本规则强化
- **M-INTEGRITY I3**：children wrap 不能撑破容器 → M43 maxLines 上限正是 I3 的具体化

---

### M42 — Clone Gate: probe brand-color paint bindings before mutate — 2026-05-26 新增

任何 Figma `node.clone()` / `createInstance()` 操作起手**必须先 probe 源元素所有 brand-color paint 的 `boundVariables`**——发现裸 hex（无 boundVariables）**必先修源 OR 在 clone 后立即补绑**，**不要把 bug propagate** 到 N 个 instance / variant。

**触发场景**（任一命中 → 强制 Clone Gate）：
- `node.clone()` 调用建 component variant
- `node.clone()` 调用建 mockup frame
- `createInstance()` 后再对 instance 做 `.findOne().fills = ...` 类的复用 paint 操作
- 任何"sourceNode.strokes / fills" 当 template 在多处赋值的模式

**Probe checklist**（mockup 端 5 步必走）：

```js
// 1. Identify source element
const source = await figma.getNodeByIdAsync(sourceId);

// 2. Enumerate all brand-color paint surfaces
const paintSurfaces = ['fills', 'strokes'];

// 3. For each surface, check every paint's boundVariables
for (const surface of paintSurfaces) {
  const paints = source[surface];
  if (!paints || paints === figma.mixed) continue;
  for (const paint of paints) {
    if (paint.type !== 'SOLID') continue;
    const isBrandColor = looksLikeBrandColor(paint.color); // user-defined heuristic
    if (isBrandColor && !paint.boundVariables?.color) {
      // 4. 失败 — 源有裸 hex brand color
      console.error(`Source ${source.id} ${surface}: raw hex ${rgbToHex(paint.color)}, missing boundVariables`);
      // 5. 决策：a) 修源 + Variable 绑 → 再 clone；b) clone 后立即在新节点上补绑
      // 不允许 propagate raw hex 到 clone
    }
  }
}
```

**为什么 Clone 是规则盲点**：

| 现有 brand color 规则 | 触发时机 |
|---|---|
| **C1**（M-COLOR）"写 paint 前绑 Variable" | 用户**画新** paint 时 |
| **consumer-product-conventions Rule 2** "写 hex 前 grep brand variables.css" | 用户**写新代码 / 文档** 时 |
| **working-principles 原则 4** "颜色硬编码 = bug" | code review 时 |

**全部都假设"写"——没有"克隆已有 bug 物料"的检查 trigger**。Clone Gate 填这个空白。

**Why（深层）**：在 component library 建设场景，1 个源 bug × N 个 variants × M 个 instances = N×M 倍 bug。修复需 N×M 次操作。预防 = probe 1 次 + 修源 = O(1)。

**Acceptance:**
- 任何含 `node.clone()` / `createInstance()` 多次调用的 mockup task 必先 probe 源元素 brand-color paint bindings
- Probe 结果必入 handoff doc（"clone source probe table"）
- 发现裸 hex 必修源**或**clone 后立即补绑——不允许 "先 clone 之后再说"
- 违规 = 12 个 variants 都带 bug 不视为"规模问题"，视为单一 Clone Gate 违规（聚合处罚）

**实证**（2026-05-26 Video Sync Phase B Step 2）：从 M0/M1 selected row（4122:162）clone 出 5 个 anchor variants 作 12-variant 起点。**源 row 的 4px LEFT BRAND stroke 是裸 hex `{0.184, 0.71, 0.306}` 无 boundVariables**——clone 过程未 probe，bug 传到 5 anchors → 又派生 12 variants → walkthrough audit 才发现 → 一次性绑 6 selected variants 的 stroke 修复。**根因：Clone 流程缺 Gate**，C1 规则只 cover"写"不 cover"clone propagate"。

**Code 端镜像**：[`code-conventions.md` R22](./code-conventions.md#r22--clone-gate 候选)。代码端的对应：fork / copy existing component file 时必检查其引用的所有 token，不允许 hardcoded color 沿用。

---

### M42.2 — Cloned Frame Structural Audit（M42 extension）— 2026-06-02 新增

Cloning a frame inherits ALL children, 包括 invisible / decorative elements（scrollbar 占位 / spacer / overlay backdrop）。这些在源 frame 视觉不明显，clone 到新 frame 后做结构改动时容易产生**冲突或丢失隐式定位**——M42 原 scope 仅 cover brand-color paint propagate，本 sub-rule 扩到结构继承层面。

#### 两类典型病灶

1. **隐形装饰元素 propagate** — 源带 invisible decoration（如 Plan E 6×280 scrollbar 占位 Frame 3179），clone 链一路带下来但视觉融背景。新加同语义元素时会叠加。
2. **`layoutPositioning` AUTO → ABSOLUTE 失 inset** — 把 cloned 子元素改 ABSOLUTE 后，丢失 parent auto-layout 的 `paddingLeft` / `paddingTop` 隐式定位；需手动补回。

#### Audit Checklist（post-clone, pre-mutate）

1. **枚举 cloned outer frame 所有 direct children**——包括 fills 为空 / 0 opacity / 看似无关的 "Frame XXXX" 命名
2. **标记装饰 / scrollbar / spacer** —— 典型：边缘 thin rect、empty 命名 frame、overlay backdrop
3. **加新 overlay 前必先 grep 源是否已有同语义元素** —— 如加 scrollbar 前查现有 6×280 thin rect on right edge
4. **改 `layoutPositioning` 至 ABSOLUTE 前 capture 当前 x/y**——改完手动 set x/y 补回 parent auto-layout 提供的 `paddingLeft` / `paddingTop` inset

#### Acceptance

- [ ] post-clone 必跑 direct children enumeration（含 decoration），结果入 handoff
- [ ] 任何加 overlay 操作必有"已 grep 源, 既有同语义元素 N 个"的 trace
- [ ] 任何 AUTO → ABSOLUTE 转换必有"原 derived x=?, y=? → 手动 set x=parent.paddingLeft, y=..." 的 trace

#### 反例

| ❌ | 后果 |
|---|---|
| Clone frame，直接加新 scrollbar，没查源既有 Frame 3179 | 2 个 scrollbar 视觉叠加 |
| `child.layoutPositioning = "ABSOLUTE"` 后 set `x=0`，忽略 `parent.paddingLeft=40` | child 跑到 body 左边缘，与 sibling 失去 inset 对齐 |
| 假设源中 `visible: false` 就一定不显眼 | Frame 3179 `visible: true` 但黑底融合视觉不显眼，加新覆盖元素后才暴露 |

#### 实证（2026-06-02 BM-1047 S6）

Clone S3 → S6 建 Service Price expanded state（Plan E v3 风格 scrolled-down view）：

- **Bug (a) 双 scrollbar**：Frame 3179（Plan E 原生 scrollbar 占位，6×280 SOLID fill `visible: true`）从 S1 一路 clone 下来，5 个 frame 都有但黑底融合视觉极不显眼。S6 加新 `scrollbar-indicator` 后 2 个叠加，walkthrough 才捕获。
- **Bug (b) License section x=0 失 inset**：将 License section `layoutPositioning = "ABSOLUTE"` 模拟 scrolled-down 状态，set `x=0`。但 body 有 `paddingLeft=40`。结果 Assigned row 在 `x=0`（body 边缘），与下方 Token 区 sibling 卡片（`x=40` content area inset）失对齐。

两个 bug 同根：M42 brand-color paint scope 不足以 cover "clone + structural mutation"。本 sub-rule 把 Clone Gate 扩到结构继承层面，把"clone 后再改 layout"的隐式依赖纳入 audit。

---

### M42.3 — Clone Placement Discipline: 就地改优先，禁旁路克隆搭样板（M42 extension）— 2026-06-17 新增

迭代 / 重构**既有节点**时**默认就地修改（in-place edit）**。**禁止**把源克隆到旁路空白区当"待审样板 / 草稿"、图后续再替换原物——这会在文件里沉淀**并行重复集**（旧版 + 克隆版 + 用户精修版并存），制造混乱、难辨真源、逼用户逐套核对。

- **判定**：要改的是**已存在**的物 → 就地改其节点；只有要**新增一个原本不存在**的元素时才 create/clone。
- clone 出的中间草样若确需 review，**决策后立即删**，不得与既有物并存。
- **Acceptance**：迭代既有 mockup 后，文件内**同一语义物只剩一套**（无 "v1 + v2 + 精修版" 并存）；handoff 能答"我改的是哪个既有节点，无新建并行副本"。
- **与 M42 / M42.2 区别**：M42/M42.2 管"clone 时不传播 bug / 结构"；本条管"**该不该 clone**"——优先不 clone，就地改。
- **实证 — 2026-06-17 BM-1047 V3**：账户下拉重构时，AI 把原卡克隆到右侧空白当"待审样板"，结果文件里**三套并存**（旧按钮矩阵 + 带 chevron 的 v2 + 用户精修 6369:85）。用户多次纠正："记得是修改，不是新建""仍然不理解为什么你没有修改而是新建"。根因："先旁路搭、确认后替换"的错误本能 → 制造重复、难收口。

---

### M46 — Library Instance Iteration Discipline（2026-05-28 新增）

迭代既有 mockup 时（典型：clone 上一期 page 改成本期 page，需要重命名列 / 加列 / 改 cell 内容），**起手第一动作必须 probe library instance 的可改动能力**，**不准跳到 detach instance 或自画 overlay 覆盖原表**。

#### 起手必走 3 步 probe

```
1. probe `instance.componentProperties` —— 找 TEXT property，可直接 setProperties() 改 label
2. probe instance 的 children → findAll TEXT —— 找 text override 节点，可 .characters = '新值' 改文字
3. probe row 结构异质性 —— 行 cell count 不一定一致（如某些 selected row 是 12-cell variant，
   普通 row 是 14-cell variant），按 cell count 分组处理，不要 idx-based 盲改
```

#### 决策树

```
能用 setProperties() 改吗？
  └─ YES → 用 setProperties()
  └─ NO  → 能用 text override (.characters = ...) 改吗？
             └─ YES → 改 text override
             └─ NO  → 真的需要变更 layout 结构 (加列 / 删列 / 列宽根本不够) 吗？
                        └─ YES → 才考虑 detach (range narrow，仅对必要的几个 cell)
                        └─ NO  → 不动 instance，找其他实现路径
```

#### 反例（命中即上一段输出不合格，重做）

| ❌ 反模式 | 正确做法 |
|---|---|
| 起手就 createFrame 自画 overlay 覆盖既有库 instance 表 | 先 probe instance 可改性，只在确实结构性不可行时才 detach |
| 因为"听起来要加 2 列"就 detach 整张表 14 个 cell | 加列也可能用文本 override（如把不用的"Conn IP" / "Conn Port" 列重命名为"Version" / "Build"）+ 只 detach 必要 cell（如 Extended Info 因 wrapper anchor 限制）|
| 行 idx-based 盲改没考虑 12-cell vs 14-cell variant | 先 group by cellCount，按 variant 分别处理 |
| 自画 overlay 后藏掉原 instance | 库 link 失效、视觉风格与全产品脱节、违 M32 |

#### Acceptance

- [ ] 改 mockup 前 probe 输出 instance 可改性（componentProperties / text override / cell count grouping）
- [ ] detach 限制在 minimum necessary scope（不 detach 整行整表）
- [ ] 自画 overlay 仅作 transient debugging，handoff 前必须删干净（见 design-process.md §Delivery Cleanup Gate）

#### 实证

**2026-05-28 TPC-628**：起手误判"不能改 Table/Theader/Search instance 的列文字"，于是 createFrame 在主 mockup 上盖了一层自画 overlay 表（5 行 sample + 自画 NEW badge + 自画 status dot）。用户连续抓 3 次：(1) "看起来还有别的字段需要调整的"、(2) "你把表格的显示顺序调整，那效果图的意义是什么"、(3) "你应该基于昨天的那个页面来修改，而不是重新画"。补救：删 overlay → 改回 probe instance text override → 全表 60+ cell 保留库 link，仅 Extended Info 5 cell 因 wrapper anchor 必要 detach。**根因**：起手没 probe instance 可改性就跳到自画。本规则即此回流。

---

### M47 — Font Fallback Preserves Style Binding（2026-05-28 新增）

改 instance / cell 内 text 节点的 `fontName`（典型场景：原 font 不可用、需切换 fallback 字体）时，**必须先 probe 并保留 `textStyleId`**，切完字体重新绑回，**不允许暴力覆盖 fontName 丢弃 textStyle 关联**。

#### 触发场景

- 平台特定字体（如 `Helvetica` / `PingFang SC`）在 Figma 编辑环境不可用 → 切 fallback
- 批量改 cell font 统一字体规范
- 任何 `node.fontName = {...}` / `node.setRangeFontName(...)` 调用

> **字体可用性校正（2026-06-08，live 实证）**：`Roboto` **在 TVU Figma 环境可用**——2026-06-08 `use_figma` 实跑 `listAvailableFontsAsync` 列出 36 个 Roboto 字重，`loadFontAsync` Regular/Medium/Bold 全 `loaded OK`（design-tokens.tokens.json typography.roboto 为真源）。早前把 Roboto 与 Helvetica 一起归为「不可用」是 2026-05-28 TPC-628 Helvetica 事故的过度归并。说明文字默认 EN=`Roboto` / ZH=`Noto Sans SC`（§字体规范唯一真源）——`loadFontAsync({family:'Roboto'})` 真抛错才 fallback `Inter`，且**必须显式上报用户**，不静默退回。`Helvetica` / `PingFang SC` 确实不可用。

#### 起手协议

```js
// 1. Probe 原 textStyleId
const originalStyleId = textNode.textStyleId;

// 2. 切换 font (改 fontName 后 Figma 自动解除 textStyleId 绑定)
try {
  await figma.loadFontAsync(originalFontName);
} catch {
  // Fallback flow
  await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
  textNode.fontName = { family: 'Inter', style: 'Regular' };
}

// 3. 重新绑回 textStyleId (如有)
if (originalStyleId) {
  textNode.textStyleId = originalStyleId;
}
```

#### Why（深层）

Figma 内 text 节点的 fill 颜色 / fontSize / letterSpacing 等可能来自 **textStyle 绑定**（不是 inline 设置）。当 fontName 被改变后，Figma 自动解除 textStyle 关联，此时如果没显式重新绑回 → 所有继承自 textStyle 的属性回退到 inline 默认值（往往导致 fill 颜色丢失变灰、fontSize 错位、字间距错位等）。**症状**：cell 内容更新后视觉看起来"灰了"或"小了"或"歪了"，probe fills 显示正常但渲染异常。

#### 反例

| ❌ | 表现 |
|---|---|
| `textNode.fontName = {...}` 直接改没 probe textStyleId | 改完 cell 文字突然变浅灰，看似 fill 错误实则 textStyle 绑定丢失 |
| `setRangeFontName(0, len, ...)` 后没重绑 textStyle | 同上 |
| Probe fills 看起来正常 `{r:0.2,g:0.2,b:0.2}` 就以为 OK | textStyle 绑定丢失后 fills 数据正常，但渲染使用的 layer 不一样 |

#### Acceptance

- [ ] 任何 fontName 改动前 probe 原 `textStyleId`
- [ ] fontName 改完后重新绑回 textStyleId（若 originalStyleId 非空）
- [ ] 改完截图 zoom-in 验证 cell 视觉与同列其他 cell 一致（无变灰 / 变小 / 变歪）

#### 实证

**2026-05-28 TPC-628**：批量 update T list 5 行 cell 时，由于 Helvetica 不可用，对每个 text 做 `fontName = Inter Regular` 暴力覆盖。用户抓"字体颜色也不对，好多都变成灰色的了"。probe 后发现 fill 数据正常 `{r:0.2,g:0.2,b:0.2}`，但渲染明显比同列 unmodified cell 浅。根因：fontName 覆盖时 textStyleId 解除绑定，cell 内容回退到 inline 默认 render layer。补救：force `textStyleId = ''` 清空 + 显式重设 fontName + fontSize + fills + opacity。**根因**：font fallback 流程缺保留 textStyle 这一步。本规则即此回流。

---

### M-DISCIPLINE — Rule Execution Discipline（umbrella）— 2026-06-01 新增

兜底"AI 起手读了规则但执行时不贯彻"这一类 meta 病灶。所有 M-rules / R-rules / process rules 的 **enforce 层**，与各规则正文是 orthogonal 的：规则本身规定**做什么**，本 umbrella 规定**怎么保证做了**。

#### 触发

任何 mockup 任务起手协议（cwd check + env health check + read conventions）执行**完毕之后**，画第一个产品 frame **之前**——必须先跑本 umbrella。

#### 强制要求（unconditional）

**用 `TodoWrite` 把本次任务适用的所有规则编号物化成 todo list**。每个 todo item 格式：

```
<rule ID> · <one-line title> · [pending|in_progress|completed]
```

最小 todo set 按任务类型预填：

| 任务类型 | 必含 todo 子集 |
|---|---|
| **Jira 关联型 mockup** (US-1/US-2，含 Jira issue key) | M45 (page 命名) / M23.8 (Jira annotation + hyperlink) / Pre-Phase 0 Step B (PRD 写入 Figma) / M0 (element-to-component mapping) / [画 frame 产品 UI] / M23 (state-label 每帧之上) / M23.6 (流程连线 + 触发 label) / Audit + walkthrough |
| **Greenfield single-frame mockup** (US-2) | M0 / [画 frame] / M-COLOR audit / M-INTEGRITY audit / Walkthrough |
| **US-3 增量** | M0 (新增元素 mapping) / [改 frame] / M-INTEGRITY audit (sibling layout 不破) / Walkthrough |
| **US-6 audit/review** | 跑 `audit-mockup-integrity.mjs` + `audit-mockup-colors.mjs` / 输出 audit report |

任务跨类型时取并集。

#### 执行约束

- 每完成一个 mockup 里程碑（如"画完所有 product frame" / "加完 state-label"），对应 todo **必须立即 `mark completed`**——不允许批量延后标记
- 未全部 mark completed 之前，**不准**说 "ready for walkthrough" / "ready for handoff" / "完工" / "deliverable 完成"
- harness 提醒 "use TodoWrite" 出现 ≥3 次而 todo list 未启动 = 本规则触发未响应 = 自查触发器 C（meta-rules.md）"发现新反模式实例必须沉淀"

#### 反例

| ❌ | 后果 |
|---|---|
| 起手读完 conventions 直接开画第一个 frame，跳过 TodoWrite 物化 | 8 步流程会按 atomic focus 退化为 3-4 步，剩下靠用户走查时 catch + 多轮返工 |
| TodoWrite list 建了但执行时不更新状态 | list 失去 telemetry 价值，等同于没有 |
| 完成"画 frame 产品 UI"就汇报"6 个 frame 都画完了，可以 walkthrough" | 缺 M23 / M23.6 / M45 / M23.8 等周边 deliverable；定义错了 done |
| 单 task 跨多轮对话时，新轮不读旧 todo list 直接接活 | 跨轮 atomic focus drift，回流为本规则次生病灶 |

#### M-DISCIPLINE.SYNC — 编辑 confirmed → 自动自检 + 交付物同步（状态转移触发，2026-06-18 新增，修 W3/W5）

**触发条件不是"任务起手分类"，而是"任何 mockup 编辑动作完成并 confirmed"这个动作本身。** 一旦改动 confirmed，以下属 done 定义，**无需用户提醒、不依赖 AI 自我识别处于哪个阶段**：

1. **跑 I1-I6 完整性自检** —— section 归属 / 位置 / 间距 / 重叠。
   - **「自检」= `audit-mockup-integrity.mjs` 的逐条机器输出**（如 `I3=overflow-count 0`），贴进 handoff。
   - ⚠️ **目测 / "看起来 OK" / "应该没问题" 一律不构成自检通过**（实证 2026-06-18：提醒检查后 AI 仍漏 I3 overflow，因目测代替跑脚本）。
2. **同步受影响的交付层** —— UX 卡（§M23 canonical 结构）+ M23.6 连线 + M23.7 state-label，使其反映本次改动。
3. **handoff 反映同步结果** —— 顶部加 `<!-- mockup-handoff -->` 标记（供 repo 守门 `audit-mockup-handoff-evidence.mjs` 识别）+ Rule checklist 行 + Integrity audit 行（`I1=pass / I2=… / I3=overflow-count 0 / I4=orphan-count 0`）+ conformance summary。

**唤醒词兜底**：用户说 `同步交付物` 时直接拉起本小节 checklist（与"同步 Figma 库"区分）。

**Acceptance（M-DISCIPLINE.SYNC）**：改动 confirmed 后，handoff 含 `<!-- mockup-handoff -->` 标记 + integrity 脚本机器输出 + 受影响 UX 卡/连线/state-label 已更新。缺脚本证据块 = done 不成立（`audit-mockup-handoff-evidence.mjs` repo 守门机器拦截）。

#### Acceptance

- [ ] 起手协议执行后、画第一个产品 frame 前，conversation 内可见一次 `TodoWrite` 调用，list 含本次任务适用规则编号
- [ ] handoff doc 顶部必含一行 **"Rule checklist: \[M0 ✅ / M23 ✅ / M23.8 ✅ / M45 ✅ / ...\]"**，所有适用规则状态显式
- [ ] 任何 "ready for walkthrough" 声明前，todo list 内 pending/in_progress 数 = 0

#### 实证

- **2026-05-20** Graphics Insertion v8 (`_metrics/phase0-ledger.md` ledger entry)：AI 起手读了 M31 (Auto Layout 默认)，v1-v7 整个 After frame 用 Frame + 绝对 x/y 定位 → 用户手动改 Figma 痛苦反馈 → v8 整体 refactor 47/58 frames 为 auto-layout 嵌套树。原 ledger 原话："**M31 是 TVU 现存规则，v1-v7 整个违规** — 不是规则缺失而是 AI 起手读了规则但执行时没贯彻的纪律 gap"。当时记录了 "起手协议显式 echo 'M31 已加载'" 作为 mitigation，但**未制度化**。
- **2026-06-01** BM-1047 mockup (Micro-App Token Portal Multi-Seat Subscription)：起手读完 `design-process.md` 8 步 Jira 关联型流程，实际执行**只命中 3/8** (PRD + M0 + 画 frame)。M45 / M23.8 / M23 / M23.6 / audit 全部靠用户走查时提醒补——5 步返工连带 section 重排间距 / frame 重新克隆 / 连线后置加入等。详 `retrospection/2026-06-01-rule-execution-discipline-gap.md`。
- **2026-06-01** V4-1865 mockup (TVU Pack · Config-T 双 WiFi 模块 + Hotspot 3-mode operation)：本规则 2026-06-01 当日新立时已存在，AI session 仍**完全没用 TodoWrite 物化规则**起手 → v1 误读硬件架构（凭 Jira description 字面理解"second internal WiFi module"）+ v2 跳 ② Jira component (M23.8) + v3 自画 Unicode `→` 违 M32 + Dropdown 选 Mode 违 M22（用户原话"如何让用户清晰知道当前模式"指向 radio 而非 dropdown）+ v4 M23 canonical 7 点只对 2 点（navy bg + 双语，缺 4px cyan 左 stroke / cyan title / Noto Sans SC ZH / opacity 0.45 / Layout A-逐行）+ v5 cross-row arrow Y 用 frame 中线（2360）而非 `absoluteBoundingBox` probe（1679）差 640px。五轮 walkthrough 返工。详 `retrospection/2026-06-01-v4-1865-rules-backflow.md`。

#### Legacy ID 关联

无（新规则）。与 `meta-rules.md` § 3 反模式 #3 "to-do list 思维写 prompt" 镜像但不冲突——meta-rules.md 那条说**写 prompt** 时要"产出契约"而非"任务清单"，本 M-DISCIPLINE 说**执行规则**时必须把规则**物化成 task list**。两者作用域不同。

#### 镜像规则

- **`code-conventions.md` R-DISCIPLINE**（待加）— 同样 "AI 读了 R-rules 但执行时不贯彻" 病灶，code 任务起手必 TodoWrite 物化 R-rules subset
- **`meta-rules.md` 触发器 F**（待加）— meta 层 cross-cutting "读 ≠ 做" 反模式，对所有 AI 角色生效

---

### M49 — Library Fidelity Umbrella：样式/间距 · 字段 parity · 反馈控件 · 强调预算（2026-06-12 新增）

> ⚠️ **ID 消歧**：本 §M49（Library Fidelity Umbrella）与 [`design-process.md` §M49](./design-process.md)（Design Quality Contract Gate）**不同号同名、scope 不同**——全局 M 编号撞号，待 mockup-conventions 结构整顿时统一重号。
>
> 同源命题：**mockup 必须服从库与既有交付的系统性约束；超出部分降级、进注释层或显式拍板**。BM-1047 V3 收尾 session 用户连环抓出的审查盲区回流。

#### M49.1 库样式 / 变量 Fidelity

- 找 style/variable 必须**查库发布目录**（catalog §Styles & Variables 或直读库源文件），禁止只收割产品文件"使用中"的 id 反推"库没有"——这是 §库归属验证机制 的样式/变量版
- mockup 手画 TEXT 必绑库 Text Style（行高随 style；禁裸 fontSize + AUTO 行高）；大字号同样有 20/22/24 档，先查再下结论
- 间距/padding 必取库 scale 值并 `setBoundVariable`；**非 scale 值 = 违例，起手吸附最近档**——库缺档不是自由值的理由，也不默认触发库新增（owner 显式发起才加 token）
- 手画 popup 卡阴影对齐库基线（probe 库组件 `effectStyleId`，当前 = L3 shadow）
- **真源数据 + 机检（2026-06-12 补）**：起手消费 `figma-data/normalized/binding-source-map.json`（发布 catalog 实查产出：4 原语真名/值/key/可绑性 + bindApi；**CJK 字号有 Roboto 拉丁 + PingFang SC 孪生两个 key，按字符集选**，别假设 1 token→1 style）。绑定保真机器 gate = `scripts/audit-mockup-binding-fidelity.mjs`（D16：查 `textStyleId`/`effectStyleId`/`boundVariables`/on-scale + 语义色优先 §C5），已接 `audit:mockup-conformance`——补上 M49 原本只有手动 probe 清单、无脚本 gate 的缺口。

**实证**：2026-06-12 BM-1047 V3 —— 扫使用现场误报库缺 20/24 字号与 spacing 变量，用户两次纠正；回流后 119/128 文本绑 style、110 处间距绑定、74 处非 scale 值吸附（3→#4 / 6→#8 / 10→#8 / 18→#16）。

#### M49.2 重构 / 拆分必做字段级 Parity 表

- 任何「重构 / 拆分 / 合并 / 简化既有模块」动手前：probe 源模块全部 TEXT/affordance → 机械生成字段表，每字段标 kept / moved / merged / **dropped(+rationale)**；dropped 行必经用户确认才开建
- 与 Stage 0.5 状态枚举**成对**执行：状态轴 + 字段轴是两条正交完整性轴，起手同时跑

**实证**：2026-06-10/11 V2→V3 拆模块静默丢 assigned 名单 / 总价 / 权益行 / S7 态，用户 4 次连环抓；事后 probe diff 一分钟即可全拦——证明该表在重构前生成成本极低。

#### M49.3 反馈类控件同源 Inventory

- 走查固定层：全部反馈类 UI 按 **toast / inline alert / modal notification / popover hint** 分类拉 inventory；同类必同 component 源 + 同宽/padding/icon/title/body/action 规则；偏差要么归 variant 要么标违例
- 反馈类一律取 `Notification` set variant（见 catalog 条目），不自画

**实证**：2026-06-11 V3 五张反馈卡四种规格仅一张库源（自画 Payment failed 橙 title vs Case5 Notification 白 title）；用户给出分类方法处方，归一后类内零偏差。

#### M49.4 强调预算（Emphasis Budget）

- 每张 popup 卡 **≤1 条语义色状态行 + ≤2 条灰 hint + 1 主 CTA**；超出的信息合并、降级中性色或进注释层
- 走查横向对比 = 数每卡**信息行数与彩色元素数 vs 同 lane 中位数**，不只查 token 同源
- 评审轮每新增一条诉求，必须伴随一次合并/降级决策（防评审轮增生——less is more 在多轮协作中失守的标准路径）

**实证**：2026-06-12 Case1 增生至 9 行/绿2/灰4（兄弟卡 8/1/2），用户「重点太多≈没有重点，全是提示≈没有提示」；瘦身后与兄弟卡同构且信息零丢失（降级 + 注释层承接）。

#### M49 共用 Acceptance

- [ ] 交付前 probe 四清单：style 绑定率 / 间距绑定 + scale 合规 / 字段 parity 表（重构任务，存档 handoff）/ 反馈类 inventory + 每卡强调预算
- [ ] 不满足任一项 → 不进 walkthrough

---

## AI 工具集成

本规则被 `skills/tvu-design-mockup/SKILL.md`（`tvu` Claude Code plugin skill）引用作为真源。

**规则改动权**：Path A 专属改本文件；通用 process rules 改 [`design-process.md`](./design-process.md)；TVU 业务规则改 [`domain-tvu.md`](./domain-tvu.md)。**不**修改 skill，skill 只做 trigger + 指路。仅当本文件新增规则改变起手协议时才回头同步 skill。
