主题
Lampio — 项目指南
本文件是 Claude Code 的项目控制中心,每次会话自动加载。 最后更新:2026-08-28
📍 本仓是 app-centric monorepo:根本体 = 桌面端(Tauri),
admin/·landing/·supabase/·rb-debug-mcp/·rvh/是卫星子项目(见 §2 仓库地图)。
📍 先看这里:本文件之外还有六份 CLAUDE.md,按你碰哪个目录叠加
| 你这次要碰 | 除本文件外还会/还应叠加 | 本文件里对你有用的 |
|---|---|---|
src-tauri/**(Rust / Tauri / content-script / 迁移 / 预装库) | src-tauri/CLAUDE.md(架构要点 · 15 条桌面端红线 · 本地数据库 · commands 模块) | §4 双端契约红线 · §5 Supabase 共享表 · §9 双端整合 |
src/**(React / TS / UI) | src/CLAUDE.md(导航模型 · 红线 #8 · 前端约定索引)+ docs/ui-standards.md | §12 文件组织 · §7 skill 链 |
admin/** | admin/CLAUDE.md + scoped skill(admin:code-review 等) | §5 Supabase 共享表(改契约时) |
landing/** | landing/CLAUDE.md | 基本无 |
rvh/**(Flutter / Dart 移动端) | rvh/CLAUDE.md(红线索引 + 跨仓编号对照表)· 碰 sync 目录再叠 rvh/lib/features/sync/CLAUDE.md(Dart 落地 + 守卫)+ scoped skill(rvh:code-review 等 8 个) | §4 双端契约红线 · §3 Auth & Sync · §9 SM-2 三端一致性 |
supabase/** · 跨端 · 发版 · git | 无子文件 | 本文件就是主场 |
⚠️ 子目录 CLAUDE.md 的注入挂在文件工具上(Read / Edit / Write),
cat/grep/sed读同一个文件不触发(2026-08-28 实测,见docs/plans/archive/rvh-merge-plan.md§2 P0-2)。 所以:全程走 Bash 干活时,改src-tauri//src//rvh/前请显式读一次对应的 CLAUDE.md。 这也是本文件 §4 保留全量红线索引(含已下沉那 16 条)的原因——索引在根侧永远看得见。
目录
1. 项目概览
产品定位
把你感兴趣的英文内容,变成你的英语课堂。
- 以阅读兴趣为驱动力,以智能学习为底层能力的英文阅读工具
- 竞品是浏览器/Kindle 阅读器,不是 Duolingo 等教育产品
- 用户自带内容(整个互联网就是内容库),我们提供学习增强层
- 不做强制学习机制(无每日任务、无打卡、无学习计划)
- 学习自然发生:CEFR 高亮、词汇追踪、SRS 复习、阅读统计在底层默默工作
技术栈
| 层 | 技术 |
|---|---|
| 框架 | Tauri 2.10.3 (unstable feature) |
| 前端 | React 19 + TypeScript + Vite 8 |
| 样式 | Tailwind CSS 4 (@theme 自定义主题色) |
| 状态 | Zustand |
| 后端 | Rust + SQLite |
| 同步 | Supabase (Auth + PostgreSQL) |
| UI 库 / 业务 molecule | 见 src/CLAUDE.md §1(headless + utility,无组件库;组件清单归 docs/ui-component-inventory.md) |
2. 项目结构
仓库地图(一张树:谁是本体 · 谁是卫星 · 谁耦合谁)
本仓是 app-centric monorepo:根本体 = Lampio 桌面端(Tauri)。
src/·src-tauri/·index.html·vite.config.ts·tsconfig*·public/·dist/·根package.json(rb-scaffold) 合起来就是桌面端本体,不是与卫星平级的子目录(这解释了目录树的"不对称"——它是结构事实,非命名问题)。
reading-browser/ ← git 根(单仓 = 单记忆库 = 单 MCP 配置 = 根 CLAUDE.md 恒加载)
│
├─ 【根本体】Lampio 桌面端(Tauri + React + Rust)
│ src/ · src-tauri/ · index.html · vite.config.ts · tsconfig* · public/ · package.json(rb-scaffold)
│ ├─ 运行时耦合 supabase(sync.rs 手抄映射共享表 + 按稳定 URL invoke edge functions)
│ └─ 约定:前端见 src/CLAUDE.md · Rust/content-script 见 src-tauri/CLAUDE.md
│
├─ 【卫星】admin/ 独立 pnpm 项目(Next.js 运营后台)
│ └─ 强耦合 supabase:lib/database.types.ts 由 `supabase gen types` 派生(schema-as-code 单一真相源)
│ + 消费 llm_call_log(成本看板)+ 调 discover-sites/analyze-articles
│
├─ 【卫星】landing/ 独立 pnpm 项目(Next.js 公开落地页)· 纯静态
│ └─ 零耦合:无 Supabase / 无 service_role(拆分即为隔离爆炸半径)
│
├─ 【卫星】supabase/ 后端定义真相源(12 Edge Functions + _shared/{llm,auth,quota,tts-log,governance,url}.ts + sql/ 共享表·缓冲池·配额 DDL)
│ └─ = 桌面端 & admin & rvh 的共享契约中枢(landing 不连)
│
├─ 【卫星】rb-debug-mcp/ 独立 pnpm 项目(MCP server,TypeScript)
│ └─ 只服务桌面端 dev app:根 `.mcp.json` 指向它的 `dist/index.js`。
│ ⚠️ 改它之后要 `pnpm --dir rb-debug-mcp build` 并重启 MCP 才生效(加载的是 dist/ 不是 src/)
│
├─ 【卫星】rvh/ Lampio 移动端(Flutter / Dart)· 2026-08-28 由 filter-repo 前缀化后合入
│ └─ 与桌面端**共享数据模型**(同一套 Supabase 同步表 + 同一份预装库 `lampio_dict.db` ——
│ `rvh/assets/databases/lampio_dict.db` 是指向 RB 那份的 symlink,红线 #10 由此结构化)。
│ 工具链完全另一套;skill / CI / 验证命令的边界见 §9「子项目边界」表。
│ 自带 `rvh/rvh-debug-mcp/`(adb 调试,根 `.mcp.json` 的 `rvh-debug`)
│
└─ 【非子项目】docs/ 活跃文档,索引 docs/README.md;完成的进 docs/archive/ 与 docs/plans/archive/
tools/ 预装词库 pipeline + 短语评测,见 tools/README.md
scripts/ 工程脚本(守门 / 发版 / 备份),索引 scripts/README.md
CHANGELOG.md 桌面端 + admin + supabase + 工程;移动端在 rvh/CHANGELOG.md🔒 这张地图有机械守卫:顶层成员的机器可读副本在
scripts/check-repo-layout.mjs的ALLOWED表 (pnpm run check:repo-layout,也在ci.yml里)。新增顶层目录 = 同时改这两处, 只改一处会红 —— 这就是「建目录前先确认」的结构化形式,不再依赖记忆。 ⚠️ CI 只看得见 tracked 的目录;未跟踪的孤儿目录只有本机那一跑能发现 (2026-08-31 清掉的 436 MB 仓根data/正是这一类,gitignored、躺了五个月)。
⚠️ 卫星有五颗,其中
rvh/与其它四颗性质不同:它是另一个产品端,不是围绕桌面端的配套。 合仓的理由不是构建耦合(零共享),而是双端契约从此有唯一物理归属 (见 §9 与docs/plans/archive/rvh-merge-plan.md)。
| 维度 | 共享(整体级) | 私有(局部级) |
|---|---|---|
| 依赖/配置 | 无(非 pnpm workspace,无根 workspace 配置) | 根/admin/landing 各自 package.json + lockfile + node_modules + eslint + tsconfig + .gitignore,全独立 |
| CLAUDE.md | 根 CLAUDE.md(恒加载) | 6 份子目录 CLAUDE.md,碰对应文件时叠加 |
| 记忆 | 单一记忆库(按 git 根,全仓共用;索引 MEMORY.md) | 无子项目级记忆 |
| MCP 工具 | 根 .mcp.json:rb-debug(桌面端 dev app)+ rvh-debug(adb) | 无 |
| Skill / 文档 | 见 §9「子项目边界」表(别在这里记第二份) | 同左 |
契约中枢 = Supabase schema(supabase/sql + supabase/functions):admin 经 gen-types 编译期绑定它;桌面端经 Rust sync.rs 手抄绑定(无 gen-types 等价物,靠 /cross-end-check + Rust 类型兜底);landing 完全不连。改共享表须同时想到这三条绑定。
已下沉的三小节(正文在子目录 CLAUDE.md)
| 原小节 | 现所在 | 内容 |
|---|---|---|
| 非显然约定索引 | src-tauri/CLAUDE.md §5 + src/CLAUDE.md §4 | 「读代码得不到」的坑。⚠️ 两份都有「加新行前先自问」的写作纪律(行数会长,不在这里记数) |
| 顶层导航模型 | src/CLAUDE.md §2 | Workspace 模块 + 🔒 模块状态保全三条 |
| Rust Commands 模块 | src-tauri/CLAUDE.md §4 | 七个已目录化模块的路径陷阱 |
3. 架构要点(双端契约部分)
本节只留 RB ⇄ RVH 共享语义。 Multi-Webview 架构 / DB 双通道 / Content-Script IPC / 三条架构设计原则(能用 Tab 就不造特殊实体 · 谁渲染谁拥有 · 跨边界只用两种模式) 已下沉到
src-tauri/CLAUDE.md§1。
Auth & Sync
- Supabase Auth via REST API,token 持久化在 settings 表
- 用户切换检测:logout 时保留
auth_user_id,下次 login 时比对 - 自动同步:登录后 60s 间隔 polling(useAuthStore 管理)
- 同步矩阵 = 10 张表:learning_entries · reading_notes · reading_pages · word_page_links · word_cloze_contexts · known_words · favorite_sites · rss_feeds · domain_prefs · page_annotations。两个刻意不同步的:
reference_words(改为纯系统共享表)·rss_items(靠孤儿清理,不进矩阵)。逐张的加入时间见 CHANGELOG - soft-delete 墓碑传播覆盖 = 这 10 张全部。三分支 pull(remote 删标本地 / 本地删不复活 / 本地无+remote 删不落墓碑)+ push 墓碑脏检查。完备性不靠人记——单测
migration_chain_tests::all_synced_tables_have_tombstone_at_head守住:加第 11 张表而不给墓碑列,测试直接红 - 快照文件:页面正文不进 DB,落磁盘 + 镜像到 Storage。上传每轮 sync 做,取回是按需的(细节见 §2
snapshot.rs/cache_protocol.rs两行) - Merge 策略:取 remote 若 higher repetitions / farther next_review_date / higher mastery_level。
reading_pages.last_opened_at取 MAX(双端各自 touch 后双向 sync 不丢新值;RVH 无阅读功能,只 pull 不 push 此列)。word_cloze_contexts(一词多语境池,应用层封顶 5)双端共写——2026-08-17 评审放开 RVH 的 OCR 采集路径(见docs/cross-end/23-rb-ocr-cloze-context-decision.md;此前的事实分布是「RB 采集 / RVH 只消费」,那从来不是数据模型约束:池子按word单键组织,无来源维度)。额外做合并后重新封顶:pull 墓碑传播 + sense_gloss 取非空一方后,对本批次每个 word 跑reconcile_cloze_pool(按句 COLLATE NOCASE 去重 + 软删最旧超额行)——否则两端各 5 条 merge 成 10 条。🔒 两条只对采集侧成立的约束(pull 侧无条件 INSERT、不校验质量,故任何新增采集路径必须自带闸门):① 入池句必须能被目标词整词命中(cloze.ts::buildContextCloze定位失败 → 该语境不进 position list,于是那行占着 5 个坑位之一却在 UI 里不可见,属静默占位);②created_at必须 UTC ISO8601——它是两端 reconcile 的排序键(created_at ASC, id ASC)兼「留新删旧」的唯一依据,写本地时区串会让该端的行恒显更晚,把淘汰规则悄悄变成「留某一端、删另一端」(无报错、无不一致可观测)
2-Button 复习系统
UI 只有 Hard / Easy 两个按钮,但底层仍是 SM-2 的 quality 1-5——靠 get_quality(last_quality, is_easy) 从前次质量推导本次 quality(故 schema 里有 last_review_quality 列)。这个映射表是三端行为契约,改任一端必须同步另外两端(见 §9)。
4. 技术红线 🔒
🔒 本节 = 桌面端技术红线的唯一真相源。 不要在别处再维护一份副本——
docs/coding-standards.md§1 曾并行维护 R1-R8,结果它的 R5 长期写着updated_at=gt.(正是下面 #5 明令禁止的写法,2026-05-22 已根治), 两份红线互相矛盾约 3 个月。该副本已改为指回本节。/arch-check的机械扫描映射仍在 coding-standards.md。
🔒 红线归属规则(哪条红线该写在哪)
红线按作用域分层,各有唯一归属;下表之外的地方一律只引用、不定义:
| 作用域 | 归属 |
|---|---|
| 桌面端(Tauri/Rust/React/SQLite/sync) | 本节 |
| admin 运营后台 | admin/CLAUDE.md §3 |
| landing 落地页 | landing/CLAUDE.md |
| UI 设计系统(token / 变体 / 间距 scale) | docs/ui-standards.md |
| 数据库 schema 与迁移纪律 | 本节 #11 + docs/database-schema.md §10 |
| 操作流程类护栏(发版 / 部署 / 事故处置 / reseed) | 对应 skill 的 SKILL.md |
🚫 红线绝不能只存在于 docs/plans/ 的一次性计划里。 计划会归档,红线不会—— 一旦归档,那条约束就从活跃视野里消失,而代码还在依赖它。 发现新不变量时:先写进上表某个稳定归属,plan 里只留指针。
这不是假想风险:admin 的「
force-dynamic是 nonce-CSP 硬前提,不是性能取舍」 曾只写在一次性计划里,而admin/CLAUDE.md§3 把它描述成性能取舍——正是那条红线 明令否定的框定;照控制文件读会以为小页面可以摘掉它,摘掉即白屏且 HTTP 仍 200。 已修正为 admin 红线 #3(2026-08-15)。 守门:/arch-checkS28 扫docs/plans/*.md(排除常驻清单)里的 🔒 标记并告警。
🔒 内容归属判据(这条内容该住在哪一层)
上表管「哪个文件」,本表管**「哪一层」——判据是加载时机**,不是重要性。 🔑 总原则:放在「需要它的会话集合最窄」的那个深度。
| 这条内容是 | 落点 |
|---|---|
| 每会话都要遵守的规则,违反会出事 | 恒加载 CLAUDE.md(只留规则 + 一行理由) |
| 只在碰某棵子树时才遵守的规则 | 该子树的 CLAUDE.md |
| 为什么这么定 / 谁被它带偏过 / 实测证据 | 记忆库(索引 MEMORY.md);已有 plan 的指回 docs/plans/archive/ |
| 反复重跑的判据 | docs/verification/ + scripts/<专题>-verify.sh |
| 一次性方案 | docs/plans/,做完归档 |
往恒加载区加任何一段前先过这句:
一个不知道这段历史的会话,会不会因此把事情做错? 会 → 规则,留。不会、只是会把同一议题再讨论一遍 → 是记忆,不是指令。
🔒 全量红线索引(21 条,正文分布在三个文件)
🔒 编号是全局唯一标识符,分表后也不重排(
docs/cross-end/*按编号引用)。 下表是全量索引,21 条一条不少;正文按归属分布在三个文件里。 索引行只给一句话标题,不复述理由——新增红线时索引与正文必须同时动,漂移立刻可见。
| # | 一句话 | 正文所在 |
|---|---|---|
| 1 | rusqlite 锁定 v0.31 | src-tauri/CLAUDE.md §2 |
| 2 | SQLite 禁 LOWER(),用 COLLATE NOCASE | src-tauri/CLAUDE.md §2 |
| 3 | Content-script 用 __TAURI_INTERNALS__.invoke() | src-tauri/CLAUDE.md §2 |
| 4 | DOM 操作前断开 MutationObserver | src-tauri/CLAUDE.md §2 |
| 5 | Sync pull 用 server_updated_at=gt.{last_sync_at} | src-tauri/CLAUDE.md §2 |
| 5a | last_sync_at 仅在 errors.is_empty() 时推进 | src-tauri/CLAUDE.md §2 |
| 5b | pull_* 的 INSERT 必须写 user_id | src-tauri/CLAUDE.md §2 |
| 5c | 登出必须 resetAllTabs() + 重置 review | src-tauri/CLAUDE.md §2 |
| 5d | watermark 推进到本次实际返回行的 MAX(server_updated_at) | src-tauri/CLAUDE.md §2 |
| 5i | push 的 dirty-check 必须按归属过滤(对端 #5h) | 本节 ↓ |
| 6 | soft-delete 用 deleted_at,不 hard DELETE | src-tauri/CLAUDE.md §2 |
| 6a | 父行删除必须同事务显式软删整棵子树,禁靠 FK CASCADE | src-tauri/CLAUDE.md §2 |
| 6b | push payload 的 deleted_at 传本地真值,禁恒写 null | src-tauri/CLAUDE.md §2 |
| 6c | pull 的父行守卫必须显式回答三态(活 / 墓碑 / 缺失) | src-tauri/CLAUDE.md §2 |
| 6d | 凡写 synced_at,写完那一刻该行必须立刻不脏(W / P1 / P2 / P3) | 本节 ↓ |
| 6e | pull 分支②「本地已删 + remote 活」的 skip 必须以「墓碑仍待推」为条件,且判据由 push 的脏检查常量拼出 | 本节 ↓ |
| 7 | save_word 须重新激活 soft-deleted 条目 | src-tauri/CLAUDE.md §2 |
| 8 | 组件内禁止 hex 字面量 | src/CLAUDE.md §3 |
| 9 | 写 word 字段前必经 lemmatizer::normalize()(含 NFC) | 本节 ↓ |
| 10 | lampio_dict.db 双端只有一份物理文件(rvh/ 那份是 symlink) | 本节 ↓ |
| 11 | 已应用迁移的 SQL 本体一个字节都不能改 | src-tauri/CLAUDE.md §2 |
🔒 双端契约红线正文(#5i · #6d · #6e · #9 · #10)
这五条正文留根,判据是它们的规则文本里明写 RVH / 对端 / 双端 / 三端 / byte-equal—— 下沉进
src-tauri/等于把它们藏进「只有改 Rust 时才可见」的地方,而改 Dart 的会话再也看不到。🔒 每条的形状固定为「规则(两端同文)→ 为什么 → RB 落地 → RVH 落地」(2026-08-29 T4-4 收敛, 此前是 4 个各两三千字的表格单元,规则与两端实现搅在一起)。规则只写一遍, 两端各自的落地形状挂在它下面;RVH 的落地细节与守卫判据在
rvh/lib/features/sync/CLAUDE.md对应编号里(rvh/CLAUDE.md只留索引 + 编号对照表),本节只给指针。改规则 → 改这里(两端同时受影响);改某一端的落地 → 改那一端的文件。
关于编号:两仓不统一,靠双向指针消歧(2026-08-29 T4-4 裁定)
rvh/CLAUDE.md 的编号对照表此前写着「收敛成单一编号是 T4-4 的活」。改判为不统一:
- RVH 的 #5x/#6x 是它自己那套 sync 协议编号,2026-08-29 实测
rvh/内约 350 处引用 (lib/test注释 130+ 处、docs/cross-end/的冻结 handoff、CHANGELOG、SQL 注释)。 重编号 = 在 §0 明令禁改的历史记述里造死链 —— 与 T4-1「编号消歧而不重编号」同一判据。 - 两套编号已经在混用且工作正常:凡 RVH 没有本地编号的条目,其 Dart 注释直接写 RB 的号 (
红线 #7/#9/#6a/#1实测都在场)。真正会出事的只有 6x 段那几个同号不同义的 (RVH #6d =last_opened_at,RB #6d = 墓碑synced_at),而那正是对照表在挡的。 - 重编号没有任何机械守卫 —— 没有 lint 能验「这个裸
#6d指的是哪个仓」,做完就开始漂。
替代做法 = 双向指针:本节每条写明对端编号与落地位置,rvh/CLAUDE.md 的对照表反向写明本节位置。 🔒 跨仓引用一律带仓名(写「RB #6d」或「RVH #6g」,不写裸 #6d)。
🔒 #5i push 的 dirty-check 必须按归属过滤
规则(两端同文):push 的 dirty-check 必须带 user_id 过滤,且原有脏条件要整体括起来。
为什么:payload 一律写 "user_id": &config.user_id;脏检查若不带 user_id = ?1, 任何遗留在本地表里、不属于当前登录用户的未同步行都会被以当前用户身份 upsert 到云端 (静默跨用户泄漏,无报错)。schema.sql 里 10 张同步表的 user_id 全部可空, 唯一屏障是「所有写入都走 current_user_id」这条运行期约定而非约束——加游客模式或某写入路径漏填即破。
三款细则(两端同文):
- ⚠️ 括号是最危险处:
AND比OR结合更紧,漏外层括号退化成(user_id=?1 AND synced_at IS NULL) OR updated_at>synced_at OR ...,「改过」「软删」两支 完全绕开过滤,比不加过滤还糟;且只测新增行抓不到(新增行走的正是被括进去那支, RVH 实测注入该缺陷后 13 个新增行用例全绿),回归测试必须造「已同步过、之后又被改/被软删的外来行」。 - 用
= ?1不用IS ?1(不认领user_id IS NULL的游客遗留行;RB 读侧 65 处用= ?1)。 - ⚠️ 任何时候都不要把 NULL 行回填成当前 user_id 再推。
RB 落地:统一复用 push.rs::USER_SCOPED_DIRTY 常量,禁止手搓。 同理适用于 upload_snapshots(它把选中行字节传到当前用户的 Storage 前缀)与 get_sync_status 的待推送计数(口径不一致 = 清不掉的「待同步」计数)。 回归测试 push.rs::push_user_scoping_tests(含漏括号反向断言 + 结构守卫)+ sync/mod.rs::pending_push_sql_tests。
RVH 落地 → rvh/lib/features/sync/CLAUDE.md RVH #5h:6 个 _pushXxx 的 dirty-check 带 WHERE user_id = ?,getSyncStatus 的 pendingCount 口径逐表一致。 ⚠️ 两端的 clearLearningData* 语义相反(RB 传到来用户、RVH 传离开用户), 互抄那句 user_id IS NULL OR user_id != ?1 会正好删反 —— 细节在 RVH 那条正文里。
出处:docs/cross-end/20-rb-push-user-scoping-handoff.md(RVH→RB 交接)· 21-rb-push-user-scoping-confirmation.md(RB 2026-08-05 修完并核实)。
🔒 #6d 凡写 synced_at,写完那一刻该行必须立刻不脏
规则(两端同文):2026-08-27 由 cross-end/38 从「RB 侧实现细节」升格为双端契约(此前的条文只有下面的 P2)。四条子规则缺一不可:
- W(写侧):本端软删同步表任一行时,同一条 UPDATE 里
deleted_at与updated_at必须绑同一个参数(RB 27 处写侧全部如此)。word_cloze_contexts在对端一直没炸就是靠这条, 此前它是未写进契约的巧合,RB 改任一条 cloze 软删路径即无声破功。 - P1(禁本端时钟):pull 墓碑分支的
synced_at只能由该远端行自己的列拼出 (updated_at/created_at/deleted_at),任何形式的本端now都不许出现。 这条独立于 P2:RVH 那 5 张错表压根没写 MAX,形式上不违反 P2、只违反 P1 —— 照旧条文审对端会全部蒙混过关。 - P2(取最大):
synced_at = MAX(sync_ts, deleted_at),sync_ts = 远端 updated_at ?? 远端 created_at, 绝不能是 NULL(SQLite 多参MAX()只要有一个操作数为 NULL 就整体返回 NULL → 命中脏检查的synced_at IS NULL支 → 同一个回声环换个身份复发)。10 张表里updated_at可空的恰是 reading_pages / word_page_links / word_cloze_contexts 三张,回落created_at(Supabase DDL 上 NOT NULL)。 - P3(收敛判据覆盖 push 侧):「写完立刻 clean」对每一条写
synced_at的路径成立, 包括 push 成功后的 mark。
⚠️ 比较方式必须与脏检查同构:这里的 MAX 取的不是「时间上更晚」而是「让谓词为假」, 故两个操作数必须就是这一行将要存下的那两个字符串,且用字符串序比(SQL MAX() / Dart compareTo)—— 禁止 parse 成 DateTime 再比,也禁止拿本端值与远端值比:RB 写 +00:00、RVH 写 Z, 同一微秒时 Z 恒大,小数位位数不同也能反序。守住 P1 之后格式差异自动不成问题 (同一远端行 = 同一个写入方 = 同一种格式)。
为什么:否则 push 脏检查 deleted_at > synced_at 恒真 → 该行每轮 sync 都被重推 = 回声环。 两条独立触发路径,堵一条不够:① 对端软删时不 bump updated_at(RVH 删笔记本时对 word_page_links 正是如此:deleted_at=04:53 而 updated_at 仍是 01:15); ② 两端时钟差(违反 P1 时)—— autoSync 60s 意味着「对端删除 → 本端 sync 起始」常只隔几秒, ⇒ 半分钟量级的漂移就够(RVH 2026-08-27 真机反算的实测门槛是 27 秒,见 cross-end/39 §5.3; 「几秒」是按 autoSync 60s 估的理论最坏值,真实观测更宽松),飞行模式、手动改时间、NTP 没跟上都能造出来。 症状不是报错而是静默灼烧:2026-08-03 实测 57 行每 60s 重推、server_updated_at 被自己不断刷新、 对端每轮重新拉一遍,永不收敛。
RB 落地:common.rs::mark_synced 2026-08-27 补 MAX(..., deleted_at)(P3)。 回归测试 pull/library.rs::tombstone_echo_tests + common.rs::mark_synced_tests(各含反向断言)+ 结构守卫 pull/mod.rs::tombstone_synced_at_guard(全仓每条墓碑 UPDATE 二选一落在 P1+P2 或 W 上; 三条规则各注入一次缺陷实证过)。
RVH 落地 → rvh/lib/features/sync/CLAUDE.md RVH #6g(四条子规则同名同义): W = 全仓 11 处软删站点 · P1 = _remoteSyncTs 单点(回落链终点刻意是空串不是 now)· P2 = 6 处墓碑分支的 MAX(?, ?) + Dart 侧 _maxIso 走 compareTo · P3 = _markSynced。 🔒 两项证据只在 RVH 侧有,别指望本节:27 秒真机门槛的反算过程;以及「丢 P3 在 RVH 是 真实可达的、在 RB 只是防御性」。
🔒 #6e 分支②「本地已删 + remote 活」的 skip 必须以「墓碑仍待推」为条件
规则(两端同文):pull 的分支②「本地已删 + remote 活」不许无条件 skip,必须拆两支—— ②a 墓碑仍待推 → skip(等 push 传播,= 2026-08-31 之前的唯一行为); ②b 墓碑已不待推 → 接受远端、复活本地行。 🔒 「仍待推」必须由 push 的脏检查常量拼出,禁止在 pull 侧手抄一份谓词。 复活的那条 UPDATE 同时受 #6d 约束:updated_at / synced_at 只能由那个远端行自己的列 拼出(P1,禁本端时钟),且必须同时写 synced_at 让该行写完立刻不脏(P3)。
为什么:分支② 的注释一直写着「本地墓碑靠 push 传播,不复活」——那句话默认墓碑还是脏的。 而按 #6d 规则 W + push 成功后的 mark,墓碑推完就恒不脏:于是 pull 指望 push、push 说没得推, 两端各说各话地永久停住(无异常、无告警,用户看到的是「这台设备上有、那台上永远没有」)。 2026-08-31 真机实例 word_cloze_contexts 3a50ad2b。两端写侧都刻意实现了「再遇到同一句/同一个词 → 复活旧行」(#7 精神),所以这不是某一端的 bug —— 单方面改哪一端才是制造真正的不对称。
🔑 判据是因果的,不是时序的:问的是「push 还会不会送它」,答案只由本地那一行自己的三列决定, 全程不比较任何两个跨设备的时间戳(updated_at 跨端比大小正是 #6d 禁止的那类)。 远端赢的决定性理由是可恢复性不对称:判错了(对端推的是陈旧活行,#6b 那条路径)用户 再删一次就收敛;而分歧态下任何用户操作都修不了墓碑那一端(对已墓碑行再删是 no-op)。 ⚠️ 不是某一张表的事:分支② 在两端所有 pull 里逐字都在,RB 侧 10 张表里 8 张有复活写路径 —— 最贵的是 learning_entries(手机删词、桌面再存 ⇒ 手机永远没有)。
RB 落地:单点 sync/pull/mod.rs::resolve_tombstone_vs_remote_alive,谓词由 push.rs::USER_SCOPED_DIRTY 格式化拼出;10 个 pull_* 的分支② 全部经它,②b 命中写一条 log::warn(可观测,见裁决单 §7.3)。回归测试 pull/mod.rs::redline_6e_tests(6 例, 含「墓碑仍脏 → 仍 skip」的防过度修复反向用例)+ pull/vocab.rs::redline_6e_cloze_cap_tests (与 #5g cap-5 的交互)+ 结构守卫 sync/mod.rs::sync_matrix_tests 三条(判据来自 push 常量 / 10 张表都经它且清墓碑只有一处 / 复活语句守 #6d P1+P3)。九处注入各实证过一次。
RVH 落地 → rvh/lib/features/sync/CLAUDE.md RVH #6i (2026-08-31 落地):判据 _tombstonePendingPush 由 _kUserScopedDirty 拼出(该常量本轮才由 6 个 _pushXxx 的内联副本收敛而来)· 判据+复活+warn 合成 _resolveTombstoneVsRemoteAlive · 6 处分支② 经 _keepSkippingTombstone 逐字同形(②b 还必须 total++,否则 watermark 卡住 —— 真机实证)。 回归 test/features/sync/tombstone_remote_alive_test.dart(21 例,13 处注入实证)。已装机真机验收,存量清零。🔴 前置 RVH-0 已一并修:_pullKnownWords 从前压根没有分支②, 远端活行路径把 synced_at 刷成本端 now ⇒ 本地墓碑变 clean 却从未上行(该删除今天就在静默丢失), 且会让本条的前提(clean ⇒ 服务端见过这条墓碑)在那张表上不成立。
出处:docs/cross-end/47-rb-tombstone-vs-remote-alive-adjudication.md(裁决单)· 49-rvh-tombstone-vs-remote-alive-confirmation.md(RVH 回执,含对裁决单三处判断的订正)。
🔒 #9 写 word 字段前必经归一
规则(两端同义,落地形状不同):写入 vocabulary / learning_entries / known_words / reference_words 的 word 字段前必经归一(含 NFC)。
为什么:vocabulary.word 是 PK 且 COLLATE NOCASE,跨端共享;漏归一会破坏跨端 PK 一致性, 触发 sync 端的 backfill skip + log::warn(2026-04-28 Phase 1/2 落地)。
归一所需数据:Layer 1/2 自 2026-06-15(cross-end/13) 起从 lampio_dict.db 的 lemma_* 表运行时装载(原 include_str! JSON 已下线)—— 即这一半由红线 #10 的「只有一份」保证两端同源;Layer 3/4 是两端各一份的算法代码,对齐义务照旧 (docs/verification/learning-loop.md K6 记着:单词归一的 Layer 3/4 分叉目前没有跨端断言, 只有短语归一有 CSV 回归集)。
🔑 键空间细则(两端同义,2026-08-28 由 cross-end/43 裁定): 「必经归一」不等于「一律 normalize」。因为 vocabulary 里同时住着 lemma 行和非 lemma 行 (bear/bearing、people/person 都是行),真正的判据是解析成一个真实存在的 vocabulary 行; 而同一个串从「页面 surface」来还是从「已取出的 vocabulary.word」来意思不同, 字符串本身不携带这个信息、只有调用方知道 ⇒ 分流在调用方。 规则本体与反例写在实现文件的头注释里(RB src-tauri/src/db/word_key.rs),本节不复述第三份。
RB 落地:lemmatizer::normalize();键空间单点 db/word_key.rs。 守卫 redline9_guard(2026-08-27 建,当场抓到 add_reference_word 手搓半个 normalize)+ write_and_delete_sides_are_paired(写/删两侧必须共用同一个键推导 —— 只改写+读的后果是 「加得进、删不掉」且不报错)。RB 需要分流:它比 RVH 多一条 content-script 查词弹窗的 页面 surface 腿。
RVH 落地 → rvh/lib/features/sync/CLAUDE.md §跨端契约镜像 backlog 的「RB 红线 #9」条 (2026-08-28 落地,回执 cross-end/46): 判据单点 lib/shared/data/database/word_key.dart。RVH 当前所有调用方传的都是 vocabulary.word, 故本端暂不需要分流(surfaceWord 零调用方但刻意保留 —— 删掉等于把「这里需要分流」从代码里抹掉)。
🔒 #10 预装库 lampio_dict.db 双端只有一份物理文件
规则(两端同文):双端共用同一个物理文件 —— rvh/assets/databases/lampio_dict.db 是指向 src-tauri/assets/lampio_dict.db 的相对 symlink(../../../src-tauri/assets/lampio_dict.db)。 禁止对该文件本身就地 ALTER / UPDATE —— 运行时只 ATTACH 只读读取。
🔴 2026-08-29(rvh-merge-plan T4-5)由「两端字节相等」升级为「只有一份」。 旧表述描述的是同一个意图的弱形式:字节相等要靠断言守,只有一份则结构上不可能违反。 读老文档(docs/cross-end/* / CHANGELOG)时遇到「byte-equal」= 同一条契约的旧形式,不是别的规则。 ⚠️ 合仓前就没有「省 65MB 历史体积」这回事:git 按内容寻址,byte-equal 的两份天然只存一个 blob(实测 HEAD 两路径同为 b7c3bdd4;全历史 RB 7 / RVH 6 个 distinct blob,并集 8 不是 13)。 symlink 换来的是判据结构化 + 工作区 checkout 少落 65 MB,不是仓体积。
为什么:预装词库单点产出。任一端的就地修改会让两端 schema 漂移、跨端 sync 时触发 backfill skip + log::warn,且 VOCABULARY_SEED_VERSION 常量失去版本语义(2026-05-14 v10 落地)。
唯一豁免(2026-08-18,cross-end/24): tools/vocabulary_builder_v3/bin/rebake_lemma_tables.dart 是唯一允许就地改这个 db 的路径 —— 它确定性重写整张 lemma_* 表(不是 ad-hoc UPDATE),带「vocabulary 行数必须不变 + 4 表白名单」守卫, 跑完 RVH 侧自动就位(symlink,无需再同步任何东西)。 「禁止就地 ALTER/UPDATE」针对的是手搓 SQL 改单行 —— 那种改动无法复现、两端必漂移。
RB 落地:唯一产源 = tools/vocabulary_builder_v3/(Dart pipeline,2026-07-19 从 RVH 迁入)。 版本锚 VOCABULARY_SEED_VERSION(src-tauri/src/db/helpers.rs)。 提交前闸门 = scripts/check-vocab-asset.sh(2026-08-29 T4-6 由 sync-rvh-vocabulary.shgit mv 而来 —— cp 随 symlink 消失,留下的是它顺带装着、没有任何替代品的两道闸: ① 体积闸(≥80 MB 告警 / ≥100 MB 退出 1,盯的是 GitHub 单文件硬限,超了 push 直接被拒) ② 资产卫生(4 表白名单 + lemma_* 行数 + audio_local_path / last_accessed_at 全 NULL)。 /vocab-reseed skill 的 Step 3 跑它,ci-cross-end.yml 也跑它 —— ⚠️ 但 CI 挡不住体积闸 那条(push 先于 CI 被拒),提交前那一次人工执行不能省。
RVH 落地 → rvh/lib/features/sync/CLAUDE.md RVH #5e(同一条契约在那侧的编号): 纯消费方,只 ATTACH 读;版本锚 _preinstalledVocabVersion。 🔒 两个版本锚不比相等 —— pipeline 反转成 RB→RVH 之后两个计数器各自独立走 (RVH 曾为自身导入逻辑单独 bump 过一次),判据是「资产 sha 变了两端锚都要动」。
机械守卫:scripts/cross-end-check.sh §B —— 结构三态断言(2026-08-29 T4-5 换型; 旧的 sha256 比对在只剩一份之后是拿同一个文件跟自己比,恒绿、什么也发现不了): ✅ 是 symlink 且 -ef 解析到 RB 那份 · ⚠️ 是 40 字节的链接目标文本(Windows core.symlinks=false 的检出形态,判「本次没验」)· ❌ 其余(普通文件副本 / 硬链接 / 指错地方)。 四种缺陷各反向注入实测过。随 .github/workflows/ci-cross-end.yml 进 CI(ubuntu,不受 ⚠️ 那支影响)。
⚠️ Windows 上这个路径不是文件:本仓会被 clone 到 Windows(ci-rust.yml / release.yml 都跑 windows-latest),而 git 在 core.symlinks=false 的 clone 上把 symlink 落成一个装着 目标路径的 40 字节文本。当前无破坏面(那两条 workflow 只编译 src-tauri/,RVH 不在 Windows 上构建);真要在 Windows 上碰 rvh/,先 git config core.symlinks true 再重新 clone。
沿革(读历史 SHA 日志时会用到):
| 时间 | 变化 |
|---|---|
2026-06-15(cross-end/13) | lemmatizer 折叠:db 内增 3 张 lemma_* 表(surface_to_base / base_forms / meta,Layer 1/2,运行时直读、不进 seed upsert),sync 脚本断言从「1 表」放宽为「4 表白名单」;surface_to_base.json / base_forms.json 不再打包,降级为 cargo run --example build_dict 产物 + pipeline 构建输入(byte-equal 要求随之从「JSON 运行时资产」迁到「db 内 lemma_* 表」) |
| 2026-07-19 | pipeline 从 RVH 迁入 RB 成为单一产源,sync-rvh-vocabulary.sh 方向反转为 RB→RVH(断言/白名单不变)。RVH 降级为纯消费方。方案见 rvh-vocab-pipeline-migration-plan.md |
| 2026-07-26 | 改名 reading_vocab.db → lampio_dict.db(避与用户库 lampio.db 混淆;纯改文件名、字节不变、无版本 bump)。历史 docs/cross-end/* SHA 日志里的 reading_vocab.db = 同一资产。两端 cutover 已完成(2026-08-29 实测:两侧都是新名且 byte-equal) |
| 2026-08-29(rvh-merge-plan T4-5) | 两份 → 一份:RVH 那个路径改成 symlink。实测 flutter build bundle 跟随 symlink,产物是真实的 65,777,664 B db(sha 一致、AssetManifest.bin 有它),flutter test 1359 passed 与基线逐字相同。规则、守卫、Windows 注意事项见上 |
| 2026-08-29(rvh-merge-plan T4-6) | symlink 之后 sync-rvh-vocabulary.sh 的 cp 没有对象了,git mv 成 scripts/check-vocab-asset.sh(只留体积闸 + 卫生断言)并接进 ci-cross-end.yml。读老文档遇到「跑 sync-rvh-vocabulary.sh 整包覆盖」= 今天的「跑 check-vocab-asset.sh 过闸门,RVH 侧无动作」 |
| 2026-08-31 | 同型扩展到 lemmatizer 资产:rvh/assets/nlp/{base_forms,surface_to_base}.json 也从「byte-equal 两份」改成指向 RB 产物的 symlink。cross-end-check.sh §C 随之从 sha256 比对换成结构三态(否则跟随 symlink = 拿同一个文件跟自己比、恒绿),三态逻辑收敛成 §B/§C 共用的 assert_single_file。⚠️ 在 symlink 化的资产上做反向注入必须先 rm 链接本身——cp X 链接 / > 链接 会穿透写坏 RB 真身(2026-08-31 实测把 base_forms.json 截成 45 B) |
| 2026-08-31 | 资产「进得了 git」补成第二道断言(cross-end-check.sh §B 的 check_committable):rvh/.gitignore 的 *.db 例外行曾同时坏了两处(旧名 reading_vocab.db + 行尾 # 在 .gitignore 里不是注释),而文件早已 tracked、git check-ignore 默认跳过已跟踪路径 ⇒ 零现象,直到哪次重新 git add 才静默丢资产。判据只能看命中的 pattern 是不是 ! 开头,不能看退出码(命中否定规则同样 rc=0) |
/arch-checkskill 自动扫描这些规则 + 文件大小/store 污染/未归档 plans 等软规则。 已下沉的 16 条同样在扫描范围内——/arch-check读代码,不读本文件。
5. 数据库(Supabase 共享部分)
本节只留 RB / RVH / admin 三方共享的部分。 RB 本地迁移链(版本 / 冻结纪律 / 三道守门)与预装数据(
VOCABULARY_SEED_VERSION/ build.rs 注入 Supabase 配置)已下沉到src-tauri/CLAUDE.md§3。 逐条迁移明细的唯一真相源仍是docs/database-schema.md§10。
Supabase 同步表
定义在 supabase/sql/sync-tables.sql:10 张用户表(learning_entries / reading_notes / reading_pages / word_page_links / word_cloze_contexts / known_words / favorite_sites / rss_feeds / domain_prefs / page_annotations)+ 3 张推荐表 + RLS + deleted_at(user_reference_words 已废弃,待 Dashboard DROP)
⚠️ 上面这句「+ deleted_at」是对整组表的概括,逐表 DDL 请以
supabase/sql/sync-tables.sql为准,别把它当作某张表一定有该列的证据。历史教训:user_reading_notes直到 2026-08-03 才补上deleted_at,而这句话早就写着「+ deleted_at」——cross-end/19 的 RVH 会话正是被它带偏,先推断出一个不存在的破口才发现真缺口。
Supabase vocabulary 表角色(v10 之后)
缓冲池(不是权威源):只存预装库外的用户查询词,每次发版清空。
- 权威源 = 双端共用同一份预装库
lampio_dict.db(红线 #10:一个物理文件) - 缓冲池流程:RB/RVH 用户查到预装库外的词 → 走 Edge Function
lookup-or-fetch-word兜底入库到 Supabase vocabulary → 跨端其他用户的backfill_word_inner能命中 - 管理员定期 review 缓冲池,决定是否把高价值新词晋升进 RB pipeline(
tools/vocabulary_builder_v3/,2026-07-19 迁入)→ 下次发版进预装库
Soft-Delete 策略
- Push:payload 包含 deleted_at +
deleted_at > synced_atfallback 条件 - Pull:remote deleted_at → 标记本地;本地已删但 remote 未删 → skip(让 push 传播)
- word_page_links pull:插入前做 FK 存在性检查
6. 安全规则 🔒
需要用户确认的操作
| 类别 | 操作示例 | 危险等级 |
|---|---|---|
| Git 危险操作 | git reset --hard, git push --force, git branch -D | 🔴 高 |
| 文件删除 | 删除源代码文件、删除数据库文件 | 🔴 高 |
| 数据库操作 | DROP TABLE、DELETE 无 WHERE、修改 migrations | 🔴 高 |
| 环境配置 | 修改 Cargo.toml 依赖版本、修改 tauri.conf.json | 🟡 中 |
安全操作(无需确认)
cargo check/cargo buildpnpm install/pnpm build/pnpm dev- 读取文件、搜索代码
- 创建新文件(不覆盖现有)
git add/git commit(用户明确要求时)git status/git log/git diff
7. 项目 Skills
Skill 定义在 .claude/skills/,每个有独立 SKILL.md。触发条件不在这里列—— 每个 skill 的 description 每会话自动注入你的工具集,抄一份在这里只会变成第二本账。 这里只放 description 里没有的两件事:深度 与 它在不在提交流程链上。
流程链五步(互相无硬依赖,可单独跑,推荐按序):
代码修改完成
↓ /arch-check 极浅 <30s 机械规则(grep 能判):hex / 文件超标 / store 污染 / spacing / svg 计数
↓ /ui-check 浅 <60s 语义规则(要看上下文):列表走 ListItem / 空态走 EmptyState / hover 一致性;仅 .tsx 变更触发
↓ /build-check 浅 <60s cargo check + pnpm build + content-script review
↓ 手动测试 / tauri dev
↓ /rb-code-review 深 2-5min Rust/TS/content-script 三端:技术红线 + SQL 安全 + 跨端一致性
↓ /doc-sync-check 中 <1min 这次改动要同步哪些文档(6 类变更 × 文档映射)
↓ git commit动作类七个(不在链上,各由明确的手工或事故信号触发): /vocab-reseed(预装库 reseed;收到 RVH 词库交接文档时也触发)· /edge-deploy(Edge Function 部署 + 实机验证)· /cross-end-check(三端结构一致性只读诊断,漂移只报不修)· /release(Windows 发版编排)· /db-incident(生产数据事故处置,诊断完成前禁写)· /phrase-eval(短语识别质量评测)· /docs-audit(文档体系周期性体检)。深度:/docs-audit 深,其余六个中等。
三条边界(最容易混):
| 易混对 | 分界 |
|---|---|
/arch-check vs /ui-check | 前者机械规则(grep 能判),后者语义规则(要看上下文) |
/arch-check vs /rb-code-review | 前者量化硬规则(计数 / 版本 / 字面量),后者语义与最佳实践 |
/doc-sync-check vs /docs-audit | 前者跟着一次代码变更走,后者是周期性体检(整个体系有没有腐烂 / 重复 / 错位) |
admin/有自己的 scoped 变体(admin:code-review/admin:doc-sync-check/deploy), 碰admin/文件时才注册进工具集。landing 无 scoped skill。
🔒 本仓的深度审查叫
/rb-code-review,不叫/code-review(2026-08-30 改名)。 原名与 Claude Code 的内置code-reviewskill 同名,项目 skill 会把内置的整个遮蔽掉 —— 于是/code-review ultra(多 agent 云端评审)/low/--fix/--comment在本仓一律够不着, 而且没有任何报错,只是那个名字解析到了另一个东西。改名当场两个都回到工具集里。 ⚠️admin/CLAUDE.md与rvh/CLAUDE.md里的裸/code-review指的是各自的 scoped skill (admin:code-review/rvh:code-review,带前缀故不撞名),不要跟着改。docs/plans/archive/与 CHANGELOG 里的/code-review是历史记述,按 §0 同样不改。
8. 开发工作流
提交流程链(编码 → arch-check → ui-check → build-check → 手测 → code-review → doc-sync-check → commit)见 §7,那里连深度一起写了,此处不再抄第二份。
Plan Mode 计划持久化
进入 plan mode 后,计划文件必须额外保存一份到
docs/plans/目录。
.claude/plans/中的计划文件会随会话异常中断而丢失- 计划写完后,复制到
docs/plans/<feature>-plan.md(中文内容) - 新会话继续任务时,先读
docs/plans/中的计划文件恢复上下文 - 命名示例:
docs/plans/archive/epub-intra-chapter-position-plan.md、docs/plans/archive/telemetry-opt-in-plan.md(<feature>-plan.md;交接类用<feature>-handoff.md)
代码提交规范
格式:<type>(<scope>): <subject>
| type | 用途 |
|---|---|
feat | 新功能 |
fix | Bug 修复 |
docs | 文档更新 |
refactor | 重构(无功能变更) |
style | 样式/格式调整 |
perf | 性能优化 |
chore | 构建/工具变更 |
示例:feat(sync): add soft-delete propagation for learning_entries
分支管理 & 版本发布规范(个人开发 · 主干开发 trunk-based)
📖 人类翻阅完整版见
docs/coding-standards.md §10(含多会话并行纪律 / 分支卫生命令速查)。本节为速查摘要。
模式 = 主干开发(trunk-based),不用 GitFlow。 个人开发单机维护
develop/release常驻分支是纯负债。默认在main上线性提交,只在真正需要隔离时切短命分支,合入即删。禁止让分支长期存活当档案——历史留在 main 上就够了。
何时直接在 main / 何时切短命分支:判据与四类触发场景见 docs/coding-standards.md §10.1-10.2。 一句话版:能保持 main 绿的小改动直接提交(约 80%);大/险重构、可能丢弃的实验、 想过一遍 CI/PR 门、跨多会话大工程——命中任一即切短命分支,合入后 git branch -d 立即删。
多会话并行纪律(触发不靠用户记忆):共用同一工作树有三条静默丢失路径——路径 A 文件级:两会话改同一文件 = 后写者赢、先写者静默丢失(git 无从发现);路径 B 工作树级:任一会话跑
reset --hard/checkout ./stash push,无差别抹掉整棵树上所有未提交改动;路径 C 暂存区级:git add与git commit之间,谁先 commit 谁就把共享 index 里的全部内容提交走——不丢代码,丢的是历史可读性(你的改动被塞进一条与之无关的 commit 消息底下),且**「改完立刻提交」正好覆盖不到它**(危险窗口就在 add 与 commit 之间,显式git add反而是触发条件;2026-08-14 实测踩过)。路径 B 不看文件域,"我们不相交"和显式git add都挡不住(2026-08-01 实测踩过)。约定挡不住,只有独立 worktree 能挡三条;退而求其次:改完立刻提交(挡 B)+ 提交用git commit <pathspec>绕过 index(挡 C)。隔离决策由 Claude 承担,不要求用户每次发问:① 首选——用户说"并行做 X/Y"时,Claude 默认用Agent(isolation:"worktree")让每个任务跑独立 worktree(零记忆、无盲窗);② 补充——Claude 动手改代码前自查git status/git worktree list(含会话启动时 harness 给的 git status 快照),探到并行痕迹且会碰枢纽文件(commands.ts/lib.rs/strings/**/*/mod.rs/migrations.rs/sync/)则主动开 worktree 并告知,不等用户问;即使不碰枢纽文件、文件域不相交,也要把"每完成一个可编译小步就提交"当硬要求执行(路径 B 只认未提交状态,不认文件域)。纯文档/规划/不相交模块可共用树 + 廉价护栏(勤提交 + 显式git add勿-A+ 禁切 HEAD + 禁无 pathspec 的reset --hard/checkout ./stash push)。三层保障:worktree 挡静默覆盖 →git merge暴露文本冲突 →/build-check+CI 抓语义冲突。成本压缩:pnpm 共享 store(默认)+ 共享CARGO_TARGET_DIR。详见docs/coding-standards.md §10.3。
发版 = tag,不是分支(/release skill 编排):
- 版本唯一真相源 =
src-tauri/tauri.conf.json的version(决定 app 版本 + tag)。派生的另外三处由scripts/set-version.mjs写、pnpm run check:version-sync断言:src-tauri/Cargo.toml·package.json·src-tauri/Cargo.lock的app包(第四处 2026-09-01 才补进来)。⚠️ 「package.json的version是死字段(长期0.0.0)」这句已过期(2026-07-26 起它跟着 bump,实测0.1.0-dev.11)—— 死的是它的消费方,不是它的值。 - 递进:
v0.1.0-dev.N(当前内测)→ 功能冻结 →v1.0.0-rc.N→ 首个真实外部用户安装的版本 =v1.0.0。 - v1.0 发出后 main 多一条义务:永远保持可发布。任何险活先走短命分支(上面四类触发之一),别直接怼 main。
- 热修按需、不预建 release 分支:v1.0 后线上要热修 → 从对应 tag(如
v1.0.0)切临时release/v1.0.x→ 修 → 打v1.0.1→ cherry-pick 回 main → 删分支。 - v1.0 后语义化:fix→PATCH、加功能→MINOR、破坏性 data-model 变更→MAJOR(有迁移链,基本用不到)。
分支卫生:merge 后立即删;定期清 0-ahead 僵尸分支;本地 main 定期 git push github main + git push origin main 双推备份(单机开发 = 未推的 commit 无备份)。命令速查见 docs/coding-standards.md §10.5。
数据库变更流程
- 在
src-tauri/src/db/migrations.rs添加新迁移(递增版本号) - 同步更新
supabase/sql/sync-tables.sql(如涉及同步表) - 编译检查
cargo check - 测试迁移(删除本地 DB 文件重新运行)
缺陷修复原则
- 不做 workaround,必须根因修复
- 修复前先 review 代码逻辑
- 检查边界条件
- 验证依赖有效性
9. 双端整合
本节只留规则。「为什么当初这么定 / 谁被它带偏过 / 实测证据」按 §4 内容归属判据已移出 —— 各处指针指向承载它的归档件或记忆库。
子项目边界(skill / 验证命令 / 契约耦合)
| 子项目 | dev-flow skill | 编译 / 验证命令 | 与 Supabase 契约 |
|---|---|---|---|
admin/ | 排除;用 admin:code-review · admin:doc-sync-check · deploy | pnpm --dir admin exec tsc --noEmit | 编译期绑定(supabase gen types) |
landing/ | 排除;无 scoped skill | pnpm --dir landing exec tsc --noEmit + pnpm --dir landing build | 零耦合 |
rvh/ | 排除;自带 8 个 rvh:* | flutter analyze + flutter test | 手抄绑定 |
🔒 两条硬约束:
- 改 admin 里手抄的表/列之前必核对
supabase/sql/sync-tables.sql的当前列集 —— admin 曾静默漂移过(2026-07 表名改名 + 2026-04-28 vocabulary word-PK 无 id 列)。 - landing 的 Vercel env 绝不含
SUPABASE_SERVICE_ROLE_KEY—— 拆分的全部理由就是 隔离这个爆炸半径;landing 是真实外部用户访问的公开页,必须与持 service_role 的部署分体。
缘起与完整论证:
docs/plans/archive/admin-merge-plan.md·docs/plans/archive/landing-split-plan.md。
RB + RVH 关系
- Lampio (RB) = 桌面端(Tauri + React)= 本仓根本体(见 §2 仓库地图); Lampio 移动端 (RVH) = Flutter/Dart = 本仓
rvh/子目录 (2026-08-28 由独立仓filter-repo前缀化合入,见docs/plans/archive/rvh-merge-plan.md) - 两端共享数据模型,Supabase 共用(同步 = 数据搬运,同一套表结构)
- 统一命名规范:本地基名三端一致(RB=RVH),Supabase 每用户同步表加
user_前缀。 表名 vocabulary / learning_entries / reading_notes / reading_pages / word_page_links / known_words / favorite_sites(RB-only)/ default_stopwords(纯本地预装、不同步); FK 列名learning_entry_id/reading_page_id。读老文档遇到 notebook_entries / reading_sources / word_sources / sites / excluded_words / recommended_excluded_words 时,旧→新对照与三端闭环记录在
docs/plans/archive/table-rename-three-end-plan.md。
路径写法
- 本仓一律仓根相对路径;移动端的文件写
rvh/...(如rvh/lib/core/algorithms/sm2_algorithm.dart)。 反引号包住的仓内路径由pnpm run check:claude-paths校验,7 份 CLAUDE.md 全覆盖。 - ⚠️
docs/cross-end/*与docs/plans/archive/*里仍有大量~/reading_vocab_helper/...: 老双仓写法,等价于今天的rvh/...,属历史记述不要去改。scripts/check-doc-links.mjs为此保留了对它的负向断言 —— 挡的不是"违规写法", 是"别把历史文档里的老路径误报成悬挂"。
进度追踪
| 想知道 | 去处 |
|---|---|
| 跨端做过什么、怎么做的 | CHANGELOG.md(桌面端 + admin + supabase + 工程)+ rvh/CHANGELOG.md(移动端) |
| 跨端交接(谁改了什么、对端要跟什么) | docs/cross-end/ —— 编号总表在其 README |
| 还剩什么没做 | docs/plans/backlog.md §跨端整合尾项(均为独立功能) |
会话隔离规则(合仓后收窄)
涉及 RVH 代码的修改仍须新开会话;纯文档 / 契约类改动不必。 判据是要不要动另一套工具链,不再是"碰到 rvh/ 就换会话"。
| 原因 | 说明 | 合仓后还成立吗 |
|---|---|---|
| 技术栈差异 | RB = Tauri/Rust/TS,RVH = Flutter/Dart,混合上下文易出错 | ✅ 成立 |
| 工具链差异 | cargo/pnpm vs flutter/dart,验收命令完全不同(cargo check+pnpm build vs flutter analyze+flutter test);CI 也分两条(ci.yml/ci-rust.yml vs ci-rvh.yml) | ✅ 成立,且是主要理由 |
| 控制文件与 skill 分两套 | rvh/CLAUDE.md 只在碰 rvh/ 文件时叠加;RB 四个 dev-flow skill 排除 rvh/,RVH 自带 8 个 rvh:* scoped skill | ✅ 成立 |
❌ 已消失 —— 合仓后路径带 rvh/ 前缀,同名概念不再解析到同一个字符串 |
实践边界(2026-08-29 T4-4 实测走通):改 rvh/CLAUDE.md、rvh/docs/** 这类 不需要跑 Flutter 就能验收的改动,在 RB 会话做是安全的 —— 它们的验收是 check-claude-md-paths / check-doc-links,本来就是根侧脚本。 一旦改动需要 Flutter 才能验收 —— 改 rvh/lib/** / rvh/test/** / rvh/pubspec.yaml 的行为, 或验收要跑 flutter analyze / flutter test(例如 rvh-merge-plan 的 T4-3 / T4-5)——回到新会话。 唯一的例外是 Dart 文件里纯注释的路径修正(2026-08-29 T4-7 做过一次:归档计划后补 /// 里的指针), 它不改行为;除此之外别在 RB 会话里碰 rvh/lib/**。
Schema 同步协议
当修改涉及同步表结构时(同步矩阵 10 张,见 §3;其中与 RVH 共享 6 张 —— learning_entries / reading_notes / reading_pages / word_page_links / known_words / word_cloze_contexts, 另 4 张是 RB-only。vocabulary 不在同步矩阵里,是缓冲池,见 §5):
1. 先改 RB 本地表(migrations.rs 新增版本)
2. 同步更新 Supabase DDL(supabase/sql/sync-tables.sql + Dashboard 执行)
3. 更新 docs/database-schema.md §9 映射 + §10 迁移历史
4. 写一份 docs/cross-end/NN-<主题>-handoff.md(编号接 cross-end/README.md 的总表)
5. 提醒用户:另开一个会话同步 Dart 端 schema(**同一个仓**,换的是上下文不是目录——见下面的会话隔离规则)谁先改:RB 优先(Rust 类型系统能更早发现问题),RVH 跟进。
共享表变更检查清单:
- [ ]
migrations.rs新版本号 - [ ]
supabase/sql/sync-tables.sqlDDL 更新 - [ ]
database-schema.md§3-§5 本地表 + §9 Supabase 表 - [ ]
src-tauri/src/commands/sync/的 push/pull 逻辑适配(不是sync.rs—— 早已拆成目录) - [ ]
rvh/assets/sql/01_create_tables.sql+ Dart 侧对齐(新会话,要跑flutter test) - [ ] 跑一次
bash scripts/cross-end-check.sh—— §E 会以 Supabase DDL 为仲裁者比两端列集合, 新列漏在某一端会直接红(这道闸门 2026-08-29 起也在 CI 里)
SM-2 算法一致性
三端实现必须行为一致:
| 端 | 文件 | 语言 |
|---|---|---|
| RB 后端 | src-tauri/src/commands/srs.rs | Rust |
| RB 前端 | src/lib/sm2.ts | TypeScript |
| RVH | rvh/lib/core/algorithms/sm2_algorithm.dart(类 SM2Algorithm) | Dart |
一致性规则:
get_quality(last_quality, is_easy)映射表三端完全相同easy_factor下限 1.3,上限无interval计算公式:repetitions=1 → 1天,repetitions=2 → 6天,之后interval * easy_factor- 修改任一端的 SM-2 逻辑时,必须同步验证其他两端
- 🔒 这条纪律已经机械化:三端消费同一个物理文件
docs/cross-end/sm2-golden-vectors.json(2026-08-29 T4-3 起无副本)—— 三个消费方:srs.rs::sm2_golden_tests·src/lib/sm2.test.ts·rvh/test/sm2_golden_test.dart。 改任一条expected会让三端测试同步红。守卫scripts/learning-loop-verify.shL1(全仓不许出现第二份同名文件)+ L2(Dart 消费方读的确实是根那一份)。scripts/cross-end-check.sh§D 从这份 JSON 现推打印,只硬断「EF 下限 1.3 在场」一条 heuristic ——「三端是否真的符合期望」由三端各自的黄金向量测试断言,不由脚本承担。
变更通知流程
RB 改了同步相关代码
→ 写 docs/cross-end/NN-<主题>-handoff.md(what + why + 对端要做什么)
→ 更新 docs/cross-end/README.md 编号总表
→ 提交 commit
→ 告知用户:需要另开一个会话对齐 rvh/(同一个仓,换上下文)反向同理(RVH 改了 → 需在 RB 会话中对齐)。改的是同一个仓的两个目录,但仍是两次会话 (理由见上面的会话隔离规则:工具链,不是路径)。
⚠️ docs/cross-end/ 是唯一一份,两侧编号有 15 组重号,靠该目录 README 的重号总表消歧、 不重编号。写新 handoff 时编号接 README 总表往下发,别按某一侧的旧序列续。
10. 当前状态
⚠️ 本节刻意保持很薄。 它曾是第二本流水账("已完成"长列表),结果停在 2026-05-21 整整三个月—— 里面把早已完成的 Round 2B / 2C-1 列作待办、把已清零的 T3 列作长尾。每会话恒加载的文件里放会腐烂的 进度快照,代价是每个新会话都按错误地图开工。流水账归 CHANGELOG,优先级归 roadmap/backlog,这里只放指针。
当前基线:0.1.0-dev.10(版本真相源 = src-tauri/tauri.conf.json)· 数据基线已冻结(2026-07-26,见 §5)· 已带 Tauri Updater 对外分发 · macOS 已签名公证 · Windows 未签名。
想知道什么,去哪看
| 问题 | 真相源 |
|---|---|
| 做过什么、怎么做的(as-built) | 每端各一份,别只查一份:桌面端 + admin + supabase + 工程 → CHANGELOG.md;移动端 → rvh/CHANGELOG.md(合仓后仍独立更新);admin/CHANGELOG.md 只到合仓前(2026-06 后 admin 的变更记在根这份里) |
| 结构化优先级、本轮方向 | docs/plans/product-iteration-roadmap-2026h2.md |
| 零散任务池、"以后做" | docs/plans/backlog.md |
| 上架/发行/运营基建 | docs/plans/product-launch-todo.md |
| 正在实施的具体方案 | docs/plans/*.md(完成即移入 archive/) |
| 跨端待 RVH 镜像的事项 | docs/plans/table-rename-* · sprint-c1-* · §9 |
重构长尾(详见 docs/plans/project-refactoring-plan.md,按"自然触达时顺手"原则推进)
T1:—— 已完成(2026-08-25,拆成 5 块reading.rs拆 3 块reading/{analysis,view,stats,progress,recommend};最大一块 568 行,原 2144 行)- 其余:VocabPanel/NotesPanel 抽 ListWithDetail organism、三面板抽 useListPanel hook、ReviewPanel/SettingsPanel 拆分等
T3: 105 处组件直连—— 已清零(2026-08-14 实测invoke()src/components+src/pages直连 invoke = 0)
跨端尾项(优先级低,详见 backlog.md §跨端整合尾项)
- RVH 复习推送通知 · 跨端学习报告 · 跨端引导入口 · 账号删除与数据导出(合规向)
11. 环境信息
| 项目 | 版本 |
|---|---|
| Node.js | 22.14.0 |
| pnpm | 10.8.0 |
| Rust | 1.94.0 |
| Tauri CLI | 2.10.3 |
网络注意:中国大陆环境,Rust 下载可能需要国内镜像。
12. 文件组织 & 代码规范
完整规范详见 docs/coding-standards.md(技术红线 / 组件分层 / 颜色系统 / Hook 使用 / Store 分离 / 错误处理 / 文件归属 / 提交规范)。
UI 规范详见 docs/ui-standards.md(设计 token / 组件变体 / 交互原则 / 间距字号 scale)。由 /arch-check S10-S12 在提交前扫描违反。
速查:
- React 组件 →
src/components/PascalCase.tsx(atom/molecule/organism) - 页面 →
src/pages/PascalCase.tsx - Zustand store →
src/stores/useCamelCase.ts(UI / 领域数据分离) - 工具函数 →
src/lib/camelCase.ts - React hook →
src/hooks/useCamelCase.ts - Rust 命令 →
src-tauri/src/commands/snake_case.rs(新命令用CommandResult<T>) - 数据库 →
src-tauri/src/db/snake_case.rs - 文档 →
docs/kebab-case.md(文件名英文 kebab-case、禁数字前缀;正文中文。 活跃计划在docs/plans/,完成后归档docs/plans/archive/。 唯一例外 =docs/cross-end/NN-*.md,那里的序号是时序信息。守门/arch-checkS29) - 专题验证清单 →
docs/verification/<专题>.md(🔒 常驻,永不归档)。 它不是 plan —— plan 是一次性的(做完归档),验证清单是反复重跑的。 放进docs/plans/等于把它排进归档队列,下一轮没人找得到。 ⚠️ 配套铁律(详见docs/verification/README.md): 能用数字或集合表达的判据一律下沉成断言(scripts/<专题>-verify.sh或单测), 清单里只留问句 + 假绿风险,且禁止写数值快照—— 散文里的数字必然静默漂移(2026-08-25 实测:交接单自身有 4 处数字过期)。 每轮产物不留报告,拆散进 CHANGELOG(修好的)/ backlog(待修的)/ 清单的「已知未修」。 - 临时文件 →
logs//backups//temp/(已 gitignore),禁止放根目录或 src/。 ⚠️temp/有一个非 scratch 的居民:temp/library/<user_id>/{web,epub,text}是 dev 模式的 快照缓存(src-tauri/src/db/helpers.rs写、scripts/reset-dev-data.sh管),清理时别整目录rm -rf。 RVH 侧另有一套rvh/logs/(rvh/scripts/run_android.sh按相对路径写)
常见禁止:
- 根目录散落
.sql、.json、.log docs/下英文文档(项目文档统一中文)- 组件内 hex 字面量(用
lib/design-tokens.ts) - 组件直接
invoke()(走src/lib/commands.ts) Result<_, String>新命令(用commands::error::CommandResult)- JSX 中直接写中文字面量 /
aria-label="中文"/toast.xxx('中文')(走src/lib/strings/,详见该目录 README;守门脚本pnpm run check:no-inline-zh,/arch-checkS19 自动跑)
i18n-ready 状态(2026-05-20 收口,详见 docs/plans/archive/i18n-readiness-plan.md):strings/ 架构已 i18n-ready——参数化函数模式 + namespace import + 守门脚本基线为 0。当前不引入 runtime 库(保持零运行时开销);未来真有多语言需求时,迁移成本 = 替换 strings/*.ts 18 个文件的函数体(export const fn = (n) => '...' → t('key', { count: n })),不动组件层。Rust 端错误消息 / content-script 中文按 README 既定边界不进 strings/。