# Retrospect — INFRA-F39 verifier 拓扑准确性 (step 1-3) + Table Figma 还原 + PillPage

> 2026-06-03 · plan owner direct (Infrastructure/verifier, path B) · commit 见 STATUS

## 一句话

修 render-verifier 量错元素的拓扑问题，让 `pnpm drift:summary` 从 **247 A → 87 A** 可信化；FormItem 94 全幻影清零、Table 66 拆成 54 幻影清 + 12 真 drift 暴露（owner 当场确认后按 Figma 还原 code → Table 清零）；顺带交付欠着的 PillPage docs 页。

## 背景

上一 session 立 INFRA-F39：`drift:summary` 报 247 A，但逐一对 Figma 核实后大部分是 verifier 拓扑幻影（量错 DOM 元素），不是 code drift。用户拍板 option 1（修 verifier 准确性）。本 session 执行 step 1-3 + 处理暴露的真 drift。

## 做了什么

### Step 1 — FormItem textFillHex 46 幻影（3-part fix）

**根因**：`extract.mjs firstTextNode` DFS 取第一个 TEXT 节点作为变体代表 text。FormItem 全 64 变体的第一个 text 节点是 required 星号 `* `，且 `visible:false`（Status=Normal/Error 都隐藏），fill `#ea4233`（--red）。verifier `textTargetSelector='.form-item__label'` 量的是灰 label，拿灰 label 对隐藏红星号 → 46 假阳。

**三处协同修**（缺一不可）：
1. `extract.mjs firstTextNode`：跳过 `node.visible===false`（根因；下次 re-extract 会取可见 label 而非隐藏星号，恢复真覆盖）。
2. `generate-render-verification-manifest.mjs expectedFromVariant`：当 `variant.text.visible===false` 视为无 text（textFillHex→null）。**为什么需要这条**：raw 已被 extract 扁平化（无 children tree），无法 re-derive 可见 label，只能在当前 baked data 上把隐藏 text 当无。
3. `manifest-verifier.spec.ts`：`pushExact` 对 textFillHex 无 null-guard（不同于 `pushNumber`），置 null 后会变成 `灰 === null` FAIL 而非跳过 → 加 null-skip（一致于 pushNumber / rootBorderHex 的"无 Figma 数据就不断言"）。

**关键约束发现（差点白干）**：原计划在 **normalize** 层把 `variant.text` 置 null（pickup 也指向 normalize），但 `audit:tokenized-diff` (`audit-tokenized-diff.mjs:140`) 断言 `JSON.stringify(raw) === JSON.stringify(stripped-tokenized)`（raw↔tokenized 结构恒等，tokenize 只能 ADD token 字段不能改其它）—— normalize 置 null 会让该 strict gate（prepublishOnly）FAIL。**所以 fix 必须留在 verifier 层（manifest-gen + spec），不动 tokenized data。** 这个约束是动手前 grep tokenized 消费方时发现的，避免了一次返工。

副产物：switch/Rating 的 28 个 null-expected warned-fail（本就 B 不计 A）顺带转成 clean skip。

### Step 2 — Table 66：retarget `<table>` → cell + 真 drift 按 Figma 还原

Figma「Table」组件是**单元格**（6 变体 Type=Header/Tbody × Align，w120 h33，padding **L0/R8/T8/B8**，gap 4，无填充）。verifier 之前量整个 `<table>`（padding/gap/fill 全 0）→ 54 幻影。

- retarget `rootTargetSelector` 为 per-variant 函数：Type → `.tbl-head/.tbl-body`，Align → `.tbl-cell--{align}`；`mapCodeProps` 给 column 设 align 让渲染 cell 命中。
- gap 在内层 `.tbl-cell__content`（flex），不在 `<th>` → 用新 `gapTargetSelector`。
- cell 的 120×33 是 component-frame，真 table 里 cell box 由 layout 驱动 → 新 `suppressRootSize` 跳 rootWidth/rootHeight（否则 cell≠120 会 +12 噪声）。

清 54 幻影（top/right/bottom=8✓ / rootFillHex transparent✓ / header text --text-tips✓ / body text --text-body✓ / gap 4✓）。

**暴露 12 真 drift**：`rootPadding.left (ex=0 ac=8)`。Figma cell L0（`get_design_context` 4265:16132 deterministic 实证 `pr-8 py-8` 无 `pl`，全 6 变体），code `.tbl-cell` 对称 8px。**此前在 `<table>` 层 left=0 与 Figma L0 巧合相等，真 drift 被掩盖**——retarget 才暴露。

