# Next Session Pickup — render-gate 豁免判据字段化（[[INFRA-F104]]）

> 生成：2026-08-07 session AJ 收尾。本 session 做的是**全系统体检（只读）+ 一条路线的目的测试与方向裁定**，**未改任何机制代码**。
> 起手第一句：`按 docs/internal/_plans/next-session-pickup-2026-08-07-render-gate-field-level-excuse.md 接 INFRA-F104`
>
> ⛔ **动手前先走 `superpowers:brainstorming`** —— 本仓库纪律：出「要不要加个闸 / 要不要改判据」提案前必须 brainstorm（pickup-2026-08-03 §3 记着上一轮就是跳过这步被 owner 用 `/superpowers:using-superpowers` 提醒）。下面是**测量与裁定**，不是 spec。brainstorm → spec 落 `docs/superpowers/specs/` → `writing-plans` → 执行。

---

## 0. 真源分工（别在本文件重复这些内容）

| 要什么 | 去哪 |
|---|---|
| 问题陈述 / 实测数字 / ⛔清单 / 增长曲线 | **[backlog [[INFRA-F104]]](../backlog.md)** ← 唯一真源，起手全文 Read |
| 判据现状（四条测量量下界 S1–S4、为什么不能进 prepublishOnly） | `scripts/audit-render-drift-gate.mjs` 头注释（**每次运行自印**） |
| 分类器现状 | `tests/visual-verify/lib/drift-compare-core.ts` `classifyFailedCheck()`（**395–421 行**） |
| Figma 真源原则 / 合法 divergence 六类 | `docs/FIGMA_AS_SOURCE_OF_TRUTH.md` |
| 本文件负责的 | **只有 39 条的逐条方向裁定 + 落地形态 + 已排除的路线** |

---

## 1. owner 已拍板的（2026-08-07，别重开）

1. **选路线 A** —— 把豁免从「对散文 warning 做正则」换成**结构化字段级豁免表 + 双向红**。
   ⛔ 路线 B（正则收成 field 级）**已被否**：它会把「padding/fill/border/radius…」这组词表制度化成判据，而判据依赖人写的散文措辞 → 一句 `harness limitation across all checks` 就能无声重开免疫（今天已有 63 条走这条路）。
   ⛔ 路线 C（只加降级量上限 S8）**不作为终态**，但**可作为 A 的第 0 步**先钉住增长。
2. **修法方向以 Figma 为真源**（= 仓库既有硬规则，不是本轮新增）。
3. **本条在新 session 开工**，不在体检 session 里做。

---

## 2. 39 条搭便车 fail 的逐条方向裁定（2026-08-07 已查完，**别重查**）

体检量出「39 条 fail 搭便车、收紧后会打红」。本 session 逐组回活源查完方向，**结论是这 39 条不是同一类东西，落地形态因此与原方案不同**：

### 桶 1 · 判据 bug —— 不进豁免表，改判据（Switch `rootRadius` ×4）

| 项 | 值 |
|---|---|
| 现象 | expected `100` → actual `20`，4 条（switch dark/light × status on/off） |
| Figma 侧**两个**数 | 节点 `337:18075` 等的 `cornerRadius` 字面值 = **100**；而 Figma token `radius.xxl` = **20**（活源 `figma-data/normalized/figma-styles.json` → `scaleTokens.radius.xxl` = `{"value":20,"canonicalToken":"--r-xxl","figmaPath":"radius.xxl → #20"}`） |
| code 侧 | `src/components/Switch/Switch.vue:51` `border-radius: var(--r-xxl)`；`variables.css:137` `--r-xxl: 20px` |
| 历史 | commit **`5c84e051`** `fix(tokens): --r-xxl 100px → 20px 对齐 Figma Radius/XXL（修假 alias drift）` —— 2026-06-12 已经把 100 判为**假 alias**并修掉 |
| 几何 | Switch track 实测 **40×20**（manifest `expectedFromFigma.rootWidth=40 / rootHeight=20`，且这两个字段本身 **pass**）。40×20 的盒子上 `border-radius:100` 与 `:20` **都会被 clamp 到 height/2=10** → 视觉等价（`variables.css:137` 的注释逐字写着这一条） |

**⇒ 方向：不改 code、也不进豁免表。判据加「有效半径」语义 —— 比 `min(r, min(w,h)/2)` 而非比字面值。** 加完这 4 条变**真 PASS**。
⚠️ 这是原体检方案里**没有的第 4 类**（原方案假定 39 条全进表）。别照原方案给 Switch 开豁免行 —— 那会把一个判据缺陷冻结成"已知例外"。
⚠️ evidenceLevel：token 值 / commit / 几何数字 = direct；「两者 clamp 后都是 10」= CSS Backgrounds §5.5 corner-overlap 的规范结论 + 仓库 2026-06-12 已验的注释，**没有本轮的浏览器实测**。做判据前**跑一次实测确认**（同一 Switch 实例读 `getComputedStyle().borderRadius` 与实际渲染盒），别只信推理。

