# INFRA-F67 (c) clipped-unreachable triage — 2026-07-29
> **扫描面**：`pnpm audit:docs-overflow` 实跑命中的**全部溢出条目**（逐条补三项探测器不采集的活源事实 + 截图复核）—— ⛔ 非静态推断

> 数据来源：`pnpm audit:docs-overflow` 实跑（原始日志见附录），非静态推断。分类判据不止「探测器给的数字」——
> 每条都补了三项探测器从不采集的活源事实，再用截图亲眼复核：
> ① 这条溢出**到底有没有人 clip**（自身 + 所有祖先直到 `<html>` 的 `overflow-x`）；
> ② 溢出的内容**实际画到哪**（对文本节点用 `Range.getClientRects()` 量，不靠 `scrollWidth` 反推）；
> ③ 溢出条带**是否落进后置兄弟的盒子**（后置兄弟绘制更晚，其不透明背景会盖住溢出内容）。
> 探测脚本 = `tests/docs-overflow/_f67-triage-probe.mjs`（**随本报告一并入库**，作 §建议 1 的现成实现参考；
> 下划线前缀 + `.mjs` 扩展名 → 不被 `playwright.docs-overflow.config.ts` 的 `testMatch: '**/*.spec.ts'` 收进套件）。
> 截图落 `/tmp/f67-triage/`（未入库，可用该脚本按需重生）。此外还跑过几个一次性量测脚本（computed
> `grid-template-columns` / 离屏 `width:min-content` 克隆量 min-content / 修后两页列宽复核），其结论已逐条
> 写进本报告正文，脚本本身已删。
>
> **环境如实记录**：跑扫描时工作树有另一条并行 session 在飞（light theme on-fill + tab shell tokens：
> staged `src/components/Tab/Tab.vue` / `UserMenu.vue` 仅换 border 颜色 token、`.changeset/`、
> `docs/working-principles.md`、`playground-dist/` 整轮重建；unstaged `docs/STATUS.md`、
> `react-pilot/src/demos/{steps,tabs}-demo.css`、`design-review-queue.md`、`divergences-decisions.json`；
> untracked `scripts/audit-on-fill-content-color.mjs`）。已核这批 staged `src/` 改动**只动颜色 token、不含几何属性**，
> 对本轮测量无影响；未 stash、未消化。`lsof -i:5173` 跑前为空，无并发 vite server 污染 `.vite` 缓存。

## 汇总

| 类别 | 条数 | 处置 |
|---|---|---|
| **A 真差异**（肉眼可见内容丢失） | **2** | ✅ 本轮已修（靶向 CSS，无架构改动）；复跑后归零 |
| **B 字体噪声** | **0** | ⚠️ **backlog 的「字体噪声」假设被证伪** —— 16 条在两次独立 load 之间 `jitter = 0`（逐条见明细），没有一条抖动。**不要给 spec 加噪声下限阈值**，那会掩盖真信号而不是过滤噪声 |
| **C 探测盲区**（溢出真实存在，但无视觉损失） | **14** | 不改样式。真问题在探测器少一个谓词，修法见 §建议 1 |

**扫描口径与门状态**：`pages=33 viewports=2`，`page-level-overflow=0`（阻塞门，本轮未回归）；
`clipped-unreachable` **16 → 14**；`intentional-scroll=190`（report-only，按设计）。

**这一轮最值钱的结论**：`clipped-unreachable` 这个桶名与注释里的 "real bugs" **系统性高估**。
探测器只问了「有没有可滚动的祖先」，从不问「到底有没有人 clip」——16 条里 **16 条自身 `overflow-x: visible`
且向上走到 `<html>` 全程无 clip 祖先**，内容其实完整绘制在盒子外。真正会让用户丢信息的机制不是 clip，
而是**溢出条带落进后置兄弟、被兄弟的不透明背景盖住**——这正好是那 2 条 A 类，也正好是探测器没测的那一项。

## 逐条明细

> `Δ` = `scrollWidth - clientWidth`。`jitter` = 两次独立 page load 之间 Δ 的差。
> `溢入兄弟` = 内容右缘 − 最近的后置兄弟左缘（负值 = 停在兄弟之前，不构成遮挡）。