**→ owner 当场确认按 Figma 还原**（FIGMA_AS_SOURCE_OF_TRUTH「Figma 真源、确认的真 drift 一律按 Figma 改 code」）：改 base `Table.vue` `.tbl-head/.tbl-body .tbl-cell` padding `var(--sp-xs)` → `var(--sp-xs) var(--sp-xs) var(--sp-xs) 0`（L0/R8/T8/B8）→ **Table 12→0**，TablePage 视觉 baseline 重抓（dark+light，内容左移 8px 贴左符合 Figma）。CANONICAL-029 resolved。

### Step 3 — FormItem rootGap 48

gap 在内层 `.form-item__main`（flex row，1-line gap 8 / 2-line gap 0），verifier 量外层 `.form-item`（block，gap 0）→ 48 幻影。加 `gapTargetSelector='.form-item__main'` → 全清（1-line 8==8✓ / 2-line 0==0✓）。

### 新 reusable verifier capability

- `gapTargetSelector`：从内层 flex 元素读 gap（serves FormItem + Table）。
- `suppressRootSize`：rootTargetSelector 指 sub-element 时跳 component-frame w/h。
- selector 支持 per-variant 函数（`resolveSelector(sel, axes)`）。

### PillPage（欠 deliverable）

`playground/docs/pages/PillPage.vue` 仿 SliderPage：Figma Coverage（轴 chips）+ Development Usage（code）+ Status×Active 8-pill grid + Status-with-count + PillCounter 4-kind（标注 code-first 无 Figma SoT）+ API 表。主题感知单 grid（Pill token theme-aware，不 side-by-side，过 H1 audit）。注册 4 处（pageLoaders/pageComponents/navigation/CanonicalPageId）+ site-review-manifest 声明（in-review）+ Playwright 视觉 baseline（dark+light）。全页双主题视觉自查通过（用户已审 Pill 主题适配 OK）。

## 结果

| 度量 | before | after |
|---|---:|---:|
| `drift:summary` total A | 247 | **87** |
| FormItem | 94 | **0**（全幻影清）|
| Table | 66 | **0**（54 幻影清 + 12 真 left-pad 按 Figma 还原）|
| input box/line+filled | 77 | 77（EXTRACT-007 blocked）|
| select/dropdown | 11 | 10（discovery；含 race ±1 噪声）|

vue-tsc ✓ · 193 vitest ✓ · H1 hard-rule ✓ · docs-site ✓（pill in-review warn）· component-affordances ✓。3 audit exit1 全 pre-existing（CANONICAL-013/015/017 + PillCounter code-first），零新增。

## 教训 / pattern

1. **「量错元素」既虚高也掩盖真 drift**：247 不只是幻影虚高，Table 的真 left-pad drift 被掩盖在错误的 `<table>` 测量里（left 都=0 巧合 pass）。retarget 到正确元素同时清幻影 + 暴露真 drift。这是 FIGMA_AS_SOURCE_OF_TRUTH「deterministic manifest 验证」的正面价值：拓扑对了，数字才双向可信。
2. **修 fix 前 grep 下游消费方**：原计划 normalize 置 null 会破 `audit:tokenized-diff` 的 raw↔tokenized 恒等约束。动手前 grep tokenized `.text` 消费方发现该 strict gate → 改 verifier 层，避免返工。[[feedback_review-result-vs-rationale]] 同源（先实证约束再动手）。
3. **没盲信 pickup**：pickup step 1 指向 extract/normalize；实测发现 normalize 路被 audit 挡 + raw 无 children 无法 re-derive label → fix 落 verifier 层。pickup step 2 假设 Table 是清幻影；实测发现是 54 幻影 + 12 真 drift（pickup 自己也警示了 L0 可能=0，实证证实）。[[feedback_baseline-before-plan]] / INFRA-F38 先例。
4. **真 drift 走 owner/designer 确认再按 Figma 还原**：Table left-pad 是确认的真 drift，但 L0 对 center/right cell 不寻常 + 改 shipped 组件 padding 是视觉变更 → 没在 verifier sprint 里 AI 单方拍，先立 CANONICAL-029 surface 给 owner。**owner 当场确认「默认 Figma 没错、按 Figma 还原」→ 改 code 左0 + 重抓 baseline**。机制对：AI 不盲改 shipped 组件视觉，但 owner 一句确认即按 deterministic Figma 真源还原（非 AI 自由发挥）。[[feedback_audit-whitelist-vs-sot-refactor]] / [[feedback_owner-direct-master-ok]]。