### 桶 2 · extract 管线缺口，豁免理由已在、只是字段列没写全（Tooltip ×18）

- 该 entry 的警告逐字：`Tooltip Figma extract drops root autolayout padding/gap/fill (similar to PopupBox EXTRACT pipeline gap). Code padding 4/8/4/8 + bg #353535 is real; Figma expected null/-6 reflects missing extract data. verifier resolution: real visual matches Figma rendering.`
- 搭便车的三个字段属**同一形态**：`rootBorderHex null→#595959`（expected `null` = 没抓到，不是"Figma 说无边框"）· `rootHeight 30.48528289794922→26`（expected 是分数 = 文字 hug 测量）· `rootWidth 81→82.859375`（2.3%，落已有 `C_BOUNDARY(≤5%)`，本就不 gate）
- **⇒ 方向：不改 code。把这条豁免的 `fields` 从 padding/gap/fill 扩到 border/width/height，理由沿用原句。**
- ⚠️ 表里必须标 `fixDirection: extract-pipeline`（根治是补 EXTRACT 管线，与 PopupBox 同族）+ `reviewBy`，**别让它变成永久豁免**。

### 桶 3 · 真 schema/harness 限制，字段搭便车（DropDownListSelect ×7）

| fail | entry | 依据 |
|---|---|---|
| `rootFillHex #0b2b13→null` ×6 | `Disabled=Yes, Hover=Yes` | 警告逐字 `code intentionally suppresses for runtime UX correctness`，且**已登记 divergence**：`translation/divergences.md#dropdown-disabled-hover-preview-state` |
| `textFillHex #8fcc9d→#bfe2c7` ×1 | `Is Parent=Yes` | 警告逐字 `Figma Is Parent=Yes has no DropDownListSelect runtime prop; visual delta should be treated as a boundary/schema gap` |

**⇒ 方向：不改 code。扩 `fields` 列，并在表行里回指已有的 divergence id**（让豁免可追溯到已登记决策，而不是自说自话）。

### 桶 4 · 真 code 缺口 —— **要 owner 拍**（Button ×10）

- 警告只为 typography/underline 开脱：`Button/url link is rendered via canonical Button with style=rimless approximation; harness limitation — Figma URL link variant has its own typography/underline that canonical Button does not implement.`
- 但搭便车的是：`rootPadding.left/right 4→16` ×8（Figma URL-link 变体横向 padding **4**，code 渲成 **16**）+ `rootFillHex null→#434343` ×2（Figma hover **无底色**，code 给了灰底）
- 这是**视觉可见差异**：一个文字链接被撑出 4 倍横向 padding、hover 还出灰底。padding 与 hover 底色**不在**那句豁免的辩护范围内。
- **⇒ 方向：按 `FIGMA_AS_SOURCE_OF_TRUTH.md` 默认 = 改 code。两条路二选一，owner 拍：**
  1. canonical Button 补 `style=link` 变体（padding 4 / 无 hover 底 / 带下划线），或
  2. owner 裁定「URL link 不由 Button 承载」并按合法 divergence 第 6 类（用户显式 code-first）登记 —— 需带 `user approved code-first YYYY-MM-DD`
- ⛔ **AI 不得自行选**。落地当天该行 `fixDirection: owner-decision-pending` + 较短的 `reviewBy`。

### 裁定汇总

| 桶 | 条数 | 落地形态 |
|---|---|---|
| 1 判据 bug（Switch radius） | 4 | **改判据（clamp 语义），不进表** |
| 2 extract gap（Tooltip） | 18 | 进表，扩 `fields`，`fixDirection: extract-pipeline` |
| 3 schema/harness（DropDownListSelect） | 7 | 进表，扩 `fields` + 回指 divergence id |
| 4 真 code 缺口（Button） | 10 | 进表，`fixDirection: owner-decision-pending` |

⇒ **39 条里真正的 code 缺口只有 10 条**，且集中在一个组件的一个变体。

---

## 3. 落地形态（路线 A 的四问答案，brainstorm 时逐条复核别推翻）

**表**：`figma-data/audit-allowlist/render-excused-fields.json`，行 = `{ component | manifestIdPattern, fields: ['rootPadding.*'] | '*', reason, addedAt, reviewBy, fixDirection, divergenceRef? }`
**规模实测**：按 (组件, 字段) 建行 = **38 行**；按警告句建行 = **17 行**。不是 300 行。

