Skip to content

专题验证 · 数据基线与迁移

常驻清单,不归档。判断层专用——机械层先跑完再看这里。 对象:src-tauri/src/db/migrations.rs + assets/sql/ 三个冻结文件 + shipped_migrations.txt

  • db/helpers.rs::init_db_state 的兜底 + tauri-plugin-sql(sqlx) 的迁移运行时 体裁与纪律见 README.md
bash
cargo test --lib db::migrations && ./scripts/migration-verify.sh

0. 这个域为什么危险

一次改动会同时命中「已经装了这个应用的每一台机器」,而你手上没有任何一台。

sqlx 对已应用迁移按 SHA-384 校验(Sha384::digest(sql.as_bytes()),只哈希 SQL 本体)。 改动其中一个字节,装过那一版的库启动即 VersionMismatch;sqlx 在那一条上返回错误, 于是它之后的每一条 pending 迁移永不执行——不是这次,是从此以后每次启动都一样。

三种触发方式,后果完全相同:

你做了什么sqlx 说什么之后
改了一条已出厂迁移的 SQL(哪怕多一个空格)VersionMismatch(v)v 及其之后全部不再执行
删掉 / 改号一条已出厂迁移VersionMissing(v)在任何 pending 应用之前就返回,整条链一步不走
新迁移在有数据的表上执行失败ExecuteMigration停在这条,每次启动重试、每次同样失败

而这个域的基线已经进入野外:数据基线冻结于 2026-07-26(0.1.0-dev.8), 随首个带 Tauri Updater 的构建发出——updater 会把新构建自动推给存量用户, 他们的本地库持久存在,删库重建不再是选项。

🔴 「我本地跑得好好的」是本域最没有信息量的一句话。 你的本地库要么是新建的 (新装路径),要么恰好和 HEAD 自洽。每一格要问的是:装了上一版的那台机器, 现在还升得上来吗?

⚠️ 元前提一:本轮验的是「源码」+「本机那一个存量库」,不是「野外所有存量库」

机械层的证据分三档,强度递减时别混着读:

  • 源码层(指纹清单 vs 发布 tag):覆盖所有已出厂版本,但比的是我们自己算的摘要;
  • 本机 ledger_sqlx_migrations vs HEAD):只覆盖这一台机器装过的那些版本, 但比的是 sqlx 亲手写下的字节——这是本域唯一「让字节真的走一趟」的位置;
  • 判断层:野外那些没有任何一台在你手上的机器。

本机没有存量库时(CI、新机器),中间那档判 SKIP——SKIP 是「本次没验」,不是通过

⚠️ 元前提二:这个域的守卫比前四个都多,先盘再补

红线 #11 存在很久了,围着它已经长出一整套东西。新增检查之前先读 §1, 否则造出来的是第二本账,而不是覆盖面。


1. 机械层已覆盖(不要在判断层重复)

位置守什么
源码scripts/check-schema-frozen.mjs(CI ci.yml frontend job)assets/sql/ 三个文件的 SHA-256 冻结。⚠️ 它守不到 v28+ 的内联 SQL
源码migration_chain_tests::flattened_baseline_matches_terminal压平等价性:v1+v2+v3 终态 == 冻结前 golden
源码migration_chain_tests::fresh_chain_applies_cleanly_to_head空库 v1→head 干净跑通 · 关键表/列在场 · 退役表不在 · 墓碑列完备(清单取自 PENDING_PUSH_TABLES)· 版本严格递增 · 逐个 vNN 的建表/加列/唯一约束断言
源码migration_chain_tests::shipped_migrations_are_byte_frozen已出厂迁移的字节冻结(含内联),兼抓「被删 / 改号」。清单真相源 = 发布 tag
源码migration_chain_tests::sqlx_checksum_helper_is_not_constant🔴 上一条的阳性对照(哈希退化成常量会安静全绿)
源码migration_chain_tests::pending_migrations_apply_over_a_populated_legacy_db非空存量库升级:起点取自出厂清单,每张表灌两行再跑剩余迁移
本机scripts/migration-verify.sh D1–D4冻结守门复用 · 冻结名单覆盖面 · 摘要口径可信(阳性对照 + shasum 独立复算)· 指纹清单 vs 发布 tag
本机scripts/migration-verify.sh D5–D6🔴 本机存量库的 _sqlx_migrations 与 HEAD 逐条比 + 它的阳性对照
本机scripts/migration-verify.sh D7–D9冻结后迁移必须幂等加法式 · database-schema.md §10 逐条登记 · 红线措辞覆盖全部冻结文件
别处db/helpers.rs::init_db_statev26 断链事故留下的 rusqlite 幂等兜底建表(只有 domain_zoom 一张,见 F4)
别处/release skill Step 1.5 + 护栏 #6发版前的冻结 gate
别处docs/database-schema.md §10逐条迁移明细的唯一真相源
别处sync-consistency.md N1/N2/S19远端同步表的列 / trigger / 加列顺序

