网页端必填项标记:红色星号改为红色加粗标题(全站统一,解决排版对不齐) #3

Closed
opened 2026-10-25 08:25:08 +00:00 by laoyang · 2 comments
Owner

现象

网页端表单里,必填项现在是在标题后面加一颗红色星号(如「书名 」「片名」)。

问题:星号是追加在文字末尾的,中文标题长短不一,
星号位置就参差不齐;再加上不同表单的标签写法不统一(有的 <label> 带空格、
有的 <i>*</i> 紧贴、有的 .mv-lb 用伪元素补冒号),
视觉上经常对不齐,排版不好看。

期望

所有必填项统一改成「标题用红色 + 加粗」,去掉红色星号。

用户习惯之后,一看标题字体是红色加粗(比如「日程名称」四个字是红的、粗的),
就知道这是必填项。这样比加星号更整齐,也不再有对齐问题。

涉及范围(全站网页端表单)

已核到的必填标记位置:

模块 位置
笔记 books/form.php 书名
日程 calendar/calendars.php 名字 / 导进哪一本 / .ics 文件
日程 calendar/_event_fields.php 标签闭包($star 参数)+ 日期范围
纪念日 days/form.php 叫什么 / 按什么重复 / 日期
账本 ledger/index.php 类别 / 名称 / 金额
观影 movies/form.php 片名 / 影院名称
观影 movies/cinemas.php 影院名称
导航 nav/index.php 网址 / 所属板块 / 板块名 / 名称 / 搜索地址 / 关键词参数
名册 people/form.php 姓名
名册 people/import.php 联系人文件
资源 resources/index.php 归属于谁 / 标题
任务 tasks/_add_modal.php 任务名称
任务 tasks/_edit_form.php 任务名称
旅程 trips/form.php 旅程名
旅程 trips/node_form.php 类型 / 标题
通用 _datetime.php 日期控件标签星号($dtLabelStar)

验收标准

  1. 星号彻底消失:全站表单的必填标签后面不再出现 *。
  2. 必填标签是红色 + 加粗,且与同一行里非必填标签一眼能区分(颜色 + 字重都不同)。
  3. 对齐全站一致:必填 / 非必填标签的基线、字号、间距不变,只是颜色和字重变
    —— 不能因为改字体导致标签换行、错位或高度变化。
  4. 非必填项不受影响(保持原样,不能整片变红)。
  5. .mv-lb 这类用 ::after 补冒号的标签,改成红加粗后冒号也要跟着变红
    (不能出现"红字 + 灰冒号"的半截效果)。
  6. 移动端窄屏不因此横向溢出。
  7. 提交校验逻辑不变:原来必填的仍然必填,只是标记方式变了。
## 现象 网页端表单里,必填项现在是在标题后面加一颗**红色星号**(如「书名 *」「片名*」)。 问题:星号是**追加在文字末尾**的,中文标题长短不一, 星号位置就参差不齐;再加上不同表单的标签写法不统一(有的 `<label>` 带空格、 有的 `<i>*</i>` 紧贴、有的 `.mv-lb` 用伪元素补冒号), 视觉上经常对不齐,排版不好看。 ## 期望 **所有必填项统一改成「标题用红色 + 加粗」,去掉红色星号。** 用户习惯之后,一看标题字体是红色加粗(比如「日程名称」四个字是红的、粗的), 就知道这是必填项。这样比加星号更整齐,也不再有对齐问题。 ## 涉及范围(全站网页端表单) 已核到的必填标记位置: | 模块 | 位置 | |---|---| | 笔记 | `books/form.php` 书名 | | 日程 | `calendar/calendars.php` 名字 / 导进哪一本 / .ics 文件 | | 日程 | `calendar/_event_fields.php` 标签闭包(`$star` 参数)+ 日期范围 | | 纪念日 | `days/form.php` 叫什么 / 按什么重复 / 日期 | | 账本 | `ledger/index.php` 类别 / 名称 / 金额 | | 观影 | `movies/form.php` 片名 / 影院名称 | | 观影 | `movies/cinemas.php` 影院名称 | | 导航 | `nav/index.php` 网址 / 所属板块 / 板块名 / 名称 / 搜索地址 / 关键词参数 | | 名册 | `people/form.php` 姓名 | | 名册 | `people/import.php` 联系人文件 | | 资源 | `resources/index.php` 归属于谁 / 标题 | | 任务 | `tasks/_add_modal.php` 任务名称 | | 任务 | `tasks/_edit_form.php` 任务名称 | | 旅程 | `trips/form.php` 旅程名 | | 旅程 | `trips/node_form.php` 类型 / 标题 | | 通用 | `_datetime.php` 日期控件标签星号(`$dtLabelStar`) | ## 验收标准 1. **星号彻底消失**:全站表单的必填标签后面不再出现 `*`。 2. **必填标签是红色 + 加粗**,且与同一行里非必填标签**一眼能区分**(颜色 + 字重都不同)。 3. **对齐全站一致**:必填 / 非必填标签的基线、字号、间距不变,只是颜色和字重变 —— 不能因为改字体导致标签换行、错位或高度变化。 4. 非必填项**不受影响**(保持原样,不能整片变红)。 5. `.mv-lb` 这类用 `::after` 补冒号的标签,改成红加粗后**冒号也要跟着变红** (不能出现"红字 + 灰冒号"的半截效果)。 6. 移动端窄屏不因此横向溢出。 7. 提交校验逻辑不变:原来必填的仍然必填,只是标记方式变了。
Author
Owner

