FDE·工程方法
知识库目录
测试从 0 到 1 PG 迁移预案 证据页 · 门禁脚本

复盘 34 · 证据页:四套门禁脚本,和它们各自抓到过什么

这一篇不讲方法,只摆证据。前面 33 篇讲的是「我怎么想」,这一篇摆的是我怎么证明我没在瞎说——四套脚本全部公开可下载,全部可复现,其中两套真在生产环境里抓出过东西。

项目锚点

对象:memberhandbook.cn/fde/kb/ 知识库本体(39 篇正文 + 7 张模板 + 42 个 HTML)。工具 kb/check.sh、kb/tools/crosscheck.py、kb/tools/scan_pg_ready.py、kb/tools/make_charts.py——四套全部只读,都不连生产库。

189项单页门禁
7组跨页一致性
18处 PG 待改
4张实测口径图

方法论出处:27 · 测试从 0 到 1讲测试怎么起步,本篇讲另一件事——怎么给自己已经写完的东西上锁;32 · PG 迁移预案是 scan_pg_ready.py 的完整结论页。

01

先说结论:写完不等于对,得有东西能证明

39 篇正文是手工写的,4 套脚本是机器查的。这两件事的关系是:手写保证内容有判断,机器查保证判断没在几十次迭代里悄悄失效。

举个具体例子。38 篇正文里,篇数从 26 涨到 37 再涨到 39,展示页的文案要跟着改 7 次;28 篇的四张图,有 3 张的 src 漏了 .png 后缀,线上是裂图,而单页体检跑了 189 项全部通过。——因为单页体检查的是「链接指向的文件存不存在」,图片 src 缺后缀在它眼里是「链接存在,只是名字怪」。

这就是我写第 2 套脚本的原因:门禁查不出自己没覆盖的盲区,得再加一道跨页视角的。
不是「检查越多越安全」,是「换个角度查同一个东西,才可能撞见盲区」。补完 .png 那天 check.sh 依然是 189 项全绿——它永远发现不了自己看不见的东西。

四个脚本的分工,一句话各自说清:

脚本查什么不查什么(重要)字节
check.sh单页:class="on" 唯一性、本地断链、身份禁用词、样式表路径、展示页篇数哨兵不查图片 src 是否真能加载;不查跨页数字口径是否漂移4,106
tools/crosscheck.py跨页:篇数自指、数字口径漂移、断图断链、陈旧待办残留不判断数字本身对不对,只判断口径有没有写清7,681
tools/scan_pg_ready.py扫另一个项目的 PG 迁移就绪度,输出带行号的待改清单只读文本,不连库、不写文件;只诊断不修4,219
tools/make_charts.py用 Pillow + numpy 手绘 4 张口径图(零图表库)不做交互、不做动态;只画「已经算好的数」15,591
02

脚本一 · check.sh:189 项单页体检

最朴素的脚本,也是唯一从第一轮就在跑的。5 组检查,其中第 5 组是后来补的——补它的原因是被它自己抓到的。

前四组是标准动作:每个非目录页的 class="on" 必须恰好 1 个(多一个导航就错乱)、同目录 html/tpl 引用不许断、身份禁用词(禁用主理人/双品牌/直营店/自有门店,规则说明行本身不算违规)、样式表必须指向 assets/kb.css。

第 5 组「showcase 一致性」是被逼出来的:补完 29/30/31 三篇后正文从 34 涨到 37,7 个展示页还写着 34——7 个页面全 FAIL。从那天起我不再靠眼睛记篇数,脚本自己数 ls *.html 再减去 3 个入口页,跟展示页文案对。人肉同步早晚会漏,脚本不会。

它同时把对外口径钉死成「N 篇正文 + 7 张模板」——不写「共 N 篇」,因为那会把 3 个入口页算进「篇」里虚标。这是字面意义上的一次虚标修正。