新增的守卫全部做过注入式反向验证(2026-08-27,见 §3):3 条 Rust 守卫 + 9 处脚本判据 各注入一次真实缺陷,全部变红,还原后 git diff 零残留。注入在独立 worktree 里做 (这个域要动 schema.sql / migrations.rs 这种枢纽文件,共享工作树里一次 checkout . 就卷走了)。

⚠️ 刻意不重做的

  • 压平等价性、空库跑通、墓碑完备、版本单调——migration_chain_tests 早就有,且判据比 脚本能写的更准(它拿的是 rustc 解析出来的真字符串)。脚本只补那些测试结构上够不到的。
  • check-schema-frozen.mjs 的 SHA 比对——D1 直接调用它,不另写一份判据。 另写一份的下场就是本域最初的病:两份账,其中一份比另一份窄一档。
  • 远端 schema——生产库那一侧归 sync-verify.sh。本域的主战场在本地 (SQLite 迁移链、include_str! 的指纹、存量库升级路径),刻意不连生产库
  • sqlx 的迁移执行与比较逻辑——只复刻了「哈希什么」这一行,没有复刻 migrator。 真要验行为,看 D5(拿它写下的字节比),不要写第二个 migrator。

🔴 建成这一轮时补上的那个洞

v28 起的迁移 SQL 是 migrations.rs 里的内联字面量,在此之前没有任何守门。check-schema-frozen.mjs 从名字到实现都只盯 assets/sql/,而红线 #11 的条文 (以及 CLAUDE.md §5)当时也只写 schema.sql——照着控制文件读,会以为内联迁移可以改。 改一个空格 → CI 全绿 → 存量用户从此不再迁移。已补 shipped_migrations_are_byte_frozen (注入验证:v28 SQL 末尾加一个空格即红)+ 更正红线条文(D9 当场把这条措辞漂移抓了出来)。


2. 判断层逐格判据

2.1 冻结面(哪些字节改不得)

#问句假绿风险
E1本次改动碰了任何已出厂迁移吗?✅ 已下沉成断言。这里要人判的是动机:绝大多数「想改一下那条迁移」都应该改成新编号迁移。⚠️ 唯一合法的例外是「明确宣告一次新的 baseline 压平 = 接受重置存量用户的库」,那是用户的决定,不是顺手做的
E2指纹清单的「已出厂」集合,还跟得上发布节奏吗?🔴 本域最可能腐烂的一格。 清单由 --update-manifest 从发布 tag 现推,2026-08-30 起由 /release Step 8.1 接住(此前没有任何东西强制发版后去跑它,见 K3)—— 但那是 skill 步骤不是机械闸,跳过它不会有任何东西变红,所以这一格仍要人来问。清单没更新 = 新出厂的那几条迁移回到「改了不会有任何东西变红」的状态,而 D4 只比较「清单 vs tag」,清单少一条它红——所以真正的问题不是它抓不到,是没人跑
E3「只是改了个注释」安全吗?分两种,差别是致命的:迁移字符串外面的 Rust 注释不参与指纹(随便改);写在 SQL 字符串里面-- … 参与(一个字都不能动)。同理 description 不参与指纹(sqlx 只哈希 SQL),改它是安全的——别把这条记反了:记反成「description 也不能改」只是保守,记反成「SQL 里的注释也没事」就出事
E4v2 / v3 里那几处已经过时的注释,还没被人「顺手更正」吧?init_data.sqlauto_save_words 的种子行与 context_disambiguation 的注释都已与当前行为不符(见 migrations.rs v2 处的说明)。它们明知有错也必须留着——更正一个字就是改 v2 指纹。复发信号:有人在 code review 里把它当成笔误提出来
E5新增的迁移是幂等加法式吗?✅ 已下沉成断言(D7)。要人判的是它没覆盖的形态:INSERT 不带 OR IGNOREUPDATE 依赖当前数据、以及任何「跑第二遍结果不同」的写法。链断掉时 sqlx 每次启动都会重跑同一条,非幂等在那种情况下会持续制造新的伤害