| # | pageId | vp | selector | scroll/client | Δ | jitter | 有无 clip 祖先 | 溢入兄弟 | 类别 | 亲眼所见 | 处置 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | color | 1280 | `div.theme-row__head` | 144/137 | 7 | 0 | 无 | −8.6px（`div.theme-row__values`） | C | `--bg-layer1` / `Color Type/Background/Layer_1` 折两行完整可读 | 不改 |
| 2 | color | 1280 | `div.theme-row__head` | 147/137 | 10 | 0 | 无 | −5.5px（同上） | C | 同上，`Layer_2` 行完整可读 | 不改 |
| 3 | border | 1280 | `div.recipe-meta` | 262/248 | 14 | 0 | 无 | 无后置兄弟 | C | chip `src/components/Pagination/Pagination.vue` 完整在卡内可读 | 不改 |
| 4 | border | 1280 | `div.recipe-meta` | 256/248 | 8 | 0 | 无 | 无后置兄弟 | C | chip `src/components/Checkbox/Checkbox.vue` 完整可读 | 不改 |
| 5 | border | 1280 | `div.recipe-meta` | 256/248 | 8 | 0 | 无 | 无后置兄弟 | C | 同上 | 不改 |
| 6 | effect | 1440 | `div.tag-row` | 207/202 | 5 | 0 | 无 | 无后置兄弟 | C | chip `figma-data/normalized/figma-styles.json` 折行后完整可读 | 不改 |
| 7 | icon | 1280 | `article.stats-card` | 207/181 | 26 | 0 | 无 | **+9.3px（后置 `article.stats-card`，绘制更晚）** | **A** | 3× 放大图确认：路径渲染成 `figma-data/published/icons/index.jso`，**末字 `n` 被右邻卡片的不透明背景吃掉** | ✅ 修：`.stats-note` 加 `overflow-wrap: anywhere` |
| 8 | form-item | 1280 | `div.member-grid` | 592/580 | 12 | 0 | 无 | 无后置兄弟 | C | 溢出 12px 落进外层 demo card 的 padding，5 张卡文字/控件全可读 | 不改（修 #10 后仍在，属预期残余） |
| 9 | form-item | 1280 | `label.form-item__label` | 131/120 | 11 | 0 | 无 | +3.2px（`div.form-item__content`） | C | 3× 放大图：红色 `*` 完整绘出、可读，仅与输入框贴得紧 | 不改（见 §建议 3） |
| 10 | form-item | 1440 | `article.member-card` | 293/234 | 59 | 0 | 无 | **+42px（后置 `article.member-card`）** | **A** | 3× 放大图确认**两处损失**：自身说明 `FormItem + Select` 被截成 `FormItem +`；溢出的 `Please Select` 控件盖住右邻卡的 `Mode *`，只剩 `…de *` | ✅ 修：见下方修法 |
| 11 | form-item | 1440 | `label.form-item__label` | 131/120 | 11 | 0 | 无 | +3.2px（同 #9） | C | 同 #9 | 不改 |
| 12 | form-item | 1440 | `article.docs-demo-card--wide` | 751/738 | 13 | 0 | 无 | 无后置兄弟 | C | 「Label width members」三张内卡（120px / 200px / Dynamic）全部完整可见，13px 被卡片 padding 吸收 | 不改 |
| 13 | tabs | 1280 | `div.tab.tab--line` | 258/248 | 10 | 0 | 无 | 无后置兄弟 | C | `Overview / Sources / Monitor` 三项完整可读 | 不改 |
| 14 | tabs | 1280 | `div.tab-list--filled` | 263/246 | 17 | 0 | 无 | 无后置兄弟 | C | filled 三项完整可读 | 不改 |
| 15 | tooltip | 1280 | `div.tooltip-wrap` | 189/114 | 75 | 0 | 无 | 无后置兄弟 | C | Δ 来自绝对定位的 tooltip 气泡（`.tooltip-wrap` 是它的 containing block）；气泡 `(Required) Hover to read full description` 完整渲染 | 不改（定位浮层 = 探测器已知盲区） |
| 16 | tooltip | 1440 | `div.tooltip-wrap` | 189/114 | 75 | 0 | 无 | 无后置兄弟 | C | 同上 | 不改 |

## A 类修法（本轮已落地）

两条都是靶向 CSS，不动三栏布局 / 不动断点 / 不动 Contents 面板宽度（延续 F67 已 ship 的方案 A 口径）。

### #7 icon `.stats-note` 长路径不可断行

`playground/docs/pages/IconPage.vue`（scoped，只影响 icon 页）：`.stats-note / .icon-copy / .icon-note` 加
`overflow-wrap: anywhere`。根因 = `.stats-grid` 在 1280 下是 `repeat(auto-fit, minmax(180px, 1fr))` → 卡片
`clientWidth = 181`，而 `figma-data/published/icons/index.json` 折行后最长不可断片段宽 207px。

### #10 form-item member-card 被挤窄 —— 根因是**跨页 CSS 类名撞车**，不是布局设计

活源实测链条（每一步都可复核）：