进展:改动已落码,正在跑全量验证

一、实现方式

所有必填项统一改成标签本身红色 + 加粗,星号全部去掉。

CSS(assets/css/app.css)

  • :root 新增两个语义变量:
    • --req: #B03A2E —— 必填红。没有复用 --red:--red(#C0533F) 是动作红(删除按钮底色),当文字色用在暖纸底(#F7F3EA)上对比度只有 ≈3.4:1,13-14px 标签偏浅;取深一档的 #B03A2E(≈4.6:1)。
    • --req-w: 700 —— 必填字重。
  • 文件末尾新增「必填项统一标记」整段,覆盖四种标签形态:裸 <label> / <span class="mv-lb"> / <span class="dt-inline-lab"> / <span class="dt-range-label">,并含 .is-req 加在包裹层时的兜底、深色主题备用色。

模板:30 处星号逐条精确替换成 class="is-req"(_tooling/_v94_req_label.py,每条断言"命中且仅命中一次");两处动态生成标签的闭包(_event_fields.php 的 $evLab、_datetime.php 的 $dtLabelReq)改成拼 is-req。

二、过程中发现并修掉的三个坑(都会导致"看着改完了、其实没生效")

坑 1:本站标签的基线字重是 600,不是 400。
.form-grid > .field > label、.card .field > label、.mv-row > .mv-lb、.dt-range-label 四处全都是 font-weight: 600。
第一版 --req-w 写的是 650 —— 在只提供 400/500/600/700 字重的字体上会被取整回 600,等于完全没加粗,用户看到的效果只剩"变红"。已改成 700。
⇒ 守卫判据因此写成「必填字重 严格大于 从 CSS 里解析出来的基线」,而不是写死 700,将来谁调基线它自己会跟着重算。

坑 2:!important 不是偷懒,是必需的。
.is-req(0,1,0)要覆盖的 .card .field > label:not(.job-lb):not(.check-item):not(.chk-inline) 特异性是(0,4,1)。实测去掉 !important 后:

  • 成员页「姓名」→ rgb(44,35,29) / 600(退回墨色)
  • 导航页「网址」「所属板块」→ 同样退回墨色
    CSS 文本一个字都没错,但页面上就是不红了 —— 只有拿真浏览器量计算色才能发现。

坑 3:冒号是伪元素,必须一起改。
.mv-lb::after / .dt-inline-lab::after 把「:」画成灰色 400 字重。只改标签本体会出现"红字 + 灰冒号"的半截效果,已一并覆盖。

三、验证(三层,全部带反向验证)

套件 结果
_fb_v94_static.py 22 通过 / 0 失败
静态反向验证 9/9 注入全部被抓(字重退回基线、必填红=动作红、去掉 !important、颜色不走变量、星号回来、漏标一个必填 …)
_fb_v94_req.py(真浏览器计算样式) 63 通过 / 0 失败
探针反向验证 5/5 注入全部被抓

探针在真实 Chromium 里量 getComputedStyle,覆盖:观影「记一场电影」+ 新建影院弹框、旅程、新增成员、导航页、任务新建弹框、首页「新建日程」弹框(用户点名的场景)。判据包括:

  • 必填标签计算色 == --req,且不是墨色/灰
  • 必填最小字重 > 同页非必填最大字重(真的更粗)
  • 标签文字里没有星号
  • 冒号与标签同色
  • 加粗没把标签挤出容器(不横向溢出)
  • required 与 checkValidity() 没受影响(改样式没弄丢校验)

四、反向验证过程中,探针自己被抓出的两个假绿