2.2 存量库能不能升上来

#问句假绿风险
F1新迁移在有数据的表上跑得通吗?✅ 已下沉成断言。⚠️ 但种子器灌的是固定形状的两行(PK / 唯一索引列取不同值、其余列刻意重复)。它能抓「在有重复值的列上建唯一索引」,抓不到「只有当某列出现三种以上取值时才失败」这类依赖数据分布的迁移(K5)。判据是:这条迁移的成败,除了「有没有行」,还取决于别的什么吗?
F2这条迁移读了「运行时才有」的数据吗?🔴 12,291 条预装词汇不是迁移灌的,是启动时 helpers.rs ATTACH 进来的——它们不在链里。任何依赖 vocabulary 有行的迁移,在空库和种子库上都验不出来,只会在真机上炸
F3链是顺序的——这条迁移假设了它之前哪一条已经跑过?顺序在链里是成立的,但存量库不一定停在你以为的位置:野外同时存在停在不同版本的库。起点已由测试从出厂清单现推,别在测试里手挑版本号
F4init_db_state 的兜底建表,覆盖面是「够」还是「只覆盖了当年出事的那一张」?🔴 只有 domain_zoom 一张(v26 事故的遗留补丁)。链一旦断在 vN,vN 之后建的东西全部缺失:v31 word_encounters / v32 epub_positions 缺表 → 读它的命令直接失败;v34 那条索引缺了只是不是错。要人判的是:新迁移建的东西属于哪一类,以及要不要再加兜底(见 K4——兜底本身是第二本账)
F5冻结基线之前的存量库(0.1.0-dev.7 及更早)呢?升不上来,而且是设计如此(见 K1)。别把这条读成缺陷,也别把它读成「没事」——它的含义是:那批库只能靠重装,且用户不会收到任何提示

2.3 断链之后会发生什么

#问句假绿风险
G1用户那头看到什么?按 HEAD 的代码路径:App.tsx 的启动 effect 里 ensureDbMigrated() 抛出 → initialize() 永不执行 → isInitialized 恒 false → 应用停在全屏「Loading…」,没有报错、没有重试、没有超时。⚠️ 这与 CLAUDE.md 里 v26 那段叙述(「应用照常启动,直到 signup 时崩」)对不上,未实测(K2)。最可能的调和是下面 G3 那条
G2你这头会收到信号吗?那次抛出是一个未处理的 promise rejectionvoid (async () => …)(),没有 .catch),@sentry/react 的默认 global handlers 会捕获它——前提是 DSN 已注入且网络可用。Rust 侧的 Sentry 只接 panic,接不到这个。⚠️ 同样未实测:这一格目前是读代码得出的结论,不是观测(K2)
G3🔴 有没有人给启动路径加了 retry / catchtauri-plugin-sqlload 命令在 migrate 之前就把迁移清单 remove() 掉了。于是第二次 Database.load 会「成功」——因为它已经没有任何迁移可跑。后果是把「卡在 Loading」换成更坏的「在一个缺表的 schema 上正常启动」。今天没有任何地方 retry,唯一会双跑的是 dev 下的 StrictMode(这大概就是 v26 当年看到的形状)。这一格的复发信号非常具体:任何人给 App.tsx 那个 effect 加 catch + 重试
G4兜底建表在扩张吗?每加一张兜底表,就多一份必须与迁移保持一致的 DDL(第二本账)。它们的正当性来自「已经发生的断链」,不是「以防万一」。判据:这张表的 DDL 和对应迁移里的那份,现在还逐字一致吗

2.4 边界(这些不属于本域,别在这里验)