1. 页面上有**两条** `.member-grid` 规则同时生效：`formitem-demo.css` 的 `minmax(320px, 1fr)`
   与 `inputnumber-demo.css` 的 `minmax(220px, 1fr)`。两份 stylesheet 在 docs 站都是全局加载，
   **后者在层叠中胜出** → 实测 computed `grid-template-columns`：1280 = `282px 282px`，1440 = `236px 236px 236px`。
2. member-card 自身内容需要 **277px**（离屏 `width: min-content` 克隆量得）：`.form-item__label` 被钉死 120px 且
   `flex-shrink: 0`，加控件 `.form-item__content` 的 ~149px min-content。
3. 于是 1440 挤出第三列（卡片 `clientWidth = 234`）→ 277 装不进 → 溢 42px 进右邻卡片。

**修法（跟随仓库已有约定，非新发明）**：`.table-member-grid` / `.breadcrumb-member-grid` 早已把这个类命名空间化，
`inputnumber-demo.css` 是唯一的例外（且违反了它自己文件头「Rules here: InputNumber-UNIQUE classes only」的声明）。
所以：

- `react-pilot/src/demos/inputnumber-demo.css`：`.member-grid` → `.inputnumber-member-grid`（**220px 原值不动**）
- `playground/docs/pages/InputNumberPage.vue` + `react-pilot/src/demos/InputNumber.tsx`：同步改 class（双框架同改，不制造 parity 分叉）
- `react-pilot/src/demos/formitem-demo.css`：`minmax(320px → 280px, 1fr)`

**280 不是随手取的**：撞车消除后 FormItem 会拿回自己写的 320px，但 320 会让 1280 下 ~580px 的 `.docs-main` 掉成
**单列**（2 列需 656px）——那是能修掉溢出、却顺手改掉现有观感的过度修正。280 = 既 ≥ 内容所需、又保住 1280 的两列。

**修后实测**（两页 × 两视口，读 live computed `grid-template-columns` + 逐卡 `scrollWidth>clientWidth` 计数）：

```
form-item   @1280 .member-grid            cols=282px 282px          溢出卡 1/5   （残余 = 明细 #8，13px 落进 gap，无视觉损失）
form-item   @1440 .member-grid            cols=362px 362px          溢出卡 0/5   ✅（原 3 列 / 溢 42px）
inputnumber @1280 .inputnumber-member-grid cols=282px 282px         溢出卡 0/4   ✅ 与改前一致
inputnumber @1440 .inputnumber-member-grid cols=236px 236px 236px   溢出卡 0/4   ✅ 与改前一致（命名空间化未改变它的任何行为）
```

**回归验证**：`pnpm audit:docs-overflow` → `page-level-overflow=0`（阻塞门未回归）· `clipped-unreachable 16 → 14` ·
`pnpm test` → `857 passed | 10 skipped`（exit 0，与基线一致）· `pnpm audit:demo-slot-boolean` → `in-scope=30 side-failures=0` PASS ·
`pnpm audit:demo-framework-parity` → `components=25 fails=0` PASS。

## 建议

### 1.〔最高价值〕给 spec 补一个谓词，然后这个桶就能翻成阻塞门

现状：探测器只问「有没有可滚动祖先」，于是把 14 条**无任何视觉损失**的溢出报成 "real bugs"，
同时**漏报机制**——真正吃掉内容的是「溢出落进后置兄弟、被兄弟不透明背景覆盖」。两条 A 类都属这一机制。

建议改法（两项都是廉价 DOM 计算，`_f67-triage-probe.mjs` 里已有可直接搬的实现）：

- 判定 clip：从自身向上到 `<html>`，取第一个 `overflow-x !== 'visible'` 的祖先。**没有** → 内容并未被裁掉。
- 判定遮挡：内容右缘（文本用 `Range.getClientRects()`）与最近后置兄弟左缘比较，**为正才计入**。

按本轮数据，加上这个谓词后 14 条残余全部出桶 → 该桶归零 → **可以直接翻成阻塞门**，
且它拦住的正是 A 类那种真事故。**注意不要**改成「按 Δ 大小设阈值」：本轮 Δ 5–75px 与「是否真丢内容」毫无相关性
（Δ=75 的 tooltip 完全无损，Δ=26 的 icon 丢了一个字符）。

### 2. 不要加噪声阈值

16/16 在两次独立 load 间 `jitter = 0`。backlog 里「字体噪声」那句是当时的假设、本轮证伪。

### 3. 残余 C3（明细 #9/#11）交 owner，不在本任务改

`.form-item__label` 钉 120px + `flex-shrink: 0` 是 demo 要演示的东西（label width 轴）；
让它可收缩就改变了该 demo 的语义。当前视觉代价 = 红色 `*` 与字段贴紧 3.2px、字形完整可读。属观感项，非缺陷。