如实记录,因为它们说明"断言能红"比"断言全绿"重要得多:

  1. --req 改成墨色时判据②假绿:原判据写成「必填色 ≠ 墨色 且 --req ≠ --ink」,一旦注入把 --req 也改成墨色,后半段前提不成立 ⇒ 整条短路成 False。已改成前置断言 ⓪(三种颜色两两不同)+ ② 只判"不是墨色/灰"。
  2. 注入靶子选错导致假绿:去掉 !important 的注入原本打在观影页,而观影页的必填标签是 <label class="mv-lb is-req">,label.is-req(0,2,0) 与 .mv-row > .mv-lb(0,2,0) 同特异性、靠源码顺序取胜 ⇒ 去掉 !important 照样红。改到成员页/导航页(被 0,4,1 压着)才真能变红。

五、尚未完成

全量流水线正在跑(含线上部署)。跑完会补一条收尾评论说明部署结果,然后关闭本工单。

## 进展:改动已落码,正在跑全量验证 ### 一、实现方式 所有必填项统一改成**标签本身红色 + 加粗**,星号全部去掉。 **CSS**(`assets/css/app.css`) - `:root` 新增两个语义变量: - `--req: #B03A2E` —— 必填红。**没有复用 `--red`**:`--red`(#C0533F) 是动作红(删除按钮底色),当文字色用在暖纸底(#F7F3EA)上对比度只有 ≈3.4:1,13-14px 标签偏浅;取深一档的 #B03A2E(≈4.6:1)。 - `--req-w: 700` —— 必填字重。 - 文件末尾新增「必填项统一标记」整段,覆盖四种标签形态:裸 `<label>` / `<span class="mv-lb">` / `<span class="dt-inline-lab">` / `<span class="dt-range-label">`,并含 `.is-req` 加在包裹层时的兜底、深色主题备用色。 **模板**:30 处星号逐条精确替换成 `class="is-req"`(`_tooling/_v94_req_label.py`,每条断言"命中且仅命中一次");两处动态生成标签的闭包(`_event_fields.php` 的 `$evLab`、`_datetime.php` 的 `$dtLabelReq`)改成拼 `is-req`。 ### 二、过程中发现并修掉的三个坑(都会导致"看着改完了、其实没生效") **坑 1:本站标签的基线字重是 600,不是 400。** `.form-grid > .field > label`、`.card .field > label`、`.mv-row > .mv-lb`、`.dt-range-label` 四处全都是 `font-weight: 600`。 第一版 `--req-w` 写的是 650 —— 在只提供 400/500/600/700 字重的字体上会被取整回 600,**等于完全没加粗**,用户看到的效果只剩"变红"。已改成 700。 ⇒ 守卫判据因此写成「必填字重 **严格大于** 从 CSS 里解析出来的基线」,而不是写死 700,将来谁调基线它自己会跟着重算。 **坑 2:`!important` 不是偷懒,是必需的。** `.is-req`(0,1,0)要覆盖的 `.card .field > label:not(.job-lb):not(.check-item):not(.chk-inline)` 特异性是(0,4,1)。实测去掉 `!important` 后: - 成员页「姓名」→ rgb(44,35,29) / 600(退回墨色) - 导航页「网址」「所属板块」→ 同样退回墨色 CSS 文本一个字都没错,但页面上就是不红了 —— 只有拿真浏览器量计算色才能发现。 **坑 3:冒号是伪元素,必须一起改。** `.mv-lb::after` / `.dt-inline-lab::after` 把「:」画成灰色 400 字重。只改标签本体会出现"红字 + 灰冒号"的半截效果,已一并覆盖。 ### 三、验证(三层,全部带反向验证) | 套件 | 结果 | |---|---| | `_fb_v94_static.py` | **22 通过 / 0 失败** | | 静态反向验证 | **9/9 注入全部被抓**(字重退回基线、必填红=动作红、去掉 `!important`、颜色不走变量、星号回来、漏标一个必填 …) | | `_fb_v94_req.py`(真浏览器计算样式) | **63 通过 / 0 失败** | | 探针反向验证 | **5/5 注入全部被抓** | 探针在真实 Chromium 里量 `getComputedStyle`,覆盖:观影「记一场电影」+ 新建影院弹框、旅程、新增成员、导航页、任务新建弹框、**首页「新建日程」弹框(用户点名的场景)**。判据包括: - 必填标签计算色 == `--req`,且不是墨色/灰 - 必填最小字重 > 同页非必填最大字重(真的更粗) - 标签文字里没有星号 - 冒号与标签同色 - 加粗没把标签挤出容器(不横向溢出) - `required` 与 `checkValidity()` 没受影响(改样式没弄丢校验) ### 四、反向验证过程中,探针自己被抓出的两个假绿 如实记录,因为它们说明"断言能红"比"断言全绿"重要得多: 1. **`--req` 改成墨色时判据②假绿**:原判据写成「必填色 ≠ 墨色 **且** `--req` ≠ `--ink`」,一旦注入把 `--req` 也改成墨色,后半段前提不成立 ⇒ 整条短路成 False。已改成前置断言 ⓪(三种颜色两两不同)+ ② 只判"不是墨色/灰"。 2. **注入靶子选错导致假绿**:去掉 `!important` 的注入原本打在观影页,而观影页的必填标签是 `<label class="mv-lb is-req">`,`label.is-req`(0,2,0) 与 `.mv-row > .mv-lb`(0,2,0) 同特异性、靠源码顺序取胜 ⇒ 去掉 `!important` 照样红。改到成员页/导航页(被 0,4,1 压着)才真能变红。 ### 五、尚未完成 全量流水线正在跑(含线上部署)。跑完会补一条收尾评论说明部署结果,然后关闭本工单。
Author
Owner