#问句
H1远端 Supabase 没有迁移链,只有 Dashboard 手工 DDL——本域的「不可变」纪律对它不成立。共享表加列的顺序(先远端、后客户端)归 sync-consistency.md S19
H2预装词库 lampio_dict.db 的 byte-equal(红线 #10)是另一条线:它不经迁移链,靠 /cross-end-check/vocab-reseed
H3新迁移编号读的是 migrations.rs(最大那个往下排),不是 CLAUDE.md §5——那里的 v28/v29 只是举例,两份表并存期间还漏记过 v33

3. 反向验证配方

只验通过路径 = 没验。 以下全部不依赖「记得改回来」。

3.1 🔒 先说不要做的事

  • 绝不动 assets/sql/ 下那三个文件(除非明确宣告一次新的 baseline 压平——那是用户的决定)。 注入验证时改它们必须在独立 worktree 里,且验完立刻还原 + git diff --quiet 复核。
  • 不要删、不要改开发机上的 lampio.db 它正是一个宝贵的「存量库」样本—— 它的 _sqlx_migrations 记着哪一版的指纹,本身就是数据,且是 D5 唯一的证据来源。 migration-verify.sh 全程拷一份出来再读,绝不在原库上开写连接。要动它先备份并问用户。
  • 不要为了试而重置数据基线。 重新压平 = 明确接受重置全部存量用户的库, 与「换签名密钥」同级:不是验证手段,是一次产品决策。
  • 不要在共享工作树里注入。 这个域要改的是 schema.sql / migrations.rs 这种枢纽文件。

3.2 注入 → 必须红 → 还原 → 复核零残留(在独立 worktree 里)

bash
git worktree add --detach /tmp/migverify-wt HEAD       # 隔离,不碰共享工作树
cp src-tauri/.env.local /tmp/migverify-wt/src-tauri/   # build.rs 要它才编得过

⚠️ 共享 CARGO_TARGET_DIR 跨 worktree 会留下一颗哑弹(本轮实测踩到): build.rsenv!("CARGO_MANIFEST_DIR").env.local,而那个常量是编译进 build-script 二进制的。在 worktree 里编译过一次之后,共享 target 里的 build-script 指着 worktree 的路径;worktree 一删,回主树跑 cargo test 就报 「Missing build-time config RB_SUPABASE_URL」—— 看起来像密钥没配,其实是缓存指错了地方。 touch src-tauri/build.rs 即可。共享 target 仍然值得(省一次全量重编),只是要认得这个症状。

十二种值得常备的注入(2026-08-27 全部实测变红):

注入该红的检查
已出厂的内联迁移 SQL 末尾加一个空格(v28)shipped_migrations_are_byte_frozen建成这轮之前,这一处零守门
整块删掉一条已出厂迁移(v29)同上,报 VersionMissing 那一支
新增一条「在非唯一列上建唯一索引」的迁移pending_migrations_apply_over_a_populated_legacy_db鉴别力对照见 §3.4
冻结文件末尾加一个换行D1 冻结守门 D5 本机 ledger
新增一条文件型迁移(冻结名单覆盖不到)D2 名单覆盖面
把摘要函数改成返回常量D3 阳性对照(证明扫描器不是瞎的
从指纹清单里删一行 / 手改一个 shaD4 双向对账
改一条尚未出厂但本机已应用的迁移(v34)只有 D5 会红(见 §3.4)
让 ledger 只读回一行D6 阳性对照
新增一条带 DROP 的迁移D7 幂等加法式
新增 v35 而 §10 不登记D8 文档对账
从 CLAUDE.md 抹掉一个冻结文件名D9 红线措辞覆盖面

⚠️ 注入必须能通过编译,否则测试根本没跑——那既不是红也不是绿,是没验, 而输出里看起来只是一条报错。改 migrations.rs 时用 Python 按锚点插整块 Migration { … }, 别用裸 sed 碰结构。

⚠️ 探针选错了不等于守卫瞎。 本轮第一次注入 CREATE UNIQUE INDEX ON learning_entries(user_id) 没红——因为那一列在 UNIQUE(user_id, word) 里,种子器本就给两行不同值, 而真实世界里那条索引也确实建得起来。换成 mastery_level 立刻红。 看到「没红」先问是守卫瞎了还是探针选错了,两者的修法完全相反。

3.3 🔴 阳性对照:证明扫描器不是瞎子

这个域有三条断言天生是「没找到问题」形式的:

  1. 「指纹一致」——哈希函数若退化成常量(或永远读到空串),它会一直全绿。 故 sqlx_checksum_helper_is_not_constant 断言两条不同迁移摘要不同、多一个空格摘要就变。
  2. 「摘要口径对」——提取器读错一个 Rust 转义,全部摘要一起错、内部仍自洽。 故 D3 用 shasum -a 384 另一个工具独立复算文件型迁移,且 D5 拿 sqlx 亲手写的字节做交叉。
  3. 「本机 ledger 逐条一致」——库读不到 / 只读回一行,比对会安静通过。 故 D6 断言读到的条数与首条指纹确实来自库,且改一位即对不上。

本轮实测过一次假红:D7 首版用一串 grep 计数区分 CREATE TABLE IF NOT EXISTS 与裸 CREATE TABLE,把 v28/v31/v32/v34 全误报成违规。判据已搬进 migration_digest.py 用正则前瞻写。假红与假绿一样要修——一道会对正确代码报警的闸门,很快就没人看了。

3.4 🔴 让字节真的走一趟

前面每一条都只是「我们自己算的摘要互相对得上」。真正让字节走一趟的是两处:

① D5:本机存量库的 _sqlx_migrations 那张表里的 checksum 是 sqlx 自己写下的, 不是我们算的。拿它和 HEAD 的迁移逐条比,等价于问「这个库现在启动会不会 VersionMismatch」—— 没有模拟、没有第二个 migrator。它同时反向验证了整套摘要口径:本轮建成时, 提取器算出的 10 条摘要与库里 sqlx 写下的 10 条逐字节相同, 这才是「Sha384::digest(sql.as_bytes()) 就是那个口径」的证据(读 crate 源码只是线索)。

② 注入 v34 那一条的鉴别力对照。 v34 尚未出厂 → 指纹清单管不到它 → D1(冻结文件)绿、D4(清单 vs tag)绿,只有 D5 红。这说明源码层与本机层各自覆盖着 对方够不到的一段,两者都不能省。

同理,「非空存量库」那条测试的鉴别力也是这样验的:注入 CREATE UNIQUE INDEX ON learning_entries(mastery_level) 之后——

  • pending_migrations_apply_over_a_populated_legacy_dbFAILED
  • fresh_chain_applies_cleanly_to_head → ok
  • flattened_baseline_matches_terminal → ok
  • versions_are_strictly_increasing_and_unique → ok

三条既有守卫在同一个真实缺陷面前全绿。这就是「空库测试」与「存量库测试」的差

3.5 判据类不需要造故障

本域的纯判据只有一个:「sqlx 认不认这条迁移变了」。它已经做成 D3 + sqlx_checksum_helper_is_not_constant 的向量自检(正向:不同 SQL 不同摘要;🔴 反向:把哈希换成常量,两条断言当场红), 不需要真的去弄坏一个库。


4. 已知未修 / 有意接受

不要当成新发现报上来,但每轮仍要独立确认现状(还在?变严重了?条件变了?)。

#事情为什么现在不修什么条件下重新处理
K1冻结基线之前的存量库(0.1.0-dev.7 及更早)永远升不上来。 那段时期每次压平都改 v1 指纹,那批库对着 HEAD 一律 VersionMismatch这是当时声明过的开发期策略(用户库视为可重置),且 updater 从 dev.8 才有——那批安装本就收不到自动更新,只能手动重装遥测里出现 dev.7 及更早的活跃 app_version 时。届时要判断的不是「修不修迁移」(修不了),是「要不要主动通知那批人重装」
K2「断链之后应用是什么表现」只有代码路径推断,没有实测。 G1/G2/G3 三格都建立在读代码上:停在 Loading、未处理 rejection 进 Sentry、retry 会跑在坏 schema 上造一个真实的坏库要么改开发机上那个库(本域最宝贵的证物、且是 D5 唯一证据来源),要么跑起 app —— 代价与风险都不对等🔴 下次需要重置开发库时顺手做掉:备份 → 在副本上改一条已应用迁移 → 启动 dev build 与 prod build 各一次 → 记录真实表现(尤其 StrictMode 双跑是不是真的会让第二次 Database.load 无迁移地成功)→ 还原。那个窗口本来就会出现,这是免费的
K3已修(2026-08-30) —— 接进 /release skill 的 Step 8.1(硬步骤 + 护栏 #10):publish 之后跑 migration-verify.sh --update-manifest 并提交清单变更。仍刻意不做成 CI 硬闸(那会在「发版当天 → 跑清单」之间恒红,与恒绿一样坏)—— 让它跟着发版动作走,不跟着时间走。原文留档:指纹清单曾没有「发版后必须重生成」的闸门,靠人记得跑
K7清单的「已出厂」判据是本地 git tag,不是「真的发出去了」。 shipped_tags() = git tag --list 'v*',不看任何 release 的存在与发布状态 —— 一个打了 tag 但构建失败 / 从未 publish 的版本,照样被当作已出厂参与现推。已有实例v0.1.0-dev.9 是本地 tag(也在 GitHub 上),但私有仓与公开仓都没有它的 release(2026-08-30 gh release list 实测)今天没有造成污染,纯属运气 —— 没有哪条迁移是在 dev.9 首次出现的(清单里 1/2/3 归 dev.8、28/29 归 dev.10)。且误差方向是过严(把没出厂的也冻住 = 挡住合法修改),不是过松,所以不会漏掉真实风险出现「打了 tag 但那一版没发出去、且它含新迁移」的一次发版时 —— 那次 --update-manifest 的 diff 里会出现那个废弃版本号。/release Step 8.1 已写明发版中途换过 dev.N 就要看一眼 diff。修法若要做:判据从 git tag 换成「公开仓真的存在且非草稿的 release」,代价是脚本从零网络变成要 gh(那会让本机与 CI 都多一条可 SKIP 的腿,需权衡)
K4init_db_state 的 rusqlite 兜底只建 domain_zoom 一张表。 v31/v32 的新表没有兜底,链断在它们之前就会缺表兜底本身是第二本账:每加一张就多一份必须与迁移逐字同步的 DDL,而它们静默漂移不会报错。且链断了正确做法是修链,不是靠兜底续命真发生一次 v31+ 的断链事故时;或某张新表被判定为「缺了会让应用彻底不可用」(domain_zoom 当年正是这一类)
K5非空存量库的种子形状是固定的两行,覆盖不了依赖数据分布的迁移失败要覆盖任意分布就得写数据生成器,那是另一个量级的工程,而这类迁移在本仓极罕见出现第一条「成败取决于数据分布」的迁移时(判据见 F1)
K6生产 Supabase 库刻意不在本域检查远端没有迁移链、只有手工 DDL,本域的不可变纪律对它不成立;共享表的形状归 sync-verify.sh远端真的引入迁移工具时

⚠️ 这张表本身也会腐烂。 每轮验收后更新它——一条「已知未修」若其实已被修, 会让下一轮跳过一次本该做的检查,那是最贵的一种过期。


5. 验收台账

每轮一行。不新建报告文档——产物按 README.md 的规矩拆散。

日期基线机械层判断层结论留痕
2026-07-260.1.0-dev.8尚无最后一次压平 + 冻结:旧 v1→v27 折叠进 v1 baseline,check-schema-frozen.mjs 上线(当时只守 schema.sqlCHANGELOG
2026-08-120.1.0-dev.10冻结守门从 1 个文件扩到 3 个(init_data.sql / seed_reference_words.sql 此前没有任何守门CHANGELOG
2026-08-270.1.0-dev.10Rust 18 条 · 脚本 PASS=10 FAIL=0 SKIP=0(首次)建成本专题。发现并补上一个真实的洞:v28 起的内联迁移 SQL 此前零守门——check-schema-frozen.mjs 从名字到实现都只盯 assets/sql/,而红线 #11 与 §5 的措辞也只写 schema.sql,照控制文件读会以为内联迁移可以改(改一个空格 = CI 全绿 + 存量用户从此不再迁移)。新增出厂指纹清单(真相源 = 发布 tag)+ 三条 Rust 守卫 + 脚本 D1–D9。第二个洞:全部既有测试都是空库跑 v1→head,而红线 #11 说的是存量库——新增「非空存量库升级」测试,注入实测其鉴别力(同一缺陷下三条既有守卫全绿)。D5 让字节真的走一趟:本机存量库里 sqlx 亲手写的 10 条 checksum 与提取器算的逐字节相同,这才坐实了摘要口径。顺带修正 CLAUDE.md 红线 #11 + §5 的措辞(D9 当场抓出)。12 处注入全部变红、零残留。新记 K2(断链表现未实测)+ K3(清单更新无闸门);K1/K4/K5/K6 记为有意接受。本轮验的是源码 + 本机那一个存量库,不是野外所有存量库(见 §0 元前提一)本目录;K3 进 backlog;CHANGELOG