车东 Che, Dong

Mon 12 October, 2026

03:12 docs(deploy): atomic-release.sh 记录切换后的 realpath 缓存窗口 (#243) » Recent Commits to phpman:master
docs(deploy): atomic-release.sh 记录切换后的 realpath 缓存窗口 (#243)

切换是原子的,请求不会看到新旧混搭;但 PHP 每个进程只把入口脚本的符号链接解析
一次并缓存,realpath_cache_ttl(本机 300s)内常驻的 php-cgi 仍在执行
releases/<旧 id>/phpMan.php。旧对旧是自洽的(入口用 __DIR__ . '/src' 解析自己的
src),所以危害只在验证:2026-10-10 那次部署因此看起来「没生效」约 25 分钟——
新行为缺失,且一个全新 URL 的页脚显示上一版的 GIT_DESCRIBE,而那个值是准确的
(页面确实由上一版代码渲染)。

注释里写明排查姿势:先 readlink current 确认切换,再探测行为差异;若仍是旧行为
就等过 realpath_cache_ttl 重试,别据此判定部署失败或动手改代码。同时注明 opcache
不是原因(validate_timestamps=On 且 revalidate_freq=2)。

Sat 10 October, 2026

21:43 docs(search): 更正 FTS5 JOIN 注释与 CHANGELOG 里的放大机制 » Recent Commits to phpman:master
docs(search): 更正 FTS5 JOIN 注释与 CHANGELOG 里的放大机制

上一提交(9d7904f)把 LEFT JOIN 的放大说成「同名条目挂在两个 source 下
就匹配两次」,这是错的:source 与 section 在构造上是一一对应的(man 用
数字节、pydoc 用 'pydoc'、ri 用 'ri',见 search_index.php:552/641/687),
ON 里的 `m.section = s.section` 因此把 UNIQUE(name, section, source) 那条
路径完全挡掉了,没有哪个名字能在同一个 section 下同时属于两个 source。

真正能放大的是前缀分支:search_fts.name 存的是 expandNameForFts() 的输出
("git-commit git commit"),于是一条记录会匹配同 section 下所有是它空格
前缀的已索引名——"git" 和 "git-commit" 都算。

同时把「未观测到差异」写进 CHANGELOG:新旧 worker 全部收敛后,perl(301)、
ssh、tar、git-commit、autoconf 在线上都是「链接数 == 去重后链接数」;
staging 仍在 v4.11.2-46(改动前),它与生产结果的差异只出现在两台主机索引
本身不同的地方,从未出现重复条目。所以这次改的是「可证明无用且只可能产生
重复」的工作,不是在修一个已经看到的缺陷。

仅注释与文档改动,无行为变化。测试:529 passed, 0 failed。

Co-Authored-By: Claude code 2.1.286 with deepseek-flash <noreply@taotoken.net>
20:49 docs(plan): 记下第二轮分支清理(origin 上 4 个,其中 2 条先移植再删) » Recent Commits to phpman:master
docs(plan): 记下第二轮分支清理(origin 上 4 个,其中 2 条先移植再删)

05-PLAN.md 的 v4.11.3 分支清理一节原本只记了第一轮(两个本地分支)。补上
第二轮,标题从「two stale branches」改成「six」:

- worktree-json-canonical-cache / worktree-ts-bench-help:各 0 独有 commit。
- fix/errorlog-context:已吸收 —— phpManLog 的 WEB/CLI 上下文前缀就是 master
  的 src/log.php:18 _phpManLogContext();该 commit 改的 src/enhance.php 与
  cli/batch-enhance.php 在 v4.11 去 LLM 时已整个删除,没有可移植的部分。
- worktree-simplify-cleanup:13 commit、落后 master 171。10 条被取代(逐条
  写明被什么取代),2 条仍然有效,先移植到 master(9d7904f)再删。另有
  14f751c 是主动拒绝移植的 —— 「取版本最高的 tag」正是野 tag v9.9.10 得逞的
  那条规则,移植它只会放大同一个失效模式。

顺带删掉一句关于 origin 的旧断言,改为可核实的表述。

origin 现在只剩 master。

Co-Authored-By: Claude code 2.1.286 with deepseek-flash <noreply@taotoken.net>
20:45 fix(search): 去掉 FTS5 三处无用 LEFT JOIN,重复行不再吃满 LIMIT 预算 » Recent Commits to phpman:master
fix(search): 去掉 FTS5 三处无用 LEFT JOIN,重复行不再吃满 LIMIT 预算

从 worktree-simplify-cleanup 回收 3e417d3(2026-06-27,从未落地),并补上
它当时漏掉的第三处。

三处 SELECT 都带 `LEFT JOIN search_index_meta m ON m.section = s.section
AND (s.name = m.name OR s.name LIKE m.name || ' %')`,但一处 m 的列都没选,
又是 LEFT JOIN 所以不过滤 —— 它唯一的作用是放大行数:该表是
UNIQUE(name, section, source),同名条目挂在两个 source 下就匹配两次,每份
副本占一个 LIMIT 名额。FTS 路径下游没有去重(source_search.php 的
array_unique 在 apropos fallback 里,只有 FTS 返回空时才走),所以副本直接
进页面,并把真正想要的结果挤出去:展开名能前缀匹配查询的 man 条目会把
man/pydoc/ri 共用的预算占满,pydoc/ri 排不进来。

- source_search.php 两处(带 section 与不带):去 JOIN、加 DISTINCT、
  LIMIT 300 → 500,让去重后的结果数与原先相当。
- search_index.php 的 searchFtsBySource():同样处理,LIMIT 100 不动(另一
  套预算)。这一处是 3e417d3 漏掉的,正是被「饿死」的 pydoc/ri 路径本身。
- search_index.php 的 searchFtsQuery() 的 JOIN 保留 —— 它选了
  COALESCE(m.name, s.name)、m.hits、m.source,是真在干活。

同时回收 4e0f731:代码块复制按钮垂直居中。原 top: var(--space-1) 在单行块
上会被裁切;.code-block 加 min-height: 36px,按钮改 top: 50% +
translateY(-50%),padding 2px 8px → 3px 10px(触屏 4px 10px),pre 右
padding 48px → 56px 留出间距。

测试:529 passed, 0 failed(与改动前基线一致)。

Co-Authored-By: Claude code 2.1.286 with deepseek-flash <noreply@taotoken.net>
19:55 docs(plan): 记录两个陈旧分支的清理结论(核实吸收后才删) » Recent Commits to phpman:master
docs(plan): 记录两个陈旧分支的清理结论(核实吸收后才删)

pr-106 与 local-v4.9.2-mobile-form 都属于「git cherry 报未吸收、实际内容
已在 master」的情况,删除前逐一核对了内容而不是只看 patch-id。结论写进
05-PLAN.md 的 v4.11.3 sweep 一节,免得以后有人再查一遍:

- pr-106(外部贡献者,PR 已于 2026-06-09 关闭未合并):基点落后 master 387
  个 commit,它改的两个函数早已搬进 src/;三处 h() 转义都在重构后的位置
  (search_index.php:196/198、source_search.php:57/83)。
- local-v4.9.2-mobile-form:v4.8.1/v4.8.2 已吸收;v4.9.2 的 PHP 半(format
  links 移入 showFooter)已落地,CSS 半(.search-row/.mode-selector)未落地,
  但同一目标已由 .form-modes + @media(max-width:1024px) 另一套实现达成 ——
  属被取代,非丢失。

另记下 origin 上仍留的 4 个 worktree-* 分支及其未合并 commit 数。

Co-Authored-By: Claude code 2.1.286 with deepseek-flash <noreply@taotoken.net>
10:48 fix(search): FTS 清除失败不再被吞掉,并加 fts/meta 计数不变量 (#242) » Recent Commits to phpman:master
fix(search): FTS 清除失败不再被吞掉,并加 fts/meta 计数不变量 (#242)

rebuildSearchIndex() 里 `DELETE FROM search_fts` 裹在 catch 里,紧邻的
`DELETE FROM search_index_meta` 没有。FTS 清除抛异常时(索引损坏,正是
v4.9.25 那批改动针对的情形),执行继续:meta 被清空 → 每条
`INSERT OR IGNORE` 都不冲突 → $isNew 恒为真 → 每条目在幸存的 FTS 行之上
再插一份。search_index_meta 带唯一约束、search_fts 是 FTS5 无约束,所以
这个重复是永久的,且没有任何日志、返回值或信号;:711 的空索引守卫也看不见
(它数的是 meta,而 meta 刚被重填)。

- 去掉那个 catch:DELETE 失败走函数末尾既有的错误路径(记日志 + ROLLBACK
  + ERROR),与 meta 那条 DELETE 语义一致。那个 catch 本身也没有存在理由 ——
  更早的 `CREATE VIRTUAL TABLE IF NOT EXISTS` 失败时会直接 return。
- 在空索引守卫旁加计数不变量:fts 与 meta 行数必须相等。副作用是
  「FTS INSERT 失败」这一向(原先只计进 $errors 继续跑)现在也拒绝提交 ——
  回滚保住旧索引、错误里带两个计数,比提交半写好的索引好。

新增 test/integration/test_search_fts_clear_failure.php(13 条断言)。FTS5
索引损坏没法在测试里复现,所以用结构方式注入失败:把 search_fts 换成一张
带 BEFORE DELETE RAISE(ABORT) 触发器的普通表 —— 重建分辨不出来,它的
sqlite_master 预检找的是同名 *table*,而 `CREATE VIRTUAL TABLE IF NOT
EXISTS` 遇到已存在的名字是静默 no-op。对修复前的代码该测试 13 条里失败 8 条,
并精确复现缺陷:seed 1 行,跑完 search_fts 里有 2 行。

全量:529 passed, 0 failed。

Co-Authored-By: Claude code 2.1.286 with deepseek-flash <noreply@taotoken.net>
04:46 fix: 三处注释/门槛与实现不符,顺带统一 PHP 版本下限为 8.1 » Recent Commits to phpman:master
fix: 三处注释/门槛与实现不符,顺带统一 PHP 版本下限为 8.1

- **src/config.php:84** — 注释称「Emoji-enhanced output 不设过期,见 cache.php」,
  但那条永不失效的豁免随 emoji 层在 v4.10/v5.0 一起删了,cache.php 里也没有
  对应代码。改为说明现状:已无任何地方写入 ttl=0,遗留行可能仍带 0,
  这正是 cache.php 各处过期判断都写 `ttl > 0` 的原因。

- **install.sh** — PHP 门槛 7.2 与实际不符:
  * 版本判断改为 8.1+,并说明依据(format_common.php 用 array_is_list(),
    8.1;config.php 用 str_starts_with(),8.0;两者每次请求都会走到)。
  * 未知发行版分支的提示文案 7.2+ → 8.1+。

- **install.sh:174** — 注释称 webroot 会得到一份只含 PHPMAN_HOME 的
  phpman.config.php,但 do_deploy_webroot() 只拷 phpMan.php / phpman.css /
  phpman.js 三个文件,并把 PHPMAN_HOME 直接 sed 进 phpMan.php,webroot
  根本不放配置文件。按实现改写。

门槛统一(原先是 7.2 / 8.0 / 8.1 三种说法并存):
README.md、docs/04-SEARCH.md、docs/05-PLAN.md、src/cache.php、
src/format_common.php 一并改为 8.1+。CHANGELOG.md 中的两处 7.2+ 是历史记录,
不改写。

纯注释/文案改动,不改渲染输出,故不动 RENDERER_VERSION。

验证:bash -n install.sh 通过;门槛逻辑对 7.4/8.0 拒绝、8.1/8.5/9.0 放行;
make test 通过;php test/run_all.php 516 passed, 0 failed。

Co-Authored-By: Claude code 2.1.286 with deepseek-flash <noreply@taotoken.net>
01:47 docs: 按最近两个月的变更整体校正文档与 README » Recent Commits to phpman:master
docs: 按最近两个月的变更整体校正文档与 README

逐条对着代码核对,而不是照抄旧文档。主要修正:

- **部署/配置架构整段重写**(05-PLAN):旧文描述的是 webroot 里的
  phpman.config.php、src/web_router.php(从不存在)、scp 部署与 .bak 回滚。
  现在写的是真实的 releases/<tag>/ + current 符号链接原子发布、
  __PHPMAN_HOME__ 占位符机制,以及为什么"固定上传顺序"两头都不对。
- **emoji 缓存行**:CLAUDE/AGENTS/00-INDEX/03-CACHE 都称 v3→v4 迁移的
  preserve list 是"最后一处引用"——该迁移已随 v5.0 删除,grep emoji src/
  现在返回空。改为如实描述:无写入、无读取、无处命名。
- **CACHE_SCHEMA_VERSION 已不存在**:不再描述成"schema 7 → 8"。
  两个 opener 现在每次都 CREATE ... IF NOT EXISTS,无迁移梯子。
- **mcp 不是 URL 格式**:PHPMAN_OUTPUT_FORMATS 只有 html/markdown/json;
  页脚那个失效的 MCP 链接已在 #238 删除(原文写成"已知遗留问题")。
- **README What's New 按 CHANGELOG 重新归位**:多项原记在 v4.11 下的条目
  其实在 [Unreleased];补上 #238–#241(页脚链接、RENDERER_VERSION→2、
  搜索重建原子性、tools_config 原子写、无 section 模式的段收敛)。
- 行号/计数校正:phpMan.php 870→795、src 22→21、测试 296→516、
  RENDERER_VERSION 1→2、search_index.php:483→474、config.php:152→193。
- 07-STRATEGY 加时间戳横幅:它是 2026-07 快照,"正在增强 27,129 页"的前提
  已被 v4.10 推翻;tldr-fetch CLI 从未存在。
- AGENTS.md 补齐「分支约定」与「Commit 署名格式」两节,与 CLAUDE.md 对齐。
- 03-CACHE 的 Related Docs 指向已改名/不存在的文件(SEARCH_FTS5_DESIGN.md
  等),一并修正。

版本号现状在 README 与 05-PLAN 中如实标注:CHANGELOG 最后一版是 4.11.1,
git tag 停在 v4.9.31,而多处在用没有 CHANGELOG 条目的 v5.0。

验证:make test 通过;php test/run_all.php 516 passed, 0 failed;
所有相对 md 链接可解析,代码块围栏配平。

Co-Authored-By: Claude code 2.1.286 with deepseek-flash <noreply@taotoken.net>

Fri 09 October, 2026

21:39 fix(router): 不使用 section 的模式下,section 段按垃圾段收敛 (#241) » Recent Commits to phpman:master
fix(router): 不使用 section 的模式下,section 段按垃圾段收敛 (#241)

/perldoc/File::Find/notaformat 会带着段名渲染出 200(标题 perldoc(notaformat)),
/perldoc/File::Find/mcp 同理 —— 站点为一个无限大的 URL 空间返回同一内容的不同
标题。原因是 normalizeSection() 只要求 ^[A-Za-z0-9_]+$,所以这类段在语法上都是
「合法 section」,malformedSegmentRedirect() 不会处理;而 perldoc/info/pydoc/ri
的 getXPage() 根本不使用 section(仅显示),页面照常找到,也就不会触发 man 那条
resolveManSection() 的 301。

- src/util.php: malformedSegmentRedirect() 增加 $mode 参数;对 man/search 之外的
  模式,section 位上的非格式标记一并视为垃圾段,交给既有的规范化 + 301 机制。
  尾部格式标记恢复等既有行为不变(/perldoc/Foo/mcp → /perldoc/Foo,
  /perldoc/Foo/3pm/json → /perldoc/Foo/json)。路径里本来也无法表达 perldoc 真正的
  section 值(-f/-q 含 '-',normalizeSection() 判空后已由既有逻辑处理),所以不存在
  误删。$mode 省略时保持保守(不新增任何 301)。
- phpMan.php: 调用点把 path mode 传进去。
- test/unit/test_malformed_segment.php: 补 12 条断言(四种模式各一例、带真实格式
  后缀的一例、man/search 不受影响、无 section 不跳转、mode 省略时保守)。

测试 498 → 515 通过。
21:39 fix(config): tools_config.php 的兜底改为「未定义就定义」,部署改为原子替换 (#240) » Recent Commits to phpman:master
fix(config): tools_config.php 的兜底改为「未定义就定义」,部署改为原子替换 (#240)

部署用 `php cli/detect-tools.php > tools_config.php` 就地重写该文件:shell 先
截断、PHP 再写入,中间存在「文件存在但内容为空」的瞬间。此刻到达的请求会让
config.php 的 `file_exists()` 为真、`require` 却什么都没定义,而兜底只在文件
**不存在**时才跑 —— 于是 PHPMAN_HAS_* 全部未定义,请求在 showForm() 里 fatal
(生产日志 2026-10-08 那条 `Undefined constant "PHPMAN_HAS_PERLDOC"`)。

- src/config.php:无论文件是否存在,require 之后统一补齐未定义的常量。空文件、
  只写了头部的文件都退化成「假设工具齐全」,不再 fatal。
- Makefile(staging + production 两处):先写 .tmp 再 mv 原子替换,读者永远看不到
  半写状态。这一层是必需的:若写入恰好截断在语句中间,那是解析错误,兜底救不了。
- test/unit/test_tools_config_fallback.php:5 条断言,用子进程逐个加载 config.php
  (常量不能在同进程内重定义),覆盖缺失/空/仅头部/部分定义/完整定义。对修复前的
  config.php 有 3 条失败(部分定义那例返回 `0uuu`,正是未定义常量的症状)。

Thu 08 October, 2026

18:43 fix(cache): bump RENDERER_VERSION 到 2 —— 页脚改动没能到达已缓存页面 (#238) » Recent Commits to phpman:master
18:37 fix(search): 重建把清除并入事务,并拒绝提交空索引 (#239) » Recent Commits to phpman:master
18:37 fix(footer): 去掉已失效的 /{mode}/{param}/mcp 链接 (#238) » Recent Commits to phpman:master
05:20 fix(test): info 发现的期望值改到与被测实现同一环境,去掉主机相关失败 » Recent Commits to phpman:master
05:20 feat(logcheck): 按日与按类型汇总错误日志,不再只看 tail » Recent Commits to phpman:master

Tue 06 October, 2026

05:39 docs: 去掉 CHANGELOG 与测试里错引用的 (#45) » Recent Commits to phpman:master

Mon 05 October, 2026

22:57 fix(router): format 位置上的垃圾段 301 回规范 URL,不再静默降级成 200 HTML » Recent Commits to phpman:master

Sun 04 October, 2026

20:18 refactor(cache): 删掉回滚设置——两条迁移梯子、CACHE_SCHEMA_VERSION、版本戳 » Recent Commits to phpman:master
11:05 fix(cache): 中央库不再创建/迁移 page-cache 表,去掉两处请求路径上的 500 » Recent Commits to phpman:master

Sat 03 October, 2026

20:05 feat(cache): renderer_version 让渲染器变更自动失效缓存 » Recent Commits to phpman:master