车东 Che, Dong

Mon 09 November, 2026

02:40 fix(cache): RENDERER_VERSION 7 —— #282 改变了 404 兜底正文(#282) » Recent Commits to phpman:master
fix(cache): RENDERER_VERSION 7 —— #282 改变了 404 兜底正文(#282)

v4.12.25 把 pydoc 404 兜底从 `pydoc3 -k` 扫描改成 FTS 模块索引,但当时没有
bump 版本号。not_found 行按 24h TTL 缓存且带正文,部署前写入的行会继续返回
扫描时代的旧正文(或扫描超时留下的空正文)——发布本身到不了它们。

按 CLAUDE.md「渲染输出变化必须 bump」补到 7,并写明这一版改了什么、旧行里
是什么。

Co-Authored-By: Claude code 2.1.311 with deepseek-v4.1-flash-1017 <noreply@taotoken.net>
02:08 perf(pydoc): 404 兜底不再跑 8–20s 的 pydoc3 -k 扫描(#282) » Recent Commits to phpman:master
perf(pydoc): 404 兜底不再跑 8–20s 的 pydoc3 -k 扫描(#282)

根因(已复现):`/pydoc/<非点号未知名字>` 的页面兜底会再跑一次 `pydoc3 -k`
关键词扫描。实测:

    /pydoc/DefinitelyNotAModuleZzz123 → 404, 20.409s
    /pydoc/sos.report.plugins.kafka   → 200,  5.895s(含点号,跳过扫描)

扫描是「你是不是想找」的提示,不是存在性检查 —— 主查询 `pydoc3 <name>` 已经
失败过。按一天的爬虫流量量过:126 个不同的非点号名字,每个 8–20 秒,零结果。
最近的超时名字是 bzxmlrpc / bzmysql / Collection,全小写的猜测名同样吃满,
所以不能只按 CamelCase 过滤。

修法:给 `getPydocSearchPage()` 增加 `$allowShellScan`,页面兜底传 false,
改由已有的 FTS 模块索引(毫秒级)回答提示;搜索页保持扫描——用户在那儿确实
要搜索,关键词映射正是扫描擅长的。

没有扫描时,空结果按「索引里没有」处理(短 TTL),不冻结成 24h 的 404 ——
索引可能滞后,那不等于模块不存在(#247 的教训)。

ri 侧不需要改:页面兜底本来就没有第二次子进程(phpMan.php:607),剩下的
0.4–1.3s 是主查询本身。

测试:pydocShouldShellScan 的决策矩阵 + 调用点 tripwire + 「不扫描时 <2s」
(实测 0.012s)。全量 992 条断言、语料门全绿。

Co-Authored-By: Claude code 2.1.311 with deepseek-v4.1-flash-1017 <noreply@taotoken.net>
01:28 fix(release): 基线解析不出来时失败关闭,不再静默放行 » Recent Commits to phpman:master
fix(release): 基线解析不出来时失败关闭,不再静默放行

上一版守卫在 PREV_RV 为空时(上一个 tag 早于 RENDERER_VERSION,或引号风格
不同)会与任何 HEAD_RV 都不等,于是静默放行 —— 一个失败开放的判据。

- 基线为空 → 中止,并提示用 RENDERER_VERSION_BASELINE=<n> 显式提供基线。
- sed 同时接受单引号与双引号,引号风格变化不会让基线失效。
- HEAD 侧解析不出来的检查保留。

实跑验证:合成仓库里「基线 tag 缺定义」→ 拒绝;带显式基线 → 放行。
真实 tag v4.12.24 与 HEAD 都解析为 6。全量 982 条断言全绿。

Co-Authored-By: Claude code 2.1.311 with deepseek-v4.1-flash-1017 <noreply@taotoken.net>
01:21 feat(release): 语料黄金变了就必须 bump RENDERER_VERSION(#255) » Recent Commits to phpman:master
feat(release): 语料黄金变了就必须 bump RENDERER_VERSION(#255)

v4.12.23 部署了两处渲染修复,线上却原样返回旧输出:页面缓存按
RENDERER_VERSION 失效,而那次没有 bump。语料门直接调渲染函数、绕过缓存,
所以它能证明"渲染器对了",证明不了"线上会变"。发布前置检查补上这个缺口:

- 与上一个发布 tag 比较 test/corpus/golden:变了就要求 RENDERER_VERSION
  的**解析数值**也变,否则中止发布。比较数值而不是 diff 行数 —— 删掉定义或
  只改注释都会让行计数非零,守卫会被绕过(安全评审指出)。
- 触及渲染路径(format_*/source_*/web_header/footer/exec/tldr)但黄金没变时
  只打提示,不中止:无谓的 bump 会让整库缓存失效、部署后瞬间全量重建,
  正是 #259 那类 503 的触发条件。硬判据只认有证据的输出变化。
- 没有 src/config.php 的检出(测试夹具)跳过这条守卫。

测试:脚本正文 tripwire + 四条路径实跑(黄金变未 bump → 拒绝;渲染路径变未
bump → 放行并提示;已 bump → 放行;删掉定义 → 拒绝)。全量 981 条断言全绿。

Co-Authored-By: Claude code 2.1.311 with deepseek-v4.1-flash-1017 <noreply@taotoken.net>
00:25 fix(cache): RENDERER_VERSION 6 —— 渲染修复需要让旧缓存失效(#255 #281) » Recent Commits to phpman:master
fix(cache): RENDERER_VERSION 6 —— 渲染修复需要让旧缓存失效(#255 #281)

v4.12.23 部署后线上仍在返回旧输出:页面缓存按 RENDERER_VERSION 失效,
而两次渲染修复(SGR 状态机、pydoc L2 模式门)只改了渲染代码,没有 bump。
命中缓存的 HTML 仍是 `<b>-f <u></b>ARCHIVE</u>`,markdown 仍是
`**-f _**ARCHIVE_`,perldoc 正文仍是 `### chomp`。

bump 到 6,让缓存行在首次读取时全部失效、按新渲染器重建。

Co-Authored-By: Claude code 2.1.311 with deepseek-v4.1-flash-1017 <noreply@taotoken.net>

Sun 08 November, 2026

23:51 fix(render): pydoc 的 L2 启发式只在 pydoc 模式生效(#281) » Recent Commits to phpman:master
fix(render): pydoc 的 L2 启发式只在 pydoc 模式生效(#281)

`detectL2IndentedPatterns()` 的两条 pydoc 规则(`    class Name(` 与
`    name(`)没有模式门,于是 perldoc/man 正文里恰好 4 空格缩进、以
`name(` 开头的行会被提升为 L2 小节:

    chomp( LIST )        →  ### chomp
    nasty_break() is ... →  ### nasty_break

perldoc 三页语料共 58 行受影响:正文被当成标题,TOC 里冒出假条目。
给两条规则加上 `$mode === "pydoc"` 的门;man 页不受影响(see-also 是 7 空格)。

语料门先红后绿:perldoc-perlfunc/perlvar/DBI 的黄金在 8.3 上重新生成,
差异仅为这些行从 `### xxx` 变回正文,已逐行核对。

Co-Authored-By: Claude code 2.1.311 with deepseek-v4.1-flash-1017 <noreply@taotoken.net>
23:46 test(corpus): 黄金/文本完整性/结构/JSON 四条检查(#255) » Recent Commits to phpman:master
test(corpus): 黄金/文本完整性/结构/JSON 四条检查(#255)

Co-Authored-By: Claude Code <noreply@anthropic.com>
23:46 test(corpus): 采集六页真实语料与黄金输出(#255) » Recent Commits to phpman:master
test(corpus): 采集六页真实语料与黄金输出(#255)

Task 8 收尾:在生产机(man 2.12.0 / groff 1.23.0 / perl 5.38.2)冻结
六页真实工具输出(man-ls、man-tar、man-ssh_config、perldoc-perlfunc、
perldoc-perlvar、perldoc-DBI,共 922KB),黄金由生产 /usr/local/php83
(PHP 8.3.36)对冻结 raw 重渲染生成——raw 字节是唯一事实源,黄金只是
它的确定性投影。

- test/corpus/raw/:六页冻结输出;test/corpus/golden/:6×.md +
  6×.fingerprint;test/corpus/VERSIONS.md:工具版本 + 每页字节数/sha256。
- test/corpus/normalize.php(Ruling 22):corpusNormalizeText() 把
  邮箱改写为渲染器混淆后的 "user AT host" 形式,raw 与 HTML 两侧
  token 才可比——内容保持归一化,不放松断言。
- test/unit/test_corpus_normalize.php:补 5 条邮箱归一化断言。

审阅:六页 .md 无 ESC/\x08/?(0lines)/空标题/超长行/控制字符;
ad59aab、d2086fe 两轮修复后 markdown 无 _** 交错(man-tar 28 行、
man-ssh_config 4 行均呈 **_ 正确形式)。

门禁:本地 PHP 8.5 make test-corpus 六页 [OK] + [PASS] xhtml
(6 页 dtd-valid);生产 PHP 8.3.36 check 六页 [OK],xhtml 因该机
无 xmllint 按设计 [SKIP](DTD 校验由本地对同样字节完成)。token
multiset 六页 missing=0。make test-unit 949 passed, 0 failed。

Co-Authored-By: Claude code 2.1.311 with glm-5.3-flash <noreply@taotoken.net>
23:46 test(corpus): 语料清单、归一化库与结构指纹(#255) » Recent Commits to phpman:master
test(corpus): 语料清单、归一化库与结构指纹(#255)

- test/corpus/pages.txt:6 页语料清单(tab 分隔,# 注释,空 section)
- test/corpus/normalize.php:corpusReadManifest / corpusStripFormatting /
  corpusNormalizeText / corpusTokens / corpusMissingTokens / corpusTextFromHtml
- test/corpus/fingerprint.php:jsonStructureFingerprint 从
  structure_regression.php 原样移出,供后续 corpus 工具复用
- test/unit/test_corpus_normalize.php:21 断言;run_all.php 登记后
  853 passed, 0 failed

Co-Authored-By: Claude code 2.1.311 with deepseek-flash <noreply@taotoken.net>
23:46 test(corpus): 有清单但 raw 为空时必须失败,不再静默 SKIP(#255) » Recent Commits to phpman:master
test(corpus): 有清单但 raw 为空时必须失败,不再静默 SKIP(#255)

Co-Authored-By: Claude Code <noreply@anthropic.com>
23:46 test(corpus): xhtml 断言整体置于 xmllint 存在性守卫内(#255) » Recent Commits to phpman:master
23:46 test(corpus): XHTML 整页校验,DTD 随仓库离线解析(#255) » Recent Commits to phpman:master
23:46 fix(render): SGR 状态机替换交错正则 + 语料抽文行内标签不再变空格 » Recent Commits to phpman:master
23:46 fix(markdown): cleanTerminalOutput 换用共享 SGR 状态机,消除交错标记 » Recent Commits to phpman:master
23:46 feat(tldr): 增加 PHPMAN_TLDR_DISABLE,供语料渲染离线运行(#255) » Recent Commits to phpman:master
23:46 feat(corpus): render 子命令——由冻结 raw 生成黄金(#255) » Recent Commits to phpman:master
23:46 feat(corpus): capture 子命令——在生产机冻结原始输出(#255) » Recent Commits to phpman:master
23:46 docs(corpus): 实施记录与修订——语料门发现的两个线上缺陷(#255) » Recent Commits to phpman:master
23:46 docs(corpus): #255 实施计划(8 个任务) » Recent Commits to phpman:master
23:46 ci: 新增 corpus 作业(PHP 8.3 + xmllint)(#255) » Recent Commits to phpman:master