主题
专题验证 · 数据基线与迁移
常驻清单,不归档。判断层专用——机械层先跑完再看这里。 对象:
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.sh0. 这个域为什么危险
一次改动会同时命中「已经装了这个应用的每一台机器」,而你手上没有任何一台。
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_migrationsvs 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_state | v26 断链事故留下的 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 里的注释也没事」就出事 |
| E4 | v2 / v3 里那几处已经过时的注释,还没被人「顺手更正」吧? | init_data.sql 里 auto_save_words 的种子行与 context_disambiguation 的注释都已与当前行为不符(见 migrations.rs v2 处的说明)。它们明知有错也必须留着——更正一个字就是改 v2 指纹。复发信号:有人在 code review 里把它当成笔误提出来 |
| E5 | 新增的迁移是幂等加法式吗? | ✅ 已下沉成断言(D7)。要人判的是它没覆盖的形态:INSERT 不带 OR IGNORE、UPDATE 依赖当前数据、以及任何「跑第二遍结果不同」的写法。链断掉时 sqlx 每次启动都会重跑同一条,非幂等在那种情况下会持续制造新的伤害 |
2.2 存量库能不能升上来
| # | 问句 | 假绿风险 |
|---|---|---|
| F1 | 新迁移在有数据的表上跑得通吗? | ✅ 已下沉成断言。⚠️ 但种子器灌的是固定形状的两行(PK / 唯一索引列取不同值、其余列刻意重复)。它能抓「在有重复值的列上建唯一索引」,抓不到「只有当某列出现三种以上取值时才失败」这类依赖数据分布的迁移(K5)。判据是:这条迁移的成败,除了「有没有行」,还取决于别的什么吗? |
| F2 | 这条迁移读了「运行时才有」的数据吗? | 🔴 12,291 条预装词汇不是迁移灌的,是启动时 helpers.rs ATTACH 进来的——它们不在链里。任何依赖 vocabulary 有行的迁移,在空库和种子库上都验不出来,只会在真机上炸 |
| F3 | 链是顺序的——这条迁移假设了它之前哪一条已经跑过? | 顺序在链里是成立的,但存量库不一定停在你以为的位置:野外同时存在停在不同版本的库。起点已由测试从出厂清单现推,别在测试里手挑版本号 |
| F4 | init_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 rejection(void (async () => …)(),没有 .catch),@sentry/react 的默认 global handlers 会捕获它——前提是 DSN 已注入且网络可用。Rust 侧的 Sentry 只接 panic,接不到这个。⚠️ 同样未实测:这一格目前是读代码得出的结论,不是观测(K2) |
| G3 | 🔴 有没有人给启动路径加了 retry / catch? | tauri-plugin-sql 的 load 命令在 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.rs 用 env!("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 阳性对照(证明扫描器不是瞎的) |
| 从指纹清单里删一行 / 手改一个 sha | D4 双向对账 |
| 改一条尚未出厂但本机已应用的迁移(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 🔴 阳性对照:证明扫描器不是瞎子
这个域有三条断言天生是「没找到问题」形式的:
- 「指纹一致」——哈希函数若退化成常量(或永远读到空串),它会一直全绿。 故
sqlx_checksum_helper_is_not_constant断言两条不同迁移摘要不同、多一个空格摘要就变。 - 「摘要口径对」——提取器读错一个 Rust 转义,全部摘要一起错、内部仍自洽。 故 D3 用
shasum -a 384另一个工具独立复算文件型迁移,且 D5 拿 sqlx 亲手写的字节做交叉。 - 「本机 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_db→ FAILEDfresh_chain_applies_cleanly_to_head→ okflattened_baseline_matches_terminal→ okversions_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 的腿,需权衡) |
| K4 | init_db_state 的 rusqlite 兜底只建 domain_zoom 一张表。 v31/v32 的新表没有兜底,链断在它们之前就会缺表 | 兜底本身是第二本账:每加一张就多一份必须与迁移逐字同步的 DDL,而它们静默漂移不会报错。且链断了正确做法是修链,不是靠兜底续命 | 真发生一次 v31+ 的断链事故时;或某张新表被判定为「缺了会让应用彻底不可用」(domain_zoom 当年正是这一类) |
| K5 | 非空存量库的种子形状是固定的两行,覆盖不了依赖数据分布的迁移失败 | 要覆盖任意分布就得写数据生成器,那是另一个量级的工程,而这类迁移在本仓极罕见 | 出现第一条「成败取决于数据分布」的迁移时(判据见 F1) |
| K6 | 生产 Supabase 库刻意不在本域检查 | 远端没有迁移链、只有手工 DDL,本域的不可变纪律对它不成立;共享表的形状归 sync-verify.sh | 远端真的引入迁移工具时 |
⚠️ 这张表本身也会腐烂。 每轮验收后更新它——一条「已知未修」若其实已被修, 会让下一轮跳过一次本该做的检查,那是最贵的一种过期。
5. 验收台账
每轮一行。不新建报告文档——产物按 README.md 的规矩拆散。
| 日期 | 基线 | 机械层 | 判断层结论 | 留痕 |
|---|---|---|---|---|
| 2026-07-26 | 0.1.0-dev.8 | 尚无 | 最后一次压平 + 冻结:旧 v1→v27 折叠进 v1 baseline,check-schema-frozen.mjs 上线(当时只守 schema.sql) | CHANGELOG |
| 2026-08-12 | 0.1.0-dev.10 | — | 冻结守门从 1 个文件扩到 3 个(init_data.sql / seed_reference_words.sql 此前没有任何守门) | CHANGELOG |
| 2026-08-27 | 0.1.0-dev.10 | Rust 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 |