$ bash check.sh 正文 39 篇 / 模板 7 张 [showcase 一致性] OK fde-case.html 39 篇正文 + 7 张模板 OK fde-index.html 39 篇正文 + 7 张模板 OK fde-interview.html 39 篇正文 + 7 张模板 OK fde-mini.html 39 篇正文 + 7 张模板 OK fde-portfolio.html 39 篇正文 + 7 张模板 OK fde-sop.html 39 篇正文 + 7 张模板 OK fde-timeline.html 39 篇正文 + 7 张模板 ... === 汇总: 通过 189 / 失败 0 ===
03

脚本二 · crosscheck.py:抓到 3 张裂图 + 2 处口径漂移

7 组检查,跨页视角。它的存在理由就是上一节那句话:换个角度查,才可能撞见盲区。

抓到的第一个:3 张图在线上是裂的

28 篇补了 4 张实测图。第一张 src="assets/chart-1-completeness.png" 正确,后三张漏了 .png 后缀:

图片修复前 src线上状态
chart-1assets/chart-1-completeness.png正常 唯一正确的一张
chart-2assets/chart-2-model-top12裂图
chart-3assets/chart-3-dealer-readiness裂图
chart-4assets/chart-4-rfm-3x3裂图
为什么 189 项门禁没抓到:它查的是「href 指向同目录 html 文件」的引用存不存在。图片的 src 缺后缀,在它眼里是「链接存在,只是文件名怪」。这是我以前从没想过的检查维度——不是检查规则不够严,是规则只覆盖了我想得到的东西。

修的时候还踩了个坑,值得单独讲:第一次想用一条正则批量补后缀,结果 [a-z0-9]+ 贪婪吃掉了尾部字母,把本来正确的那张也改错了(completeness → completenes)。最后是从备份还原、按完整文件名精确替换才对。教训:改历史文件先备份;一条正则想覆盖 N 处同类内容时,宁可逐处精确替换。

抓到的第二:16 页在用一个没说清口径的数字

全库 16 个页面引用 33,176 这个订单数,但只有 28/32/33 三篇提到过全表是 33,178。其余页面的读者会把批次口径误读成全表数。

处理方式我选了集中说明而不是逐页加括注——在 24 篇(德莉玛那组的首页)插一个 note warn 块,把两个数的关系写清:

本库多处引用的 33,176 单来自 source='production_export' 这一批导出数据; 全表 product_order 实测 33,178 单,多出的 2 单来自 source='structured'。 两个数都不是错的,差别在口径不在对错。

另一处是 72 万字:26 篇里有 8 处写「72 万」,全都没标口径。而实测书稿本体只有 26.5 万字(体系篇 102,668 + 落地篇 162,200),72 万是含站点与素材池的全口径。对外第一落点反而是最不严谨的那个——顺手把 llms.txt 里的同一处口径也补了。

它自己也有盲区,所以带了豁免清单

首跑报了 14 条 CHECK,其中 12 条是「已知且不该改」:10 个页面沿用 33,176 批次口径(口径已在 24 篇集中说明,逐页加括注是噪音)、index 里「这 13 篇」、tech-index 里「这 12 篇」是分组子集,不是全库正文数。

我的取舍:脚本逼我去改 10 个本来没错的页面,我会选择改脚本。
但不能默默豁免——所以每条豁免都写清理由,并且结尾把豁免清单打印出来,理由失效就必须删条目,不许长期挂账。一个只会报警的门禁,报警多了就等于没报警。
$ python3 tools/crosscheck.py [1] 篇数自指(共 N 篇 = 39) [2] 分组篇数自指(这 N 篇) OK index.html 这 13 篇 → 分组子集:21~33 共 13 篇 OK tech-index.html 这 12 篇 → 分组子集:技术基础 14~19 速查 + 深入 [3] 展示页对外口径 7/7 全部 39 篇正文 + 7 张模板 [4] 订单数口径 33,176 出现于 16 页 / 33,178 出现于 4 页 OK 24-delimao-monorepo.html 承载批次口径集中说明 [5] 内容资产字数口径 提 72 万的页 = 3 / 提实测字数的页 = 4 [6] 陈旧待办残留< 图表层 / PG 预案 已清出「还没做的」 [7] 本地资源引用 断链/断图 0 处 === 结论 === 全部一致,没查到不一致项。 STALE 0 / BROKEN 0
04

