一句注释拦不住漂移
同一份档案,命令行导出 2966 条标记,扩展导出 2967 条——因为合并逻辑写在了「读文件」那一侧,而扩展读的是浏览器里的存储。那个函数的注释写着「照抄它的定义,不要另发明」,而漂的是上游。
用户在豆瓣上把一部电影删掉再重新标记,豆瓣会发一个新的条目编号。 解析器据此如实分成两条记录——那是对的,档案是事件日志,两次标记就是两件事。
但导出是当前状态,一个作品只能有一行。不合并的话, 《盗梦空间》导出了两条书架记录,短评还对不上。而 NeoDB 那边一个作品 只有一个书架条目:第二条覆盖第一条,谁覆盖谁由文件里的先后决定, 不由任何判据决定。
站点那边四天前刚修过同一个形状, 导出这边没跟上。补上,判据照抄:最后一次看到的那条—— 按标记日期挑是错的,补标一部老片可以有更早的日期,而它仍然是现存的那一条。
跑一遍核对脚本,它第一次说「每一条都跟档案对得上」。然后就该收工了。
「扩展那边呢?」
项目主人问了一句:这跟扩展里的导出一致吗。
不一致。同一份档案,两条路:
| 标记 | 《盗梦空间》的书架记录 | |
|---|---|---|
| 命令行 | 2966 | 1 条 |
| 扩展 | 2967 | 2 条 |
因为合并写进了读文件的那个函数。而扩展读的是浏览器里的 OPFS, 它有自己的那一份。
这正是这个项目把下游搬进扩展时定下的 那条线:字节从哪来是各宿主的事,字节是什么意思只能有一份实现。 把第二半做错是最贵的,因为它是静默的——两个站点都打得开,只有图片路径不一样; 这一次是两个包都导得进去,只有书架上多一条。
而「合并重标」正是第二半。它一行读写都不碰,只是从同一批记录里算出来的—— 跟它一起的还有作品索引、改过几次的计数、账号信息,全都是。
那句注释一直在那儿
扩展那个函数的开头写着:
这几行是那段逻辑的等价物——照抄它的定义,不要另发明。
写得挺清楚,也确实是照抄的。而它拦不住这次的漂移,因为漂的是上游: 改上游那个函数的人,当时根本不在这个仓库里。
这是同一个形状的第二次。上一次是 跨仓库的同步检查装在了作者不在的那一侧, 于是扩展带着一个已修好的 bug 跑了两天。检查也好、注释也好,长在正确的地方才算数。
所以修法是把重复删掉,不是再加一条测试
那几行搬进两边共用的那份纯代码里,两个宿主调同一个函数。它本来就在逐字节同步的 名单上,而那份同步两边的 CI 都跑——这一条从此不靠谁记得。
拿同一份真实档案跑两条路,比一比产出:
扩展 journal.ndjson 3429077 字节 sha 6cee967cbb831e31
命令行 journal.ndjson 3429077 字节 sha 6cee967cbb831e31
→ 逐字节相同
顺着这个问题,第三个宿主也在漂
站点那边有自己的一份合并逻辑(用途确实不同——它还要把被顶掉的那条并进时间线)。 而它挑「哪一条是现存的」时只比两层:最后一次看到、标记日期。
两者都相同时,结果由输入次序决定。而 JavaScript 的排序是稳定的, 所以它看起来还很稳——同一台机器上跑一百遍都一样。
真撞上的话,同一个作品在站点上是这一条、在 NeoDB 上是另一条, 而两边都不会报错。
补上第三层判据,与导出那边逐条对齐。测试也照 档案去重那次的做法—— 拿打乱过的输入喂它,只跑一遍是测不出来的。