ci(test831): 文档站行号 pin 的下限门(守住不再变多,不解决 #831) - #843
Conversation
#831 量出来的:docs-site 下 141 处 blob/<ref>/<file>#L<N> 引用**全部**钉在 main 上,零个钉在不可变 commit。钉 main 的锚点每次重构都会漂,而漂了不会有 任何东西报错 —— 读者点进去看到一行毫不相干的代码,文档仍然理直气壮。 🔴 这道门守的是下限,不解决 #831。它只保证已知失效的那批不会变多。 召回率是实测的,不是估计的:拿 #831 里已人工确认失效的 10 条回测, 抓到 5、漏掉 5。漏掉的都指向一行长得很正常的代码,只是不是它声称的那一行 —— 那类只有人读上下文才判得出。别拿这道门的绿色去论证 #831 已解决; #831 的解决方案是把行号锚点换成符号锚点。 判据(scripts/check-doc-source-pins.py,判据与边界写在文件头): 1. 文件不存在 2. 行号越界 3. 那一行是"平凡行" —— } / }], / ); / 空行 / 某段注释的中间一行 理由:没有人会故意把说明文字的锚点钉在 } 或空行上 基线 docs/doc-source-pins-baseline.txt 记当前已知失效的 32 条,语义是**只许缩小**: - 出现基线之外的新失效 → 红(这是这道门存在的理由) - 基线里某条已经修好 → 也红,要求删掉它 不这么做基线会变成坟场:修好的和没修的混在一起,数字再也不说明任何事 套件 tests/test831-doc-source-pins(alpine 按 digest 钉版,--network none 可跑): L0 分母:镜像内走目录遍历,断言与仓库里 git ls-files 得出同一份清单 (106 文件 / 70 唯一 pin / 141 处出现)。分叉了就红 —— 否则"容器里绿" 推不出"仓库里绿" L1 干净树必须绿 L2 witnessed-red:新增一个越界 pin → 红在"新的失效 pin"上;复原后回绿 L3 witnessed-red:基线塞一条不失效的条目 → 红在"已经不再失效"上;复原后回绿 L4 边界断言:那 3 条已知盲区必须仍然判不出来。哪天有人"改进"判据这里会红, 提醒他去更新文档里 5/10 那个数,而不是让边界悄悄漂移 已接进 .github/workflows/qa.yml(新增 doc-source-pins job + 4 条触发路径)—— 没接进 CI 的门只是装饰。 途中修掉自己两个错: - 第一版 Dockerfile 里的 python:3.12-slim digest 是我编造的,不对应任何真实 镜像。换成本地实际核对过的 alpine:3.20 digest,python3 由 apk 装。 - checker 第一版把「(pin, 文档) 对数」当成「引用总数」打印,得到 139,而原始 出现次数是 141。是同一份数据我先后量出两个数才发现的;两个数现在都打印。
镜像按 SOURCE_COMMIT=a7b12780 构建;报告自带 runsh_blob,可用 git rev-parse a7b1278:tests/test831-doc-source-pins/run.sh 独立比对。套件原样输出,未手工编辑。 RESULT: PASS exit_code=0 MUTATION_RED new-out-of-range-pin rc=1 MUTATION_RED stale-baseline-entry rc=1
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ccde2842a5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| _ref, path, line = match.groups() | ||
| pins.setdefault((path, int(line)), set()).add(rel) |
There was a problem hiding this comment.
Classify each pin against its captured ref
When a document follows the checker's suggested remediation and changes a main URL to an immutable commit, _ref is discarded here and classify() still reads the current checkout. A valid historical link will therefore be reported as broken whenever that file or line is absent/trivial in HEAD; conversely, an invalid historical link can pass if the same line looks valid in HEAD. Preserve the ref and resolve the referenced revision, or explicitly exclude immutable refs from this drift gate.
Useful? React with 👍 / 👎.
| if line > len(content): | ||
| return ("line-out-of-range", f"(文件只有 {len(content)} 行)") | ||
| text = content[line - 1] |
There was a problem hiding this comment.
Reject zero-valued line anchors
When a new link uses #L0, the regex accepts it but this check only rejects values above the file length. Python then evaluates content[-1], so if the target file ends with a nontrivial line and no trailing newline, the checker reports the malformed, non-one-based GitHub anchor as valid and the floor gate remains green. Treat line numbers below 1 as out of range.
Useful? React with 👍 / 👎.
| new = sorted(set(broken) - baseline) | ||
| fixed = sorted(baseline - set(broken)) |
There was a problem hiding this comment.
Retain baseline entries until their links are repaired
When a source edit shifts a known-broken main anchor from a trivial line onto ordinary but unrelated code, classify() stops including it in broken and this subtraction labels it fixed. CI then explicitly tells the maintainer to delete the baseline entry even though the document was untouched and the anchor remains invalid, moving the known defect into the checker's documented blind spot. A baseline entry should only be removed when the corresponding document link is removed or changed, not merely when its current target stops matching the heuristic.
Useful? React with 👍 / 👎.
| target = REPO / path | ||
| if not target.is_file(): |
There was a problem hiding this comment.
Keep source paths inside the repository
When a document contains a path such as ../../etc/passwd, joining it directly to REPO allows filesystem traversal outside the checkout. For example, blob/main/../../etc/passwd#L1 reads /etc/passwd; because its first line is nontrivial, the checker reports this invalid repository link as healthy. Reject absolute paths and .. components, or resolve the target and verify that it remains beneath REPO before reading it.
Useful? React with 👍 / 👎.
#843 的四条,全部成立。修了判据却没有断言等于没修,所以每条都加了 L5 里的 一个断言(注入 → 期望的红/绿 → 复原 → 回绿)。 ① ref 被丢掉(最重的一条) 第一版把 URL 里的 ref 解析出来就扔了,一律拿当前检出去判。后果是:**有人 按这个工具自己给的建议、把 main 改成不可变 commit,反而会被判成失效** —— 那条链接在它自己的 commit 上是对的,在 HEAD 上未必。反过来,历史版本里 本来就错的链接,也可能因为 HEAD 恰好长得对而蒙混过关。 改:钉了 7–40 位 hex ref 的引用不属于这道门(它管会漂的引用),单独计数 pins_on_immutable_ref。断言:注入一个在 HEAD 上必然越界的 SHA pin,门仍绿。 ② #L0 没被挡 只挡了上界,Python 的 content[-1] 会读到最后一行 —— 最后一行非平凡时,一个 畸形的非 1-based 锚点被判成健康。改成 line < 1 也算越界。 ③ 基线语义写反了 原来是 fixed = baseline - broken:判据不再标某条,就叫人删基线条目。但源码 一漂,一个**仍然错**的锚点会从「平凡行」挪到「普通但不相干的一行」,判据就 标不出它了 —— 文档一个字没动。照原规则 CI 会主动要求删掉这条已知缺陷, 等于把它推进本工具自己的盲区。 改:条目只在**文档里那个引用不存在了**时才判为可删(gone = baseline - present);仍被引用但判据标不出来的单列为 drifted 警告,保留在基线里、也不 计入绿色。 ④ 路径穿越 文档里写 blob/main/../../etc/passwd#L1 时,直接拼到 REPO 上会读出仓库外的 文件,而 /etc/passwd 第一行非平凡 —— 一个根本不指向本仓的链接被判成健康。 改:拒绝绝对路径与 .. 分量,并核解析后仍在 REPO 之下,类别 path-escapes-repo。 L3 的断言文案跟着 ③ 一起改了:它塞进基线的 auth.ts#L1 没有任何文档引用, 所以现在走的是「对应的引用已经不在文档里了」这条。 验证(容器内,--network none): [L5] ① 不可变 ref 被排除且单独计数(pins_on_immutable_ref=1),门仍绿 ② #L0 判为 line-out-of-range rc=1 ④ 仓库外路径判为 path-escapes-repo rc=1 ③ 引用仍在文档里时不判为可删,并给出 drifted 警告 MUTATION_RED new-out-of-range-pin rc=1 MUTATION_RED stale-baseline-entry rc=1 RESULT: PASS 退出码 0
source_commit=c6338f272a0474d84dc7f22c0fd482b9ca5de77a 按 pre-pr-selfcheck §12:改动改变了套件下次跑看到的东西,报告要一起更新。 RESULT: PASS exit_code=0
|
四条全部成立,已修( ① ref 被丢掉 —— 这条最重第一版把 URL 里的 ref 解析出来就扔了,一律拿当前检出去判。后果是: 有人按这个工具自己给的建议、把 这等于我一边在报告里劝人「钉 SHA 或改符号锚点」,一边让门去惩罚照做的人。 改:钉了 7–40 位 hex ref 的引用不属于这道门(它管的是会漂的引用),单独计数 断言:注入一个在 HEAD 上必然越界的 SHA pin,门必须仍然绿且计数 +1。 ②
|
上一轮我在 #843 里改了判据(四条审查缺陷),而这个 PR 基于它 —— 也就是说它 当时带着旧判据跑出来的绿色。这次把 ci/doc-source-pin-guard 合进来重跑, L5 的四条断言都在,分母仍是 53/107。 RESULT: PASS exit_code=0 L5 ①不可变 ref 排除 ②#L0 越界 ③drifted 保留 ④路径穿越 合并时报告文件冲突(两边都重新生成过)。报告是产物,解法是合完重新生成, 不是手工挑行 —— 手工合出来的报告不对应任何一次真实运行。 顺带:合并未提交时那次构建红在 blob 绑定上(镜像里的 run.sh 与 HEAD 声称的 不是同一份),说明那道绑定确实在起作用。
一个还没人知道的跨 PR 耦合:#810 / #834 合入后,这道门会红本轮没有新写代码,做的是一次合并前的耦合测量 —— 结果是三个 PR 之间有一处硬耦合,谁先合谁就得多做一步,而现在四个 PR 里都没有写这件事。 测出来的在 #845 的树上(已含 #843 的门 + 已改完的 mcp-tools / rest 锚点),模拟 #810 与 #834 各自的改动,然后跑门: 也就是说:#810 或 #834 合入之后, 这不是门的 bug,正是它按设计在做的事 —— 基线只许缩小,修好了就得删,不删会变成坟场(见 #843)。但它构成一个合并顺序上的动作项,需要有人知道。 每种顺序下要做什么
两种都很小,但都不能忘。忘了的表现是「一个只改文档的 PR 把 CI 弄红了」,而红的原因看起来跟那个 PR 毫无关系。 顺带一个好消息合完之后机械可证的失效面从 32 降到 1(只剩 这次测量本身也栽了一跤第一遍我用 同一件事在 |
上一轮我在 #843/#810/#834 上贴了一条跨 PR 耦合提醒:那两个 PR 一合,基线里 对应的条目会变 stale,门就红在「请从基线里删掉」。提醒是散文,执行的人还是 得自己去数该删哪几条 —— 把一个机械操作交给了记忆力。 这一轮把它变成一条命令: python3 scripts/check-doc-source-pins.py . --write-baseline 🔴 这个开关离「一键把门变绿」只差一个条件判断,所以它**只许缩小**: 重算若会引入基线里没有的条目(= 出现了新的失效 pin),它拒绝写并退出非零。 新失效该做的是把链接改对,不是追认进基线。 L7 两个方向都断言,不只测它能用: ① 干净树:不改写,报「基线已经是最新的」,且文件字节未动 ② 注入一个新失效 pin:必须拒绝(rc≠0),且**确认基线没被写** —— 只断言"它红了"不够,要断言"它红了而且没写" ③ 造一个「引用消失」场景(把某条 pin 的引用改钉不可变 SHA): 必须删对、条数变小、表头注释保留、门随后转绿 顺带在门红的提示里直接给出这条命令,不让人再去翻文档。 验证(容器内,--network none): [L7] ① 干净树:不改写,报「已是最新」 MUTATION_RED write-baseline-refuses-new-failure rc=1 ③ 引用消失时删对了(5 → 4),表头保留,门转绿 复原后回绿 ✓ RESULT: PASS 退出码 0 一处说明:这个改动逻辑上属于 #843 的脚本,但落在链尾(#845)。理由是 #843 → #844 → #845 是一条依赖链,改在链首要把两级重新合并重跑一遍;而三个 PR 是按序 合进 main 的,落在链尾到达 main 的时间相同。写在这里免得有人以为放错了地方。
把上一条的耦合提醒变成一条命令(
|
我在建 test831 时写过「没接进 CI 的门只是装饰」,下一轮建 test846 时自己就没接。 是这轮做 qa.yml 协调分析、查各 PR 各改了什么时发现的 —— 不是别人提的。 新增 doc-claims job + 3 条触发路径,形状与 #843 的 doc-source-pins 一致 (同一个锚点后追加、job 附在文件末尾)。
|
qa.yml 的四处改动我摊开对照过了,冲突面只在两处 paths 清单、且是机械并集;建议合并顺序 #803 → #843 → #846。完整对照贴在 #803 的这条评论。 |
上一轮我给出的是「冲突了怎么解」。这一轮做的是让冲突不发生。 冲突源于所有人都追加在同一处: paths 五个 PR 都插在 - 'tests/test746-setup-bun-pin/**' 之后 job #843 与本 PR 都追加在文件末尾(#803 插在 qa: 之前,#798/#801 插在 59 行) 改动: paths 改插到 - 'server/**' 之后 —— 距离 test746 九行,超出 git 默认上下文窗口 job 从文件末尾挪到 jobs: 之后(这个位置没有别的 PR 用) paths 是集合、jobs 是映射,位置变化不改变行为。结构断言(每个 job 有 runs-on 与非空 steps)已跑过。 这样合并时不需要任何人去解那个「公共上下文属于双方」的冲突 —— 那个坑我在 #803 上写清楚了,但最好的处理是不让人踩到它。
|
这个 PR 是我写的,自审没有独立性,所以我做的是评审者会想要的那件事:跑变异,证明它不是假绿。 变异证据基线(未变异): 变异A —— 新增一条失效 pin(这道门的立身之本:守住不再变多): 见证真红,而且三个计数都跟着动了(139→140 / 70→71 / 32→33),不是靠一句 FAIL 唬人。 变异B —— 清空基线文件(基线不该能被抹掉换绿): fail-closed。 🔴 我第一发变异打偏了,值得记下来第一次我把那条假 pin 追加到 DOC_ROOT = "docs-site"
🔴 由此暴露的范围限制:这道门不覆盖
|
|
量了一下这道门当前扫描根内有没有盲区 —— 因为它对读不出的文件是静默跳过的: except (OSError, UnicodeDecodeError):
continue实测(与 checker 的 当前 (对照:若按 #852 把 顺带补一句给评审:这个 |
守 #831 的下限。不解决 #831,先把这句放最前面。
它承诺什么、不承诺什么
上一条评论量出来的:docs-site 下 141 处
blob/<ref>/<file>#L<N>引用全部钉在main,零个钉在不可变 commit。钉main的锚点每次重构都会漂,而漂了不会有任何东西报错。这道门只保证一件事:已知失效的那批不会变多。
召回率是实测的,不是估计的 —— 拿 #831 里已人工确认失效的 10 条回测判据:
漏掉的都指向一行长得很正常的代码,只是不是它声称的那一行。别拿这个 job 的绿色去论证 #831 已解决 —— 这句话同时写在 checker 文件头、run.sh 头、qa.yml 的 job 注释和基线文件头,四个地方。
判据与基线
scripts/check-doc-source-pins.py判三类:文件不存在 / 行号越界 / 那一行是平凡行(}、}],、);、空行、某段注释的中间一行)。第三类的理由:没有人会故意把说明文字的锚点钉在}或空行上。docs/doc-source-pins-baseline.txt记当前已知失效的 32 条,语义是只许缩小:第二条是刻意的。不这么做基线会变成坟场,修好的和没修的混在一起,数字再也不说明任何事。
套件
tests/test831-doc-source-pins,alpine 按 digest 钉版,--network none可跑,不碰网络也不碰任何真实节点。几点值得单独说:
.git,checker 走目录遍历;这一层断言它与仓库里git ls-files得出同一份清单。分叉了就红 —— 否则「容器里绿」推不出「仓库里绿」。报告
docs/tests/report-test831.txt自带runsh_blob=6d4dca00…,可用git rev-parse a7b12780:tests/test831-doc-source-pins/run.sh独立比对。已接进 CI
.github/workflows/qa.yml新增doc-source-pinsjob 与 4 条触发路径。没接进 CI 的门只是装饰。qa.yml,但它改的是recovered-suites里的步骤顺序,这里是在文件末尾追加新 job + 在触发路径列表里插 4 行,合并顺序上应该是机械并集。途中修掉自己两个错
python:3.12-slimdigest 是我编造的,不对应任何真实镜像。换成本地实际拉到并核对过的alpine:3.20digest,python3 由 apk 装(3.12.13)。