docs(plan): perldoc 不进索引 —— 与 man 的 3pm/3perl 重叠 3,888/4,437,净增 98 条 生产实测:@INC 6,419 个模块名,Perl 的 man page 4,437 条,两边都有 3,888。只在 @INC 里、没有 man page 的 2,531 个里,2,404 个文件根本没有 POD,127 个有 NAME 行,98 个有真正的 "名字 - 说明"。 Debian 把 .pm 的 POD 同时装成 3perl/3pm man page,所以 perldoc File::Find 和 man File::Find 渲染同一份文档(18,425B vs 18,443B)。补 6,419 行索引, 3,729 行重复,有效新增 98 条 —— 不值得,而且描述还得读 .pm 才有内容。 Co-Authored-By: Claude code 2.1.311 with deepseek-v4.1-flash-1017 <noreply@taotoken.net>
fix(search): 分节列表不再跑 pydoc/ri 的 FTS 查询 (#280) /search/(1) 除了整段 1 区 man page,末尾还挂着三条 Ruby 类: ActiveRecord::Migration::Compatibility::V5_1、Regexp::Syntax::V1_9_1、 Regexp::Syntax::V2_4_1。 原因:html / markdown 分支无条件调用 searchFtsBySource(),而分节列表 到 FTS 那里就是一个关键词 "1"* —— 任何以 1 开头的名字都会命中。 json 分支从一开始就 gate 了($sectionOnly ? [] : …),html / markdown 只 gate 了 shell 回退,没 gate FTS 查询本身。 pydoc 和 ri 没有 man 分区,对一个分节列表本来就该贡献零条。 RENDERER_VERSION 4 → 5:已缓存的分节列表页面还带着那个块,而页面 缓存不 bump 就继续发旧内容。 Co-Authored-By: Claude code 2.1.311 with deepseek-v4.1-flash-1017 <noreply@taotoken.net>
fix(search): MATCH 收窄到 {name description},pydoc/ri 不再存常量描述 (#280)
裸 MATCH 会搜 search_fts 的全部四列,其中两列在这里是负担:section 是每行
一个来源常量(pydoc / ri / 3pm …),body 按设计永远是空串。再加上 pydoc
存了 330 份 "Python 3 module"、ri 存了 3,699 份 "Ruby class/module",
/search/python 会返回 100 个任意模块。
生产只读实测(收窄前 → 收窄后):
ri 3,720 → 23
1 3,198 → 255
python 352 → 352 ← 收窄治不了它,那些命中全是常量描述
module 4,295 → 4,295 ← 同上
所以两半都做:scopeFtsQuery() 把查询包成 {name description} : (…);
重建时 pydoc/ri 的 description 写 ''(清单命令本来就没有摘要)。
没有只收窄到 name:name : ("copy"* AND "files"*) 命中 0 行,而
{name description} : (…) 命中 cp(1)。/search 的语义是 apropos —— 查
NAME 段(名字 + 一句摘要),只留 name 会把每个多词查询重新推回 shell,
正是 #280 要消灭的东西。
顺带:pydoc/ri 的页面标题不再带那句常量;getTitleDescription() 对两者
返回空串。§7.5 的 source_state 复核后不建 —— 它的「点号子模块」例子
根本走不到 shell(pydocSearchWorthScanning() 对含点号直接返回)。
RENDERER_VERSION 3 → 4:已缓存的 /search/python 页面还挂着旧的 100 条。
生产索引里仍是旧常量,要 `make reindex` 跑一次才生效。
Co-Authored-By: Claude code 2.1.311 with deepseek-v4.1-flash-1017 <noreply@taotoken.net>
docs(plan): §7 补实现状态 —— 并记下 §7.5 的反对意见是对的(#280) 实现(d01333d / v4.12.20)与 §7.5 的设计有出入,如实记进去: - 索引:pydoc/ri 的代码从 v4.4.0 就在,缺的只是重建 —— 已重建, man 10,093 / pydoc 330 / ri 3,699; - 门控:用的是 sourceIsIndexed()(有没有行),不是 §7.5 设计的 source_state; - pydoc 解析 bug("Please wait…" 被当成模块名)已修,重建后清零; - perldoc 未做。 §7.5 的第 1 条反对意见经实测确认:MATCH python 命中 330 行(全部 pydoc 行, 因为 description 是常量 "Python 3 module"),而 name : python* 只命中 1 行。 所以「按行数开门控」确实会被描述列带偏 —— 这是粗糙版,不是终版。 三条待补写进文档:source_state、MATCH 收窄到 name 列、perldoc 读侧。 Co-Authored-By: Claude code 2.1.308 with deepseek-flash <noreply@taotoken.net>
docs(plan): 按当前代码复核并修正 9 个 open issue 的设计 对 `docs/10-OPEN-ISSUES-PLAN.md` 逐个设计对照 HEAD (`d01333d`) 复核。没有改 代码,只把不可实现或不成立的部分改成能落地的形式。 ## #258 RequestBudget - **单位写错会杀掉 pydoc3 -k。** `phpManExec()` 第 4 个参数是**秒**,唯一显式 调用点传的是 `PHPMAN_PYDOC_K_TIMEOUT` = 20。原文按毫秒处理 (`min($timeoutMs ?? PHPMAN_CMD_TIMEOUT*1000, …)`),20 会变成 20ms, `ceil(20/1000)` = 1 秒,10–14 秒的扫描在 1 秒时被杀。改为保持秒单位、只在 边界换算。 - **0 必须是「不限」。** `remainingMs() = max(0, deadline-now)` 把 `PHPMAN_REQUEST_TIMEOUT_MS=0` 算成 deadline 已到,于是每条命令都不 spawn; 同样会破坏现有 `PHPMAN_CMD_TIMEOUT=0` 的契约。改为 0 → `PHP_INT_MAX`。 ## #254 RenderResult - `INDETERMINATE` 的「永远 `cacheable() === false`」与 #258 D 的「保留 600s 短 TTL 行」矛盾。改为:不写最终负行,但可写 600s 超时行;接口用 `ttl()`/`cacheStatus()`,不用二值 `cacheable()`。 - 不变量 3 要求 `rc=124` 不产生负行,但 `getManPage()` 目前把 rc=124 收敛成 `''`,man 分支会写 24h 行。迁移必须给 `getManPage()` 加 indeterminate 出参, 否则不变量在 man 上不成立。 - `ALTER TABLE cache` 打错了库:`cache` 自 v4.11 起只在 `phpman_cache_<mode>.db`,中央 `cacheDb()` 没有这张表。必须按分片执行,且 `fetchRow()` 的 SELECT 要带上新列。 ## #259 并发闸 - 闸只包 `cacheOrExecute()` 的 primary generator 不够:真正贵的是 `cacheOrSearchFallback()` 在它返回**之后**才跑的 `$fallbackGenerator()` (pydoc3 -k 8–14s),以及 man 未命中的 `apropos`。这两族突发完全不取 token。 取槽点应放进每个会 spawn 的入口(或直接放 `phpManExec()`)。 - 闸满时不能返回 `''` —— 那会被 miss 分支写成 24h 负行;必须走显式 503。 ## §7 / #280 搜索全链路 - **「pydoc/ri 各 0 行」不等于「代码没灌」。** `rebuildSearchIndex()` 已经在跑 `pydoc3 modules` 与 `ri -l`(`src/search_index.php:652,716`);0 行是索引没 重建。先跑 `make reindex` 再谈代码;真正缺的只有 perldoc。 - `pydoc3 modules` 只列 top-level(约 331 行),不是 6,197;`walk_packages` 的 onerror/截断说法不适用于现有实现。含点子模块仍然不在索引里。 - 常量描述会污染 `MATCH`:搜 `python` 命中所有 `Python 3 module` 行,级联据此 跳过 `pydoc3 -k`。回退开关不能只数行数,要用完整性标记 + `name` 列过滤。 - perldoc 行会被 `parseAproposLine()` 静默丢弃(只接受数字/n 形式的 section), 灌了也显示不出来。 - `search_fts` 是**普通** FTS5 表,不是 external-content;只有 `cache_fts` 是。 ## 其余 - #253:验证写成 `release-reindex` 的 prerequisite 会在 rebuild **之前**跑; 移到 recipe 里、rebuild 之后。 - #255:`xmllint --valid` 在 DTD 拉不到时照样 exit 0;golden 需关 TLDR、 归一化时间戳/日期。 - #256:p95 用了有偏样本(机器格式只记慢的/大的,HTML 是不等概率抽样); 分位数只能用等概率样本。 - #257:`h()` 用 ENT_SUBSTITUTE,非法 UTF-8 不能 round-trip; `phpManLogSafeField()` 上限是 maxLen+3 字节,且 `substr` 会切断多字节。 - 头部:9 个 open issue 里已不含 #253/#265(均已关闭),补上 #280/#281, 基线更新到 `d01333d`。 验证:代码块围栏 26 个配平;docs/ 相对链接全部可解析; php test/run_all.php 843 passed, 0 failed。纯文档改动。 Co-Authored-By: Claude code 2.1.311 with deepseek-v4.1-flash-1017 <noreply@taotoken.net>
fix(search): 索引已覆盖的来源不再回退到命令行;pydoc 索引里那句广告删掉 (#280)
两件事。
一、索引其实一直有,只是没建
rebuildSearchIndex() 从 v4.4.0 起就索引 pydoc3 modules 与 ri -l,但生产的
索引里这两个来源**各 0 行** —— 所以每个关键词搜索都退到 pydoc3 -k(8–14s)
和 ri。跑一次重建:
man 10,093 / pydoc 331 / ri 3,699,14,138 行,9.3 秒
线上那条 22:13 的 last_indexed 是 indexAproposLines() 写的(关键词搜索顺手
灌 man 行),不是完整重建 —— 这就是「代码有、数据没有」的原因。
二、sourceIsIndexed():索引覆盖了就别再 shell
回退原来只看「FTS 返回空」。这在索引没建的机器上是对的,但索引建好之后,
FTS 空的意思是「没有这个模块」,不是「答不了」—— 而回退要花 8–14 秒把整个
Python stdlib import 一遍来确认同一个「没有」。
开关取「search_index_meta 里该来源有没有行」,不写死列表 —— 新装的机器索引
没建时仍然保留回退,而不是搜索什么都搜不到。
man 的 apropos 回退**保留**:0.03 秒,且能兜住上次重建之后新装的 man page。
验证(临时给 phpManExec 加日志,看真实子进程):
/search/zzz_no_such_thing_xyz → apropos 'zzz…' ← 只有 man 回退
(没有 pydoc3 -k,没有 ri)
/search/json → 没有 apropos、没有 shell
三、顺带修 pydoc 解析:索引里有一句广告
原来是「遇到第一个空行就当成正文开始」,而 `pydoc3 modules` **第一行就是空行**,
于是那句 "Please wait a moment while I gather a list of all available modules..."
被当成模块名灌进了生产索引 —— 搜 "wait" 能搜到它。
改成先等 "Please wait" 横幅出现,再开始收模块。
测试:新增 test/unit/test_source_indexed.php(7 条,子进程跑,因为函数按来源
做了静态记忆)。全套 843 passed / 0 failed。
Refs: #280
Co-Authored-By: Claude code 2.1.308 with deepseek-flash <noreply@taotoken.net>
docs(plan): 9 个 open issue 的解决方案设计 + 搜索全链路(#280) `docs/10-OPEN-ISSUES-PLAN.md`:依赖图、四阶段落地、逐 issue 设计 (RenderResult / RequestBudget / 并发闸 / 指标与质量门),以及新增的 §7 —— 把 pydoc / ri / perldoc 灌进 search_fts 后拆掉回退,对应 #280。 其中 #256 一节补了实现状态(A/B/C 已实现,D 未做)、B 里「5xx 分族复用 logcheck 配对」没有照做的原因(logcheck 从 stdin 进来、无法 include 兄弟 文件),以及新增的 §E:`.prom` 的落盘与消费 —— 决定暂不加 cron。 `docs/00-INDEX.md` 加上这份文档的索引条目。 Co-Authored-By: Claude code 2.1.308 with deepseek-flash <noreply@taotoken.net>
docs: 去掉误引的 #266 —— 那是安装器 PR,不是这次的分节列表修复 上一个提交(17b3251)的 CHANGELOG、util.php 注释和测试标题都把这次修复挂在 #266 上,而 #266 是已合并的安装器 PR("harden installer updates, webroot deploys and data ownership")。仓库里没有对应的 issue,所以不引用编号比引用 错误的编号好。 提交信息本身(Refs: #266)已推送,无法在不 force-push master 的前提下修改 —— 以本提交为准。 Co-Authored-By: Claude code 2.1.308 with deepseek-flash <noreply@taotoken.net>
fix(search): 分节列表不再跑 pydoc/ri 级联 —— search/(1) 从 21s 降到 apropos 的 0.03s (#266) 24 小时窗口里 mode=search 的 14 次渲染 **100% 超过 3s、p50 21.6s**,是全场最慢的 一族,其中 12 次是分节列表。 慢的不是 apropos:`apropos -s 1 .` 在服务器上 0.03 秒返回整节。慢的是它之后跑的 两件事 —— pydoc3 -k '(1)' 7.97s,0 行 ri '(1)' 0.39s,0 行 分节列表不是关键词,这两条命令对它**不可能**命中,纯属白跑。 改法:新增 isSectionListingQuery(),在三种格式分支里给 pydoc/ri 的回退各加一道闸。 顺带修掉一处不一致 —— 原先只有 HTML 分支在「apropos 有结果」时才跳过 pydoc, markdown/json 则无条件回退,所以同一页的机器格式比 HTML 慢得多。 验证方式是把 phpManExec 临时加了一行日志,记录实际跑了哪些命令: NEW /search/(1) → apropos -s '1' . (没有 pydoc3、没有 ri) NEW /search/talk → apropos 'talk'; ri 'talk' (关键词级联照旧) 有一处差点写错:JSON 分支原来用 `else` 接回退,而分节列表同样会得到空的 FTS 结果 —— 直接 `else` 会把它又推回 shell。改成 `elseif (!$sectionOnly)`。 测试:test_normalize.php 加 8 条((1)/(9)/(n)/空白/关键词/(10)/(1) gcc/(pydoc)/空)。 其中 (10) 断言为 true —— 它和 getSearchPage() 自己的 [0-9n]+ 同口径,当成列表是 「更快的错」:apropos -s 10 找不到东西,而 pydoc3 -k '(10)' 要 8 秒才确认找不到。 全套 836 passed / 0 failed。 Refs: #266 Co-Authored-By: Claude code 2.1.308 with deepseek-flash <noreply@taotoken.net>
docs(corpus): 黄金语料与参考实现差分设计(#255) 阶段 2 的 L2 层:把 man/perldoc 的真实输出冻结成语料,让"渲染是否退化" 变成机器判定。 核心决定: - 语料在生产机采集,连同工具版本与 sha256 存档;CI 只做纯函数比对, 不执行 man/groff/perl —— 否则裁判会随镜像版本漂移。 - 三条独立检查:黄金 Markdown、文本完整性、结构指纹 + XHTML 合法。 - perldoc 语料用 pod2text -w 100 管线(生产实际路径),不是 perldoc <module>。 - TLDR 在语料渲染中关闭:它是外部内容,不属于"原始输出 → 渲染"这条链路。 - 基准 URL 与 PHPMAN_HOME 固定;XHTML 验整页,DTD 随仓库离线解析; 黄金由 PHP 8.3 生成,语料作业只在 8.3 腿运行。 经独立评审修掉五个实现歧义,结论见文档 §6.1。 Co-Authored-By: Claude code 2.1.311 with deepseek-v4.1-flash-1017 <noreply@taotoken.net>