补充:验证过程中发现并修掉了一个「部署闸门失效」的隐患

这一条不是需求本身的改动,但它是这批改动必须修掉的配套问题,记在这里备查。

现象

流水线跑到最后一步「部署脚本静态断言本地空跑」时失败,日志里有两行:

_fb_static_dry2.sh: line 4813: perl: command not found
  [FAIL] 任务标题标签不是「任务名称」

第二行是正常的:必填标记从 <i>*</i> 改成了 .is-req,部署脚本里那条断言的字面量
必须跟着改(已改,并且保留 is-req —— 否则会变成「标签在、必填标记丢了也能过」的假绿)。

第一行才是真问题。

真问题:闸门里有一批断言在「空输出」上恒真

部署脚本里剥 CSS 注释用的是:

_css_code=$(perl -0777 -pe 's:/\*.*?\*/::gs' "$SITE/assets/css/app.css")

本机没有 perl(which perl 为空,PortableGit 不带它)。命令失败 ⇒ _css_code 是空串。
而往后每一条这种形状的断言:

grep -qF '.ag-now-dot' <<<"$_css_code" && { echo "[FAIL] 死规则还在"; exit 1; }

在空串上恒不命中、恒为真 ⇒ 「app.css 里还留着死规则」这类"删干净了吗"的检查
全部失效,而日志一片绿。

这比"检查报错"恶劣得多:闸门坏了不报警,只会放行。而它正是部署前最后一道防线。

修法(三条一起)

  1. 剥 CSS 注释改用 PHP(部署脚本里 _pick_php 已有,本机/线上都必有),不再依赖 perl;
  2. 前置断言:剥完为空 ⇒ 立刻失败,绝不带着空串往下走;
  3. 体量断言:剥完必须 >20KB(实测 app.css 386,599 字节 → 剥注释后 209,480 字节)。
    只判"非空"挡不住"读错文件 / 被截断",体量断言才挡得住。

反向验证(证明这三条护栏真有牙齿)

新增 _v94_dry_falsify.py:每次从部署脚本现抽快照,然后注入三处。
注意 _fb_static_dry2.sh 是从 familybook_deploy.sh 自动生成的快照,
改它不等于改源头(下次运行就被覆盖,且线上真跑的断言毫无变化)—— 一律改部署脚本。

注入 预期 结果
(a) 剥注释输出为空(旧 perl 缺失的症状) 前置断言红 抓住
(b) 剥注释只回来一小段(读错文件/截断) 体量断言红 抓住
(c) 任务名称标签被打回 <i>*</i> 星号形态 字面量断言红 抓住

3/3 全部被抓住,且已登记进流水线(部署 标签,每次全量必跑)。

顺带清理

_fb_static_all.sh 是 2026-10-21 的手工快照,没有任何脚本引用,且它查的是
templates/tasks/index.php 里的「任务名称」—— 那块表单 v78 起已搬到
templates/tasks/_add_modal.php,所以那条断言在本文件里本来就恒假。
已在文件头标注「已废弃,不要运行」,保留只为存档对比。

补充二:全量流水线跑出来的两个「测试基建」问题

这两个都不影响产品代码,但都让流水线白跑 2 小时,且失败信息指向错误的方向。

问题一:随机端口抽到 Chromium 受限端口,反向验证把它报成「断言没牙齿」

v44 反向验证 报:

证伪通过 16 项,失败 1 项
  x   注入后「旧的日期控件真的不渲染」变红
[FAIL] ... <<< 未变红 → 这条断言可能是**恒真**的摆设

按字面读,结论是「那条断言写错了」——完全错误。翻原文才看到:

