Skip to content

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. 项目概览
  2. 项目结构
  3. 架构要点(双端契约部分)
  4. 技术红线 🔒
  5. 数据库(Supabase 共享部分)
  6. 安全规则 🔒
  7. 项目 Skills
  8. 开发工作流
  9. 双端整合
  10. 当前状态
  11. 环境信息
  12. 文件组织规范

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 库 / 业务 moleculesrc/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.mjsALLOWED 表 (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.mdCLAUDE.md(恒加载)6 份子目录 CLAUDE.md,碰对应文件时叠加
记忆单一记忆库(按 git 根,全仓共用;索引 MEMORY.md无子项目级记忆
MCP 工具.mcp.jsonrb-debug(桌面端 dev app)+ rvh-debug(adb)
Skill / 文档见 §9「子项目边界」表(别在这里记第二份同左

契约中枢 = Supabase schemasupabase/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 §2Workspace 模块 + 🔒 模块状态保全三条
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_atMAX(双端各自 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-check S28 扫 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 条一条不少;正文按归属分布在三个文件里。 索引行只给一句话标题,不复述理由——新增红线时索引与正文必须同时动,漂移立刻可见。

#一句话正文所在
1rusqlite 锁定 v0.31src-tauri/CLAUDE.md §2
2SQLite 禁 LOWER(),用 COLLATE NOCASEsrc-tauri/CLAUDE.md §2
3Content-script 用 __TAURI_INTERNALS__.invoke()src-tauri/CLAUDE.md §2
4DOM 操作前断开 MutationObserversrc-tauri/CLAUDE.md §2
5Sync pull 用 server_updated_at=gt.{last_sync_at}src-tauri/CLAUDE.md §2
5alast_sync_at 仅在 errors.is_empty() 时推进src-tauri/CLAUDE.md §2
5bpull_* 的 INSERT 必须写 user_idsrc-tauri/CLAUDE.md §2
5c登出必须 resetAllTabs() + 重置 reviewsrc-tauri/CLAUDE.md §2
5dwatermark 推进到本次实际返回行的 MAX(server_updated_at)src-tauri/CLAUDE.md §2
5ipush 的 dirty-check 必须按归属过滤(对端 #5h)本节 ↓
6soft-delete 用 deleted_at,不 hard DELETEsrc-tauri/CLAUDE.md §2
6a父行删除必须同事务显式软删整棵子树,禁靠 FK CASCADEsrc-tauri/CLAUDE.md §2
6bpush payload 的 deleted_at 传本地真值,禁恒写 nullsrc-tauri/CLAUDE.md §2
6cpull 的父行守卫必须显式回答三态(活 / 墓碑 / 缺失)src-tauri/CLAUDE.md §2
6d凡写 synced_at,写完那一刻该行必须立刻不脏(W / P1 / P2 / P3)本节 ↓
6epull 分支②「本地已删 + remote 活」的 skip 必须以「墓碑仍待推」为条件,且判据由 push 的脏检查常量拼出本节 ↓
7save_word 须重新激活 soft-deleted 条目src-tauri/CLAUDE.md §2
8组件内禁止 hex 字面量src/CLAUDE.md §3
9word 字段前必经 lemmatizer::normalize()(含 NFC)本节 ↓
10lampio_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」这条运行期约定而非约束——加游客模式或某写入路径漏填即破。

三款细则(两端同文)

  • ⚠️ 括号是最危险处ANDOR 结合更紧,漏外层括号退化成 (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_atupdated_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:53updated_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 侧 _maxIsocompareTo · 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.dblemma_* 表运行时装载(原 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/bearingpeople/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/24tools/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_VERSIONsrc-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/13lemmatizer 折叠: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-19pipeline 从 RVH 迁入 RB 成为单一产源sync-rvh-vocabulary.sh 方向反转为 RB→RVH(断言/白名单不变)。RVH 降级为纯消费方。方案见 rvh-vocab-pipeline-migration-plan.md
2026-07-26改名 reading_vocab.dblampio_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 mvscripts/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-check skill 自动扫描这些规则 + 文件大小/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_at fallback 条件
  • 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 build
  • pnpm 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-review skill 同名,项目 skill 会把内置的整个遮蔽掉 —— 于是 /code-review ultra(多 agent 云端评审)/ low / --fix / --comment 在本仓一律够不着, 而且没有任何报错,只是那个名字解析到了另一个东西。改名当场两个都回到工具集里。 ⚠️ admin/CLAUDE.mdrvh/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.mddocs/plans/archive/telemetry-opt-in-plan.md<feature>-plan.md;交接类用 <feature>-handoff.md

代码提交规范

格式:<type>(<scope>): <subject>

type用途
feat新功能
fixBug 修复
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 addgit 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.jsonversion(决定 app 版本 + tag)。派生的另外三处scripts/set-version.mjs 写、pnpm run check:version-sync 断言:src-tauri/Cargo.toml · package.json · src-tauri/Cargo.lockapp(第四处 2026-09-01 才补进来)。⚠️ 「package.jsonversion 是死字段(长期 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

数据库变更流程

  1. src-tauri/src/db/migrations.rs 添加新迁移(递增版本号)
  2. 同步更新 supabase/sql/sync-tables.sql(如涉及同步表)
  3. 编译检查 cargo check
  4. 测试迁移(删除本地 DB 文件重新运行)

缺陷修复原则

  • 不做 workaround,必须根因修复
  • 修复前先 review 代码逻辑
  • 检查边界条件
  • 验证依赖有效性

9. 双端整合

本节只留规则。「为什么当初这么定 / 谁被它带偏过 / 实测证据」按 §4 内容归属判据已移出 —— 各处指针指向承载它的归档件或记忆库。

子项目边界(skill / 验证命令 / 契约耦合)

子项目dev-flow skill编译 / 验证命令与 Supabase 契约
admin/排除;用 admin:code-review · admin:doc-sync-check · deploypnpm --dir admin exec tsc --noEmit编译期绑定(supabase gen types
landing/排除;无 scoped skillpnpm --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✅ 成立
文件路径冲突两个项目有同名概念(migrations, SM-2, sync)但实现不同已消失 —— 合仓后路径带 rvh/ 前缀,同名概念不再解析到同一个字符串

实践边界(2026-08-29 T4-4 实测走通):改 rvh/CLAUDE.mdrvh/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.sql DDL 更新
  • [ ] 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.rsRust
RB 前端src/lib/sm2.tsTypeScript
RVHrvh/lib/core/algorithms/sm2_algorithm.dart(类 SM2AlgorithmDart

一致性规则

  • 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.sh L1(全仓不许出现第二份同名文件)+ 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: reading.rs 拆 3 块 —— 已完成(2026-08-25,拆成 5 块 reading/{analysis,view,stats,progress,recommend};最大一块 568 行,原 2144 行)
  • 其余:VocabPanel/NotesPanel 抽 ListWithDetail organism、三面板抽 useListPanel hook、ReviewPanel/SettingsPanel 拆分等
  • T3: 105 处组件直连 invoke() —— 已清零(2026-08-14 实测 src/components + src/pages 直连 invoke = 0)

跨端尾项(优先级低,详见 backlog.md §跨端整合尾项)

  • RVH 复习推送通知 · 跨端学习报告 · 跨端引导入口 · 账号删除与数据导出(合规向)

11. 环境信息

项目版本
Node.js22.14.0
pnpm10.8.0
Rust1.94.0
Tauri CLI2.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-check S29)
  • 专题验证清单 → 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-check S19 自动跑)

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/