v0.1.19
一次三层递进的修复,同一条因果链:起点是「HTML 注释里的链接被当成真链接」;修它引入的过滤
规则第二次错在相反方向(把真断链抹白,写门禁静默放行坏库);第三次收窄让原始 \r 第一次真的
流进扫描器,掀出四个确定性写点共有的「CRLF 静默转 LF」。三条按发生时序排,后两条里的「上一条」
即指前一条。无新功能、无接口变更;全量 1186 passed / 1 skipped。
修复
- HTML 注释里的链接不再被当成真链接(
check/lint/graph/health/reindex --prune/remove)
—— 各链接扫描器此前对<!-- … -->一视同仁,于是注释里的示例链接被当作生效引用。影响面比表象大,
按严重度排:①reindex --prune把 init 模板index.md里那四行<!-- ingest 自动追加:- [<名称>](entities/<Name>.md) — <一句话> -->整行删掉——被删的正是告诉 Agent 追加格式的说明;
②check对注释掉的[[X]]报wikilink.broken,退出码 3,即"随手注掉一段草稿"会让写门禁判库坏了;
③graph造幽灵边/幽灵断链,lint.broken_link与heal的missing_entity因派生自该图跟着误报;
④ 全新guanlan init的库跑health立刻报 4 条index_dangling(首次体检就是四条假告警);
⑤remove会连带删掉注释里提到该源的行。修法是pages.strip_html_comments一处归口,扫描前抹掉
闭合注释,四个扫描点(check断链、graph建边、index_md_links、reindex._scan_lines)共用。
两条刻意的设计:保留行数(注释替换为等量换行,按str.splitlines口径数行,reindex --prune靠
原行与判定行逐位对齐来剪枝,行数一变就错删);未闭合的<!--原样保留、不吃到文末(那会把后文
真断链静默吞掉,把漏报伪装成通过——门禁宁可多报不可少报)。回归网 14 例,其中 11 例对旧行为变异验证
为失败;另加一条test_freshly_initialized_kb_is_health_clean直接走run_init真模板体检——
这个 bug 一直没被测试看见,正是因为test_reindex的 index 夹具用的是不带链接的
<!-- ingest 自动追加 -->,与出厂模板漂移了。经 Codex 评审三项收敛:① 注释抹成等长空白
而非删空——删空会把两侧字符粘起来,凭空造出原文没有的链接([名]<!--注-->(entities/X.md)粘成
合法 markdown 链接、[[Mis<!--注-->sing]]粘成可能真解析到某页的[[Missing]]);② 行分隔符
逐字保留而非统一补\n(裸\r文本里补出的\n会与前一个\r合成一个 CRLF 边界、行数少一,
于是_prune_dangling的 strict zip 抛——这条是纵深防御,两条调用路径今天喂进来的文本已被
read_text通用换行归一过);③ Web 渲染同步对齐——render.py此前仍会把注释里的[[X]]渲成
可点链接,与 check/graph 打架;改为落在注释区间内即拒绝成链。Web 与扫描器的失活方式刻意不同:
扫描器抹成空白,Web 保留注释原文按转义文本显示(决策P4-4 关了原始 HTML 透传),视图不该让页面上
凭空少一段字。 - 注释剥离收窄为「只认独占整行的注释」,堵掉它自己引入的门禁漏报(xhigh 评审 8 条)
—— 上一条的实现用裸<!--.*?-->匹配,非贪婪只保证止于第一个-->、不保证那个-->属于
同一段注释。后果比原 bug 更重:① 正文或行内 code 里的字面<!--(讲注释写法时很常见)会与后文
任意一个-->配对,把中间的真链接整段抹白——check于是对真断链退 0,写门禁静默放行坏库,
graph/lint/heal一并失明;② init 模板自带四段注释,任何一个忘闭合的<!--都能找到配对,
于是health误报index_missing_page、reindex追加第二条重复登记行、remove留下悬空项;
③ 抹成空格并不能阻止粘连——下游link_stem/index_md_links自己会.strip(),[[Foo<!--旧名-->]]
被重新粘成有效引用[[Foo]],断链伪装成通过。修法是把匹配收窄为行锚定 + 回火
(^[ \t]*<!--(?:(?!<!--|-->).)*?-->[ \t]*$):行中间的标记永不构成注释开头,未闭合的<!--
也不与后一段注释的-->配对。代价是行尾的行内注释不再被识别、其中链接照常参与扫描——宁可
多报,不可少报。 reindex --prune不再铰碎注释块:strip_html_comments既然只认独占整行的注释,行尾开块的
写法(- [死页](…) 悬空 <!--+ 若干行 +-->)就不被它保护,照删会把块拆开——块内被注掉的
行「转正」,下一轮--prune再把它当真悬空删掉,两步内静默蒸发。新增_comment_touched_lines:
凡沾注释的行一律不删。它与strip_html_comments分工相反、各自保守——严格口径决定「忽略
什么」(放宽会漏报),宽松口径决定「保护什么不删」(收紧会毁数据),互不继承对方的失效模式。- Web 渲染改在解析前抹注释,真正与扫描器同口径:上一条只在行内
[[…]]处理器设了守卫,既漏
又散——_CodePathLinkTreeprocessor(反引号页面引用)与_RawPathTreeprocessor(裸raw/<slug>.md)
跑在元素树上、拿不到原文偏移,照常成链;而 inline 处理器只看得到当前块,含空行的注释被切成
普通段落后守卫直接失效(正是「注掉一段多段落草稿」这个动机场景)。改为_StripCommentsPreprocessor
在整篇行列表上抹一次,所有下游自然干净。行为变化:注释文本不再显示在 Web 页面上——与
Obsidian/GitHub 一致;此前能看见只是决策P4-4 关掉原始 HTML 透传后注释被转义成字面文本的副产物。
被抹的行置空串而非等长空格(4 个以上前导空格在 markdown 里是缩进代码块,会渲染出一个空的
<pre><code>)。 _prune_dangling(text, dangling)自己切行:原三参签名把「两列逐位等长」外包给三个调用方,
少传/传错就会让「模板提示行被整行剪掉」静默复发,而strict=True挡不住(两列等长、只是判定错)。
契约收回函数内部,_scan_lines这个跨模块私有符号一并消失。- CRLF 行尾不再被静默改成 LF(
reindex/remove/ingest+audit的指纹 stamp)
—— 四个确定性写点都是「Path.read_text读 → 改几行 →rawio.atomic_write_text写」。读侧走通用
换行(\r\n、裸\r一律归一成\n),写侧却是逐字节的,一读一写净效果就是把整份文件的
行尾改掉:Windows 用户跑一次guanlan reindex,index.md 27 行 CRLF 全变 LF,git 里炸出一屏与本次
操作无关的 diff;remove摘一个 slug、audit打一次指纹,同样把整张内容页/摘要页重写一遍。
reindex._split_lines里那句探测 CRLF 的代码因此从来没生效过(读侧早已归一,探到的永远是\n),
而atomic_write_text的文档还在承诺「自管 EOL 的调用方原样保真」——三处说法互相矛盾。修法是给
逐字节的写补上逐字节的读:rawio新增read_text_verbatim(atomic_write_text的读侧对偶)、
detect_eol/split_eol_lines(拆行改行再拼回时保原 EOL)、dump_frontmatter(重序列化
frontmatter 的单一归口,块按原 EOL 出、body 逐字不动),四个写点全部改用。fmrepair早就为同一个
理由走read_bytes+atomic_write_bytes,这次是把那条纪律推广到文本层。三处顺带收敛:
①detect_eol取首个出现的行尾而非「含 CRLF 即判 CRLF」——后者会让 LF 文件里混进的一个 CRLF
把整份重写成 CRLF,修 CRLF 丢失反而制造 LF 丢失;②split_eol_lines只认 CRLF/CR/LF,不像
str.splitlines连\v/\f/\x1c/\u2028也切(切开再按统一 EOL 拼回去 = 把行内字符静默换成
换行,同属"重写时悄悄改用户字节");③apply_origin改走dump_frontmatter,与另两处 frontmatter
重写共用「绝不裸拼」的同一实现(零行为变更,有用例逐字比对旧字面量)。 - 注释与链接正则改为行尾中立:上一条让原始
\r第一次真的流进strip_html_comments,而它的
行锚定用的是^/$+MULTILINE——只认\n,CRLF 文本里-->与\n之间隔着\r、$匹配不上,
整行注释当场不再被识别(前一条 CHANGELOG 里「这条是纵深防御、调用路径喂进来的文本已被read_text
归一过」的前提,至此失效)。改用「前后不是非换行符」的环视(?<![^\r\n])…(?![^\r\n]),对 LF /
CRLF / 裸 CR 一视同仁(LF 下与旧式逐字等价);WIKILINK_RE/_MD_LINK_RE一并把\r与\n同等
排除,杜绝引用跨行粘连。边界说清、不夸大:reindex --prune今天并不会因此删掉 CRLF 库的注释行
——那层由_comment_touched_lines的宽松口径兜着(实测:把正则退回只认\n,端到端用例仍绿)。
这里修的是严格口径自己不能因行尾失灵——strip_html_comments是check/graph/index_md_links/
Web 渲染共用的公共原语,让它对 CRLF 静默换一套含义,是给下一个消费方埋雷。回归网 16 例,逐条对旧
实现变异验证:读侧、frontmatter 重出、注释正则、链接正则各自的用例都能在退回旧写法时失败;其中
两条是反方向守卫——CRLF 下真断链仍被扫出、真悬空行仍被剪掉,免得「兼容行尾」写成「CRLF 库一律
不报不剪」。
Full Changelog: v0.1.18...v0.1.19