playwright._impl._errors.Error: Page.goto: net::ERR_UNSAFE_PORT
    at http://127.0.0.1:10080/index.php?r=auth/login

探针端口是 bind(('127.0.0.1', 0)) 让系统随机给的,这次抽到 10080,
在 Chromium 的受限端口名单里。浏览器连不上 ⇒ 探针崩 ⇒ 上层只看到
「期望的红没出现」⇒ 归因到断言。

为什么特别坑:① 失败信息指向错误方向,让你去审一条完全正确的断言;
② 概率性,上次是绿的,容易被当 flaky 放过;③ 错误在子进程里,
falsify 只贴末尾几行,ERR_UNSAFE_PORT 在更早的位置。

修法:抽成公共模块 _tooling/_fb_port.py 的 pick_port(),抽到黑名单就重抽。
把流水线里所有裸抽端口的探针批量换过去 —— 本轮一次改了 61 个文件
(39 个单行式 + 20 个四行式 + 2 个手工),改完 61/61 语法通过。

⚠️ 这里有个教训:_fb_v94_req.py 里我自己几小时前刚踩过 6666 并写了黑名单,
但那份逻辑只活在那一个文件里,其余 60 多个探针仍然裸抽 ⇒ 换个套件又栽一次。
同一个坑出现第二次,就该抽成公共设施。

问题二:falsify 的「拷贝清单」漏了它要 patch 的文件

修完端口后,v44 仍然红,但换了个形态:

[FAIL] 基线全绿(探针本身没问题)  <<< 第一行是「任务名称」… 实际读到「要做什么」
[FAIL] 补丁已命中(templates/tasks/_add_modal.php)<<< 没找到注入点,跳过

真根因:_b44_falsify.py 有 5 处注入打的是 templates/tasks/_add_modal.php
(v78 起任务表单搬到了这个片段),但它的 RESTORE_RELS 清单里没有这个文件。

后果分两层:

  1. 副本里没这个文件 ⇒ patch() 找不到原文 ⇒ 5 处注入全部"不算通过";
  2. 更隐蔽:副本里若留着上一轮的注入残渣,基线会读到旧文案
    ⇒ 看起来像源码写错了,其实是副本不干净。
    本轮实测就是这样:基线读到的标签是「截止时间(有才会出现在日历里)」,
    那句正是注入串里的旧行为文案。