脚本三 · scan_pg_ready.py:把一行注释变成 18 行清单

这个脚本最有代表性,因为它的起点小得可笑:「迁 PostgreSQL」这件事,在那个项目里只有一行注释——db/seed.ts 第 14 行 process.exit(0); // close MySQL connection pool,而实际跑的是 better-sqlite3。

注释不是预案。所以我写了个只读扫描器,把「要改什么、改在哪一行、改成什么、什么优先级」打成清单。规则命中就报,每条都带文件名 + 行号 + 改法:

$ python3 tools/scan_pg_ready.py "C:/.../memberhandbook-src-v2/tmp_src" · db/schema.ts: sqliteTable 表数 = 3,JSON blob 列 = 1 · db/seed.ts 提到 MySQL: True(实际跑的是 better-sqlite3) · drizzle.config.ts dialect = sqlite · db/migrations 真实迁移文件数 = 0 [P0] db/schema.ts L12 → text 存 JSON 字符串 → 拆表或改 PG jsonb;blob 没法 WHERE、没法索引 [P0] db/schema.ts L4 → pgTable(...) [P0] db/schema.ts L8 → integer(timestamp) → timestamp / timestamptz,先定时区语义 [P0] db/schema.ts L11 → integer(boolean) → boolean ...(P0 共 11 条) [P1] db/schema.ts L20 → PG 里 text 主键可用,但短码建议 varchar(n);长 ID 要加索引 ...(P1 共 7 条) 合计 18 处:P0 11 / P1 7

纪律上,这个脚本只读文本、不连数据库、不写任何文件——要改任何东西都得人批准。理由很实在:诊断工具一旦能写东西,它就不再是诊断工具了,用它的人会开始不敢跑。

脚本的结论是「现在不迁」,不是「应该迁」。数据体量小、单机部署、WAL 已在用——迁是给没疼的地方开刀。但把「真要迁时按什么顺序、哪几步会翻车、哪几步可以停」写清楚,是这份清单真正的交付物。完整推理见 32 篇。
05

脚本四 · make_charts.py:零图表库画出 4 张口径图

查了一遍知识库:recharts / chart.js / echarts / d3 命中全部是 0,而机器上 Pillow 12.3.0 + numpy 2.3.5 是现成的。结论是——我的短板在表达层,不在绘制层。那就不用引库,用现成的把图画出来。

画这 4 张图时定的规矩,比技术本身重要:

规矩为什么
分母写进标题,不写脚注脚注没人看,标题人人都看
灰块 = 无数据,绝不留白留白会被误读成「这块不重要」
「样本不足」四个字要印在图上不是脚注,是图的结论本身
样本 < 30 的格子不给文案、不给动作建议3 个样本也能算比例,但那个比例没法用来决策
单位 / 口径行 / 数据来源三件套齐全缺一个,别人就没法复核你的数

最典型的一张是 RFM 3×3:九格全灰,中心一个深蓝块写着「样本不足 / 库里没有 member 表 / 0 条会员 → 不做分层」。这张图不好看,但它是正确的——0 条会员就是不该做 RFM,画成九格彩色渐变才是造假。图和结论见 28 篇第 09 节。

出图也踩了坑,而且是版式的坑不是代码的坑:图 2 灰块贴边被裁(画布 1200→1320)、图 3 标签撞进度条(字号 18→17、起点 250→300)、图 4 网格超出画布且底部 9 格文案被中心块遮住(高度改按网格动态算,删掉 9 个预设动作文案)。修法只有一个:出完图必须真看图核对,不能只看代码认为对。这和「代码跑通 ≠ 结果正确」是同一件事。
06

门禁能证明什么、不能证明什么

