Files

110 lines
7.4 KiB
Markdown
Raw Permalink Normal View History

---
name: html-lint-false-positive-zero-visual-fix
description: 当「行级」HTML/设计检测器(如 impeccable 的 detect.mjs)对生成的静态 HTML 产物批量报「压行假红」时,如何做到**零视觉变化**地收敛——不改元素、不改配色、不改字体,只按元素/令牌拆源码行。当用户说「这些告警逐条判真红假红」「消掉检测器的假红」「检测器告警太多,别改样式」时使用。核心手法:把换行插在**解析器会吃掉的位置**(标签收尾 `>` 之前)或 **class 值内部令牌之间**。
agent_created: true
---
# 行级检测器的「压行假红」零视觉变化收敛
> 治「设计检测器一次报几十上百条告警,但逐条看下来大半是它自己判歪了;用户又明确说『样式保持现在的』,不许借机改字体改配色」。
> **前置**:先确认告警到底是**真红**还是**假红**(§1),⛔ 不要一上来就改东西。
---
## 0. 铁律
1. **先读检测器源码,再判真假。** 告警的判定逻辑写在规则里(正则 + 行级/文件级 test)。不读源码就判真假,等于猜。
2. **假红只能「消音」,真红只能「改设计」。** 两者混在一起做,会把「改样式」偷偷夹带进「消告警」批次。
3. **本技能只处理「可零视觉变化收敛」的假红。** 真要改视觉(换配色/字体/渐变)=设计决策,**必须单独问用户**,⛔ 不并入本批。
4. **⛔ 不要「重跑生成器」来修。** 产物往往已被后续手工改动领先于生成器;重跑会回退。**一律就地改产物。**
5. **每条改动都要有「渲染零变化」的**证据**,不是"我觉得没事"。** 见 §4。
---
## 1. 判真假:行级规则长什么样
典型「行级」规则(impeccable `detector/engines/regex/detect-text.mjs`):
```js
{ id: 'gray-on-color', regex: /\btext-(?:gray|slate|zinc|neutral|stone)-(\d+)\b/g,
test: (m, line) => /\bbg-(?:red|orange|...|rose)-\d+\b/.test(line), // ← 按【行】判
fmt: (m, line) => `${m[0]} on ${line.match(/\bbg-.../)[0]}` }
```
⇒ **只要同一源码行里同时出现两类令牌就报**,不管它们是不是同一个元素、也不管 `bg-*` 是不是 `hover:` 变体。
**由此产生的两类假红**:
| 类型 | 成因 | 判定依据 |
|---|---|---|
| **压行假红** | 一行里塞了多个元素(生成的 HTML 常把整段 `<main>` 压一行,或按钮组压一行):`bg-red-500` 的元素和 `text-neutral-700` 的元素**相邻但不同元素** | 灰字元素自己的底是 `bg-white`/无色 |
| **变体假红** | 同一元素,但被报的 `bg-*` 出自 `hover:bg-*`/`focus:bg-*`,静止态是 `bg-white`,且悬停时文字也换色(`hover:text-rose-600`) | 静止态与悬停态**两态都不是**规则描述的病态 |
判据脚本骨架:把告警行的行号取出 → 取该源码行 → 用正则切标签 → 看「灰令牌」和「彩色 bg 令牌」是否落在**同一个 tag** 里、且那个 bg 是否带 `hover:` 之类的变体前缀。
---
## 2. 两种零视觉变化的拆行手法(本技能的**核心**)
目标:让两类令牌落到**不同的源码行**,但**不动渲染**。关键是「换行必须插在解析器/序列化器会丢掉的位置」。
| 手法 | 插在哪 | 为什么零变化 | 保证 |
|---|---|---|---|
| **A. 标签收尾前** | 标签的 `>` 之前(自闭合则 `/>` 之前) | HTML 解析器会吃掉标签名/属性与 `>` 之间的空白;序列化时又统一输出 `>` | **DOM 逐字节不变** ⇒ `outerHTML` **原样**指纹不变 |
| **B. class 值内部** | `class="…"` 里**两个 class 令牌之间** | `class` 是**空白分隔的令牌表**,换行 ≡ 空格(CSS 匹配、`classList` 取值都按令牌集合) | `classList`/计算样式/几何全不变;只有该属性**字面量**里多一个换行 |
**什么时候用哪个**:
- 跨元素(压行假红)→ 手法 A 就够(每个带令牌的标签前都插一个)。
- 同一元素内(变体假红)→ 必须用手法 B:按令牌顺序扫,遇到「当前段已含一类、下一个令牌是另一类」就在该令牌前断行(贪心分段,保证每段不含两类)。
**切分点够不够**:给「行内每个带令牌的标签」都在其 `>` 前断一次 ⇒ 每个源码行最多只含**一个**带令牌标签的 class,于是跨元素假红必然消失。
⛔ **安全边界**:必须**全文件扫描**标签,跳过 `<!-- -->` 与 `<script>`/`<style>` **正文**里的伪标签——JS 里拼的 HTML 字符串常含 `class="text-neutral-500"`,进去插换行会改坏 JS。⛔ 别做逐行扫描。
---
## 3. 执行协议
```bash
# 1) 备份(含 md5 记账)
mkdir -p <tmp>/backup-<名>-<日期时间> && cp 0*.html <备份目录>/ && (cd <备份目录> && md5sum 0*.html > _md5-before.txt)
# 2) 干跑 → 看计数与断言;3) 落地
python fix-xxx.py # 干跑
python fix-xxx.py --apply # 落地
```
**修法脚本必备的四条断言**(任一不过就停手):
1. `new.replace("\n","") == old.replace("\n","")` —— 除新增换行外**正文逐字符不变**;
2. 标签数不变(同一套扫描函数前后各数一次);
3. `<script` 计数不变;
4. 落盘字节 == 预期字节,且 `b"\r\n" not in 落盘`。
⚠️ **Windows 坑**:`Path.write_text(...)` 默认会把 `\n` 翻成 `\r\n`,**全文件变字节**。必须 `write_text(new, encoding="utf-8", newline="\n")`。
---
## 4. 验收矩阵(「零视觉变化」的证据)
| 项 | 期望 |
|---|---|
| 检测器计数 | 目标类别**下降**,其它类别**不升**(逐页比对,⛔ 不允许某页变多) |
| 结构体检 | 原有结构脚本仍全绿(例:`health-full.py` rc=0) |
| 归一化指纹 | `outerHTML` 折叠连续空白后哈希 **不变** |
| 几何 | 每个后代元素的 `tag|id|classList排序|left/top/w/h` 串哈希 **不变** |
| 渲染文本 | `innerText` 归一化后哈希 **不变** |
| `classList` 集合 | 全后代类名排序串哈希 **不变** |
| 元素数 | 不变 |
⚠️ **巨大读数坑**:结构脚本若用 `outerHTML` 的**原样**哈希(含空白)当「DOM 指纹」,**手法 B 一定会让它变**。⛔ 别把「指纹变了」当成「渲染变了」;要比**折叠空白后**的哈希 + 几何 + `classList` + 渲染文本。
- 只用手法 A 的页面,原样指纹**应完全不变**——这是手法 A 正确性的强证据。
⚠️ **真点坑**:自动化窗口不在前台时(`document.hidden === true` / `visibilityState === 'hidden'`),真实鼠标点击(CDP `Input.dispatchMouseEvent` / 坐标点击)会被**静默丢弃**:同坐标 `elementFromPoint` 明明命中按钮,点击却毫无反应。⇒ 先 `Page.bringToFront`;仍 `hidden` 就用 JS `.click()` 验接线,并把**真点项如实记为「未取到读数」**。⛔ 别把点不着的按钮判成坏按钮。
---
## 5. 交付物要求
- **逐条判定表**落盘(文件名含日期):每条给出 `检测器报项 | 判定(真红/假红+子类)| 依据 | 是否已收敛`。
- **「有意保留」项必须逐条写明理由**,并在总览里给出「修前 → 修后」计数。
- **回滚方法**(备份目录 + md5 清单)必须写进交付物。
- 若检测器 rc 不能归零:说明「剩余全是保留项」并给出理由——多数清单的判据本就写的是「rc=0 **或**把有意保留项写明」。⛔ 别为了把 rc 压成 0 去动字体与配色。