1. **空/畸形输入会不会假绿** —— 表文件缺失/解析失败 → **FAIL**（不是"没有豁免所以放行"，也不是"读不到所以跳过"）；entry 无命中行 → 视为**空列表**（最严），绝不视为通配；⛔ **已有的 S1–S4 测量量下界原样保留**（`total>0` / `total===manifest 条数` / `navigationFailures===0` / `measuredEntries===total`）—— 它们守的正是"什么都没测所以通过"，INFRA-F97 刚补上，别动。
2. **输入形态错是否 fail closed** —— `fields` 含**不在 check 字段闭集**（`buildChecks` 产出的 `rootWidth/rootHeight/rootGap/rootPadding.*/rootFillHex/textFillHex/rootBorderHex/rootRadius`）的名字 → FAIL（拼错成另一个真字段名 = 静默扩大豁免面）；`fields:'*'` 缺 `reason`/`reviewBy` → FAIL；`addedAt` 缺失/非 ISO/未来日期 → FAIL；表里有行在 report 里**匹配不到任何 entry** → FAIL（stale 行）。
3. **判据有几个调用面 —— 四个，必须消费同一张表**：① 库 API `drift-compare-core.ts` `classifyFailedCheck()` ② CLI 闸 `scripts/audit-render-drift-gate.mjs` ③ **React 链的重复实现** `tests/render-verification-react/react-drift-full.spec.ts`（`:204` 自带一份 status 逻辑副本）④ 单测 `tests/render-report-summary.test.ts`（消费 `EntryReport`/`CheckResult` 形状）。
   ⛔ **只做"同表"，不做"同闸"** —— React 链完全无闸是 [[INFRA-F100]]，独立 entry，且那条 entry 明写「别照抄 Vue 那份闸，先判两条链判据是否本就该不同」。别顺着根因滑进去。
4. **落地当天存量** —— 桶 2/3/4 共 **35 条**进表，各带 `addedAt: <落地日>` + `reason` + `fixDirection` + `reviewBy`（照抄 `figma-data/audit-allowlist/variables-staleness-ack.json` 那套"带到期日、到期即红"的已验证形态）；桶 1 的 4 条改判据不进表。
   ⛔ **`BASELINE_A` 保持 0，一格不抬。**
   **shrink-only**：表里某行不再命中 → FAIL 要求删行 ⇒ **表空着是终态不是待办**，修完由闸自己宣布。

**enforcement 层级 = L5**：落点 `.gitea/workflows/pr-checks.yml`（1 处）+ `.github/workflows/ci.yml`（2 处）。
⚠️ **不能进 `prepublishOnly`** —— Gitea runner 非特权装不了 chromium（既有事实，非本条引入），所以"切 tag 前本机跑满"那一步仍是 `RELEASING.md` 的 **L1 纪律**。本条不改这个格局，**也别在收尾时宣称改了**。

---

## 4. 接手时必须先做的三件核实

1. `git fetch --all --prune` + `git log <本文件所在 commit>..origin/master --oneline` —— 本仓库多 session 并行（体检当天并行线在 3 小时内提了 3 个 commit、`docs/STATUS.md` 中途涨了 53 字节）。若有同任务 commit，先比对择优。
2. **实跑一次基线**，别信本文件与 F104 里的数字仍然成立：
   ```
   pnpm test:render-verification && node scripts/audit-render-drift-gate.mjs
   ```
   ⚠️ 跑前先 `lsof -nP -iTCP:5173`（并发 vite server 会污染共享 `.vite` 缓存 → render-verif 全错）。约 1.5 min。跑完 `figma-data/normalized/render-verification.report.json` 与 `docs/internal/_generated/render-verification-report.md` 会各改一行时间戳，**不改判据就 `git checkout --` 还原**。
3. 造故障前先确认探针字段名/组件名**全仓无命中**（否则表与探针互相自引用）。

---

## 5. 纪律（本轮验到有效的 + 踩到的）

- **本轮验到有效**：先量「这活能达到什么目的」再动手 —— 正是这一步把方案从「39 条全进豁免表」翻成「4 条是判据 bug、18+7 条只是字段列没写全、真 code 缺口只有 10 条」。同一纪律连续第三轮成立（F58 三层化 / F95 精简 / 本轮）。
- **本轮踩到（写下来防重犯）**：按 `pnpm run audit:*` **别名**做闸挂载普查，6 条闸假报"零挂载"—— 实际它们以 `node scripts/<file>.mjs` 形态挂在 `.husky/pre-commit`。**挂载有三种调用形态**：别名 · 文件路径 · 透过 `prepublishOnly` 传递（`pr-checks.yml:39` 跑整条 `prepublishOnly`）。任何名字普查必须三种都匹配。
- **本轮踩到②**：自建探针 shim 了 `document` 导致 `./web-components` 假红 —— 真闸的 shim 刻意**不** shim document。异常结果先排除自己是污染源。
- commit 必须 `git commit -F <msg> -- <路径逐条内联>`，提交后紧跟 `git reset -- <路径>`；push 落地只信 `git ls-remote`。
- 改 `.css`/`.vue` 需 `VISUAL_COMMIT_APPROVED=1`，AI 不自设。**桶 4 若走"补 Button link 变体"会碰 `.vue`/`.css` —— 先停下要授权。**
- `audit:stale-anchors` 不扫 STATUS.md 发出的链接 —— 在 STATUS 里新引用文件路径后自己 `ls` 一遍。