### 4.〔已登记，本轮不做〕跨页 demo stylesheet 仍有同名类残留

本轮只命名空间化了**导致缺陷的那一个**类（`.member-grid`）。`formitem-demo.css` 与 `inputnumber-demo.css`
仍同时声明 `.member-card` / `.member-card__meta`（当前只差 `gap`，无可见后果，但机制与本次缺陷同源）。
建议把「page-SPECIFIC stylesheet 只准声明本页独有类」升成机械闸（扫两份 page css 的选择器交集，
非空即 FAIL；真跨页共享的类落已存在的 `demo-shared.css`），而不是继续靠文件头注释自律——这次就是注释在、规则照撞。

**登记落点** = backlog `INFRA-F67 §剩余 (c-3)`。按性质它够格做独立 entry（下一个可用 ID = `INFRA-F74`，
已 grep 确认未占用），暂折在 F67 下的唯一原因是：本轮 `docs/STATUS.md` 正被并行 session 占用，
无法同步改 §Active 计数行，而 `audit:status-consistency` 的 C4 会因 backlog `###` 条目数 13→14 报红。
owner 要独立排期时，把那段整体搬出去 + 计数 +1 即可。

## 附录：原始日志

扫描前（16 条）—— `/tmp/f67-overflow-1600.log`：

```
## Clipped & unreachable (content wider than its box, no scrollable ancestor — real bugs)
  color @1280px: div.theme-row__head scrollWidth=144 clientWidth=137
  color @1280px: div.theme-row__head scrollWidth=147 clientWidth=137
  border @1280px: div.recipe-meta scrollWidth=262 clientWidth=248
  border @1280px: div.recipe-meta scrollWidth=256 clientWidth=248
  border @1280px: div.recipe-meta scrollWidth=256 clientWidth=248
  effect @1440px: div.tag-row scrollWidth=207 clientWidth=202
  icon @1280px: article.stats-card scrollWidth=207 clientWidth=181
  form-item @1280px: div.member-grid scrollWidth=592 clientWidth=580
  form-item @1280px: label.form-item__label scrollWidth=131 clientWidth=120
  form-item @1440px: article.member-card scrollWidth=293 clientWidth=234
  form-item @1440px: label.form-item__label scrollWidth=131 clientWidth=120
  form-item @1440px: article.docs-demo-card.docs-demo-card--wide scrollWidth=751 clientWidth=738
  tabs @1280px: div.tab.tab--line scrollWidth=258 clientWidth=248
  tabs @1280px: div.tab-list.tab-list--filled scrollWidth=263 clientWidth=246
  tooltip @1280px: div.tooltip-wrap scrollWidth=189 clientWidth=114
  tooltip @1440px: div.tooltip-wrap scrollWidth=189 clientWidth=114

pages=33 viewports=2 page-level-overflow=0 clipped-unreachable=16 intentional-scroll=190
mode=PARTIAL — page-level-overflow is BLOCKING (INFRA-F67); clipped/intentional remain report-only
```

修后（14 条，两条 A 类出桶）—— `/tmp/f67-overflow-after.log`：

```
## Clipped & unreachable (content wider than its box, no scrollable ancestor — real bugs)
  color @1280px: div.theme-row__head scrollWidth=144 clientWidth=137
  color @1280px: div.theme-row__head scrollWidth=147 clientWidth=137
  border @1280px: div.recipe-meta scrollWidth=262 clientWidth=248
  border @1280px: div.recipe-meta scrollWidth=256 clientWidth=248
  border @1280px: div.recipe-meta scrollWidth=256 clientWidth=248
  effect @1440px: div.tag-row scrollWidth=207 clientWidth=202
  form-item @1280px: div.member-grid scrollWidth=592 clientWidth=580
  form-item @1280px: label.form-item__label scrollWidth=131 clientWidth=120
  form-item @1440px: label.form-item__label scrollWidth=131 clientWidth=120
  form-item @1440px: article.docs-demo-card.docs-demo-card--wide scrollWidth=751 clientWidth=738
  tabs @1280px: div.tab.tab--line scrollWidth=258 clientWidth=248
  tabs @1280px: div.tab-list.tab-list--filled scrollWidth=263 clientWidth=246
  tooltip @1280px: div.tooltip-wrap scrollWidth=189 clientWidth=114
  tooltip @1440px: div.tooltip-wrap scrollWidth=189 clientWidth=114

pages=33 viewports=2 page-level-overflow=0 clipped-unreachable=14 intentional-scroll=190
mode=PARTIAL — page-level-overflow is BLOCKING (INFRA-F67); clipped/intentional remain report-only
```
