agentcss 0.11.3 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/01-tokens/README.md +2 -2
- package/01-tokens/focus-ring.css +1 -1
- package/01-tokens/tokens.css +1 -1
- package/03-patterns/nav/README.md +1 -1
- package/03-patterns/pagination/pagination.css +2 -2
- package/03-patterns/reveal/README.md +3 -3
- package/04-recipes/analysis-report/demo.html +2 -2
- package/04-recipes/longform/README.md +1 -1
- package/04-recipes/longform/longform.html +6 -10
- package/04-recipes/report/demo.html +1 -1
- package/04-recipes/table/demo.html +1 -1
- package/04-recipes/table/table.css +1 -1
- package/04-recipes/tierA-tokens.css +1 -1
- package/09-assets/README.md +1 -1
- package/09-assets/bar/README.md +1 -3
- package/09-assets/bar/bar.css +2 -2
- package/09-assets/bar/bar.js +6 -6
- package/09-assets/bar/demo.html +2 -2
- package/09-assets/echarts-adapter/echarts-adapter.js +8 -8
- package/09-assets/model-viewer/model-viewer.css +2 -2
- package/09-assets/scientific-plot/scientific-plot.js +1 -1
- package/CHANGELOG.md +107 -48
- package/CONTRIBUTING.md +1 -1
- package/README.md +158 -369
- package/README.zh-CN.md +508 -0
- package/SUPPORT.md +2 -2
- package/ai/START-HERE.md +1 -1
- package/ai/components.json +2 -2
- package/ai/components.meta.json +1 -1
- package/ai/tokens.json +1 -1
- package/dist/09-assets/bar/bar.js +1 -1
- package/dist/09-assets/echarts-adapter/echarts-adapter.js +1 -1
- package/dist/09-assets/scientific-plot/scientific-plot.js +1 -1
- package/dist/COSTS.md +9 -9
- package/docs/INVARIANT.md +1 -1
- package/index.html +78 -62
- package/package.json +3 -3
- package/ERRORS.md +0 -1658
- package/docs/BACKLOG.md +0 -127
- package/docs/BENCHMARK.md +0 -252
- package/docs/GUIDANCE.md +0 -154
- package/docs/RELEASING.md +0 -311
- package/docs/RFC-THEME-SCOPE.md +0 -141
- package/docs/ROADMAP.md +0 -182
package/ERRORS.md
DELETED
|
@@ -1,1658 +0,0 @@
|
|
|
1
|
-
# ERRORS —— 踩过的坑与纠正规则
|
|
2
|
-
|
|
3
|
-
> **只记真的踩到的**:门禁真的红过、或者真的做错过一次。
|
|
4
|
-
> 不记"可能会发生"的 —— 那份清单会越长越没人看,最后变成装饰。
|
|
5
|
-
|
|
6
|
-
每条三件套:**症状 → 根因 → 纠正规则**。规则必须能机械执行,
|
|
7
|
-
不能是"下次注意一点"这种执行不了的话。
|
|
8
|
-
|
|
9
|
-
## 怎么登记新条目
|
|
10
|
-
|
|
11
|
-
1. 追加到文件末尾,编号往下排(**不要改旧条目** —— 旧条目是历史,改了没法追溯)
|
|
12
|
-
2. 症状那一栏写**门禁当时报的那一句原话**,不要写"某某地方有问题"
|
|
13
|
-
3. 规则写成"做什么 / 跑什么命令",让别人照着做就能避免
|
|
14
|
-
|
|
15
|
-
## 索引
|
|
16
|
-
|
|
17
|
-
| # | 一句话 |
|
|
18
|
-
|---|---|
|
|
19
|
-
| E1 | 磁盘写满会静默吃掉工作树,且不报错 |
|
|
20
|
-
| E2 | 改组件名/路径后没重新生成 `ai/` 契约 |
|
|
21
|
-
| E3 | 门禁正在跑的时候 `git add -A` |
|
|
22
|
-
| E4 | 新写的中文文案没跑泄漏扫描 |
|
|
23
|
-
| E5 | 新增 `window` 全局没同步进 `API.md` |
|
|
24
|
-
| E6 | 发版时版本号几处不同步 |
|
|
25
|
-
| E7 | 新组件目录缺 `demo.html` 或契约 |
|
|
26
|
-
| E8 | 契约判据太粗,把正确的实现判成违规 |
|
|
27
|
-
| E9 | 手写清单会漂移(AI 入口 6 处失实)|
|
|
28
|
-
| E10 | "我记得做过了"不等于"仓库里做了" |
|
|
29
|
-
| E11 | 手写清单会漂移(人类入口漏 11 个组件)|
|
|
30
|
-
| E12 | 目录变了,描述那一层的散文没变 |
|
|
31
|
-
|
|
32
|
-
| E13 | 文档里的"待办"没人回来划掉 |
|
|
33
|
-
| E14 | 判据分不清「声称」和「引用」 |
|
|
34
|
-
| E15 | 特异度把变量吃掉,而判据只读源码文本(三档 profile 全钉在同一字号)|
|
|
35
|
-
| E16 | `newPage({viewport})` 在本机不生效 ⇒ 判据量到的是默认宽度 |
|
|
36
|
-
| E17 | 断言"栅格行为"量了容器,而差异在 grid 项 |
|
|
37
|
-
| E18 | em 变量只比 px,字号小的版本天然过关 ⇒ 判据聋 |
|
|
38
|
-
| E19 | 两道门禁各有一套敏感词表 ⇒ 同一内容一边干净一边 17 处 |
|
|
39
|
-
| E20 | 裸字符串匹配命中注释(`@page` 假红)|
|
|
40
|
-
| E21 | CSS 计算值带增量后缀,`===` 比较会假红 |
|
|
41
|
-
| E22 | CSS counter 生成的数字不在 DOM / 无障碍树,断言不到 |
|
|
42
|
-
| E23 | 边注/正文列宽的账没先算,正文被挤到 12em |
|
|
43
|
-
| E24 | 生成物是一条链,跑了 build-dist 却漏了 dist-cost |
|
|
44
|
-
| E25 | 按 token 判「是否在合法位置」会让重复出现一起逃逸 |
|
|
45
|
-
| E26 | 为「检测泄漏」而把真实账号名/路径硬编码进门禁脚本 |
|
|
46
|
-
| E27 | 生成脚本读不到输入时静默退回旧清单/空值 |
|
|
47
|
-
| E28 | 「一个脚本覆盖多个组件」时按文件名探测会误判成"没有门禁" |
|
|
48
|
-
| E29 | 判据认"文件里出现过某字符串"——用到的地址会让它永久为真 |
|
|
49
|
-
| E30 | 照 BEM 直觉写文档 ⇒ 凭空造出源码里不存在的标记与类名 |
|
|
50
|
-
| E31 | 同一个判定逻辑写两份 ⇒ 两道门禁互相打架 |
|
|
51
|
-
| E32 | shell heredoc 逐层转义把 `` 变成退格符 ⇒ 正则永不匹配 |
|
|
52
|
-
| E33 | 装一个包时 npm 清掉未声明的其它包 |
|
|
53
|
-
| E34 | 只 grep 一个属性名⇒ 漏掉它的逻辑属性别名,得出"库里没有"的错误结论 |
|
|
54
|
-
| E35 | 路径里写了不展开的 HOME 占位符 ⇒ 该候选永远不存在 |
|
|
55
|
-
| E36 | 判据第一版过宽 ⇒ 把 1px 分隔线与「两条边 + rotate 拼出的箭头」也抓进来 |
|
|
56
|
-
| E37 | 「统一到微行为核」被当成目标本身 ⇒ 为统一而丢功能 |
|
|
57
|
-
| E38 | 条件里用了本文件不存在的变量 ⇒ 恒假、门禁靠假绿过关 |
|
|
58
|
-
| E39 | 反向控制的突变体与当前真实状态耦合 ⇒ 代码改好之后自测自己失效 |
|
|
59
|
-
| E40 | 照想象写测试 API ⇒ 批量假失败;批量正则改测试 ⇒ 越修越少 |
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## E1 · 磁盘写满会静默吃掉工作树,且不报错
|
|
64
|
-
|
|
65
|
-
- **症状**:工作树源文件全部丢失(0 个文件,只剩空目录骨架),`.git` 损坏
|
|
66
|
-
(无 HEAD / config / index),而此前所有的门禁运行**没有一条报错说磁盘满了**
|
|
67
|
-
- **根因**:磁盘写满是**静默失败** —— 重定向写日志失败不会让命令返回非 0,
|
|
68
|
-
于是看起来只是"门禁又红了"
|
|
69
|
-
- **纠正规则**:
|
|
70
|
-
1. 门禁日志为空 / 命令莫名失败时,**先 `df -h` 看剩余空间**,再怀疑代码
|
|
71
|
-
2. 开工前第一件事是看 C 盘剩余空间
|
|
72
|
-
3. npm 一律带 `--cache <非系统盘路径>`,别让缓存撑爆系统盘
|
|
73
|
-
|
|
74
|
-
## E2 · 改了源码(**含版本号**)没重新生成 `ai/` 契约
|
|
75
|
-
|
|
76
|
-
- **症状 1**:`maturity-gate --selftest` →
|
|
77
|
-
`FileNotFoundError: 04-recipes/analysis-report/report.css`
|
|
78
|
-
- **症状 2**(0.8.1 发版时踩到):`ai-contract` →
|
|
79
|
-
「`ai/components.json` 与源码不一致」—— 只改了 `VERSION` 与 `package.json` 的版本号,
|
|
80
|
-
没动任何组件,照样红
|
|
81
|
-
- **根因**:`ai/components.json` 是**生成物**,里面带着 `libraryVersion`。
|
|
82
|
-
改组件名/路径会让它指向旧路径;改版本号会让它记录旧版本。
|
|
83
|
-
生成物不能手改,也不能靠"跑一次门禁看看"
|
|
84
|
-
- **纠正规则**:改组件名 / 增删组件 / **发版改版本号** 之后,都要
|
|
85
|
-
`python 05-audit/gen-ai-contract.py` → `python 05-audit/gen-ai-contract.py --check`
|
|
86
|
-
|
|
87
|
-
## E3 · 门禁正在跑的时候 `git add -A`
|
|
88
|
-
|
|
89
|
-
- **症状**:`gate-fixture` 报"兜底视野与 git 视野不一致"(兜底少 `overlay.js.__reverse.bak`)
|
|
90
|
-
- **根因**:反向控制脚本运行中会生成 `.bak`,此时 `git add -A` 把**瞬态文件**暂存进索引,
|
|
91
|
-
文件随后被删 ⇒ 索引里留了一个磁盘上不存在的路径
|
|
92
|
-
- **纠正规则**:门禁跑的时候不要 `git add -A`。提交前先
|
|
93
|
-
`git status --porcelain | grep -E '\.(bak|tmp|orig)$'` 确认清零
|
|
94
|
-
|
|
95
|
-
## E4 · 新写的中文文案没跑泄漏扫描
|
|
96
|
-
|
|
97
|
-
- **症状**:`leak` 报"内部流程 4 处"(命中词是黑名单里的内部流程词,
|
|
98
|
-
词表见 `05-audit/terms.py` 的 `PROCESS_WORDS`,不在此处复述 —— 复述等于再泄漏一次)
|
|
99
|
-
- **根因**:新增示例文案里用了本库黑名单里的内部流程词。技术内容没问题,
|
|
100
|
-
但**对外分发物**不能出现
|
|
101
|
-
- **纠正规则**:
|
|
102
|
-
1. 新增中文文案先跑 `python 05-audit/leak-scan.py`
|
|
103
|
-
2. 替换方向:批量计数的说法 → 批号 / 样本;任务单的说法 → 任务
|
|
104
|
-
|
|
105
|
-
## E5 · 新增 `window` 全局没同步进 `API.md`
|
|
106
|
-
|
|
107
|
-
- **症状**:`api-doc` 报"实现里有、文档没写:`ScientificPlot`"
|
|
108
|
-
- **根因**:新增了挂载到 `window` 的全局对象,没同步进 `API.md` 的全局表
|
|
109
|
-
- **纠正规则**:**新增全局 = 同一改动里必须进 `API.md` 全局表 + 用法示例**。
|
|
110
|
-
使用者不知道它存在,等于没有
|
|
111
|
-
|
|
112
|
-
## E6 · 发版时版本号几处不同步
|
|
113
|
-
|
|
114
|
-
- **症状**:`release` 报 `package.json` 与 `VERSION` 不一致
|
|
115
|
-
- **根因**:版本有**五处**要同步:`VERSION` / `package.json` / `CHANGELOG` 最新版 /
|
|
116
|
-
`package-lock.json`(两处)
|
|
117
|
-
- **纠正规则**:一次性改 5 个位置,改完立刻跑 `release-gate.py`
|
|
118
|
-
|
|
119
|
-
## E7 · 新组件目录缺 `demo.html` 或契约
|
|
120
|
-
|
|
121
|
-
- **症状**:`admission` 报 analysis-report 缺 demo.html、缺契约;
|
|
122
|
-
`coverage` 报 3 个组件有 demo 无契约(91%)
|
|
123
|
-
- **根因**:`coverage-gate.py` 按「目录里有没有被任何契约提到」判定,
|
|
124
|
-
不看你口头有没有"验证过"
|
|
125
|
-
- **纠正规则**:组件目录(有 css/js 的)**必须**有 `demo.html` + 一份 `<目录名>-check.js`,
|
|
126
|
-
并在 `check-all.sh` 注册一行。用 `index.html` 不算。coverage 阈值 100%,
|
|
127
|
-
**不要靠降阈值解决**
|
|
128
|
-
|
|
129
|
-
## E8 · 契约判据太粗,把正确的实现判成违规
|
|
130
|
-
|
|
131
|
-
- **症状**:自己写的契约误报(假红)—— 把滚动容器里的宽表格报成"越界";
|
|
132
|
-
把无量纲序列报成"必须有单位"
|
|
133
|
-
- **根因**:判据写成"元素右缘不得越过视口",但宽内容在 `overflow-x:auto` 祖先里
|
|
134
|
-
本来就该越界;"必须有单位"也没区分"声明了就得画出来"
|
|
135
|
-
- **纠正规则**:**契约报错时,先确认"正确的实现会不会也被它判红",再动手改代码。**
|
|
136
|
-
这类错误最危险 —— 门禁红了以后人的第一反应是去改实现,而实现其实是对的
|
|
137
|
-
|
|
138
|
-
## E9 · 手写清单会漂移(AI 入口 6 处失实)
|
|
139
|
-
|
|
140
|
-
- **症状**:`ai/START-HERE.md` 里 6 处与实际不符:组件数 27≠28、
|
|
141
|
-
`04-recipes/` 漏列新示例、写了不存在的路径 `ai/tree`
|
|
142
|
-
- **根因**:入口是人手写的散文 —— **改代码的人不会来翻入口,读入口的人不会去数目录**
|
|
143
|
-
- **纠正规则**:**会漂移的数字与清单必须由脚本生成**。已由 `fix-start-here.py`
|
|
144
|
-
生成 `ai/START-HERE.md` 的实测清单;新增目录没登记就直接报错退出
|
|
145
|
-
|
|
146
|
-
## E10 · "我记得做过了"不等于"仓库里做了"
|
|
147
|
-
|
|
148
|
-
- **症状**:验收条款要求逐项回仓库核,核出来发现两条交付与仓库对不上
|
|
149
|
-
- **根因**:长任务里,先做的事到最后会被当成已完成。**事实只有跟仓库对过才算**
|
|
150
|
-
- **纠正规则**:每个工作包收工前执行一次 ——
|
|
151
|
-
「档案里提到的每个路径存在吗?每个数字是从文件读出来的还是手抄的?」
|
|
152
|
-
|
|
153
|
-
## E11 · 手写清单会漂移(人类入口漏 11 个组件)· 0.8.0 收口时发现
|
|
154
|
-
|
|
155
|
-
- **症状**:根 `START-HERE.md` 的组件表只列 15 个,库里实际 26 个(+2 个页面示例);
|
|
156
|
-
且第 39 行列着徽章、文末却把它写成「没有」—— 同一份文件自相矛盾
|
|
157
|
-
- **根因**:与 E9 同一个病,但发生在**人类复用者的第一份文件**上,比 AI 入口更严重:
|
|
158
|
-
漏掉的 11 个里有下拉选择、分页、日期区间、树形、抽屉 —— 读者会以为库里没有
|
|
159
|
-
- **纠正规则**:
|
|
160
|
-
1. 组件表 / beta 标注 / 页面示例清单全部由 `fix-start-here.py` 生成
|
|
161
|
-
2. 「必需的 JS」这种**人工判断**必须被源码反向校验(本库已加三条判据:
|
|
162
|
-
漏登记 / 把纯增强标必需 / 把行为层标可选,都会红)
|
|
163
|
-
3. 散文部分("有 / 没有"那一节)脚本管不了,写的时候必须对着目录数一遍
|
|
164
|
-
|
|
165
|
-
## E12 · 目录变了,描述那一层的散文没变
|
|
166
|
-
|
|
167
|
-
- **症状**:`09-assets/README.md` 标题写「素材(图标)」,
|
|
168
|
-
而这一层早就是 5 个图表能力,正文一个字没提
|
|
169
|
-
- **根因**:同名 —— 目录加了新东西,但描述这一层的 README 是当初建目录时写的
|
|
170
|
-
- **纠正规则**:这类"描述某一层是干什么的"文档,清单部分也交给脚本生成
|
|
171
|
-
(本库已把 `09-assets/` 清单并入 `fix-start-here.py`,与 AI 入口同源)
|
|
172
|
-
|
|
173
|
-
## E13 · 文档里的"待办"没人回来划掉
|
|
174
|
-
|
|
175
|
-
- **症状**:`docs/ROADMAP.md` 第一节 6 行写「待办 W3 / W7 / W8 / W9」,
|
|
176
|
-
而第三节里同样编号已标 ✅ 完成 —— 同一份文件里两处互相打架
|
|
177
|
-
- **根因**:处置列记的是**当时的决定**,事情做完后没人回来改那一列
|
|
178
|
-
- **纠正规则**:把"待办"写进文档时,同时写清**谁会回来改它**。
|
|
179
|
-
收工前扫一遍:`grep -rn '待办\|TODO' docs/`,有 ✅ 的就划掉
|
|
180
|
-
|
|
181
|
-
## E14 · 判据分不清「声称」和「引用」
|
|
182
|
-
|
|
183
|
-
- **症状**:给 `doc-facts-gate.py` 加了中文名的 F4b 判据之后,它立刻报了 2 处 ——
|
|
184
|
-
而那 2 处是**我在描述那个历史错误**("以前把徽章写成了没有……"),
|
|
185
|
-
并不是在声称徽章没做
|
|
186
|
-
- **根因**:判据看的是文字形状,看不出上下文是不是引用
|
|
187
|
-
- **纠正规则**:
|
|
188
|
-
1. **引用历史错误时,不要连着写「❌ + 还没做」** —— 改写成「把它写成没有」
|
|
189
|
-
2. **不给判据开豁免**。误报的正确处置是改文字,不是给门禁开洞 ——
|
|
190
|
-
开洞会让人学会忽略这道门禁(见 E8 的同一条道理)
|
|
191
|
-
|
|
192
|
-
## E15 · 特异度把变量吃掉,而判据只读源码文本 · 0.8.2 建 report 时踩到
|
|
193
|
-
|
|
194
|
-
- **症状**:三套 profile 的文章标题实测**全是 24px**,与 `--rep-title-fs` 的三档取值毫无关系;
|
|
195
|
-
而只读 CSS 源码的契约判据("这个变量在文件里出现过")**全绿**
|
|
196
|
-
- **根因**:`.rep h3`(0,1,1)压过 `.rep__title`(0,1,0)。
|
|
197
|
-
变量**写了 ≠ 生效** —— 源码文本判据看不见级联
|
|
198
|
-
- **纠正规则**:
|
|
199
|
-
1. 凡是"变量分档"的判据,必须读**计算样式**(`getComputedStyle`),不许只读源码文本
|
|
200
|
-
2. 组件内要压过元素选择器时,把选择器加一层:`.rep .rep__title`(0,2,0)
|
|
201
|
-
3. 新判据写完立刻做反向控制(注入缺陷看它红不红),别等它自己撞上缺陷
|
|
202
|
-
|
|
203
|
-
## E16 · `newPage({viewport})` 在本机不生效 · 0.8.2
|
|
204
|
-
|
|
205
|
-
- **症状**:把 `minmax(0,1fr)` 改成 `1fr` 之后,「② minmax(0,1fr) 真在起作用」**仍然 PASS**
|
|
206
|
-
(反向控制 RC-2 打不出红 ⇒ 判据是聋的)
|
|
207
|
-
- **根因**:puppeteer 的 `browser.newPage({ viewport })` 在这台机器上**不改窗口**,
|
|
208
|
-
页面停在默认 800px —— 窄屏下正文与边注根本不并排,差异被抹平
|
|
209
|
-
- **纠正规则**:要量任何宽度,先 `await page.setViewport({ width, height })`,
|
|
210
|
-
不要用 `newPage` 的参数。判据写完用注入法证明它听得见(见 E15 第 3 条)
|
|
211
|
-
|
|
212
|
-
## E17 · 断言"栅格行为"量了容器,差异其实在 grid 项 · 0.8.2
|
|
213
|
-
|
|
214
|
-
- **症状**:第一版判据比较两个 profile 的**栅格容器** `.rep__grid` 宽度,
|
|
215
|
-
注入 `1fr` 缺陷后两侧完全一样 ⇒ 判据聋
|
|
216
|
-
- **根因**:`minmax(0,1fr)` 防的是 **grid 项被内容顶宽**;容器宽度由版心决定,本来就不变
|
|
217
|
-
- **纠正规则**:断言栅格行为必须量**列(grid item)**的宽度,
|
|
218
|
-
且必须**先塞一段超宽内容**再看正文列有没有被顶宽(实测 1fr 让正文列 184→598px)
|
|
219
|
-
|
|
220
|
-
## E18 · em 变量只比 px,字号小的版本天然更窄 ⇒ 判据聋 · 0.8.2
|
|
221
|
-
|
|
222
|
-
- **症状**:「① 正文行长真的分档」第一版用 px 比,三套 profile 全部通过 ——
|
|
223
|
-
其中金融版只是**字号小一档**所以 px 值天然最低,行长变量根本没生效也照样过
|
|
224
|
-
- **根因**:`--rep-measure` 的单位是 em,实测值必须除以该元素自己的 `fontSize` 才能比
|
|
225
|
-
- **纠正规则**:凡 em / rem 类变量,断言一律写成 `px / fontSize` 换回 em 再比大小
|
|
226
|
-
|
|
227
|
-
## E19 · 两道门禁各有一套敏感词表 · 0.8.2
|
|
228
|
-
|
|
229
|
-
- **症状**:`leak-scan.py` 报「OK 无敏感信息」,同一棵树 `outsider-audit.py` 报「🔴 共 17 处」
|
|
230
|
-
—— 同一份内容,一边干净一边脏
|
|
231
|
-
- **根因**:两处各自手抄了一份词表与正则,口径不一致(一边认「工单」,另一边不认)
|
|
232
|
-
- **纠正规则**:
|
|
233
|
-
1. 词表单一来源:`05-audit/terms.py`,两道门禁都 `import terms`
|
|
234
|
-
2. 改词表只改一处,改完**两边都跑一遍**
|
|
235
|
-
3. 新增判据后跑 `rc-terms.py` 那类反向控制:真泄漏要红、行业词(批次收率 / LIMS 批次)要绿
|
|
236
|
-
|
|
237
|
-
## E20 · 裸字符串匹配命中注释(`@page` 假红)· 0.8.2
|
|
238
|
-
|
|
239
|
-
- **症状**:「⑤ 反向:没有偷偷做分页 —— report.css 不得出现 @page / break-after」首跑 FAIL,
|
|
240
|
-
报 report.css 里出现 `@page`
|
|
241
|
-
- **根因**:命中的是**文件头注释**里那句"本轮明确不做 @page"
|
|
242
|
-
- **纠正规则**:扫源码前先 `kit.stripComments(src)` 再匹配。
|
|
243
|
-
凡是"反向判据"(断言某物**不**出现),第一步永远是脱注释
|
|
244
|
-
|
|
245
|
-
## E21 · CSS 计算值带增量后缀,`===` 比较会假红 · 0.8.2
|
|
246
|
-
|
|
247
|
-
- **症状**:「③ 图与表走两路独立计数」首跑 FAIL ——
|
|
248
|
-
`getComputedStyle` 拿到的 `counterIncrement` 是 `"rep-fig 1"`,不是 `"rep-fig"`
|
|
249
|
-
- **根因**:counter 的 computed value 是「名 + 增量」两段
|
|
250
|
-
- **纠正规则**:比较前 `split(/\s+/)[0]` 取前缀
|
|
251
|
-
|
|
252
|
-
## E22 · CSS counter 生成的数字不在 DOM,也不在无障碍树 · 0.8.2
|
|
253
|
-
|
|
254
|
-
- **症状**:想断言"图 1 / 图 2 / 表 1 的**数字**排得对不对",
|
|
255
|
-
`innerText` 里没有,`page.accessibility.snapshot()` 也抓不到(实测取证过)
|
|
256
|
-
- **根因**:`::before { content: counter(...) }` 是**渲染产物**,不进 DOM / a11y 树
|
|
257
|
-
- **纠正规则**:
|
|
258
|
-
1. 降级断言两件事 —— ① counter 名与 `counter-increment` 用对了(结构);
|
|
259
|
-
② 把 `content` 临时关掉量生成框宽度差 > 4px(它**真的占了像素**)
|
|
260
|
-
2. **具体数字(是不是 1、2、3 连着排)列为未验证**,写进 README「已知限制」
|
|
261
|
-
3. 不许用"门禁绿"冒充"数字对" —— 见 D 工作包口径:无法实测的必须保留为未验证
|
|
262
|
-
|
|
263
|
-
## E23 · 边注/正文列宽的账没先算,正文被挤成一根棍 · 0.8.2
|
|
264
|
-
|
|
265
|
-
- **症状**:桌面 1440 实测正文列 **220px ≈ 12em**(而设计意图是 30em),
|
|
266
|
-
宽图塞进边注后被压成缩略图
|
|
267
|
-
- **根因**:库页宽 `--measure-page` 只有 832px,边注 `--measure-aside` 写 32em
|
|
268
|
-
直接把正文吃掉了:`832 - 32em ≈ 220px`
|
|
269
|
-
- **纠正规则**:
|
|
270
|
-
1. 改列宽前先做减法:**页宽 − 边注 = 正文**,正文不得窄于 22em
|
|
271
|
-
2. 把下限写进判据(已加「② 宽屏下正文列保有可读宽度」),别只靠肉眼
|
|
272
|
-
3. 宽图/大图表**不能放进边注**,必须回到正文流出全宽
|
|
273
|
-
|
|
274
|
-
## E24 · 生成物是一条链,跑了一件漏了下一件 · 0.8.2
|
|
275
|
-
|
|
276
|
-
- **症状**:`dist-cost` → `[FAIL] dist/COSTS.md 与实算不一致 ⇒ 手改了?跑 --update 重算`
|
|
277
|
-
—— 我明明已经跑过 `build-dist.py`(dist 是新的),COSTS.md 却是旧的
|
|
278
|
-
- **根因**:**生成物不止一件,也不存在一个命令把它们全跑完**:
|
|
279
|
-
`ai/components.json`、`ai/START-HERE.md`、`dist/`、`dist/COSTS.md`、
|
|
280
|
-
`size-baseline.json`、`10-review/shots/_baseline/` 各自独立。
|
|
281
|
-
跑了一件会让人产生"生成物都更新了"的错觉
|
|
282
|
-
- **纠正规则**:改完源码 / 改完版本号,按下表**逐条跑,跑完再核 `git status`**:
|
|
283
|
-
1. `python 05-audit/gen-ai-contract.py` 然后 `--check`
|
|
284
|
-
2. `python 05-audit/fix-start-here.py` 然后 `--check`
|
|
285
|
-
3. `python 05-audit/build-dist.py` 然后 `--check`
|
|
286
|
-
4. `python 05-audit/dist-cost-gate.py --update` ← **本次漏的就是这条**
|
|
287
|
-
5. `python 05-audit/size-baseline.py --record`(有意变大才可录)
|
|
288
|
-
6. `python 05-audit/shot-baseline.py --update`(先看图确认方向对)
|
|
289
|
-
7. 最后 `bash 05-audit/check-all.sh` —— **不要用 `tail` 截日志**,
|
|
290
|
-
截掉的那一段里可能正躺着那个 FAIL(本次就是这么丢的)
|
|
291
|
-
|
|
292
|
-
## E25 · 按 token 判「是否在合法位置」,会让重复出现一起逃逸 · 0.8.2
|
|
293
|
-
|
|
294
|
-
- **症状**:反向控制里这条**该红却绿** —— 一份 package.json 里
|
|
295
|
-
`repository.url` 写着 `…/<账号>/<仓库>.git`、
|
|
296
|
-
`author.name` 也写着同一个 `<账号>`,判据把后者整条放过了
|
|
297
|
-
(⚠️ 这里用占位写法:把真账号名写进文档会**被自家门禁抓到** —— 本轮真踩了一次)
|
|
298
|
-
- **根因**:判据问的是「这个**字符串**有没有出现在合法取值里」(token 级)。
|
|
299
|
-
只要**有一次**落在合法位置,其它所有出现都被一起赦免
|
|
300
|
-
- **纠正规则**:凡是"某些位置可以出现、其它位置不行"的判据,
|
|
301
|
-
必须按**出现位置**判:用 `finditer` 取每一次命中的 span,
|
|
302
|
-
再判断**这一次**是否落在合法区间里(本库:`terms.hits_outside_repo_field`)。
|
|
303
|
-
顺带一条:**别按文件豁免**(`if rel == 'package.json': 跳过`)——
|
|
304
|
-
那会让同文件里别的字段也漏过去
|
|
305
|
-
|
|
306
|
-
## E26 · 为「检测泄漏」而把真实账号名/路径硬编码进门禁脚本 · 0.8.2
|
|
307
|
-
|
|
308
|
-
- **症状**:外人视角扫描报「🔴 自有域名 3 处 / 1 个文件」,
|
|
309
|
-
而那 3 处**全在门禁脚本自己身上** ——
|
|
310
|
-
`outsider-audit.py` 里有 `OWNER = '<账号名>'`、
|
|
311
|
-
`terms.py` 的词表里有账号名字面量、判据里有 `E:/<仓库名>|/e/<仓库名>`。
|
|
312
|
-
也就是说:**为了检测泄漏,脚本自己携带了泄漏**,并随发布版一起公开
|
|
313
|
-
- **根因**:把"本仓库身份"当成常量抄进了代码。而它其实是**数据** ——
|
|
314
|
-
`package.json` 的 `repository / homepage / bugs` 里就有,读一次即可
|
|
315
|
-
- **纠正规则**:
|
|
316
|
-
1. 门禁脚本里**不许出现**真实账号名 / 真实绝对路径的字面量
|
|
317
|
-
(检查方式:`grep -rn "<账号名>" 05-audit/`,非注释命中必须为 0)
|
|
318
|
-
2. 需要身份时**从 `package.json` 读**(本库:`terms._identity_tokens()`)
|
|
319
|
-
3. 自检:`python 05-audit/terms.py` 同级加一条 —— 扫三个门禁脚本,
|
|
320
|
-
非注释命中数必须为 0;>0 就说明又抄了一遍
|
|
321
|
-
4. ⚠️ 别把**仓库名**也算进身份片段:仓库名在每个 CSS 文件头都写着,
|
|
322
|
-
算进来会一次报 100 处假红(本轮真踩了一次,5 分钟才发现是判据写错)
|
|
323
|
-
—— 要保护的是**账号名**,不是仓库名
|
|
324
|
-
|
|
325
|
-
## E27 · 生成脚本读不到输入时静默退回旧清单 · 0.8.3
|
|
326
|
-
|
|
327
|
-
- **症状**:`START-HERE.md` 的组件表少一层(09-assets 整层不在里面),
|
|
328
|
-
而生成脚本一切正常、门禁全绿
|
|
329
|
-
- **根因**:两个叠加的病。① `ORDER` 是**手写**的 26 个 id,层目录**写死**两层
|
|
330
|
-
⇒ 目录一变就漂移;② 更坏的是**兜底逻辑会掩盖它** ——
|
|
331
|
-
手写的 `for base in (...)` 找不到就`continue`,然后生成一份
|
|
332
|
-
"看起来完整、实际少了东西"的表
|
|
333
|
-
- **纠正规则**:
|
|
334
|
-
1. 生成脚本的**输入只能来自机器可读来源**(本库是 `ai/components.json`),
|
|
335
|
-
不许手写清单
|
|
336
|
-
2. 读不到输入时**直接报错**,不静默退回旧清单/空值/默认值 ——
|
|
337
|
-
静默兜底正是这类漏报能长期存活的土壤
|
|
338
|
-
3. 判据方向要与生成方向相反:**从目录出发查契约**(`admission-gate` ⑩),
|
|
339
|
-
才抓得住"目录里有、契约里没有"
|
|
340
|
-
|
|
341
|
-
## E28 · 「一个脚本覆盖多个组件」时按文件名探测会误判 · 0.8.3
|
|
342
|
-
|
|
343
|
-
- **症状**:`bar` 在契约里 `maturity = alpha`、`gates = []`,
|
|
344
|
-
而 `chart-check.js` 明明第 70 行就列了 `09-assets/bar`
|
|
345
|
-
- **根因**:探测规则是「脚本文件名以组件名开头」。
|
|
346
|
-
一个脚本覆盖多个组件时,名字就对不上 ⇒ `bar` 被记成"没有专属门禁"
|
|
347
|
-
- **纠正规则**:探测要认两种写法 ——
|
|
348
|
-
① 按名字(`bar-check.js`);② 脚本用**清单变量**声明覆盖谁
|
|
349
|
-
(`const CHART_DIRS = ['09-assets/bar', ...]`)。
|
|
350
|
-
⚠️ 两种写法都要认:只认①漏掉多组件脚本,只认②则改名后漏改声明会静默失效
|
|
351
|
-
|
|
352
|
-
## E29 · 判据认「文件里出现过某字符串」⇒ 用到的地址让它永久为真 · 0.8.3
|
|
353
|
-
|
|
354
|
-
- **症状**:反向控制打不出红。把 `CHART_DIRS` 里的 `bar` 删掉,
|
|
355
|
-
契约仍然记 `gates = ['chart-check.js']`
|
|
356
|
-
- **根因**:判据用 `if needle in src`扫全文。而
|
|
357
|
-
`const BAR_URL = 'http://.../09-assets/bar/demo.html'` 这行
|
|
358
|
-
**一直是那个字符串** ⇒ 判据永远为真 ⇒ 门禁被架空
|
|
359
|
-
- **纠正规则**:判据必须限定**语法位置**,不能只判出现:
|
|
360
|
-
- 认"**清单变量**里出现独立项"(`XXX_DIRS = [... 'a/b' ...]`),不认任意位置
|
|
361
|
-
- **反向控制必须包含「把声明删掉」这一步** ——
|
|
362
|
-
只测"内容对不对"的判据,靠正向跑是测不出它有没有牙的
|
|
363
|
-
- 附带一条:探测器**必须排除自己**(它的注释里就写着那个样例)
|
|
364
|
-
|
|
365
|
-
## E30 · 照 BEM 直觉写文档 ⇒ 凭空造出源码里不存在的标记 · 0.8.3
|
|
366
|
-
|
|
367
|
-
- **症状**:写 `date-range/README.md` 时按直觉写了 `data-drange-start` /
|
|
368
|
-
`data-drange-end` / `data-drange-clear`,而源码里真实标记是
|
|
369
|
-
`data-dr-from` / `data-dr-to` / `data-dr-clear`
|
|
370
|
-
- **根因**:BEM 的类名/标记有很强的"可预测性",写文档时会不自觉按规律**补全**,
|
|
371
|
-
补出来的东西源码里根本没有。同一批里还编了 `popover__body`、
|
|
372
|
-
`Pagination.initAll()`、`create(node, {page, pageSize})` 等
|
|
373
|
-
- **纠正规则**:
|
|
374
|
-
1. 写组件文档时,**类名、标记名、API 签名一律从源码 grep**,
|
|
375
|
-
不按惯例写(哪怕确信"应该是这个")
|
|
376
|
-
2. 写完**逐条 grep 核实**,数不对就改;
|
|
377
|
-
核实成本远低于让读者按一个不存在的标记去写代码
|
|
378
|
-
3. 文档里的 `maturity` / `behaviorDebt` 也从 `ai/components.json` 读,
|
|
379
|
-
**不写得比契约更好看**(实测:我给 drawer 编了第二条债,契约只有一条)
|
|
380
|
-
|
|
381
|
-
## E31 · 同一个判定逻辑写两份 ⇒ 两道门禁互相打架 · 0.8.3
|
|
382
|
-
|
|
383
|
-
- **症状**:`ai-contract` 绿(`bar` 是 stable),
|
|
384
|
-
而 `maturity`红:「bar:契约写 stable,按判据重算是 alpha」。
|
|
385
|
-
两道门禁对**同一个组件**给出相反结论
|
|
386
|
-
- **根因**:「这个组件有没有专属门禁」这条判定,
|
|
387
|
-
`gen-ai-contract.py` 与 `maturity-gate.py` **各写了一份**。
|
|
388
|
-
生成脚本后来修好了(认清单变量声明),重算那份还是老的(只认文件名)
|
|
389
|
-
⇒ 两份逻辑漂移 ⇒ 一个说 stable 一个说 alpha
|
|
390
|
-
- **纠正规则**:
|
|
391
|
-
1. **判定逻辑只允许有一份**。第二处要用 ⇒ `import` / `importlib` 复用,
|
|
392
|
-
不许复制(`maturity-gate.py` 现在直接用
|
|
393
|
-
`gen-ai-contract.dedicated_gates()`)
|
|
394
|
-
2. 凡是"生成用"和"校验用"的同一判定,都要检查是不是各写了一份。
|
|
395
|
-
本库已中招三次:`ai-layer` 的 schema、`maturity` 的门禁探测、
|
|
396
|
-
`leak-scan`/`outsider-audit` 的敏感词(E19)
|
|
397
|
-
3. 两道门禁结论冲突时,**先怀疑它们各写了一份**,而不是改判据
|
|
398
|
-
|
|
399
|
-
## E32 · shell heredoc 逐层转义把 `` 变成退格符 · 0.8.3
|
|
400
|
-
|
|
401
|
-
- **症状**:正则明明写着 `[A-Z]..._DIRS`,文件里看起来完全正常,
|
|
402
|
-
但探测恒返回空 ⇒ 所有 asset 组件掉成 alpha。查了半天才定位
|
|
403
|
-
- **根因**:在 bash heredoc 里写 Python,`` 经**两层**解析变成 ``,
|
|
404
|
-
正则里的**词边界**被编译成真正的**退格符 0x08**
|
|
405
|
-
⇒ `cat -A` 显示 `^H` 才看得出来,`sed`/`grep` 一切正常
|
|
406
|
-
- **纠正规则**:
|
|
407
|
-
1. 含反斜杠的代码**不要经 heredoc**——用 Write / Edit 工具直接落盘,
|
|
408
|
-
或写一个 `.py` 文件再执行
|
|
409
|
-
2. 必须经 shell 时,**改完必须验证运行时常量**:
|
|
410
|
-
`python -c "import dis; print([c for c in f.__code__.co_consts if isinstance(c,str) and '\b' in c])"`
|
|
411
|
-
或 `cat -A` 看有没有 `^H`
|
|
412
|
-
3. 正则尽量少用需要转义的简写。本库最终改成
|
|
413
|
-
`(?<![A-Za-z0-9_])`(不含反斜杠转义)规避这个坑
|
|
414
|
-
|
|
415
|
-
## E33 · 装一个包时 npm 清掉未声明的其它包 · 0.8.3
|
|
416
|
-
|
|
417
|
-
- **症状**:为修`tsc` 的 SKIP,在受管node 目录里
|
|
418
|
-
`npm install typescript` ⇒ 输出 `added 1 package, and **removed 2 packages**`
|
|
419
|
-
⇒ 下一道门禁 `firefox` 直接崩:`MODULE_NOT_FOUND`(缺 playwright)
|
|
420
|
-
- **根因**:那个目录**没有 package.json 声明依赖**,
|
|
421
|
-
npm 把"未被声明"的包视为多余,装新包时顺手清掉。
|
|
422
|
-
⇒ `npm install X` 在这种目录里**不是增量**,可能删掉别人的东西
|
|
423
|
-
- **纠正规则**:
|
|
424
|
-
1. 在共享的node 目录装包前,先`ls node_modules` 记下现状
|
|
425
|
-
2. 装完**立刻对比**,少了什么要装回去(本轮 `playwright` 就是这么丢的)
|
|
426
|
-
3. 更稳的做法:这类目录应有 package.json 声明依赖,
|
|
427
|
-
或改用 `npm install --no-save X`
|
|
428
|
-
4. 装包后**重跑依赖它的门禁**,别等下次全量才发现
|
|
429
|
-
|
|
430
|
-
## E34 · 只 grep 一个属性名 ⇒ 漏掉逻辑属性别名 · 0.8.4
|
|
431
|
-
|
|
432
|
-
- **症状**:我断言「全库 `border-left` 实代码 0 处,库里没有竖条」,
|
|
433
|
-
还据此跟用户下了结论。**而库里明明有 6 根竖条**
|
|
434
|
-
- **根因**:扫的是 `border-left:`,但 CSS 有**逻辑属性别名**:
|
|
435
|
-
`border-inline-start` / `border-inline-end`(RTL 下自动镜像),
|
|
436
|
-
本库的竖条几乎都用它。⇒ 只扫物理属性名会得出**"该事物不存在"**的错误结论
|
|
437
|
-
- **纠正规则**:
|
|
438
|
-
1. 扫"某属性是否被使用"时,**连同它的逻辑别名一起扫**
|
|
439
|
-
(border-left ↔ border-inline-start / border-right ↔ border-inline-end)
|
|
440
|
-
2. 更稳的做法是扫**语义**(`border-` 开头的 solid/dashed 且宽度 ≥2px),
|
|
441
|
-
而不是某一个具体属性名—— 语义扫法天然覆盖所有别名
|
|
442
|
-
3. 下结论前问一句:「我搜的词,覆盖这个东西的**全部写法**吗?」
|
|
443
|
-
|
|
444
|
-
## E35 · 路径里写了不展开的 HOME 占位符 · 0.8.4
|
|
445
|
-
|
|
446
|
-
- **症状**:`shot-baseline.py` 在受管环境下「一张截图都没截出来」,
|
|
447
|
-
只报「视觉回归未执行」—— 看上去像环境问题,不像代码问题
|
|
448
|
-
- **根因**:两条路径问题叠加。
|
|
449
|
-
① 候选路径里写了一个**不展开的 HOME 占位符**(形如 `$` + 花括号 + HOME),
|
|
450
|
-
Python 不会对它做变量展开 ⇒ 该候选永远不存在
|
|
451
|
-
(⚠️ 本条自己在ERRORS.md 里也不能原样写出那个占位符 ——
|
|
452
|
-
`leak-scan` 有一条「未替换占位符」判据会把它抓出来,本轮真被抓了一次)
|
|
453
|
-
② 脚本的 `NODE_PATH` 只拼了本地 `node_modules`,
|
|
454
|
-
**没认`NODE_MODULES` 环境变量** —— 而 `check-all.sh` 就是用后者传受管目录的
|
|
455
|
-
- **纠正规则**:
|
|
456
|
-
1. Python 里拼路径一律用 `os.path.expanduser('~')`,
|
|
457
|
-
**不写「美元符号 + 花括号」这种占位写法**(那是 shell 的写法,Python 不认)
|
|
458
|
-
2. 环境变量的名字要认全:check-all.sh 传的、本脚本自己认的,两边对齐
|
|
459
|
-
3. 「跑了但产出为 0」这类失败,**报错要说清是"没跑"还是"跑了但环境不全"** ——
|
|
460
|
-
否则每次都要重新查一遍
|
|
461
|
-
|
|
462
|
-
## E36 · 判据第一版过宽,把不该管的也抓进来 · 0.8.4
|
|
463
|
-
|
|
464
|
-
- **症状**:门禁 ⑪(竖条必须写明理由)首跑报出 **11 处**,
|
|
465
|
-
其中包含 `1px solid var(--border-decor)`(普通分隔线)、
|
|
466
|
-
`2px solid transparent`(占位空边框)、以及 accordion 的 marker
|
|
467
|
-
- **根因**:判据只问「有没有 3px 以上的实色竖条」,没问「**这是不是装饰竖条**」。
|
|
468
|
-
而 accordion 的 marker 是**用两条边 + `rotate(45deg)` 拼出的箭头** ——
|
|
469
|
-
单看 `border-inline-end` 完全像一根竖条
|
|
470
|
-
- **纠正规则**:
|
|
471
|
-
1. 判据第一版必然偏宽,要**按真实误报逐条收紧**,不是加白名单
|
|
472
|
-
(本轮收紧了两次:先排除 `transparent`/`<1px`,再排除 `transform: rotate` 邻域)
|
|
473
|
-
2. 收窄条件用**语义特征**(宽度、颜色、邻域规则),不用文件名/行号
|
|
474
|
-
3. 每收窄一次都要**重跑反向控制**,确认"该抓的还抓得住"
|
|
475
|
-
|
|
476
|
-
## E37 · 「统一到微行为核」被当成目标本身 · 0.8.6
|
|
477
|
-
|
|
478
|
-
- **症状**:打算把 9 条 manual 全收进核。做到第3 条(combobox)时发现收编会**丢功能**
|
|
479
|
-
- **根因**:核的 `escOn` 语义是「Esc 一律触发 `onDismiss`」,
|
|
480
|
-
而 combobox 的 Esc 是**两级**的:先关列表、列表已关才清空输入。
|
|
481
|
-
收编 ⇒ 第二级行为消失。它是**更完整**的实现,不是"还没收编的落后写法"
|
|
482
|
-
- **纠正规则**:
|
|
483
|
-
1. 收编前先问「**核能完整表达这个组件的语义吗**」。
|
|
484
|
-
表达不了就别收,并把理由写进矩阵(`behaviors.json` 的 `manual` 项说明)
|
|
485
|
-
2. **收编的收益必须是「修掉真 bug」或「删掉重复」**。
|
|
486
|
-
若手写版本来就是正确写法(如 combobox 用 `root.contains`)⇒ 收益为0,不收
|
|
487
|
-
3. 同样地,dialog(模态)**不该**接dismissable 核的 outside 分支——
|
|
488
|
-
那会引入"点外部关闭",违反 modal 定义。**不收是对的**
|
|
489
|
-
4. 「收编率」不是 KPI,**行为正确才是**
|
|
490
|
-
|
|
491
|
-
## E38 · 条件里用了本文件不存在的变量 ⇒ 恒假、门禁靠假绿过关 · 0.8.6
|
|
492
|
-
|
|
493
|
-
- **症状**:`overlay` 的 dialog 关闭时**焦点从来没还过**,
|
|
494
|
-
但门禁「关闭后焦点归位」一直显示 OK
|
|
495
|
-
- **根因**:原代码是
|
|
496
|
-
```js
|
|
497
|
-
if (focusIsMine && myTrigger && document.contains(myTrigger)) { myTrigger.focus(); }
|
|
498
|
-
```
|
|
499
|
-
而 **`focusIsMine` 在这个文件里根本不存在** ⇒ 条件**恒假** ⇒ 归位永不执行。
|
|
500
|
-
门禁那条判据量的是「关掉后焦点在不在触发器」,它**通过**说明…
|
|
501
|
-
不对——它本该红。实际通过是因为**收编前**那条路径压根没跑到
|
|
502
|
-
- **纠正规则**:
|
|
503
|
-
1. JS 没有编译期检查,**拼错的变量名静默为 `undefined`** ⇒ 条件恒假
|
|
504
|
-
2. 门禁绿**不能证明**"这条代码路径被执行过"。必须做**反向控制**:
|
|
505
|
-
手动破坏该路径 ⇒ 看门禁是否变红(本次验证:去掉 `restore()` 后门禁确实报红)
|
|
506
|
-
3. 收编微行为核的额外价值就在这里:**把散落的手写归位换成一处,
|
|
507
|
-
恒假条件这类问题就无处藏身**
|
|
508
|
-
|
|
509
|
-
## E39 · 反向控制的突变体与真实状态耦合 ⇒ 代码改好后自测自己失效 · 0.8.6
|
|
510
|
-
|
|
511
|
-
- **症状**:把 `tooltip` 收编成 core、把 `dropdown` 的 focus-return 收编之后,
|
|
512
|
-
`beh-reverse` 报「突变没被抓到 ⇒ 门禁是瞎的」。
|
|
513
|
-
**而门禁本身完全没变** —— 是它自己的**反向控制自测**失效了
|
|
514
|
-
- **根因**:两条自测的突变体是**照着当时的真实状态**挑的:
|
|
515
|
-
- `behavior-matrix-gate`:「把 tooltip 的 dismissable **改成** core」
|
|
516
|
-
—— 可tooltip 后来**真的**变成 core 了 ⇒ 这个"突变"不再改变任何东西
|
|
517
|
-
- `maturity-gate`:「dropdown 的 debt 少报一条,只留 typeahead:gap」
|
|
518
|
-
—— 可 dropdown 收编后 debt **正好只剩** typeahead:gap ⇒ 同样不再是突变
|
|
519
|
-
- **纠正规则**:
|
|
520
|
-
1. **反向控制的突变体必须与当前真实状态解耦** ——
|
|
521
|
-
挑一个"**无论我怎么改代码都不会变**"的组合来造假(如 `combobox/dismissable`
|
|
522
|
-
至今仍是 manual ⇒ 把它谎报成 core 永远有效)
|
|
523
|
-
2. 看到「判据是瞎的」这类报错时,**先分清是判据瞎了还是自测失效** ——
|
|
524
|
-
判据本身是否还成立,要单独复跑原状态(本次矩阵门禁单跑是绿的)
|
|
525
|
-
3. **代码变好 ⇒ 自测要跟着换**。这是「棘轮/收编类改动」的固定连带成本,
|
|
526
|
-
写进 CHANGELOG 的「收编」条目里提醒下一个人
|
|
527
|
-
|
|
528
|
-
⚠️ 顺带记一个同类失误:换突变体时我在注释里写
|
|
529
|
-
「combobox 有一条 focus-return 债」—— 核实矩阵后并没有(它是 n/a)。
|
|
530
|
-
⇒ 写**关于某个具体组件的事实**之前必须核实,不能凭印象(与 E30 同源)。
|
|
531
|
-
|
|
532
|
-
## E40 · 照想象写测试 API;批量正则改测试 · 0.8.7
|
|
533
|
-
|
|
534
|
-
- **症状**:给 5 个行为核补 47 条单测,第一版43 条里**21 条失败**。
|
|
535
|
-
更糟的是我用正则批量修,越修越少:43 → 22 → 16 → 14
|
|
536
|
-
- **根因(两层)**
|
|
537
|
-
① **测试是照想象写的**,凭空捏造了 API:
|
|
538
|
-
· `flEmitOn()` —— emit 核**没有**这个函数,它是直接 dispatch CustomEvent
|
|
539
|
-
· `t.key('a')` —— 真名是 `type(ch)`
|
|
540
|
-
· `r.key({...})` —— 真名是 `step(i, d)` / `rove(元素)`
|
|
541
|
-
· 还以为 `rove` 收下标、还触发 `onMove` —— 真实现**收元素、不触发 onMove**
|
|
542
|
-
· 还以为 `axis` 默认 'y' —— 真默认是 **'x'** ⇒ 默认配置下 ArrowDown 不响应
|
|
543
|
-
② **批量正则改测试代码是高危的**:正则吃掉了引号(`loadCore(roving)`)、
|
|
544
|
-
把 `ctx` 改成 `ct`、把测试体外的 `el()` 也换成了作用域外的 `E()`。
|
|
545
|
-
⇒ 我是在**制造**错误,不是修复错误
|
|
546
|
-
- **纠正规则**
|
|
547
|
-
1. **测任何东西之前先读它的真实签名**,一行就够:
|
|
548
|
-
`sed -n '/^var flX/,/^ function/p' file.js`
|
|
549
|
-
或直接跑一段 10 行的探针脚本,把真实返回值打出来
|
|
550
|
-
2. **批量改测试代码后,立刻看「通过数」的变化方向**。
|
|
551
|
-
改完变少 ⇒ 说明改坏了,立刻停,不要继续往上打补丁
|
|
552
|
-
3. **测试失败先怀疑"我调错了",不是先怀疑"代码错了"**——
|
|
553
|
-
真代码有 bug 的概率,远低于我记错 API 的概率
|
|
554
|
-
4. 用桩测别人的代码时,**桩自己坏掉的现象最隐蔽**:
|
|
555
|
-
本轮 `mini-dom` 的 `focus()` 引用了不存在的变量 ⇒ 抛错 ⇒
|
|
556
|
-
被`focus-return.restore()` 的 `try/catch` 吞掉 ⇒
|
|
557
|
-
表现为「焦点明明回去了,restore() 却说没成功」。
|
|
558
|
-
⇒ 桩里凡是"本该不可能错"的地方,一律**显式抛错**而不是静默
|
|
559
|
-
|
|
560
|
-
### ✅ 这次做对的一件事
|
|
561
|
-
|
|
562
|
-
**没有停在"全绿"。** 47/47 通过后,又做了三轮反向控制:
|
|
563
|
-
故意破坏 `focus-return` 的清空、`dismissable` 的 stopPropagation、
|
|
564
|
-
`roving` 的循环导航 ⇒ 分别抓出 1 / 1 / 2 条红 ⇒ 证明这些测试有鉴别力。
|
|
565
|
-
**全绿 + 反向控制红 = 真通过**;只有全绿 = 不知道它有没有在看。
|
|
566
|
-
|
|
567
|
-
---
|
|
568
|
-
|
|
569
|
-
## E41 · 门禁的参数约定错,七道核心门禁在官方调用下扫 0 文件 · 0.8.9
|
|
570
|
-
|
|
571
|
-
- **症状**:外部审计报告「**七个**门禁是假绿」,我独立复现无误:
|
|
572
|
-
```
|
|
573
|
-
$ python 05-audit/numeric-gate.py --dir 01-tokens 02-primitives 03-patterns 04-recipes
|
|
574
|
-
数字排印:全部走 tokens 的 .num / .num-lg。 exit=0 ← 假绿(扫了 0 个文件)
|
|
575
|
-
$ python 05-audit/numeric-gate.py
|
|
576
|
-
66 项:数字排印没有走统一机制。 exit=1 ← 真值
|
|
577
|
-
```
|
|
578
|
-
受影响:`tierA-gate` `icon-gate` `measure-gate` `license-gate`
|
|
579
|
-
`numeric-gate` `hardcode-gate` `reuse-check`
|
|
580
|
-
|
|
581
|
-
- **根因**:`check-all.sh:241` 用 `--dir <目录列表>` 调用,
|
|
582
|
-
而这 7 个脚本写的是 `root = os.path.abspath(sys.argv[1])`
|
|
583
|
-
⇒ root 变成字面量 `<cwd>/--dir`(不存在)⇒ glob 到 0 个文件
|
|
584
|
-
⇒ 无违规 ⇒ exit 0 ⇒ `run()` 判PASS。
|
|
585
|
-
🔴 **最刺的一点**:`check-all.sh:237-239` 的注释**恰好写着这一类警告**,
|
|
586
|
-
只针对 `es5-gate` 一个脚本 —— **同一个错由 7 个脚本各写一遍,修一个不等于修全部**。
|
|
587
|
-
|
|
588
|
-
- **我据此又犯了E40 的变种**:修它时我连续写了三版自动化补丁,**三版全错**:
|
|
589
|
-
|
|
590
|
-
| 版 | 改法 | 为什么错 |
|
|
591
|
-
|---|---|---|
|
|
592
|
-
| v1 | `root = 第一个存在的位置参数` | 这 7 个脚本的 root 语义是**库根**(在 root 下找 `02-primitives` / `0*`)。root 变成 `01-tokens` ⇒ 脚本去 `01-tokens` 下面找 `02-primitives` ⇒ **同样扫 0 个文件** |
|
|
593
|
-
| v2 | 字符串替换坏块 | 转义不匹配,什么都没改 |
|
|
594
|
-
| v3 | 批量插零文件守卫 | 守卫插到 `return` 之后 ⇒ `NameError: total` |
|
|
595
|
-
|
|
596
|
-
⇒ v1 与旧版的**假绿理由不同但结果一样**,这是最危险的一种错:
|
|
597
|
-
**它看起来"修好了"(参数不再被当路径),实际仍然什么都不检查。**
|
|
598
|
-
|
|
599
|
-
- **纠正规则**
|
|
600
|
-
1. **改参数解析之前必须先搞清这个参数在脚本里的语义**。
|
|
601
|
-
这 7 个脚本的 root 都是「库根」,不是「扫描范围」——
|
|
602
|
-
看它们 glob 的是 `os.path.join(root, '02-primitives', ...)` 就该知道。
|
|
603
|
-
⚠️ 按 argparse 的直觉改会连续踩两次。
|
|
604
|
-
2. **凡是"批量改同一类东西",必须有共享 helper**,
|
|
605
|
-
不能让 7 个脚本各写一遍 —— 各写一遍就意味着"修一个不等于修全部"
|
|
606
|
-
(这正是本条事故的结构)。
|
|
607
|
-
3. **每个门禁都要有「零文件即失败」守卫 + 一条「断言扫到 > 0 个文件」的正向控制**。
|
|
608
|
-
只修参数约定不够:传一个拼错的目录同样会"扫 0 个文件然后说全部通过"。
|
|
609
|
-
4. **传了路径但一个都不存在 ⇒ 必须判红,不能静默当成"没传参数"**。
|
|
610
|
-
`args = [a for a in raw if isdir(a)] or [ROOT]` 这个写法会把
|
|
611
|
-
「调用方传错了」和「全量检查」混成同一个结果。
|
|
612
|
-
5. **豁免规则要按"页面/文件承诺了什么"写,不能一刀切**:
|
|
613
|
-
· `tierA-gate` 原来扫 `04-recipes/**`,把普通 demo 页也算成「精简档」⇒ 4 处误报;
|
|
614
|
-
实际只有 `04-recipes/longform/` 承诺了零外链。
|
|
615
|
-
· `license-gate` 把 `09-assets/model-viewer/vendor/`(第三方库原样文件)
|
|
616
|
-
也扫了 ⇒ 9 处误报;第三方英文注释一个字都不能改,已在协议台账登记。
|
|
617
|
-
6. **扫代码前先剥注释**。`model-viewer/demo.html` 里有一段注释
|
|
618
|
-
`<!-- 这里**没有** <script src="three.min.js"> -->`,
|
|
619
|
-
而正则不看上下文 ⇒ 报「引用的 three.min.js 不存在」。
|
|
620
|
-
⇒ 与我在 CSS 扫描上栽过的同一类。
|
|
621
|
-
|
|
622
|
-
- **这次做对的一件事**
|
|
623
|
-
|
|
624
|
-
**没有一次改完就宣布成功。** 7 个门禁**逐个手改**,每个改完立刻跑
|
|
625
|
-
**五种调用方式**(`--dir <列表>` / `<列表>` / 无参 / 不存在的路径 / 不含目标目录),
|
|
626
|
-
全部符合预期才算过。
|
|
627
|
-
而且每改一个都跑真实数据 ⇒ **抓出了 4 处真问题**:
|
|
628
|
-
· `content.css:23 max-width: 68ch` ——
|
|
629
|
-
68ch 是 tokens 里**拉丁语系**那一档的值(中文下是 34em),
|
|
630
|
-
⇒ **中文读者拿到的是拉丁的行宽**。已改成 `var(--measure-prose)`。
|
|
631
|
-
这条是 P0-1b(measure-gate 只扫 HTML、看不到 CSS)暴露的。
|
|
632
|
-
· `icon-gate` 一直在扫 `09-assets/`,而那目录**一个 svg 都没有**。
|
|
633
|
-
· `license-gate` 的 9 项全是第三方库误报。
|
|
634
|
-
· `reuse-check` 的 1 项是注释误报。
|
|
635
|
-
|
|
636
|
-
⭐ **门禁从假绿变成真红,本身就是修复成功的证据**——
|
|
637
|
-
一个从不报问题的门禁,修好之后第一件事就是开始报问题。
|
|
638
|
-
|
|
639
|
-
---
|
|
640
|
-
|
|
641
|
-
## E42 · 一次性迁移脚本的坏正则污染 80 个文件,而门禁失败时不显示原因 · 0.8.9
|
|
642
|
-
|
|
643
|
-
- **症状**:修完 P0-1 跑全量门禁,**60 多道浏览器门禁全 FAIL**,
|
|
644
|
-
耗时全是 0s,看起来像"产品坏了 60 多处"。
|
|
645
|
-
但逐个单跑发现根本不是产品问题 —— 是**文件本身坏了**:
|
|
646
|
-
```
|
|
647
|
-
$ node -e "require('./05-audit/browser.js')"
|
|
648
|
-
Error: Cannot find module '<库根>/05-audit/browser.js'
|
|
649
|
-
```
|
|
650
|
-
打开文件一看:
|
|
651
|
-
```js
|
|
652
|
-
const path = require(REPO + REPO + '//path'); // ← 原文是 require('path')
|
|
653
|
-
const REPO = path.resolve(__dirname, REPO + '/..'); // ← REPO 引用自己
|
|
654
|
-
```
|
|
655
|
-
全库 **80 个文件**被同样污染,中文注释里的 `"常见写法:纯红"` 也变成了
|
|
656
|
-
`REPO + REPO + '//常见写法:纯红'`。
|
|
657
|
-
|
|
658
|
-
- **根因**:`05-audit/leak-fix-paths.py` 的 `fix_js()`:
|
|
659
|
-
```python
|
|
660
|
-
RE_SLASH = re.compile(r"['\"]([^'\"]*)['\"]") # 匹配**任意**引号字符串
|
|
661
|
-
t = RE_SLASH.sub(lambda m: "REPO + '/" + m.group(1) + "'", t)
|
|
662
|
-
```
|
|
663
|
-
⇒ 一次运行就把全仓库 .js 的**每一个字符串字面量**都改了。
|
|
664
|
-
它**没有 dry-run、没有确认、没有备份**,是典型的定时炸弹。
|
|
665
|
-
|
|
666
|
-
- **🔴 为什么 60 多个 FAIL 一个字都看不出原因**(这才是 E42 真正的要害):
|
|
667
|
-
`check-all.sh` 的 `run()` 只 `grep` 门禁自己的判词:
|
|
668
|
-
```bash
|
|
669
|
-
echo "$out" | grep -E '^\s*\[|不达标|不符|未定义|未门控|需人工|超' | head -6
|
|
670
|
-
```
|
|
671
|
-
⇒ `Cannot find module` 这类**环境/崩溃信息**被整条丢掉,
|
|
672
|
-
只剩下 60 多个光秃秃的 `FAIL`。
|
|
673
|
-
⇒ **看日志的人会以为是自己刚改坏了 60 个地方**,
|
|
674
|
-
于是往完全错误的方向去查(我确实先怀疑是 dist 没重建、依赖没装)。
|
|
675
|
-
|
|
676
|
-
- **纠正规则**
|
|
677
|
-
1. **写"批量改代码"的脚本时,正则必须锚定语义,不能锚定字符**。
|
|
678
|
-
`['\"]([^'\"]*)['\"]`(任意字符串)当替换目标,是事故的根因。
|
|
679
|
-
真要匹配路径就把**前缀锚定**(前缀 + [^'"]*),不能只匹配引号内容。
|
|
680
|
-
2. **一次性迁移脚本用完即删**(ROADMAP 执行纪律第 4 条)。
|
|
681
|
-
留着它 = 留着一颗随时会响的雷。
|
|
682
|
-
本条已删除 `leak-fix-paths.py`。
|
|
683
|
-
3. **门禁失败时必须说清是「检查不通过」还是「根本没跑起来」**。
|
|
684
|
-
`run()` 现在会识别 `Cannot find module` / `MODULE_NOT_FOUND` /
|
|
685
|
-
`ECONNREFUSED` / `找不到浏览器` / Traceback,
|
|
686
|
-
并额外打印原始输出的前几行。
|
|
687
|
-
🔴 这与 P0-1 是同一个病根的两面:
|
|
688
|
-
**假绿是"该红却绿了",这条是"红了却不说为什么红"。**
|
|
689
|
-
4. **新增 `rewrite-residue-gate.py` 抓这类拼接痕迹**
|
|
690
|
-
(`REPO + REPO +` / `X + 'a' + 'b'` / `REPO + '//'`),
|
|
691
|
-
已接进 check-all.sh,带反向控制。
|
|
692
|
-
5. ⚠️ **「未替换占位符」这一类不要重复判**:`leak-scan.py` 已有
|
|
693
|
-
「未替换占位符」检查且带成熟豁免。
|
|
694
|
-
我第一版又写了一遍 ⇒ 把门禁脚本自己的注释全报成残留
|
|
695
|
-
⇒ **同一个判定不要写两遍**(E31 的老教训)。
|
|
696
|
-
|
|
697
|
-
- **这次做对的一件事**
|
|
698
|
-
|
|
699
|
-
**没有在 60 多个 FAIL 面前乱改产品。**
|
|
700
|
-
逐个单跑 → 发现是文件本身坏了 → `git diff` 看真实改动形态 →
|
|
701
|
-
认出「坏正则替换」的指纹(`REPO + REPO + '//`)→
|
|
702
|
-
一次性识别出 80 个受影响文件并 `git checkout` 恢复,
|
|
703
|
-
**同时保住了本轮真正的 29 个改动**(先分类:污染的备份、只恢复污染的)。
|
|
704
|
-
|
|
705
|
-
⭐ 教训:**大面积失败时,先判断"是我的改动导致的还是环境/工具导致的"。**
|
|
706
|
-
我一度以为是 dist 没重建、依赖没装 —— 方向完全错。
|
|
707
|
-
|
|
708
|
-
---
|
|
709
|
-
|
|
710
|
-
## E43 · 同一个判定写了三份,CHANGELOG 格式一改就三处各自坏 · 0.8.9
|
|
711
|
-
|
|
712
|
-
- **症状**:CHANGELOG 从自创格式(`## 0.8.8 — 日期`)改成 Keep a Changelog
|
|
713
|
-
(`## [0.8.8] - 日期`)之后,三个消费者**各自坏成了不同的样子**:
|
|
714
|
-
|
|
715
|
-
| 脚本 | 坏成什么样 |
|
|
716
|
-
|---|---|
|
|
717
|
-
| `bump-version.py` | 读不到真版本标题 ⇒ 往下匹配到正文里的一条小节标题 ⇒ 读出「CHANGELOG = 0.4.0」⇒ `version-sync` 红 |
|
|
718
|
-
| `doc-facts-gate.py` 的 `version_at()` | 恒返回 None ⇒ `if cur and ver != cur: continue` 对**每一行**都成立 ⇒ 整段 CHANGELOG 检查**静默空转**(判据在跑,什么都没查) |
|
|
719
|
-
| `doc-facts-gate.py` 的 F5 | 「版本条目必须带日期」这条**再也匹配不到任何行** ⇒ 静默失效 |
|
|
720
|
-
| `release-gate.py` 第 ① 条 | 第三份本地正则,**不要求日期** ⇒ 匹配到正文里任何 `x.y.z` 字样 |
|
|
721
|
-
|
|
722
|
-
- **根因**:**三份不同的正则做同一件事**(E31 已经写过一次,这是第三次发作)。
|
|
723
|
-
它们的"松紧"还各不相同:有要求日期的、有不要求的 ⇒
|
|
724
|
-
同一次格式变更让它们各自以不同方式失效,
|
|
725
|
-
**其中两种是"静默失去鉴别力"**(比报错更难发现)。
|
|
726
|
-
|
|
727
|
-
- **纠正规则**
|
|
728
|
-
1. **共享解析模块**:新建 `05-audit/_changelog.py`,
|
|
729
|
-
暴露 `headings()` / `latest_version()` / `HEADING` / `UNRELEASED`,
|
|
730
|
-
三处全部改为 import。判据只有一份,改格式只需改一处。
|
|
731
|
-
2. **零视野守卫**:任何"读某一类东西再核对"的判据,
|
|
732
|
-
必须断言**读到的数量 > 0**,否则报红并说明"视野已空"。
|
|
733
|
-
(本次两处静默失效都是靠这条才暴露的。)
|
|
734
|
-
3. ⚠️ **别让被测文本里出现能骗过解析器的字面量**:
|
|
735
|
-
我在 CHANGELOG 条目里写「往下扫到了正文里的 `## 0.4.0 的第二部分…`」,
|
|
736
|
-
结果把 `release-gate` 那条**不要求日期**的正则骗了(它读成 0.4.0)。
|
|
737
|
-
⇒ 说明里引用标题时不要照抄 `## x.y.z` 前缀。
|
|
738
|
-
(这条同时也是"解析器必须锚定语义、不能锚定字符"的实例。)
|
|
739
|
-
4. 新写解析器/量具时,**先想"它的输入变形状了会怎样"** ——
|
|
740
|
-
本次三处都是"输入形状变了但没人测过这个形状"。
|
|
741
|
-
|
|
742
|
-
---
|
|
743
|
-
|
|
744
|
-
## E44 · 门禁报的位置是"内部坐标",拿到提示的人定位不到 · 0.8.9
|
|
745
|
-
|
|
746
|
-
- **症状**:`tag-balance.py` 报
|
|
747
|
-
「期望 `</main>`(开在 L15),实际 `</div>`」——
|
|
748
|
-
而 L15 是 CSS 规则。**照这个提示去找,永远找不到问题**。
|
|
749
|
-
另一面:它只打消息、不打"在哪一行撞上的",所以连"出错点"也没有。
|
|
750
|
-
|
|
751
|
-
- **根因**:它先把 `<script>` / `<style>` 的内容和注释**挖空**(为了不被里面的
|
|
752
|
-
`<` 和标签字样干扰),但挖空时把**换行也一起吃了** ⇒
|
|
753
|
-
后面按 `body[:pos].count('\n')` 算出来的行号,是"挖空后文本"的坐标。
|
|
754
|
-
一个报不出**源文件坐标**的判据,等于让人重新找一遍。
|
|
755
|
-
|
|
756
|
-
- **纠正规则**
|
|
757
|
-
1. **挖空/剥除必须保留换行**:替换时只留 `\n`,行数不变。
|
|
758
|
-
并在函数里加断言 `挖空前后行数必须相等` —— 这是"行号可信"的唯一保证。
|
|
759
|
-
2. **报错要同时给出两样**:问题是什么(哪个标签没闭合)+ 在哪一行撞上的。
|
|
760
|
-
只给一半等于没给。
|
|
761
|
-
3. **修这类门禁的顺序**:先让它报得准(坐标对),再接进门禁。
|
|
762
|
-
接一个报不准的门禁进 CI,只会训练人忽略它。
|
|
763
|
-
|
|
764
|
-
---
|
|
765
|
-
|
|
766
|
-
## E45 · 顺序反了:注释里的标签字样把后面的真标签一起吞掉 · 0.8.9
|
|
767
|
-
|
|
768
|
-
- **症状**:`tag-balance.py` 对 `09-assets/model-viewer/demo.html` 报
|
|
769
|
-
「`<head>` 没有闭合(开在 L3)」,而 `</head>` 明明就在第 30 行。
|
|
770
|
-
**是一个指向错误位置的假红。**
|
|
771
|
-
|
|
772
|
-
- **根因**:`check()` 的顺序是「先挖空 `<script>`/`<style>`,再剥注释」。
|
|
773
|
-
该文件第 10-12 行的注释里有一句
|
|
774
|
-
`<!-- 注意:这里没有 <script src="three.min.js"> … -->` ——
|
|
775
|
-
它**没有闭合标签**。挖空用的非贪婪正则 `<script[^>]*>(.*?)</script>`
|
|
776
|
-
于是找到了**几十行之后**真正的 `</script>` ⇒ 中间的
|
|
777
|
-
`</head>` / `<body>` / `<main>` 全被当成 script 内容挖掉。
|
|
778
|
-
|
|
779
|
-
- **纠正规则**
|
|
780
|
-
1. **剥注释必须在挖空之前**:注释是"最外层的噪声",先去掉它,
|
|
781
|
-
后面的挖空/解析才不会被注释里的字样骗。
|
|
782
|
-
2. **凡"按标签名挖空"的写法,都要问一句"输入里有没有**不带闭合**的同名标签字样"**
|
|
783
|
-
(注释、字符串、示例代码里最常见)。
|
|
784
|
-
3. 反向控制里**必须包含这条形状**(注释里有未闭合的 `<script src=…>`),
|
|
785
|
-
否则下次重犯还是查不出来。
|
|
786
|
-
4. ⭐ **假红比假绿更容易误导人**:假绿让人以为"没问题",
|
|
787
|
-
假红让人**去改一个本来正确的地方**。两者都要当事故处理。
|
|
788
|
-
|
|
789
|
-
---
|
|
790
|
-
|
|
791
|
-
## E46 · 报告错位:结论打在中段、计数少算 · 0.8.9
|
|
792
|
-
|
|
793
|
-
- **症状**
|
|
794
|
-
1. `check-all.sh` 跑到中段就打「全部通过。」—— 而它后面还有约 20 道门禁。
|
|
795
|
-
日志里会出现「**全部通过。**」紧接着若干 `FAIL` 行。
|
|
796
|
-
读到"全部通过"的人不会往下看。
|
|
797
|
-
2. `MODE=fast` 末尾写「执行 125 道」,而实际是 142 道
|
|
798
|
-
(68 PASS + 2 FAIL + 72 SKIP)。
|
|
799
|
-
|
|
800
|
-
- **根因**
|
|
801
|
-
1. 结论块(原本就在中段)与 0.8.9 新增的计数行都插在那里,
|
|
802
|
-
而脚本一直跑到最后一行才 `exit $fail`。
|
|
803
|
-
2. 跳过分**两种来源**(分层跳过 / 环境缺失如缺 Pillow),
|
|
804
|
-
计数只加在分层那一处 ⇒ 另一种来源的跳过全没算。
|
|
805
|
-
|
|
806
|
-
- **纠正规则**
|
|
807
|
-
1. **结论必须放在脚本最后一行之前**。判断方法很机械:
|
|
808
|
-
`结论块的输出里不许出现"之后还有 run"`。
|
|
809
|
-
⇒ 0.8.9 已把结论块移到 `exit $fail` 前。
|
|
810
|
-
2. **计数放在"唯一的入口函数"里**:`skip()` 是所有跳过的必经之路,
|
|
811
|
-
把计数放进去,一处覆盖两种来源。
|
|
812
|
-
分散在各调用点 ⇒ 必然漏(实测漏 17 道)。
|
|
813
|
-
3. **汇总数字必须能与明细对得上**:
|
|
814
|
-
`执行 + 跳过` 应等于注册总数。对不上就是计数器错了。
|
|
815
|
-
这一条要写进输出旁边(本次就是这么发现的)。
|
|
816
|
-
4. ⚠️ **我修这条时自己引入了一次坏语法**:搬移块时漏掉了块尾的 `fi`
|
|
817
|
-
⇒ `bash -n` 直接报错。
|
|
818
|
-
⇒ 搬移代码块后**必须立刻 `bash -n`**(这次的幸运之处是它当场就炸了)。
|
|
819
|
-
|
|
820
|
-
## E47 · 用"安全上下文"测分享功能,把缺陷测成了通过 · 0.8.9 之后
|
|
821
|
-
|
|
822
|
-
**现象**:真机探针页的「复制结果」按钮,在手机上一点就弹
|
|
823
|
-
「复制失败,请手动长按上面的文本」。而我在交付前测过 —— **测出来是成功的**。
|
|
824
|
-
|
|
825
|
-
**根因**:`navigator.clipboard` 只在**安全上下文**里存在(`https` / `localhost` /
|
|
826
|
-
`file://`)。手机用 `http://<电脑内网地址>:8099` 打开时,这个对象是 `undefined`,
|
|
827
|
-
代码直接走到 `else { done(false) }`。
|
|
828
|
-
|
|
829
|
-
而我做验证时用的两个地址恰好都是安全上下文:
|
|
830
|
-
|
|
831
|
-
| 我测的 | `isSecureContext` | `navigator.clipboard` |
|
|
832
|
-
|---|---|---|
|
|
833
|
-
| `file:///…/capability-probe.html` | `true` | 存在 |
|
|
834
|
-
| `http://127.0.0.1:8000/…`(门禁里的桩) | `true` | 存在 |
|
|
835
|
-
| **`http://<电脑内网地址>:8099`(真实用法)** | **`false`** | **`undefined`** |
|
|
836
|
-
|
|
837
|
-
⇒ 量具的前提与产品的真实前提**不一样**,于是量出来的"成功"是假的。
|
|
838
|
-
|
|
839
|
-
**为什么门禁没拦住**:`webview-probe-check.js` 只判"页面能跑、15 项都给出结论、
|
|
840
|
-
JSON 能解析",**没有一条判据碰过按钮**。它验的是探针**测别人**的能力,
|
|
841
|
-
没验探针**自己能不能交作业**。
|
|
842
|
-
|
|
843
|
-
**纠正规则**
|
|
844
|
-
|
|
845
|
-
1. **验证"分享 / 导出 / 上传"这类功能,必须在产品真实的**上下文**里跑**:
|
|
846
|
-
手机走局域网就是 `http://192.168.x.x`(非安全上下文),
|
|
847
|
-
不要用 `file://` 或 `127.0.0.1` 代替 —— 那两个是安全上下文,会掩盖整整一类 API 不可用。
|
|
848
|
-
判断方法很机械:页面里 `window.isSecureContext` 打印出来,必须与你声称的场景一致。
|
|
849
|
-
2. **凡是 `navigator.*` 上只在安全上下文存在的 API**
|
|
850
|
-
(`clipboard` / `share` / `serviceWorker` / `geolocation` / `mediaDevices`),
|
|
851
|
-
都必须有**不依赖安全上下文的退路**,并且退路要**真的能完成这件事**:
|
|
852
|
-
- `document.execCommand('copy')`(老接口,非安全上下文仍可用)
|
|
853
|
-
- 同源 `fetch` 把数据送回服务端(最可靠,不碰剪贴板)
|
|
854
|
-
3. **失败提示必须给出可执行的下一步**,不能只说"请手动长按"。
|
|
855
|
-
长按只会选中一个词 —— 帮用户把目标文本**用 Range 全选**,
|
|
856
|
-
提示才配得上"可以长按复制"这句话。
|
|
857
|
-
4. **门禁要覆盖"这东西自己能不能用"**,不只是"它测出来的东西对不对"。
|
|
858
|
-
⇒ `webview-probe-gate.py` 已加判据 ⑥:凡用 `navigator.clipboard`,
|
|
859
|
-
必须同时存在 `document.execCommand('copy')` 或同源 `fetch` 退路;
|
|
860
|
-
反向控制把两处调用形式改掉,确认它会红。
|
|
861
|
-
|
|
862
|
-
**代价**:用户拿手机实测、发现失败、再反馈回来 —— 本来在我这边一次就能避免。
|
|
863
|
-
教训不是"测了就等于验了",而是**"在谁的环境里测的"本身就是判据的一部分**。
|
|
864
|
-
|
|
865
|
-
## E48 · 生成器写文件没指定 `newline`,和「行尾统一」直接打架 · D5 期间
|
|
866
|
-
|
|
867
|
-
**现象**:跑完 `theme-sync.py` 之后,`01-tokens/tokens.css` **整个文件变成 CRLF**
|
|
868
|
-
(832 行里 832 行带 `\r`);`make-tiersnippet.py` 同样,生成的
|
|
869
|
-
`04-recipe/tierA-tokens.css` 也是 CRLF。`git` 立刻给出警告:
|
|
870
|
-
|
|
871
|
-
warning: in the working copy of '01-tokens/tokens.css',
|
|
872
|
-
CRLF will be replaced by LF the next time Git touches it
|
|
873
|
-
|
|
874
|
-
**根因**:`io.open(path, 'w', encoding='utf-8')` 在 Windows 上默认 `newline=None`
|
|
875
|
-
⇒ 写入时把每个 `\n` 翻成 `\r\n`。而 D4 已经把全库统一成 LF,
|
|
876
|
-
两道工序互相拆台:**D4 手工统一一次,生成器每跑一次就翻回去**。
|
|
877
|
-
|
|
878
|
-
**为什么门禁没拦住**:`build-dist` 的"只改换行符不该报不新鲜"自检
|
|
879
|
-
(`归一化后比较`)只在**它自己那条链**上生效;没有任何门禁直接检查
|
|
880
|
-
"受版本控制的文本文件必须是 LF"。所以 tokens.css 被写成 CRLF 后,
|
|
881
|
-
只有 `git` 的警告能看出来 —— 而警告不会让任何东西变红。
|
|
882
|
-
|
|
883
|
-
**纠正规则**
|
|
884
|
-
|
|
885
|
-
1. **凡是写"进入版本库的产物"的 `open(..., 'w')`,一律显式 `newline='\n'`**。
|
|
886
|
-
临时文件、自检夹具可以不管;仓库里的产物必须管。
|
|
887
|
-
2. **行尾统一(D4)不是一次性的搬运,而是一条持续约束**。
|
|
888
|
-
做 D4 时只统一了当时存在的文件,没堵住"生成器会翻回去"这条路径 ——
|
|
889
|
-
这就是"修了症状没修机制"。
|
|
890
|
-
3. 判断方法很机械:`grep -rn "open([^)]*'w'" 05-audit/*.py | grep -v newline=`
|
|
891
|
-
—— 逐个看它写的是不是产物。
|
|
892
|
-
|
|
893
|
-
**已修**:`theme-sync.py`、`make-tiersnippet.py`。
|
|
894
|
-
|
|
895
|
-
---
|
|
896
|
-
|
|
897
|
-
## E49 · 正则扫 CSS 命中了注释里的示例,生成出 0 个令牌却报"一致" · D5 期间
|
|
898
|
-
|
|
899
|
-
**现象**:分层后跑 `theme-sync.py`,输出
|
|
900
|
-
|
|
901
|
-
✓ 已生成 `[data-theme="dark"]` 段(0 个令牌)
|
|
902
|
-
[OK] 暗色真值同步:`@media` 段 0 个令牌 ⇒ `[data-theme="dark"]` 段一致
|
|
903
|
-
|
|
904
|
-
—— 暗色段明摆着有 30 个令牌,它读出 0 个,而 `--check` **判它通过**。
|
|
905
|
-
|
|
906
|
-
**根因**:`RE_MEDIA_DARK` 是 `@media[^{]*prefers-color-scheme:\s*dark[^{]*\{`。
|
|
907
|
-
文件里有两处注释**举着**这个媒体查询当例子(一处是讲"为什么不用 `light-dark()`",
|
|
908
|
-
一处是我新写的 L1 层说明)。`search()` 先命中**注释里**那段,
|
|
909
|
-
再靠 `[^{]*` 一路吃到后面某个 `{` —— 那正好是一个空规则块。
|
|
910
|
-
|
|
911
|
-
于是 `find_block()` 拿到 5 个字符的空块 ⇒ `collect()` 读出 0 个令牌。
|
|
912
|
-
|
|
913
|
-
**最危险的地方不是读错,是判据跟着一起错**:
|
|
914
|
-
`check_css` 比对"@media 段 == 生成段",两边都是空,**0 == 0 ⇒ 通过**。
|
|
915
|
-
这类"输入集合塌缩成空集"的假绿,比直接报错难发现得多。
|
|
916
|
-
|
|
917
|
-
**纠正规则**
|
|
918
|
-
|
|
919
|
-
1. **在 CSS 上做定位之前,先把注释遮蔽掉**(`_css.mask_comments()`:
|
|
920
|
-
替换成等长空白、保留换行 ⇒ **偏移量与原文完全一致**,定位出来的下标
|
|
921
|
-
可以直接拿去切原文,注释也就不会丢)。
|
|
922
|
-
`strip_comments` 不能替代它 —— 它改变长度,之后所有下标都错位。
|
|
923
|
-
2. **判据里凡是"两个集合必须一致"的,必须同时断言集合非空**。
|
|
924
|
-
否则删掉全部内容也能让判据通过。
|
|
925
|
-
3. 写注释时**不要在注释里贴完整的选择器/at-rule 字面量**,或者贴了就必须
|
|
926
|
-
保证定位代码跳过注释。这一条不是"注释写得不好",是**定位代码的缺陷** ——
|
|
927
|
-
别人写注释时不该知道你的正则长什么样。
|
|
928
|
-
|
|
929
|
-
**已修**:`theme-sync.py`(5 处定位)、`layer-tokens.py`(2 处)、
|
|
930
|
-
`gen-ai-contract.py`(暗色段定位)、`token-tree-gate` 之外的探针也用上了。
|
|
931
|
-
|
|
932
|
-
---
|
|
933
|
-
|
|
934
|
-
## E50 · fixture 用字符串拼接做 `replace`,冒号后的对齐空格让它静默失效 · D5 期间
|
|
935
|
-
|
|
936
|
-
**现象**:新门禁 `layer-tokens.py` 的反向控制里,第 ① 条
|
|
937
|
-
「语义名直写颜色」期望红,实际绿。
|
|
938
|
-
|
|
939
|
-
**根因**:fixture 这么构造坏样本:
|
|
940
|
-
|
|
941
|
-
light.replace('%s: %s' % (name, value), '%s: #123456' % name, 1)
|
|
942
|
-
|
|
943
|
-
而原文是 `--paper: var(--raw-paper);`(冒号后 12 个空格,为了对齐)。
|
|
944
|
-
拼出来的 `--paper: var(--raw-paper)` **一个字符都对不上** ⇒
|
|
945
|
-
`replace` 什么也没改 ⇒ 传给判据的是原样的好样本 ⇒ 判据当然绿。
|
|
946
|
-
|
|
947
|
-
**这是本项目第 N 次踩同一个坑**(E31 的变体):`replace`/`find` 匹配不到时
|
|
948
|
-
**不报错,返回原串**。而"原串是好样本",于是"期望红 ⇒ 实际绿"。
|
|
949
|
-
|
|
950
|
-
**纠正规则**
|
|
951
|
-
|
|
952
|
-
1. **fixture 构造完之后必须断言"确实改了东西"**:
|
|
953
|
-
|
|
954
|
-
body = re.sub(..., light)
|
|
955
|
-
if body == light:
|
|
956
|
-
print('[FAIL] 自检样本失效:没能改动 XXX')
|
|
957
|
-
return 1
|
|
958
|
-
|
|
959
|
-
这一条比 fixture 本身写对更重要 —— 它把"静默失效"变成"响亮报错"。
|
|
960
|
-
2. **不要用拼接字符串去 `replace` 带对齐空白的源码**,用正则:
|
|
961
|
-
`re.sub(r'(\n\s*' + re.escape(name) + r'\s*:\s*)var\(...\)', r'\1#123456', ...)`。
|
|
962
|
-
3. 反向控制里凡"期望红却绿",**第一嫌疑是样本没构造对**,不是判据没牙。
|
|
963
|
-
本项目已经用这条经验抓到过三次真问题(fixture 写错伪装成判据失效)。
|
|
964
|
-
|
|
965
|
-
---
|
|
966
|
-
|
|
967
|
-
## E51 · 在 heredoc 里写 `\n`,被 shell 吃掉变成真换行 · 反复 5 次
|
|
968
|
-
|
|
969
|
-
**现象**:用 `python - <<'PY' ... PY` 生成一段含 `newline='\n'` 的代码,
|
|
970
|
-
写进文件后变成
|
|
971
|
-
|
|
972
|
-
with open(DST, 'w', encoding='utf-8', newline='
|
|
973
|
-
') as f:
|
|
974
|
-
|
|
975
|
-
⇒ `SyntaxError: unterminated string literal`。
|
|
976
|
-
|
|
977
|
-
**根因**:即使 heredoc 用引号(`<<'PY'`,不做变量展开),
|
|
978
|
-
**多行字符串里的转义序列在跨层传递时极容易被解释一次**。
|
|
979
|
-
这不是"这次不小心",是**这个通道本身不适合传带转义的代码**。
|
|
980
|
-
|
|
981
|
-
**纠正规则**
|
|
982
|
-
|
|
983
|
-
1. **要写含 `\n` / `\\` / 反引号的代码,不要用 heredoc 拼**。
|
|
984
|
-
要么用编辑器工具直接改,要么用**定位替换**:
|
|
985
|
-
|
|
986
|
-
start = s.index('锚点开头')
|
|
987
|
-
end = s.index('锚点结尾')
|
|
988
|
-
s = s[:start] + new + s[end:]
|
|
989
|
-
|
|
990
|
-
并且 `new` 里的反斜杠用 `chr(92)` 拼,不写 `'\\'`。
|
|
991
|
-
2. 已经写坏的文件,**不要试图再拼一次** —— 直接定位替换整段。
|
|
992
|
-
3. 教训的层级:这不是"手滑",是**工具选择错误**。
|
|
993
|
-
同一类操作在本次任务里重复 5 次,说明该换工具而不是"下次小心"。
|
|
994
|
-
|
|
995
|
-
## E52 · 刚登记完 E51 就再犯两次 —— 说明"提醒"没用,必须换工具 · D5 收尾
|
|
996
|
-
|
|
997
|
-
**现象**:写完 E51(heredoc 里写转义会被吃)之后不到半小时,同一个通道又炸两次:
|
|
998
|
-
|
|
999
|
-
| 次 | 写什么 | 结果 |
|
|
1000
|
-
|---|---|---|
|
|
1001
|
-
| 6 | `newline='
|
|
1002
|
-
'`(三处) | 变成 `newline='` + 真换行 + `'` ⇒ `SyntaxError: unterminated string literal` |
|
|
1003
|
-
| 7 | 注释里的 `\x00` | 写成**真的 NUL 字节** ⇒ `SyntaxError: source code cannot contain null bytes` |
|
|
1004
|
-
|
|
1005
|
-
第 7 次尤其难看:文件里出现 0x00,`ast.parse` 直接拒绝整个文件,
|
|
1006
|
-
而 `grep` 把它当二进制、静默不显示 —— 排查成本比前六次都高。
|
|
1007
|
-
|
|
1008
|
-
**根因不是"手滑"**:`python - <<'PY' ... PY` 这个通道在**跨层传递**时
|
|
1009
|
-
(shell → heredoc → Python 字符串字面量 → 目标文件)对反斜杠做了至少一次
|
|
1010
|
-
多余的解释。**通道本身不可靠**,与操作者是否小心无关。
|
|
1011
|
-
|
|
1012
|
-
**证据就是时间线**:E51 写完之后我立刻又用它,两次全炸。
|
|
1013
|
-
⇒ "下次注意"这个策略在同一个任务里已经被证伪了。
|
|
1014
|
-
|
|
1015
|
-
**纠正规则(流程级,不是提醒级)**
|
|
1016
|
-
|
|
1017
|
-
1. **凡是"要写进文件的内容包含反斜杠",一律不走 heredoc**。可用通道只有两个:
|
|
1018
|
-
- 编辑器工具(`Write` / `Edit`)—— 首选
|
|
1019
|
-
- **定位替换**:`s.index(锚点头)` / `s.index(锚点尾)` 切开,中间整段换掉;
|
|
1020
|
-
必须拼反斜杠时用 `chr(92)` / `chr(10)` / `chr(0)`,不写 `'
|
|
1021
|
-
'` 字面量
|
|
1022
|
-
2. **写完立刻 `ast.parse` 验证语法**,并且额外查一次 NUL:
|
|
1023
|
-
`open(p,'rb').read().count(b'\x00')` 必须是 0。
|
|
1024
|
-
语法检查能抓到第 6 次,抓不到"注释里混进 NUL 但语法仍合法"的情况。
|
|
1025
|
-
3. **一次事故记一条规则不够,要记到"换掉通道"这一层**。
|
|
1026
|
-
E51 记的是"别用 heredoc 写转义",但没写"**用什么替代**",
|
|
1027
|
-
于是下一次仍然回到原通道 —— 这是本条存在的理由。
|
|
1028
|
-
|
|
1029
|
-
## E53 · `npm publish --dry-run` 污染了子进程的环境变量 · 0.9.0 发布时
|
|
1030
|
-
|
|
1031
|
-
**现象**:`npm publish --dry-run` 报
|
|
1032
|
-
|
|
1033
|
-
FAIL npm pack 没产出 /tmp/tmp.xxxx/agentcss-0.9.0.tgz
|
|
1034
|
-
|
|
1035
|
-
但同一条命令 `bash 05-audit/pack-smoke.sh` **单独跑是好的**。
|
|
1036
|
-
|
|
1037
|
-
**根因**:`npm publish --dry-run` 把 `--dry-run` 写进了**环境变量**
|
|
1038
|
-
`npm_config_dry_run=true`,所有子进程继承它:
|
|
1039
|
-
|
|
1040
|
-
npm publish --dry-run
|
|
1041
|
-
└─ prepublishOnly → npm run publish:check
|
|
1042
|
-
└─ bash 05-audit/pack-smoke.sh
|
|
1043
|
-
└─ npm pack … ← 继承了 dry_run ⇒ 只打印,不产出文件
|
|
1044
|
-
|
|
1045
|
-
⇒ `pack-smoke` 里"检查 tarball 存在"的那一条必然失败。
|
|
1046
|
-
|
|
1047
|
-
**为什么这条特别值得记**
|
|
1048
|
-
|
|
1049
|
-
它属于「**只在验证路径上失败**」的一类:
|
|
1050
|
-
|
|
1051
|
-
- 真实 `npm publish` 不设这个变量 ⇒ 真跑没事
|
|
1052
|
-
- 只有 `--dry-run` 会撞上
|
|
1053
|
-
- 而 `--dry-run` 恰恰是**发布前唯一的全链路预演**
|
|
1054
|
-
|
|
1055
|
-
结果就是:预演必红 ⇒ 人不再相信预演 ⇒ 干脆跳过预演直接发。
|
|
1056
|
-
**一个只在验证时失败的门禁,比没有门禁更危险** —— 它教人绕过验证。
|
|
1057
|
-
|
|
1058
|
-
**纠正规则**
|
|
1059
|
-
|
|
1060
|
-
1. **凡是调 `npm` / `git` / 其它会读 `npm_config_*` 的工具,进来先 `unset`
|
|
1061
|
-
已知的污染变量**。`pack-smoke.sh` 现在开头 `unset npm_config_dry_run`。
|
|
1062
|
-
2. **验证一条命令时,要在"它将来真正被调用的那个上下文"里验证**。
|
|
1063
|
-
我单独跑 `pack-smoke.sh` 是通过的,因为我在干净的 shell 里跑;
|
|
1064
|
-
而它实际的调用者是 `npm run`(带着 `npm_config_dry_run`)。
|
|
1065
|
-
—— 这和 E47(用安全上下文测非安全上下文)是同一个病:
|
|
1066
|
-
**测的环境不等于跑的环境**。
|
|
1067
|
-
3. 判断方法很机械:`env | grep npm_config_` 看当前继承了哪些。
|
|
1068
|
-
|
|
1069
|
-
## E55 · 把已经发布成功的包删掉了 —— 在"后台处理中"就下结论 · 0.9.0 发布当天
|
|
1070
|
-
|
|
1071
|
-
**发生了什么**
|
|
1072
|
-
|
|
1073
|
-
`npm publish` 返回 `RC=0` 且打印 `+ agentcss@0.9.0`,但随后 `curl` 查 registry 只看到
|
|
1074
|
-
`versions: ['0.0.0-stage']`。我据此判定"发布没成功",去网页点了 **Delete package**
|
|
1075
|
-
(确认框里手打 `agentcss`),删除请求发出后页面刷新,才显示:
|
|
1076
|
-
|
|
1077
|
-
(维护者账号)published 0.9.0 · 2 minutes ago
|
|
1078
|
-
|
|
1079
|
-
**包其实已经发布成功了,是我把它删掉了。**
|
|
1080
|
-
|
|
1081
|
-
随后重发被拒:
|
|
1082
|
-
|
|
1083
|
-
403 agentcss cannot be republished until 24 hours have passed.
|
|
1084
|
-
|
|
1085
|
-
**根因(两层)**
|
|
1086
|
-
|
|
1087
|
-
① **时间竞态**:npm 的提示写得很清楚 ——
|
|
1088
|
-
`Your package is being processed and may take a few minutes to become available.`
|
|
1089
|
-
我只等了 20 秒就去查,而 registry 在那段时间里**仍返回旧的 `0.0.0-stage` 占位**。
|
|
1090
|
-
`0.0.0-stage` 是更早一次失败 publish 留下的占位包,它的存在**不说明新版本没传上去**。
|
|
1091
|
-
|
|
1092
|
-
② **对不可逆操作没有二次确认的"事实依据"**:
|
|
1093
|
-
删除前我的唯一依据是"registry 里没有 0.9.0",而这个依据本身是**时效性不明的读数**。
|
|
1094
|
-
**拿一个可能过期的观测去支撑不可逆动作,是这一类错误的共同结构。**
|
|
1095
|
-
|
|
1096
|
-
**纠正规则**
|
|
1097
|
-
|
|
1098
|
-
1. **"发布成功"的唯一判据不是 CLI 返回码,也不是立刻查 registry**,而是:
|
|
1099
|
-
- `npm view <pkg>@<version> version`(带版本查,不是查 latest)
|
|
1100
|
-
- **并且**等到 CLI 提示的 "few minutes" 真的过去(至少 60–120 秒)
|
|
1101
|
-
- 期间不改动任何东西
|
|
1102
|
-
2. **任何 `Delete` / `Unpublish` / `--force` 之前,必须换一条独立证据链再确认一次**
|
|
1103
|
-
(例如:网页 Packages 列表里的 "published X.Y.Z · N minutes ago",
|
|
1104
|
-
或 `npm view pkg time`)——**不能只用同一个读数**。
|
|
1105
|
-
3. **不可逆操作前先做可逆的事**:这次如果先做的只是"再等 2 分钟再查",
|
|
1106
|
-
就不会有损失。**先等,再判断,最后才动手。**
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
## E56 · 门禁的"前提"交给调用者决定 —— 于是 fast 档把它读成了代码坏了 · 0.11.1 开发期
|
|
1110
|
-
|
|
1111
|
-
**发生了什么**
|
|
1112
|
-
|
|
1113
|
-
`MODE=fast` 全跑,`selftest-all` 红:
|
|
1114
|
-
|
|
1115
|
-
[FAIL] 跑不起来:net::ERR_CONNECTION_REFUSED
|
|
1116
|
-
at http://127.0.0.1:8000/03-patterns/reveal/demo.html
|
|
1117
|
-
|
|
1118
|
-
追下去:`selftest-all` 会跑 `motion-concurrency.js --selftest`,而那一条要连
|
|
1119
|
-
`127.0.0.1:8000`。但 fast 档在 `check-all.sh` 里被**明确设计为不起服务**
|
|
1120
|
-
(理由正当:不抢用户本地 dev server 的端口、保持"秒级档")。
|
|
1121
|
-
⇒ 同一道门禁在 full 档绿、在 fast 档红,**红的原因和它要检查的东西毫无关系**。
|
|
1122
|
-
|
|
1123
|
-
**根因**
|
|
1124
|
-
|
|
1125
|
-
判据的**前提**(这里是一个同源静态服务)被留给了调用方,
|
|
1126
|
-
于是结果由"今天谁先跑、用什么模式跑"决定。这违反本库自己的
|
|
1127
|
-
交接纪律第 8 条:「判据的前提要自己造,不能让磁盘今天的状态决定自检结果」。
|
|
1128
|
-
|
|
1129
|
-
更危险的是它的**处置诱惑**:把这条自检塞进 `SKIP` 就"好了"——
|
|
1130
|
-
那是用"没人知道"换"没人看见"。
|
|
1131
|
-
|
|
1132
|
-
**纠正规则**
|
|
1133
|
-
|
|
1134
|
-
1. **判据需要外部前提时,判据自己建立它**,并在结束时还原(`selftest-all`
|
|
1135
|
-
现在自己起 `:8000`、退出时关掉)。
|
|
1136
|
-
2. **复用别人的前提必须先验真身**:只看"8000 通"会把**另一棵树**当真
|
|
1137
|
-
(`check-all.sh` 已被这件事坑过一次)⇒ 必须做 **canary 字节比对**。
|
|
1138
|
-
3. **"跑不起来" 与 "检查出问题" 必须分开报**,且都不能算通过:
|
|
1139
|
-
⇒ 新增判据 ④ —— **`RC=0` 但输出里有「跑不起来」的,判红**。
|
|
1140
|
-
`RC=0` 只说明进程正常结束,不说明检查真的做了。
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
## E57 · 注释里写死"当前有 N 个" —— 它比文档漂得更快,且没有判据守 · 0.11.1 开发期
|
|
1144
|
-
|
|
1145
|
-
**发生了什么**
|
|
1146
|
-
|
|
1147
|
-
同一轮里撞到两处,都是**代码/文档的注释在给数量下结论**:
|
|
1148
|
-
|
|
1149
|
-
| 位置 | 注释写的 | 当时真值 |
|
|
1150
|
-
|---|---|---|
|
|
1151
|
-
| `05-audit/selftest-all.py` 的 `SKIP` 上方 | 「当前为空 —— 26 个全都跑得动」 | 51 个自检 + 1 条跳过 |
|
|
1152
|
-
| `CONTRIBUTING.md` 第 25 行标题 | 「为什么门禁是 159 道而不是 5 道」 | 173 道 |
|
|
1153
|
-
|
|
1154
|
-
第一处还**顺带把跳过理由写错了**("本脚本的环境里没有 :8000 服务"——
|
|
1155
|
-
修完 E56 之后这句已经不成立)。留着它,下一个人会以为环境有洞、再"修"一遍。
|
|
1156
|
-
|
|
1157
|
-
**根因**
|
|
1158
|
-
|
|
1159
|
-
`doc-facts` 能守的是**它认得的那几种句式**(声明总数的"NNN 道门禁")。
|
|
1160
|
-
散文标题、代码注释里的"26 个""为空"它守不住。
|
|
1161
|
-
⇒ 这不是判据不够多,是**数字被写在了判据够不着的地方**。
|
|
1162
|
-
|
|
1163
|
-
**纠正规则**
|
|
1164
|
-
|
|
1165
|
-
1. **注释与散文里不写会变的数量**。要写就写"由运行时打印"或"见某处的真值",
|
|
1166
|
-
让数字只有**一个**出处(I-7 单一真源)。
|
|
1167
|
-
2. 想强调"现在很少/很多"时,用**不带数字的说法**("一百多道"比"159 道"安全)。
|
|
1168
|
-
3. **改完实现后回头搜一遍自己刚写下的说明**:
|
|
1169
|
-
`git diff` 里凡是出现数字的注释,都要问一句"这个数会不会变、谁守它"。
|
|
1170
|
-
4. 判据**不要**为了覆盖这类而扩宽正则:实测全库有 6 处"NNN 道",
|
|
1171
|
-
其中 4 处是历史事故记录(本来就是旧数字,改了才是篡改)。
|
|
1172
|
-
**扩宽只会误伤历史叙述** —— 判据咬错人比不咬更坏。
|
|
1173
|
-
|
|
1174
|
-
## E58 · 把内部产品代号写进了公开仓库的示例页 · 0.11.2 开发期
|
|
1175
|
-
|
|
1176
|
-
**发生了什么**
|
|
1177
|
-
|
|
1178
|
-
新写 `examples/finance-report/index.html` 时,顶部品牌位直接用了内部产品名
|
|
1179
|
-
(就是 `terms.py` 的 `HARD_BAN` 词表里屏蔽的那个)。写完没跑 leak,
|
|
1180
|
-
直到整套门禁跑到 `leak` 才被抓出来:
|
|
1181
|
-
|
|
1182
|
-
```
|
|
1183
|
-
X 内部代号 1 处 / 1 个文件
|
|
1184
|
-
examples/finance-report/index.html x1
|
|
1185
|
-
```
|
|
1186
|
-
|
|
1187
|
-
**根因**
|
|
1188
|
-
|
|
1189
|
-
写这个页面时脑子里想的是"我们自己的产品页长什么样",
|
|
1190
|
-
于是**把它当成内部页面在写**,忘了这个仓库是**公开的**、示例是给外人看的。
|
|
1191
|
-
门禁词表一直在,问题是我没有在"写下那个词"的当下想到它。
|
|
1192
|
-
|
|
1193
|
-
**为什么这条值得记**
|
|
1194
|
-
|
|
1195
|
-
它不是"不小心泄露了密钥"那种一眼可见的事故 —— 它看起来只是一个品牌名,
|
|
1196
|
-
放进示例页甚至显得挺自然。**越是看起来自然的词,越容易漏。**
|
|
1197
|
-
|
|
1198
|
-
**纠正规则**
|
|
1199
|
-
|
|
1200
|
-
1. **公开仓库里的示例内容一律用中性/虚构名。** 判断标准不是"这个词敏感吗",
|
|
1201
|
-
而是"这个词对外人是否等于内部信息"——产品名、代号、客户名、内部系统名都算。
|
|
1202
|
-
2. **新文件写完,先单独跑一次 `leak-scan`**,不要等整套门禁(三分钟)。
|
|
1203
|
-
一次 1 秒的检查,能省掉一轮 40 分钟的往返。
|
|
1204
|
-
3. 台账里记录这类事故时**也不要再写那个词**(本文件自己也是被扫的对象)。
|
|
1205
|
-
写"词表里屏蔽的那个"就够了 —— 门禁不能成为泄漏源,这条在本库是既有的。
|
|
1206
|
-
|
|
1207
|
-
---
|
|
1208
|
-
|
|
1209
|
-
## E59 · 给 SVG 传了"设计宽度",没量容器 —— 于是"自适应"把字缩成 6px · 0.11.2 开发期
|
|
1210
|
-
|
|
1211
|
-
**发生了什么**
|
|
1212
|
-
|
|
1213
|
-
示例页第一版的正文列是 420px,而 `Bar` 按 640 设计、`scientific-plot` 按 720 设计。
|
|
1214
|
-
两者都带 `viewBox` + CSS `width:100%` ⇒ 整张图连同 **SVG 里的绝对字号**
|
|
1215
|
-
一起按 0.6 倍缩放。实测:设计 12px 的刻度字,渲染结果是 `4×7` 像素;
|
|
1216
|
-
移动端 393 视口下柱状图的字更是 `3×6` 像素 —— 等于没有。
|
|
1217
|
-
|
|
1218
|
-
而当时 `bar.js` 的注释是这么写的:
|
|
1219
|
-
|
|
1220
|
-
```js
|
|
1221
|
-
var W = 640; /* viewBox 宽;靠 CSS width:100% 自适应 */
|
|
1222
|
-
```
|
|
1223
|
-
|
|
1224
|
-
**根因(两层)**
|
|
1225
|
-
|
|
1226
|
-
1. **概念错**:`width:100%` 对带 `viewBox` 的 SVG 是**等比缩放**指令,
|
|
1227
|
-
不是"重排"。设计者把它当成了响应式布局,于是把宽度写死了。
|
|
1228
|
-
2. **流程错**:我写页面时按"设计宽度"传参(720/640),没有量容器。
|
|
1229
|
-
第一版截图上我甚至觉得"图有点小",但没有去读数 —— **凭观感判断可读性**。
|
|
1230
|
-
|
|
1231
|
-
**纠正规则**
|
|
1232
|
-
|
|
1233
|
-
1. **SVG 里只要有一个绝对字号,`width:100%` 就等于缩放指令。**
|
|
1234
|
-
容器可能比设计窄时,必须把**实测宽度**传进去(`host.clientWidth`)。
|
|
1235
|
-
2. **组件的 API 缺口要当成缺陷修,不是当成用法问题绕过。**
|
|
1236
|
-
`Bar` 只收 `height` 不收 `width`,这是 API 少了一个参数,不是"调用方的责任"。
|
|
1237
|
-
3. **判断"图能不能读"的唯一标准是实测像素**,不是"看起来还行":
|
|
1238
|
-
`getBoundingClientRect()` 量一个文字节点,宽度 < 2 位数字的合理值就是坏。
|
|
1239
|
-
4. 高度也要跟着宽度走。只改宽度不改高度 ⇒ 窄屏上图变成方块,
|
|
1240
|
-
同一张图在两个断点上看起来不是同一张(宽高比 2.9 → 1.6)。
|
|
1241
|
-
|
|
1242
|
-
---
|
|
1243
|
-
|
|
1244
|
-
## E60 · 示例页的 33 条勾稽错了 6 条,而所有门禁都是绿的 · 0.11.2 开发期
|
|
1245
|
-
|
|
1246
|
-
**发生了什么**
|
|
1247
|
-
|
|
1248
|
-
财务示例页第一次交付时,我逐块截图自检、跑了 174 道全量门禁、像素基线一致,
|
|
1249
|
-
然后报告"页面已完成"。但那张**表里的数加不上**:
|
|
1250
|
-
|
|
1251
|
-
| 错在哪 | 差多少 |
|
|
1252
|
-
|---|---|
|
|
1253
|
-
| 利润表:收入 − 六项成本费用 ≠ 营业利润(中间科目被省掉) | 79.4 亿元 |
|
|
1254
|
-
| 资产负债表:负债 + 归母权益 ≠ 资产总计(少了少数股东权益行) | 49.5 亿元 |
|
|
1255
|
-
| 五年表研发费用 2.1 亿 vs 同一页利润表 12.1 亿 | 6 倍 |
|
|
1256
|
-
| KPI 的 ROE 34.6% 与同一页资产负债表的权益对不上 | 8.4pp |
|
|
1257
|
-
| 市盈率 22.4 / 市净率 7.8 / 股息率 2.9% 三数互斥 | — |
|
|
1258
|
-
| 摘要「251 亿元来自销量」在分部数据里找不到出处 | — |
|
|
1259
|
-
|
|
1260
|
-
这些数**全部通过了** HTML 校验、对比度门禁、体积门禁、视觉基线比对。
|
|
1261
|
-
因为没有任何一道门禁在看"数与数之间对不对"。
|
|
1262
|
-
|
|
1263
|
-
**根因(两层)**
|
|
1264
|
-
|
|
1265
|
-
1. **概念错**:我把"示例数据"理解成"数字可以是编的",于是每个数字单独看都
|
|
1266
|
-
合理,但没有一个是被**算出来**的。编出来的数天然不勾稽 —— 它们各自成立,
|
|
1267
|
-
彼此不成立。
|
|
1268
|
-
2. **流程错**:自检时我量了像素、量了溢出、量了断点,**唯独没有拿计算器**。
|
|
1269
|
-
而我服务的读者(做财务数据的人)看这一页的第一件事,就是拿计算器加一遍。
|
|
1270
|
-
|
|
1271
|
-
**为什么这条最值得记**
|
|
1272
|
-
|
|
1273
|
-
它是"看起来对"的最高级形态:版面漂亮、数字量级正确、门禁全绿、报告里
|
|
1274
|
-
写着"页面已完成"。**唯一的缺陷是这张表不能信** —— 而这一页存在的全部意义
|
|
1275
|
-
就是证明"库能拼出可信的一页"。
|
|
1276
|
-
|
|
1277
|
-
**纠正规则**
|
|
1278
|
-
|
|
1279
|
-
1. **示例页里的每个数,要么是算出来的,要么旁边注明"未核对"。**
|
|
1280
|
-
不许出现"看起来合理但没算过"的数。
|
|
1281
|
-
2. **财务类页面的自检清单里,第一条是勾稽,不是像素。**
|
|
1282
|
-
顺序:数对不对 → 结构对不对 → 读得清不清 → 好看不好看。
|
|
1283
|
-
我原来的顺序整个是反的。
|
|
1284
|
-
3. **能机械判定的正确性,必须写成门禁。**
|
|
1285
|
-
这一轮补了 `05-audit/finance-example-ties.py`(30 条判据),
|
|
1286
|
-
并且用 8 个变异探针确认它真的会红 —— 否则只是多了一个永远绿的装饰。
|
|
1287
|
-
4. **缺少的行本身就是错误**,不是"表格简化"。
|
|
1288
|
-
BS 少一行少数股东权益、IS 少一行其他项目净额,读者加不上就会停止相信
|
|
1289
|
-
整页,**而且他不知道是哪一处的错**。
|
|
1290
|
-
|
|
1291
|
-
---
|
|
1292
|
-
|
|
1293
|
-
## E61 · 加了第二个 JS 之后,校验它的那道门禁自己静默失效了 · 0.11.2 开发期
|
|
1294
|
-
|
|
1295
|
-
**发生了什么**
|
|
1296
|
-
|
|
1297
|
-
给 `03-patterns/nav/` 加了 `nav.js`(行为层)之后,`fix-start-here.py` 报错:
|
|
1298
|
-
|
|
1299
|
-
```
|
|
1300
|
-
nav:登记的是 toc.js,目录里实际是 nav.js ⇒ 改名后没同步
|
|
1301
|
-
```
|
|
1302
|
-
|
|
1303
|
-
这句话**是错的**。真实原因是它取 JS 的写法:
|
|
1304
|
-
|
|
1305
|
-
```python
|
|
1306
|
-
cands = [f for f in os.listdir(d) if f.endswith('.js')]
|
|
1307
|
-
if len(cands) == 1: # ← 目录里只有一个 .js 才认
|
|
1308
|
-
...
|
|
1309
|
-
else:
|
|
1310
|
-
n_js, js_name = 0, None # ← 两个文件 = 放弃
|
|
1311
|
-
```
|
|
1312
|
-
|
|
1313
|
-
两个文件时它退化成"没有 JS",于是:
|
|
1314
|
-
- START-HERE 的表格这一栏显示「—」(读者以为不用抄 JS)
|
|
1315
|
-
- `check_js_classification()` 第一行 `if not js_name: continue` —— **直接跳过**
|
|
1316
|
-
|
|
1317
|
-
**真正的问题不是表格显示错了,是那道门禁从这一刻起什么都没查。**
|
|
1318
|
-
它会一直打印"全部通过",而实际检查数是 0。这比它要防的 bug 隐蔽得多:
|
|
1319
|
-
门禁失效的样子和门禁通过的样子**一模一样**。
|
|
1320
|
-
|
|
1321
|
-
**根因**
|
|
1322
|
-
|
|
1323
|
-
"一个组件只有一个 JS"这个假设,从来没被写下来过,也从来没被质疑过 ——
|
|
1324
|
-
它藏在 `if len(cands) == 1` 这一行里,而这一行**不是判据,是退化路径**。
|
|
1325
|
-
`ai/components.json` 里 `"js": "<单个路径>"` 的 schema 是同一个假设的另一半。
|
|
1326
|
-
|
|
1327
|
-
**纠正规则**
|
|
1328
|
-
|
|
1329
|
-
1. **凡是"取唯一一个"的代码,都要问一句"有两个的时候呢"。**
|
|
1330
|
-
这类退化路径不会报错,只会安静地退回默认值。
|
|
1331
|
-
2. **门禁的"检查数"必须能被看到。** 一条扫 0 个对象还报 OK 的门禁,
|
|
1332
|
-
和一条扫 35 个对象报 OK 的门禁,输出应该长得**不一样**。
|
|
1333
|
-
⇒ 这条已改进:分类校验现在逐**文件**核对,漏登记 / 陈旧登记都会列出来。
|
|
1334
|
-
3. **schema 也是判据。** `"js": "单个路径"` 逼着实现只能报一个文件,
|
|
1335
|
-
于是第二个文件在 AI 契约里彻底消失。补 `files.jsAlso` 是对的,
|
|
1336
|
-
但更好的问法是"为什么当初假定只有一个"。
|
|
1337
|
-
|
|
1338
|
-
---
|
|
1339
|
-
|
|
1340
|
-
## E62 · 宽通道门禁的第一版判据,把正确实现报成了失败 · 0.11.2 开发期
|
|
1341
|
-
|
|
1342
|
-
**发生了什么**
|
|
1343
|
-
|
|
1344
|
-
新写的 `rep-wide-channel.js` 第一版跑出来 4 条 FAIL,其中 3 条是**我的判据错**:
|
|
1345
|
-
|
|
1346
|
-
| 判据 | 实际 | 谁错 |
|
|
1347
|
-
|---|---|---|
|
|
1348
|
-
| 「正文块比正文列窄 40px 以上」 | report demo 里 34em(=544px) **恰好等于**列宽 | 判据错 |
|
|
1349
|
-
| 「图内文字宽度 > 18px」 | 单个数字「0」在 10px 字号下宽 6px,**正常** | 判据错 |
|
|
1350
|
-
| 「带 --wide 的图比正文宽 40px」 | 列本来就≈行长,没有可去的空间 | 判据错 |
|
|
1351
|
-
|
|
1352
|
-
真正被缩掉的字表现为**高度**只有 6px(实测过 3×6),不是宽度小。
|
|
1353
|
-
|
|
1354
|
-
**为什么这条值得记**
|
|
1355
|
-
|
|
1356
|
-
判据写错比没有判据更糟:它会**逼着人去改对的东西**。
|
|
1357
|
-
如果当时我信了这 3 条 FAIL,就会去动 `--rep-measure`(把行长改宽)——
|
|
1358
|
-
那正是这一轮刚刚推翻的错误做法。
|
|
1359
|
-
|
|
1360
|
-
**纠正规则**
|
|
1361
|
-
|
|
1362
|
-
1. **判据要表达"约束",不是表达"我看到的值"。**
|
|
1363
|
-
「正文不超过列」是约束(任何 profile 都成立);
|
|
1364
|
-
「正文比列窄 40px」是我在一页上看到的数字(换个 profile 就假红)。
|
|
1365
|
-
2. **写 FAIL 之前先问:是页面不对,还是我的期望不对。**
|
|
1366
|
-
这一轮 4 条 FAIL 里有 3 条是我的期望不对。
|
|
1367
|
-
3. **量"有没有被缩放"要看高度**:等比缩放同时改变宽高,而文字宽度本身
|
|
1368
|
-
与字符数强相关 —— 用宽度会误判单字符标签。
|
|
1369
|
-
|
|
1370
|
-
---
|
|
1371
|
-
|
|
1372
|
-
## E63 · 契约只记"一个全局",于是第二个全局在量具眼里不存在 · 0.11.2 开发期
|
|
1373
|
-
|
|
1374
|
-
**发生了什么**
|
|
1375
|
-
|
|
1376
|
-
`nav/` 目录里现在有两个脚本:`nav.js` 导出 `Nav`、`toc.js` 导出 `Toc`。
|
|
1377
|
-
`ai/components.json` 每个组件只有一个 `global` 字段 ⇒ 生成器记下 `Nav`,
|
|
1378
|
-
`Toc` 从契约里消失。
|
|
1379
|
-
|
|
1380
|
-
于是 `autoinit-gate` 报:
|
|
1381
|
-
|
|
1382
|
-
```
|
|
1383
|
-
③ 表里出现了库里没有的全局 `Toc`
|
|
1384
|
-
③ 全局 `Nav` 没进「自动初始化」表
|
|
1385
|
-
```
|
|
1386
|
-
|
|
1387
|
-
第一句是**误报**:`Toc` 确实在库里(`03-patterns/nav/toc.js` 最后一行),
|
|
1388
|
-
API.md 那张表是对的。错的是契约。
|
|
1389
|
-
|
|
1390
|
-
**为什么这条值得记**
|
|
1391
|
-
|
|
1392
|
-
修这条有两个方向,长得都很像"修 bug":
|
|
1393
|
-
|
|
1394
|
-
| 修法 | 结果 |
|
|
1395
|
-
|---|---|
|
|
1396
|
-
| 把 `Toc` 那行从 API.md 删掉 | 门禁绿了,文档少了一个真实存在的全局 |
|
|
1397
|
-
| 让契约能表达"一个目录多个全局" | 门禁绿了,文档仍然完整 |
|
|
1398
|
-
|
|
1399
|
-
第一种是**拿真实信息换绿灯**,而且是门禁自己"建议"的——它说"要么名字写错,
|
|
1400
|
-
要么组件已删",两个选项都是错的(名字没写错、组件没删)。当门禁的暗示把你
|
|
1401
|
-
往删信息的方向推时,几乎一定是门禁的模型窄了。
|
|
1402
|
-
|
|
1403
|
-
**纠正规则**
|
|
1404
|
-
|
|
1405
|
-
1. **schema 的字段数就是判据的视野。** 一个字段容不下的事实,等于不存在。
|
|
1406
|
-
(同型:E61 的 `"js": "单个路径"`。)
|
|
1407
|
-
2. **误报里如果带着"删除某个事实"的暗示,先怀疑量具。**
|
|
1408
|
-
3. 补字段时**从源码推**(`globalAlso` 是用抓 `window.X =` 的同一个正则推出来的),
|
|
1409
|
-
不要手写一张平行表 —— 平行表迟早和源码分叉。
|
|
1410
|
-
|
|
1411
|
-
---
|
|
1412
|
-
|
|
1413
|
-
## E64 · 自测红了,但红的是"判据空转" —— 变异体必须断言红的**理由** · 0.11.2 开发期
|
|
1414
|
-
|
|
1415
|
-
**发生了什么**
|
|
1416
|
-
|
|
1417
|
-
给 `autoinit-gate` 补两个样本("同目录第二个全局"漏登记 ⇒ 红 / 登记完整 ⇒ 绿)。
|
|
1418
|
-
第一个样本红了,判据"通过"。第二个样本也红了 —— 期望是绿。
|
|
1419
|
-
|
|
1420
|
-
查下来:这两个样本里**一个含 `DOMContentLoaded` 的脚本都没有**,
|
|
1421
|
-
门禁第一句就报 `判据空转(一个含 DOMContentLoaded 的脚本都没扫到)`。
|
|
1422
|
-
所以第一个样本"红"是真的,但**红错了理由**——它根本没验到"漏登记"。
|
|
1423
|
-
|
|
1424
|
-
**为什么这条值得记**
|
|
1425
|
-
|
|
1426
|
-
如果我只跑第一个样本、看到红就收工,新增的 `globalAlso` 路径**一条都没被验过**,
|
|
1427
|
-
而自测会打印"9 个样本全部判对"。这正是本库反复记的那句话:
|
|
1428
|
-
门禁失效的样子和门禁通过的样子一模一样。这次是**变异体失效**的样子
|
|
1429
|
-
和变异体生效的样子一模一样。
|
|
1430
|
-
|
|
1431
|
-
**纠正规则**
|
|
1432
|
-
|
|
1433
|
-
1. **变异体只断言"红"是不够的,必须断言"为什么红"。**
|
|
1434
|
-
⇒ 现在样本的第 4 项是可选的关键词,命中才算通过(本例断言 `没进`)。
|
|
1435
|
-
2. 造样本时要保证**被测判据有东西可查**:目标判据之外的前置条件
|
|
1436
|
-
(这里是一份会自动初始化的脚本)也得搭出来。
|
|
1437
|
-
3. 补一个"绿的样本"比补十个"红的样本"更能证明路径是通的 ——
|
|
1438
|
-
绿样本专治"红了但没红对地方"。
|
|
1439
|
-
|
|
1440
|
-
---
|
|
1441
|
-
|
|
1442
|
-
## E65 · attach 出来的实例,原型方法不算数 —— 假红会教人改错文档 · 0.11.2 开发期
|
|
1443
|
-
|
|
1444
|
-
**发生了什么**
|
|
1445
|
-
|
|
1446
|
-
`API.md` 的全局表新增 `Nav`(attach 形态),成员列写了实例方法。
|
|
1447
|
-
`api-form` 报:
|
|
1448
|
-
|
|
1449
|
-
```
|
|
1450
|
-
Nav 文档写了实现没有的成员
|
|
1451
|
-
文档=close, destroy, isOpen, open, toggle 实现=attach
|
|
1452
|
-
```
|
|
1453
|
-
|
|
1454
|
-
真因在判据里:
|
|
1455
|
-
|
|
1456
|
-
```python
|
|
1457
|
-
real_members = set(actual.get('members', {}))
|
|
1458
|
-
if real_form == 'construct': # ← 只有 construct 才并原型
|
|
1459
|
-
real_members |= set(actual.get('protoMethods', {}))
|
|
1460
|
-
```
|
|
1461
|
-
|
|
1462
|
-
`attach` 和 `create` **同样返回实例**,实例方法一样在原型上。
|
|
1463
|
-
旧写法让所有"文档提到实例方法"的非 construct 组件必然假红。
|
|
1464
|
-
|
|
1465
|
-
**为什么这条值得记**
|
|
1466
|
-
|
|
1467
|
-
这条假红本身不贵,贵的是它会教人怎么"修":报错说"文档写了实现没有的成员",
|
|
1468
|
-
最自然的反应就是把那几个方法名从文档里删掉 —— 删完门禁就绿了,
|
|
1469
|
-
而文档也就少了四个真实可调的方法。
|
|
1470
|
-
|
|
1471
|
-
同时它**顺手抓出一条真错**:我凭记忆写了 `Nav` 的实例方法含 `toggle`,
|
|
1472
|
-
而原型上只有 `isOpen` / `open` / `close` / `destroy`(没有 `toggle`)。
|
|
1473
|
-
判据修好之后这条仍然红,红得对。
|
|
1474
|
-
|
|
1475
|
-
**纠正规则**
|
|
1476
|
-
|
|
1477
|
-
1. **"实例方法"的判据不该依赖"实例是怎么造出来的"。**
|
|
1478
|
-
形态(create / attach / construct / direct)决定的是**怎么拿到实例**,
|
|
1479
|
-
不决定实例上有什么。
|
|
1480
|
-
2. 修假红时,先把**同一条判据下的其它红**看一遍:可能有的红是对的。
|
|
1481
|
-
这次就是"3 条假红 + 1 条真错"混在一起。
|
|
1482
|
-
3. 假红必须当成缺陷修,不能靠改输入绕开 —— 绕一次,这条判据以后就没人信了。
|
|
1483
|
-
|
|
1484
|
-
---
|
|
1485
|
-
|
|
1486
|
-
## E66 · 门禁的成功消息里写死了数字 · 0.11.2 开发期
|
|
1487
|
-
|
|
1488
|
-
**发生了什么**
|
|
1489
|
-
|
|
1490
|
-
`behavior-matrix-gate.py` 通过时打印:
|
|
1491
|
-
|
|
1492
|
-
```
|
|
1493
|
-
OK 18 个组件 × 4 种行为全部登记
|
|
1494
|
-
```
|
|
1495
|
-
|
|
1496
|
-
实际扫到 **21** 个组件。这个 18 从组件数还是 18 的时候写下,之后每次加组件
|
|
1497
|
-
都没人改它——因为它只是 `print` 里的一句字符串,不在任何判据的检查范围里。
|
|
1498
|
-
|
|
1499
|
-
**为什么这条值得记**
|
|
1500
|
-
|
|
1501
|
-
它不造成任何功能问题,但它**削弱了整份输出的可信度**:如果一句总结里的数字
|
|
1502
|
-
可以是陈旧的,那读者凭什么相信上面那些 `OK` 是这一轮跑出来的?
|
|
1503
|
-
|
|
1504
|
-
**纠正规则**
|
|
1505
|
-
|
|
1506
|
-
1. **打印出来的数字必须是从这一轮的实际数据算的**,不写字面量。
|
|
1507
|
-
2. 总结行里的每个数量,都要能指出它是从哪个变量来的。
|
|
1508
|
-
3. 反过来也成立:当你要写「N 个组件」时,先跑一遍看 N 到底是多少 ——
|
|
1509
|
-
这次就是这么发现"18 其实是 21"的。
|
|
1510
|
-
|
|
1511
|
-
---
|
|
1512
|
-
|
|
1513
|
-
## E67 · 实现把约束挪了位置,判据还在读老位置 —— `null em` · 0.11.2 开发期
|
|
1514
|
-
|
|
1515
|
-
**发生了什么**
|
|
1516
|
-
|
|
1517
|
-
宽通道的修法是把行长从**列容器**(`.rep__body`)挪到**内容块**(`p` / `figure` …)上。
|
|
1518
|
-
`report-check.js` 里的"三套 profile 行长互不相同"那条判据读的是:
|
|
1519
|
-
|
|
1520
|
-
```js
|
|
1521
|
-
const el = document.querySelector(id + ' .rep__body');
|
|
1522
|
-
const mw = parseFloat(getComputedStyle(el).maxWidth); // → NaN
|
|
1523
|
-
```
|
|
1524
|
-
|
|
1525
|
-
现在 `.rep__body` 的 `max-width` 是 `none` ⇒ `parseFloat('none')` = NaN。
|
|
1526
|
-
输出把 NaN 印成 `null`:
|
|
1527
|
-
|
|
1528
|
-
```
|
|
1529
|
-
行长 杂志 nullem > 科研 nullem > 金融 nullem
|
|
1530
|
-
```
|
|
1531
|
-
|
|
1532
|
-
**为什么这条值得记**
|
|
1533
|
-
|
|
1534
|
-
1. 它是**第四条**同型事故(前三:E61 唯一文件、E63 唯一全局、E65 只有 construct
|
|
1535
|
-
才有原型方法)。共同点不是"忘了改判据",而是**判据写死了"东西在哪儿"的假设**。
|
|
1536
|
-
实现一挪位置,判据不是报错,而是**量到一个空值**然后判红/判绿。
|
|
1537
|
-
2. `null`/`NaN` 必须当成"没量到"处理,不能当成"量到了 0"。
|
|
1538
|
-
这一次是判红(可接受),但在别的判据上它会变成假绿。
|
|
1539
|
-
3. 它是**只有 `MODE=full` 才看得见**的:fast 档不跑 report 这道浏览器门禁。
|
|
1540
|
-
⇒ 收尾跑 full 不是流程洁癖,是这批门禁唯一的存在证明。
|
|
1541
|
-
|
|
1542
|
-
**纠正规则**
|
|
1543
|
-
|
|
1544
|
-
1. **判据要读"约束落在谁身上",不是读"上次它在谁身上"。**
|
|
1545
|
-
写的时候问一句:这个值如果被挪到子元素上,我这条还量得到吗?
|
|
1546
|
-
2. `parseFloat` 之后必须 `isFinite` 检查;不合法就返回 NaN 并让调用方判红,
|
|
1547
|
-
不许让它悄悄参与比较(`NaN > 5` 恒为 false ⇒ 假绿的温床)。
|
|
1548
|
-
3. 改完判据要**当场做一次变异**(把金融版 30em 改成 42em)确认还会红 ——
|
|
1549
|
-
修假红之后最常见的次生事故是"修成永远绿"。
|
|
1550
|
-
|
|
1551
|
-
---
|
|
1552
|
-
|
|
1553
|
-
## E68 · `404` 有两种意思,我只读了退出码 · npm 发布事故的复盘补记
|
|
1554
|
-
|
|
1555
|
-
**发生了什么**
|
|
1556
|
-
|
|
1557
|
-
10-18 发 0.9.0,成功后我隔 20 秒查 `npm view`,拿到 404,判定"没发上去",
|
|
1558
|
-
去网页点了 Delete package。事后看 registry 的正文才知道:
|
|
1559
|
-
|
|
1560
|
-
| 正文 | 含义 |
|
|
1561
|
-
|---|---|
|
|
1562
|
-
| `{"error":"Not found"}` | **从来没有这个包** |
|
|
1563
|
-
| `404 Unpublished on 2026-10-18T15:16:53Z` | **曾经有,被删了** |
|
|
1564
|
-
|
|
1565
|
-
我要的那条记录就在正文里(`time.created` / `time["0.9.0"]` / `time.unpublished`),
|
|
1566
|
-
而我只看退出码。一份 200 字的 JSON,我读了它的状态码。
|
|
1567
|
-
|
|
1568
|
-
同一天的第二例:下载 Release 附件时 `curl` 退出码 0、`http_code=302`,
|
|
1569
|
-
文件却只有 48,418 / 739,543 字节。**"命令成功"和"事情成功"是两件事。**
|
|
1570
|
-
|
|
1571
|
-
**为什么这条值得记**
|
|
1572
|
-
|
|
1573
|
-
它不是一个技术错误,是一个**读数习惯**错误:把"我能拿到一个数字"当成"这个数字
|
|
1574
|
-
回答了问题"。而下游动作(删包)是不可逆的 —— 读数越弱,动作越要不可逆,事故越贵。
|
|
1575
|
-
|
|
1576
|
-
**纠正规则**
|
|
1577
|
-
|
|
1578
|
-
1. **不可逆动作之前,证据必须是正向的**:不是"查不到所以没有",而是
|
|
1579
|
-
"查到了明确写着没有"。查不到只说明**这次没查到**。
|
|
1580
|
-
2. **读到数字先问:这个数字是在回答我的问题吗?** 时间戳在正文里、状态在 `state`
|
|
1581
|
-
字段里、完整性在 sha256 里 —— 都不在退出码里。
|
|
1582
|
-
3. **凡是"上传/下载/发布",必须用第二个独立信源核对**(另一台机器、另一个字段、
|
|
1583
|
-
另一种协议)。这一次是 VPS 下载算 sha256 与本地逐一相等,才敢说"能装"。
|
|
1584
|
-
4. `unpublish` 是核按钮:72 小时窗口内它有效,而它有效的前提恰恰是
|
|
1585
|
-
**包已经在 registry 上**——这一条本身就说明"能 unpublish"意味着"已经成功"。
|
|
1586
|
-
|
|
1587
|
-
---
|
|
1588
|
-
|
|
1589
|
-
## E69 · 加粗把"共"和数字隔开,判据就看不见了 · 0.11.2 发布期
|
|
1590
|
-
|
|
1591
|
-
**发生了什么**
|
|
1592
|
-
|
|
1593
|
-
README 第 66 行写「共 **33** 个(stable 29 / beta 4)」,契约当时已是 35 / 31。
|
|
1594
|
-
`doc-facts-gate` 检查「组件总数」的两条正则:
|
|
1595
|
-
|
|
1596
|
-
```
|
|
1597
|
-
(?:本版|当前|本库|全库|总共|一共|共)\s*(?:有\s*)?(\d+)\s*个组件
|
|
1598
|
-
(\d+)\s*个组件成熟度
|
|
1599
|
-
```
|
|
1600
|
-
|
|
1601
|
-
两处都不匹配,原因有两层,而且是**叠加**的:
|
|
1602
|
-
|
|
1603
|
-
1. `共 **33**` —— 加粗的两个星号卡在"共"和数字之间(`\s*` 吃不掉 `**`);
|
|
1604
|
-
2. `个(stable` —— 是省略式的「个(」,不是要求后缀的「个组件」。
|
|
1605
|
-
|
|
1606
|
-
任一层单独存在都不会红,**两层同时存在**就完全失明。而失明的表现是打印
|
|
1607
|
-
「文档里的事实与仓库一致」。
|
|
1608
|
-
|
|
1609
|
-
**为什么这条值得记**
|
|
1610
|
-
|
|
1611
|
-
- 它和 E61(唯一文件)、E63(唯一全局)、E65(只有 construct)、E67(读老位置)
|
|
1612
|
-
是同一族:**判据的视野窄于事实的表达方式**。这次窄在"人写文档的习惯"上 ——
|
|
1613
|
-
加粗、全角括号、省略后缀,全是排版自由,不是错误。
|
|
1614
|
-
- 更值得记的是:**数字错了两个版本都没人发现**,而这份 README 正是陌生人
|
|
1615
|
-
第一眼看到的"我们有多大"。文档少报自己是"保守",多报自己是吹牛,
|
|
1616
|
-
但两者都会让人判断错该不该依赖本库。
|
|
1617
|
-
|
|
1618
|
-
**纠正规则**
|
|
1619
|
-
|
|
1620
|
-
1. **匹配前先做"去排版"归一化**(剥 `**` / `__`),匹配用副本、报错用原文。
|
|
1621
|
-
2. **列举式写法要单独一条判据,并且三个数字一起核**:
|
|
1622
|
-
`(\d+) 个(stable (\d+) / beta (\d+)` ⇒ 总数错、拆分错都红。
|
|
1623
|
-
只核总数会留下"总数对、拆分错"的缝。
|
|
1624
|
-
3. **写文档的人不该被要求记得门禁的正则长什么样。** 当一条判据因为"写法没被覆盖"
|
|
1625
|
-
而失明时,修的是判据,不是让作者换个写法。
|
|
1626
|
-
4. 变异探针(本轮实测):把 35/31 改回 33/29 ⇒ 红,且报错里给出两边的完整三元组;
|
|
1627
|
-
改回 35/31 ⇒ 绿。
|
|
1628
|
-
|
|
1629
|
-
---
|
|
1630
|
-
|
|
1631
|
-
## E70 · `.gitignore` 写 `/*.tgz` 会让「两种视野一致」判据红 —— 两种实现,一种语义
|
|
1632
|
-
|
|
1633
|
-
**发生了什么**
|
|
1634
|
-
|
|
1635
|
-
把 `npm pack` 产物挡在门外,顺手写了根锚定的 `/*.tgz`(git 语义:只忽略根目录的
|
|
1636
|
-
tgz)。`gate-selfcheck-fixtures` 立刻红:
|
|
1637
|
-
|
|
1638
|
-
```
|
|
1639
|
-
兜底视野与 git 视野不一致(兜底多 ['agentcss-0.11.2.tgz'])
|
|
1640
|
-
```
|
|
1641
|
-
|
|
1642
|
-
真因:`_common._ignore_patterns` 自带一个**够用即可**的 gitignore 子集,
|
|
1643
|
-
不认根锚定 `/` 与 `**`(它自己的 docstring 就写着这条边界)。
|
|
1644
|
-
|
|
1645
|
-
**为什么这条值得记**
|
|
1646
|
-
|
|
1647
|
-
判据本身是对的(它抓到了两个视野不一致),但它给的是**事实**不是**归因**,
|
|
1648
|
-
而"兜底多一个文件"最自然的误读是"有文件没被 git 跟踪,赶紧 add"——
|
|
1649
|
-
真去 add 反而把构建产物提交了。
|
|
1650
|
-
|
|
1651
|
-
**纠正规则**
|
|
1652
|
-
|
|
1653
|
-
1. **两种实现同一套语义时,差异必须是"红 + 说清是哪一侧的实现窄"**,
|
|
1654
|
-
不能只报现象。已在该判据的报错里补归因提示。
|
|
1655
|
-
2. 兜底实现不认的写法(`/` 锚定、`**`)在 `.gitignore` 里**别用**,
|
|
1656
|
-
改成 `*.tgz` 这种两边都认的形式;要真用得先扩 `_ignored`。
|
|
1657
|
-
3. "够用即可的子集"是合理取舍,但必须**在红的时候把边界喊出来** ——
|
|
1658
|
-
否则下一个人会以为自己犯了 git 的错误。
|