把四套脚本摆出来,很容易被问一句:那你是不是有强迫症?我的答案是——不是。这是我能给的最便宜的质量保证。

门禁能证明门禁不能证明
39 篇的导航状态、链接、样式表、身份口径、篇数文案全部一致不能证明内容是对的。33,178 这个数要靠人查库,门禁只是让我别把 33,176 写成全表
PG 那边的待改点逐条带行号、可复核不能证明该迁。清单的结论恰恰是「现在不迁」
4 张图分母、口径、来源齐全不能保证好看。RFM 全灰那张就不好看,但它是诚实的
我有可复现的自我验证路径,不是靠「我记得我改过」不能替代人看。跨页一致性那套脚本,是因为裂图事故才写出来的——没人想得起来的维度,只有出过一次事才会被补上
我最想留下的一句判断是这一条:写完不等于对,得有东西能证明。
而「能证明」的最低成本形式,就是一个别人能跑、你自己敢让人跑的小脚本。它不需要多聪明,只需要在你偷懒的那天拦住你。
07

我明确不做的 / 还没做的

没做的(两栏分开写):

  • 没做 CI 集成:四套脚本现在要手动跑。没接 pre-commit / GitHub Actions,因为目前只有我一个人改,而且这些页面是 scp 部署不上 Git 仓库——等有第二个人改代码时再上。
  • 没做数字正确性校验:门禁只查「口径有没有写清」,不查「数字对不对」。数字对不对靠每次重新查库,这才是我下一步想做的:让 make_charts.py 的数据源自动从库里读,而不是写死。现在 real_data() 里的数是实测后写死的,库一变就得手改——这是一个我知道但还没还的债。
  • 没做交互图表:4 张全是静态 PNG。真要交互(下钻、筛选)才引 recharts,在那之前引库只是给自己添依赖。
  • crosscheck.py 还是单页视角为主:7 组检查里真正跨页的是口径类,还没有「同一段文字在不同页表述不一致」的语义级检查——这个我做不了,工具链到这儿就是我的上限。
面试怎么答

题目:你怎么保证你写的东西是对的?

先给动作,不给形容词:我给知识库写了四套门禁脚本,全部公开可下载,其中两套真在生产环境里抓出过东西。
第一套是单页体检,189 项,查导航唯一性、断链、身份禁用词、篇数哨兵。它抓到过一次 7 个页面同时忘记同步篇数——从那以后篇数由脚本自己数,不再靠我记。
第二套是跨页一致性,7 组。它抓到 28 篇有 3 张图 src 漏了 .png,线上是裂图,而单页体检 189 项全绿——因为单页体检查的是「链接指向的文件存不存在」,缺后缀在它眼里只是文件名怪。这次之后我的结论是:不是检查规则不够严,是规则只覆盖了我想得到的东西。
同一套还抓到 16 个页面在用一个没说清口径的数字(33,176 是导出批次、全表 33,178),以及 8 处「72 万字」没标口径而书稿实测只有 26.5 万。
第三套是只读诊断:把「迁 PostgreSQL」从一行注释变成 18 处带行号的待改清单(P0 11 / P1 7),而它的结论是「现在不迁」——诊断工具的价值不在于输出动作,在于它让「不做什么」也有了依据。纪律上它只读文本、不连库、不写文件,因为诊断工具一旦能写东西,它就不再是诊断工具了。
第四套是出图:recharts / chart.js / echarts / d3 命中全是 0,但 Pillow + numpy 现成,所以我用 Pillow 手绘了 4 张图。4 张里最不好看的那张是 RFM 九格全灰,中心写着「样本不足、库里没有 member 表」——0 条会员就是不该做 RFM,画成彩色渐变才是造假。
最后我会说清楚门禁不能证明什么:它不能证明内容是对的,只能证明我没在几十次迭代里把口径、链接、篇数改乱。而「内容对不对」只能靠重新查库——这正好是我下一个要还的债:把出图脚本的数据源从写死改成自动读库。