豆备 DOUBAK
← 开发日志

验一验:能不能读将来的档案

规范给读者立过两条义务,而在此之前没有任何东西验证过它们。加了一份「来自未来」的一致性用例,两个读取端各验一遍。以及过程中两次写出了恒真的断言。

一致性用例攒到今天有二十一份,全都在验同一个方向: 写出来的东西合不合规范。那是生产者方向。 读者方向一条都没有。

而规范第 10 节给读者立了两条义务——容忍未知字段、原样保留开放词表里的未知取值—— 写在那儿一个多月,没有任何东西检查过谁真的做到了。

要守的是哪一半

bundle/1.0 从未公开发布过,所以今天的代码读不了它是可以接受的。 真正必须守住的是反过来那一半:

今天写好的读取端,将来遇到更新的档案不能崩、不能静默丢东西。

这条听起来像是要等到真有 bundle/1.3 才能验,其实不用。 伪造一份来自未来的档案就行了——放进一致性用例里, 今天就能跑,而且以后每一版都会跑。

用例(valid/from-the-future)里放的是四样东西:

spec_version   bundle/1.9                          更高的小版本号
manifest       future_top_level_field: {...}       顶层多一个不认识的字段
index 行       future_line_field: 42               每行也多一个
intent         future.route.that.does.not.exist.yet 开放词表里的未知取值

校验器必须放行它。放不行,就说明 schema 把「只增不改」写死了—— 而那正是第 10 节要避免的事。实测通过,也就顺带证明了两个 schema 的 additionalProperties 确实是敞开的,不是嘴上说说。

刻意不放进去的那一类

封闭词表(verdict / surface / capture_fidelity)里的未知取值没有放进这份用例。 按规范,往封闭词表里加取值属于小版本变更,旧校验器拒绝它是刻意的—— 所以它不属于「应通过」那一档。

读者对那一类的义务是另一条:保守处置,不得当成 ok。 「未知的判定结果」和「未知的字段」听起来像一回事, 处置方式恰好相反:前者要疑,后者要放。

一份用例,两个独立实现

用例住在规范仓库里,扩展和解析器各自去跑。这不是重复劳动: 扩展那边跑在浏览器语义上(OPFS、DecompressionStream), 解析器那边跑在 Node 上(fszlib), 是两份互不相干的代码在读同一份字节。 扩展这侧五条:打得开、未知字段读出来还在、未知 intent 原样保留、 逐条取出来能解压、导入扫描认得它。

顺带定下一件事:未知 intent 不报警。 它是规范预期内的情形,报警的话,将来每加一条路线, 所有旧解析器都会对着新档案刷屏——而刷屏的告警等于没有告警。

然后我两次写出了恒真的断言

这才是这一趟真正学到的东西。两份测试,各有一条断言永远不可能失败, 而且两条都绿得理直气壮。

其一,在解析器那边。写的是「产出里没有未来路线的记录」, 遍历「产出的记录」去找。可这份用例是最小用例, 它本来就产不出任何记录——那条断言遍历的是一个空数组。 遍历空集合的断言等于没写。改成正面判据:读到了、一条都没变成记录、 且一句告警都没有。

其二,在扩展那边。写的是 meta.problem ?? null === null, 而 readBundleMeta 压根不设 problem 这个字段—— 把段文件整个删掉,它照样返回 null。 这条断言测的不是「没有问题」,是「这个字段不存在」。

凡是断言「某个字段没有值」,先确认那个字段在坏情况下真的会有值。 否则测的是自己的想象——而这种测试从不报错,只是一直绿着。

两处都改成了会变的判据,踩坑记在注释里。 这跟之前那个「声称有 N 条却一个 id 都没抽到」的自检是同一件事的两面: 一个从来没红过的检查,和一个不存在的检查,在证据上是等价的。

← 更新:抓完之后,先让你看见自己的东西 更早:私密的那一份,目录页上也得带锁 →