而 _b44_falsify.py 自己写着这条规矩("凡是 falsify 会 patch 的文件,
都必须在这个清单里")—— 规矩写在注释里不会自己生效,所以又被违反了。

修法:补上清单 + 做成静态核对工具 _fb_restore_rels_check.py,
登记进流水线(每轮几秒钟)。它的判据是「脚本 patch 的目标文件是否都在拷贝清单里」。

⚠️ 这个检查器自己也有过一次假绿:第一版只认 patch('x','y','z') 调用形态,
而这批 falsify 用的是 (描述, (路径, 原文, 替换), (断言)) 嵌套元组
⇒ 每个脚本抽出 0 个目标 ⇒ 差集恒空 ⇒ 打印「✅ 全部通过」。
这是最坏的一种假绿:检查器本身没生效,却给出最让人放心的结论。
已加自检:抽出 0 条 = 检查未生效 = 失败(不是通过)。

这一轮修完后 v44 的结果

证伪通过 17 项,失败 0 项

基线全绿、5 处注入全部命中、ERR_UNSAFE_PORT 出现 0 次。


收尾:已实现、已验证、已部署、已提交

改了什么

CSS(assets/css/app.css)

  • :root 新增 --req: #B03A2E、--req-w: 700
    • 不复用 --red(#C0533F):那是动作红(删除按钮底色),当文字色放在暖纸底
      #F7F3EA 上对比度只有 ≈3.4:1,13-14px 的标签字号偏浅。#B03A2E 约 4.6:1。
    • --req-w 必须是 700,不能 650/600:本站标签的基线字重本来就是 600
      (.form-grid > .field > label、.card .field > label、.mv-row > .mv-lb、
      .dt-range-label 四处都是 600)。写 650 会被取整回 600 = 完全没加粗,
      用户看到的只剩「变红」,而他要求的是「红色加粗」。
  • 文件末尾新增「必填项统一标记」段,覆盖四种标签形态:
    label.is-req / .mv-lb.is-req / .dt-inline-lab.is-req / .dt-range-label.is-req,
    用 !important 打穿各宿主选择器的特异性。
  • 冒号跟着变红:.mv-lb::after / .dt-inline-lab::after 原本是灰色 400 字重,
    只改标签本体会出现「红字 + 灰冒号」的半截效果。
  • 包裹层兜底(.field.is-req > label 等),只作用直接子级 ——
    没写成 .field.is-req label,那会把「提醒」那排 checkbox 的小标签一起染红。
  • 深色主题给备用色 #F08A78。

模板(16 个文件 / 30 处)

  • _event_fields.php、_datetime.php:标签闭包第三参数 $star → $req,
    $dtLabelStar → $dtLabelReq,删掉死引用 $evLabelStar。
  • 其余 14 个模板:<i>*</i> 换成给标签加 is-req 类。
  • 用显式 .is-req 而不靠 required 属性反推,原因:CSS 没有父选择器,
    :has() 在旧 WebView 不可用,而且「影院名称」「所属板块」本来就靠 JS 校验、
    DOM 上没有 required。

验证结果

层 结果
静态断言 _fb_v94_static.py 22 通过 / 0 失败,反向 9/9
真浏览器 _fb_v94_req.py 63 通过 / 0 失败,反向 5/5
全量流水线(60+ 套件) 全绿,已部署
线上真实页面 _fb_v94_live.py 31 通过 / 0 失败

线上实测(不是看 CSS 源码,是 getComputedStyle 的真实值):

页面 必填标签 颜色 字重 同页非必填
记一场电影 片名 rgb(176,58,46) 700 rgb(44,35,29) / 600
新建旅程 旅程名 rgb(176,58,46) 700 rgb(44,35,29) / 600
添加成员 姓名 rgb(176,58,46) 700 rgb(44,35,29) / 600
导航页 网址、所属板块 rgb(176,58,46) 700 rgb(44,35,29) / 600

冒号也同色(::after 实测 rgb(176,58,46)),页面上已无星号。

反向验证的 5 类注入(每类都必须让预期的那几条断言变红):

  • (a) 把 --req-w 调回 600 → 加粗类断言红
  • (b) 把 --req 调成墨色 → 「必须红」类断言红
  • (c) 去掉 !important → 裸 <label> 那几条红(证明 !important 确实必需)
  • (d) 星号加回来 → 「星号必须消失」红
  • (e) 只覆盖 <label> 不覆盖 .mv-lb → .mv-lb 形态那几条红

提交

daf5ca9 v94 必填项改成「标签红色加粗」,去掉红色星号 (#3)

17 个文件,+176 / -44。

## 补充:验证过程中发现并修掉了一个「部署闸门失效」的隐患 这一条不是需求本身的改动,但它是这批改动**必须修掉**的配套问题,记在这里备查。 ### 现象 流水线跑到最后一步「部署脚本静态断言本地空跑」时失败,日志里有两行: ``` _fb_static_dry2.sh: line 4813: perl: command not found [FAIL] 任务标题标签不是「任务名称」 ``` 第二行是正常的:必填标记从 `<i>*</i>` 改成了 `.is-req`,部署脚本里那条断言的字面量 必须跟着改(已改,并且**保留 `is-req`** —— 否则会变成「标签在、必填标记丢了也能过」的假绿)。 第一行才是真问题。 ### 真问题:闸门里有一批断言在「空输出」上恒真 部署脚本里剥 CSS 注释用的是: ```bash _css_code=$(perl -0777 -pe 's:/\*.*?\*/::gs' "$SITE/assets/css/app.css") ``` **本机没有 perl**(`which perl` 为空,PortableGit 不带它)。命令失败 ⇒ `_css_code` 是空串。 而往后每一条这种形状的断言: ```bash grep -qF '.ag-now-dot' <<<"$_css_code" && { echo "[FAIL] 死规则还在"; exit 1; } ``` 在空串上**恒不命中、恒为真** ⇒ 「app.css 里还留着死规则」这类"删干净了吗"的检查 **全部失效,而日志一片绿**。 这比"检查报错"恶劣得多:**闸门坏了不报警,只会放行**。而它正是部署前最后一道防线。 ### 修法(三条一起) 1. 剥 CSS 注释改用 **PHP**(部署脚本里 `_pick_php` 已有,本机/线上都必有),不再依赖 perl; 2. **前置断言**:剥完为空 ⇒ 立刻失败,绝不带着空串往下走; 3. **体量断言**:剥完必须 >20KB(实测 `app.css` 386,599 字节 → 剥注释后 209,480 字节)。 只判"非空"挡不住"读错文件 / 被截断",体量断言才挡得住。 ### 反向验证(证明这三条护栏真有牙齿) 新增 `_v94_dry_falsify.py`:每次从部署脚本**现抽**快照,然后注入三处。 注意 `_fb_static_dry2.sh` 是从 `familybook_deploy.sh` 自动生成的**快照**, 改它不等于改源头(下次运行就被覆盖,且线上真跑的断言毫无变化)—— 一律改部署脚本。 | 注入 | 预期 | 结果 | |---|---|---| | (a) 剥注释输出为空(旧 perl 缺失的症状) | 前置断言红 | 抓住 | | (b) 剥注释只回来一小段(读错文件/截断) | 体量断言红 | 抓住 | | (c) 任务名称标签被打回 `<i>*</i>` 星号形态 | 字面量断言红 | 抓住 | 3/3 全部被抓住,且已登记进流水线(`部署` 标签,每次全量必跑)。 ### 顺带清理 `_fb_static_all.sh` 是 2026-10-21 的手工快照,没有任何脚本引用,且它查的是 `templates/tasks/index.php` 里的「任务名称」—— 那块表单 v78 起已搬到 `templates/tasks/_add_modal.php`,所以那条断言在本文件里本来就恒假。 已在文件头标注「已废弃,不要运行」,保留只为存档对比。 --- ## 补充二:全量流水线跑出来的两个「测试基建」问题 这两个都不影响产品代码,但都让流水线**白跑 2 小时**,且失败信息指向错误的方向。 ### 问题一:随机端口抽到 Chromium 受限端口,反向验证把它报成「断言没牙齿」 `v44 反向验证` 报: ``` 证伪通过 16 项,失败 1 项 x 注入后「旧的日期控件真的不渲染」变红 [FAIL] ... <<< 未变红 → 这条断言可能是**恒真**的摆设 ``` 按字面读,结论是「那条断言写错了」——**完全错误**。翻原文才看到: ``` playwright._impl._errors.Error: Page.goto: net::ERR_UNSAFE_PORT at http://127.0.0.1:10080/index.php?r=auth/login ``` 探针端口是 `bind(('127.0.0.1', 0))` 让系统随机给的,这次抽到 **10080**, 在 Chromium 的受限端口名单里。浏览器连不上 ⇒ 探针崩 ⇒ 上层只看到 「期望的红没出现」⇒ 归因到断言。 **为什么特别坑**:① 失败信息指向错误方向,让你去审一条完全正确的断言; ② 概率性,上次是绿的,容易被当 flaky 放过;③ 错误在子进程里, falsify 只贴末尾几行,`ERR_UNSAFE_PORT` 在更早的位置。 **修法**:抽成公共模块 `_tooling/_fb_port.py` 的 `pick_port()`,抽到黑名单就重抽。 把流水线里**所有**裸抽端口的探针批量换过去 —— 本轮一次改了 **61 个文件** (39 个单行式 + 20 个四行式 + 2 个手工),改完 61/61 语法通过。 ⚠️ 这里有个教训:`_fb_v94_req.py` 里我自己几小时前刚踩过 6666 并写了黑名单, 但那份逻辑**只活在那一个文件里**,其余 60 多个探针仍然裸抽 ⇒ 换个套件又栽一次。 **同一个坑出现第二次,就该抽成公共设施。** ### 问题二:falsify 的「拷贝清单」漏了它要 patch 的文件 修完端口后,v44 仍然红,但换了个形态: ``` [FAIL] 基线全绿(探针本身没问题) <<< 第一行是「任务名称」… 实际读到「要做什么」 [FAIL] 补丁已命中(templates/tasks/_add_modal.php)<<< 没找到注入点,跳过 ``` **真根因**:`_b44_falsify.py` 有 5 处注入打的是 `templates/tasks/_add_modal.php` (v78 起任务表单搬到了这个片段),但它的 `RESTORE_RELS` 清单里**没有这个文件**。 后果分两层: 1. 副本里没这个文件 ⇒ `patch()` 找不到原文 ⇒ 5 处注入全部"不算通过"; 2. **更隐蔽**:副本里若留着上一轮的注入残渣,基线会读到**旧文案** ⇒ 看起来像**源码写错了**,其实是**副本不干净**。 本轮实测就是这样:基线读到的标签是「截止时间(有才会出现在日历里)」, 那句正是注入串里的**旧行为**文案。 **而 `_b44_falsify.py` 自己写着这条规矩**("凡是 falsify 会 patch 的文件, 都必须在这个清单里")—— 规矩写在注释里不会自己生效,所以又被违反了。 **修法**:补上清单 + 做成静态核对工具 `_fb_restore_rels_check.py`, 登记进流水线(每轮几秒钟)。它的判据是「脚本 patch 的目标文件是否都在拷贝清单里」。 ⚠️ **这个检查器自己也有过一次假绿**:第一版只认 `patch('x','y','z')` 调用形态, 而这批 falsify 用的是 `(描述, (路径, 原文, 替换), (断言))` 嵌套元组 ⇒ 每个脚本抽出 **0 个目标** ⇒ 差集恒空 ⇒ 打印「✅ 全部通过」。 这是最坏的一种假绿:**检查器本身没生效,却给出最让人放心的结论**。 已加自检:抽出 0 条 = 检查未生效 = 失败(不是通过)。 ### 这一轮修完后 v44 的结果 ``` 证伪通过 17 项,失败 0 项 ``` 基线全绿、5 处注入全部命中、`ERR_UNSAFE_PORT` 出现 0 次。 --- ## 收尾:已实现、已验证、已部署、已提交 ### 改了什么 **CSS**(`assets/css/app.css`) - `:root` 新增 `--req: #B03A2E`、`--req-w: 700` - 不复用 `--red`(#C0533F):那是**动作红**(删除按钮底色),当文字色放在暖纸底 #F7F3EA 上对比度只有 ≈3.4:1,13-14px 的标签字号偏浅。`#B03A2E` 约 4.6:1。 - `--req-w` 必须是 **700,不能 650/600**:本站标签的基线字重本来就是 600 (`.form-grid > .field > label`、`.card .field > label`、`.mv-row > .mv-lb`、 `.dt-range-label` 四处都是 600)。写 650 会被取整回 600 = **完全没加粗**, 用户看到的只剩「变红」,而他要求的是「红色加粗」。 - 文件末尾新增「必填项统一标记」段,覆盖**四种标签形态**: `label.is-req` / `.mv-lb.is-req` / `.dt-inline-lab.is-req` / `.dt-range-label.is-req`, 用 `!important` 打穿各宿主选择器的特异性。 - **冒号跟着变红**:`.mv-lb::after` / `.dt-inline-lab::after` 原本是灰色 400 字重, 只改标签本体会出现「红字 + 灰冒号」的半截效果。 - **包裹层兜底**(`.field.is-req > label` 等),只作用**直接子级** —— 没写成 `.field.is-req label`,那会把「提醒」那排 checkbox 的小标签一起染红。 - 深色主题给备用色 `#F08A78`。 **模板**(16 个文件 / 30 处) - `_event_fields.php`、`_datetime.php`:标签闭包第三参数 `$star` → `$req`, `$dtLabelStar` → `$dtLabelReq`,删掉死引用 `$evLabelStar`。 - 其余 14 个模板:`<i>*</i>` 换成给标签加 `is-req` 类。 - 用**显式 `.is-req`** 而不靠 `required` 属性反推,原因:CSS 没有父选择器, `:has()` 在旧 WebView 不可用,而且「影院名称」「所属板块」本来就靠 JS 校验、 DOM 上没有 `required`。 ### 验证结果 | 层 | 结果 | |---|---| | 静态断言 `_fb_v94_static.py` | **22 通过 / 0 失败**,反向 **9/9** | | 真浏览器 `_fb_v94_req.py` | **63 通过 / 0 失败**,反向 **5/5** | | 全量流水线(60+ 套件) | 全绿,已部署 | | **线上真实页面** `_fb_v94_live.py` | **31 通过 / 0 失败** | 线上实测(不是看 CSS 源码,是 `getComputedStyle` 的真实值): | 页面 | 必填标签 | 颜色 | 字重 | 同页非必填 | |---|---|---|---|---| | 记一场电影 | 片名 | `rgb(176,58,46)` | **700** | `rgb(44,35,29)` / 600 | | 新建旅程 | 旅程名 | `rgb(176,58,46)` | **700** | `rgb(44,35,29)` / 600 | | 添加成员 | 姓名 | `rgb(176,58,46)` | **700** | `rgb(44,35,29)` / 600 | | 导航页 | 网址、所属板块 | `rgb(176,58,46)` | **700** | `rgb(44,35,29)` / 600 | 冒号也同色(`::after` 实测 `rgb(176,58,46)`),页面上**已无星号**。 反向验证的 5 类注入(每类都必须让**预期的那几条**断言变红): - (a) 把 `--req-w` 调回 600 → 加粗类断言红 - (b) 把 `--req` 调成墨色 → 「必须红」类断言红 - (c) 去掉 `!important` → 裸 `<label>` 那几条红(证明 `!important` 确实必需) - (d) 星号加回来 → 「星号必须消失」红 - (e) 只覆盖 `<label>` 不覆盖 `.mv-lb` → `.mv-lb` 形态那几条红 ### 提交 `daf5ca9` v94 必填项改成「标签红色加粗」,去掉红色星号 (#3) 17 个文件,+176 / -44。
laoyang reopened this issue 2026-10-26 09:02:10 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: laoyang/FamilyBook#3