Skip to content

Lampio 代码规范

本文档管代码风格、架构不变式、文件组织的细节例外:技术红线的真相源在 CLAUDE.md §4,不在本文件(§1 只留指针,原因见该节)。

头部不再标注"最后更新"——它注定漂移。以 git log -1 -- docs/coding-standards.md 为准。


目录

  1. 技术红线(硬性禁止)
  2. 架构设计原则
  3. 前端规范(React + TypeScript)
  4. 后端规范(Rust)
  5. Content-Script 规范
  6. Zustand Store 设计
  7. 错误处理
  8. 文件组织
  9. 提交规范
  10. 分支管理 & 版本发布

1. 技术红线(硬性禁止)

🔒 红线清单的唯一真相源 = CLAUDE.md §4。本节不再维护副本。

为什么删掉这里的表:本节曾并行维护一份 R1-R8,随时间落后成了错误信息源—— 它的 R5 写着 updated_at=gt.{last_sync_at},而 2026-05-22 起正确写法是 server_updated_at=gt.(客户端 ts 会漂移,用它做 watermark 会永久漏数据; 见 CLAUDE.md 红线 #5/#5d 与已归档的 plans/archive/sync-server-timestamp-plan.md)。 与此同时 CLAUDE.md §4 已长到 #1-#11(含 5a/5b/5c/5d/5i/6a/6b/6c/6d 共 11 条本节从未有过的), 而 CLAUDE.md 却写着"详细清单见本节"——指针指向了更少且更旧的一份。 两份红线互相矛盾约 3 个月。一份规则只能有一个家。

/arch-check skill 自动扫描其中可机械判定的部分(S1-S19),语义层由 /rb-code-review 覆盖。


2. 架构设计原则

以下三条原则从 Review 流程重构中提炼,适用于所有涉及 Webview 和跨边界通信的功能开发。

原则 1:能用 Tab 就不造特殊实体

需要在 content-area 显示 webview 的功能,必须作为 Tab(useTabsStore)接入。用 tab.type 区分特殊行为,复用 Tab 系统的生命周期管理(创建/销毁/显隐/resize/切换)。

禁止在 App.tsx 中为新功能新增 webview 协调 useEffect。

原则 2:状态归属——谁渲染谁拥有

  • 只有一个组件读写 → useState(local state)
  • 多个组件读 → 最小共享 Zustand store
  • 跨边界传入 → Tauri event → 接收方 local state 或 store
  • 禁止同一信息在多处维护 + 手动同步

原则 3:跨边界通信只用两种模式

  • React/content-script → Rust:invoke()
  • Rust → React:emit eventlisten()(用 hooks/useTauriEvent
  • 禁止 UI 组件直接调用 Tauri webview 命令(通过 Tab store 方法)
  • 禁止 多个组件各自 listen 同一事件各自处理

3. 前端规范(React + TypeScript)

3.1 组件分层

组件层级 / 基础组件库 / 颜色系统 / 间距 / 字号等 UI 细节统一归 docs/ui-standards.md。本节仅列一次性索引。

快速参考:

  • Atom:无状态、无业务、纯展示(ui/Button、ui/IconButton、TabButton)
  • Molecule:组合 Atom,有自己的本地交互(WordPopup、SectionHeader)
  • Organism:面板/工具栏,可能接 store(ReviewPanel、VocabPanel)
  • Page:路由级,编排多个 Organism(HomePage)

3.2 颜色系统

禁止.tsx/.ts 中写 hex 字面量(#xxxxxx)。只能通过 src/lib/design-tokens.ts 引用 CHART_COLORS / SEMANTIC / CEFR_* / cefrChartColor() 等 token。扩展规则、语义色分支、TOGGLE/BUTTON 变体归属、类名组合顺序详见 docs/ui-standards.md §3-§13/arch-check S10/S11/S12 负责在提交前扫描违反。

3.3 数据获取的错误处理

没有统一的取数 hook。 组件按需自己写 useEffect + 取数,规矩只有一条: 错误不许直接吞进 console.*,走 logger.error 或显式 try/catch

tsx
// ❌ 错误被吞掉,线上无从排查
useEffect(() => {
  getStatsSummary().then(setStats).catch(console.error);
}, []);

// ✅ 走 logger
useEffect(() => {
  getStatsSummary()
    .then(setStats)
    .catch((e) => logger.error('stats summary 取数失败', e));
}, []);

/arch-check S4 扫 .then(setX).catch(console.error) 形态,> 5 处告警。

📌 本节 2026-08-15 订正:原文教「统一使用 src/hooks/useFetchData.ts」并给了 useFetchData(...) 的示例代码——但那个文件在 5f92a5c(2026-05)就已删除, 全仓 0 引用。照原文写会调用一个不存在的 API;更糟的是它把现在实际通行的写法 标成「❌ 旧模式」。同期 /arch-check S4 的措辞已经改对了(不再提 useFetchData), 本文档漏改,于是规则与守门相互矛盾约 3 个月。

3.4 事件监听 Hook

统一使用 src/hooks/useTauriEvent.ts,替代手动 listen + cleanup

tsx
// ❌ 旧模式
useEffect(() => {
  const unlisten = listen('word-saved', () => reload());
  return () => { unlisten.then(fn => fn()); };
}, []);

// ✅ 新模式
useTauriEvent('word-saved', () => reload(), [reload]);

3.5 组件最大行数

软上限 500 行。超过需要拆分。常见拆分模式:

  • 容器组件 + 多个子组件(Panel 拆 Overview + Session)
  • 抽取 Hook(useXxxFiltering, useXxxSelection
  • 抽取纯函数到 lib/

3.6 禁止直接 invoke

组件和页面不应直接 import { invoke } from '@tauri-apps/api'。走 src/lib/commands.ts 封装:

tsx
// ❌ 禁止
import { invoke } from '@tauri-apps/api/core';
await invoke('save_word', { word });

// ✅ 正确
import { saveWord } from '../lib/commands';
await saveWord(word);

3.7 日志规范

禁止在组件/页面/hooks/stores 中直接使用 console.*。统一走 src/lib/logger.ts

ts
// ❌ 禁止
console.error('Save failed:', err);
console.warn('No data');

// ✅ 正确
import { logger } from '../lib/logger';
logger.error('Save failed', err);
logger.warn('No data');

logger API:

方法行为
logger.debug(...args)仅在 DEV 模式输出 [RB] 前缀的 console.debug
logger.info(...args)console.info + [RB] 前缀
logger.warn(...args)console.warn + [RB] 前缀
logger.error(msg, err?)console.error + [RB] 前缀,未来可挂接错误上报

唯一例外commands.ts 中 vocabulary seed 的一次性初始化日志可保留 console.error,但其余一律走 logger


4. 后端规范(Rust)

4.1 错误处理

新命令必须使用 CommandResult<T>,不再用 Result<T, String>

rust
// ❌ 旧模式
#[tauri::command]
pub fn foo(db: State<'_, DbConn>) -> Result<i64, String> {
    let conn = db.0.lock().map_err(|e| format!("Lock: {}", e))?;
    conn.query_row("SELECT ...", [], |r| r.get(0))
        .map_err(|e| format!("Query: {}", e))
}

// ✅ 新模式
use crate::commands::error::CommandResult;

#[tauri::command]
pub fn foo(db: State<'_, DbConn>) -> CommandResult<i64> {
    let conn = db.0.lock()?;                    // PoisonError → CommandError::Lock
    let n: i64 = conn.query_row("SELECT ...", [], |r| r.get(0))?;  // rusqlite → Db
    Ok(n)
}

CommandError 变体:Db · Lock · Http · Io · Parse · NotConfigured · NotFound · InvalidArgument · Auth · RemoteRejected · Other

String 构造:Err("msg".into()) 会自动映射到 Other

4.2 模块组织

  • 按领域分模块:commands/sync/{mod,common,push,pull}.rs
  • 单文件软上限 500 行;超过需要拆
  • 辅助函数 pub(super) fn 限制在模块内访问
  • 共享辅助函数下沉到 commands/common.rs(若跨模块复用)

4.3 SQL 查询

  • COLLATE NOCASE,不用 LOWER()
  • 所有软删除表查询必须 WHERE deleted_at IS NULL
  • 多参数 IN (?1, ?2, ...)(1..=n).map(|i| format!("?{i}")).collect::<Vec<_>>().join(",")

4.4 模块命名

snake_case.rs。每个命令模块一个文件或一个目录(含 mod.rs)。

4.5 日志规范

级别使用场景示例
log::error!不可恢复错误、需要开发者关注的异常数据库连接失败、HTTP 500
log::warn!可恢复异常、降级路径无 Supabase 配置、字段缺失
log::info!关键业务事件(sync 完成、auth 切换、migration 执行)"User signed in"、"Sync completed"
log::debug!调试信息(release 不输出,仅 dev 可见)SQL 结果行数、中间计算值
log::trace!极详细追踪(一般不用)循环每次迭代

禁止记录的敏感字段

  • access_token / refresh_token / password / 含 token 的完整 URL
  • 邮箱须脱敏:a***@domain.com(用 mask_email() 辅助函数)
  • user_id 本身不算敏感,可直接记录

格式约定:每条日志以 [模块名] 前缀开头,便于过滤:

rust
log::info!("[auth] User signed in: {}", mask_email(&session.email));
log::warn!("[sync] No Supabase config, skipping push");
log::error!("[recommend] HTTP failed: {}", e);

5. Content-Script 规范

5.1 模块化

目录结构:

src-tauri/src/content-script/
├── core/          # IPC, 状态, 常量, 观察器管理
├── features/      # 各功能模块(popup, highlight, reader, ...)
├── index.js       # IIFE 入口
├── events.js      # 事件分发
└── styles.js      # CSS 注入

构建脚本 scripts/build-content-script.mjs 按顺序拼接为单个 JS 文件,include_str!() 注入到 webview。

5.2 全局状态

使用命名空间对象 __rb,不创建散落的全局变量:

js
const __rb = {
  settings: { ... },
  vocab: { ... },
  ui: { ... },
  cache: { ... },
};

5.3 IPC 必须走 __TAURI_INTERNALS__

js
// ❌ 禁止(content webview 中 @tauri-apps/api 不可用)
import { invoke } from '@tauri-apps/api';

// ✅ 正确
window.__TAURI_INTERNALS__.invoke('save_word', { word });

5.4 DOM 操作要暂停 Observer

js
// core/observer.js
export function withObserverPaused(fn) {
  mutationObserver.disconnect();
  try { fn(); } finally { mutationObserver.observe(document.body, OBS_OPTS); }
}

6. Zustand Store 设计

6.1 UI state 与 领域数据分离

Store职责示例字段
usePanelStoreUI 布局(panel 可见性、宽度、overlay 开关)activePanel, leftPanel, panelWidth, reportOverlayOpen
useReviewStore复习领域数据currentCard
useRssStoreRSS 领域数据drilldownFeedId
useTabsStore多标签状态tabs, activeTabId
useAuthStore认证 + 同步session, isSyncing, lastSyncAt

禁止usePanelStore 中混入领域数据(currentReviewCard 应在 useReviewStore)。

6.2 跨 store 组合在调用点

需要同时更新 UI store + 领域 store 的动作,不要在任一 store 中写组合方法。调用点组合:

tsx
// ❌ 禁止在 store 中耦合
openRss: (feedId) => set({ leftPanel: 'rss', rssDrilldownFeedId: feedId })

// ✅ 调用点组合
onClick={() => {
  useRssStore.getState().setDrilldownFeed(feed.id);
  usePanelStore.getState().openLeftPanel('rss');
}}

7. 错误处理

7.1 前端

  • 异步操作必须 .catch()禁止完全忽略错误(.catch(() => {})
  • 用户可见的错误通过 UI 呈现(toast、inline message);不要只 console.error()
  • 取数失败走 logger.error(见 §3.3),不要直接 console.error

7.2 后端

  • CommandResult<T>(见 §4.1)
  • log::error! / log::info! 用于日志;错误返回给前端的是序列化后的字符串(CommandError: Serialize → String
  • 同步过程中的错误收集到 errors: Vec<String> 不中断整体流程(参考 sync::sync_now

8. 文件组织

8.1 新建文件归属

文件类型归属目录命名
React 组件src/components/PascalCase.tsx
页面src/pages/PascalCase.tsx
Zustand storesrc/stores/useCamelCase.ts
工具函数src/lib/camelCase.ts
React hooksrc/hooks/useCamelCase.ts
类型定义src/types/camelCase.ts
Rust 命令模块src-tauri/src/commands/snake_case.rssnake_case/mod.rs
数据库相关src-tauri/src/db/snake_case.rs
文档docs/kebab-case.md(英文,不带数字前缀
跨端交接docs/cross-end/NN-kebab-case.mdNN = 全局递增序号,见该目录 README)
活跃计划docs/plans/kebab-case-plan.md
归档计划docs/plans/archive/同上

文档命名只有两种合法形态,判据是「这份文档有没有位置意义」:

  • docs/ 根 + docs/plans/ → 纯 kebab-case,禁数字前缀。 这些文档彼此独立、无阅读顺序, 编号只会制造「05 和 06 是不是要连着读」的假暗示,且插入新文档时被迫重编号。
  • docs/cross-end/NN- 前缀是对的,因为它记录的是时序:交接 → 对端确认 → 回执, 序号即事件顺序,编号本身就是信息。

⚠️ docs/archive/ 里的 00-项目概述.md09-双端整合实施计划.md 是历史形态,不是范本。 那是项目最初的一套规划文档,编号 = 阅读顺序,作为完整连续序列保留有意义 (拆掉前缀会让它断成「00-07 有号 / 08-09 无号」,比现状更糟)。 但新文档不要仿它——2026-08 前 09-双端整合实施计划.md 一直滞留在 docs/ 根, 与周围 13 份 kebab-case 英文文档格格不入,正是因为旧规范把 NN-中文标题.mdkebab-case.md 并列却不给判据。该形态现已无活跃实例。

守门:/arch-check S29。

8.2 临时文件

类型目录Gitignore
日志logs/
备份backups/<YYYY-MM-DD>/
临时temp/

8.3 不应创建的文件

  • 不在根目录创建散落的 .sql.json.log
  • docs/ 下不创建英文文档(统一中文)
  • 不创建额外的根目录 markdown(只允许 README.md / CLAUDE.md / CHANGELOG.md 三份。 PROGRESS.md 已于 2026-08-14 退役进 docs/archive/——它与 CHANGELOG 记的是同一类东西, 双份账本必然其一落后)
  • docs/plans/ 中完成或放弃的计划必须归档(git mvarchive/

9. 提交规范

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

type用途
feat新功能
fixBug 修复
docs文档更新
refactor重构(无功能变更)
style样式/格式调整
perf性能优化
chore构建/工具变更

示例:

  • feat(sync): add soft-delete propagation for learning_entries
  • refactor(panel): split usePanelStore into layout + domain stores
  • fix(review): hero word waits for highlighting to finish before applying

提交前工作流

1. /arch-check          ← 架构不变式(< 30s)
2. /build-check         ← 编译检查(cargo check + pnpm build)
3. 手动测试 / tauri dev
4. /rb-code-review         ← 代码质量审查
5. /doc-sync-check      ← 检查文档是否需要同步
6. git commit           ← 提交

四个 skill 无硬依赖,可单独执行,但推荐按顺序。


10. 分支管理 & 版本发布

模式 = 主干开发(trunk-based),不用 GitFlow。 个人开发单机维护 develop / release 常驻分支是纯负债。默认在 main 上线性提交,只在真正需要隔离时切短命分支,合入即删。分支是一次性脚手架、不是永久档案——历史留在 main 上就够了。 (与 CLAUDE.md §8 同源,此处为人类翻阅版;两处若冲突以本节 + CLAUDE.md 较新者为准。)

10.1 何时直接在 main(约 80% 的提交)

小的、自包含、一次坐下能收尾、且能保持 main 绿(编译过 + /build-check Step 6 命中的高危测试过)的改动——bug fix、文档、单文件功能、reseed。直接 commit 到 main

10.2 何时切短命分支

命中任一即切;合入后 git branch -d 立即删

触发例子完事
① 大/险重构,中途会让 main 红好几个 commit三端表重命名、sync 引擎重写分支上弄绿 → merge → 删
② 可能丢弃的实验 / spike探索性重绘放弃 = 删分支,不污染 main
③ 合入前想过一遍 CI / PR 门临发版前、动迁移链PR 触发 ci.yml → 绿 → merge → 删
④ 跨多会话大工程,期间要能回 main 发小修少见

合入统一用 git merge --ff-only(保持线性历史,无多余 merge commit)。

并行分支的合入序:两条分支从同一 main 分叉后,只有先合入的能 fast-forward,第二条必然被 ff-only 拒绝(main 已前进)。后合入者:git rebase main(此时才真正暴露文本冲突)→ 重跑 /build-check → 再 merge --ff-only。不要为此改用 merge commit。

10.3 多会话并行纪律

核心事实:共用同一工作树有三条独立的静默丢失路径,只有物理隔离(独立 worktree)能同时挡住三条。

路径 A · 文件级(同文件并发写):两会话同时改同一文件,文件系统层面是后写者赢、先写者的改动静默消失——git 不参与、也无从发现(它只看到最终落盘那一版)。这不是"提示你冲突",是"悄悄丢一半"。约定"别碰同一文件"挡不住(人的判断力有限)。

路径 B · 工作树级(reset --hard 类破坏):任一会话跑 git reset --hard / git checkout . / git stash push,会无差别抹掉整棵树上所有未提交改动,包括别的会话正攥在手里的。 🔴 对路径 B,"文件不相交"提供零保护——它不看谁碰了哪个文件。所以下方任何以文件为粒度的白名单(枢纽文件清单、不相交模块豁免)都永远抓不到它;能挡的只有 worktree 隔离,或"改完立刻提交"。 🔴 显式 git add 也挡不住:受害者的改动根本没进暂存区,是工作区被清。 事故实录(2026-08-01):并行会话提交完自己的 6 个文件后跑 git reset --hard HEAD 清残留,抹掉了另一会话未提交的 TitleBar.tsx。两会话文件域完全不相交、对方 git add 纪律也正确,规则按当时写法判定"共用树 OK"——照样丢。reflog 里只留一行 reset: moving to HEAD,不主动查发现不了。

路径 C · 暂存区级(共享 index 抢跑)git addgit commit 之间存在一个窗口——index 是整棵工作树共享的一份,谁先 git commit,谁就把此刻 index 里的全部内容提交走,包括另一会话刚 git add 进去、还没来得及提交的文件。 🔴 这条不丢代码,丢的是历史的可读性——受害者的改动被塞进一条与之毫无关系的 commit 消息底下。危害是延迟的:当时无感,几周后查「这个组件为什么被删」会翻到一条讲别的事的 commit。 🔴 「改完立刻提交」这条护栏正好覆盖不到它——危险窗口恰恰就在"改完"之后、"提交"之前那几秒;显式 git add 不但不防,反而触发条件。 事故实录(2026-08-14):本会话 git add T4 的 8 个文件后、git commit 之前,并行会话先 commit,把这 8 个文件连同它自己 1 个 docs 文件一起提交成了一条讲「landing 统计实测」的消息(该 commit 里 landing/ 一个文件都没有)。修法 = 未推送时 git reset --soft HEAD~1 拆成两条,拆完必须 git diff <原HEAD> HEAD 验树为空证明零改动。 事故实录(2026-08-25,第二次):本会话做完 notes.rs 拆分后用 git commit -a 提交,卷走了并行会话尚未提交的 .gitattributes + db/migrations.rs(一个 CI 行尾修复),埋进一条讲「notes.rs 拆 5 块」的消息里。同一会话前九次提交都用了 pathspec,这次图省事换成 -a 就中招——说明这条护栏的价值全在「每次都用」,破一次就等于没有;它防的不是"你不知道该用",是"你知道但偷懒了那一次"。 发现时那两条 commit 已被并行会话推上两个远程,force-push 的代价大于收益,裁定不改写历史、只留本条记录(对方随后补了 CHANGELOG,那份工作的账目已清楚)。这也说明路径 C 的修复窗口很短:未推送时 reset --soft 拆条很便宜,一旦对方推了就基本只能认。

廉价免疫:共用树时用 git commit <pathspec>(如 git commit path/a path/b -m ...)——它绕过 index 直接按路径提交,窗口归零。这是共用树场景下唯一不依赖时序运气的写法。

触发不靠人的记忆(隔离决策由 Claude/harness 承担,不要求用户每次发问)

下面的分级是判定标准,但"每个任务前记得判定"不能压在用户记忆上。触发靠这两条机制:

  • 机制①(首选,零记忆·无盲窗):并行工作默认作为 worktree 隔离的子代理启动。用户只说"并行做 X 和 Y",Claude 用 Agent(isolation: "worktree") 让每个任务跑在自动创建/自动清理的独立 worktree 里——隔离是"启动方式"自带的属性,用户无需判断、无需碰 git,也没有"两会话同时改同一新文件"的盲窗(物理隔离先于任何编辑)。适用于可委派的实现任务。
  • 机制②(Claude 自查,补充机制①覆盖不到的手动多终端场景):Claude 在动手改代码前自己跑 git status + git worktree list探到并行痕迹(有别的 worktree、或有不是本会话改的未提交文件——包括会话启动时 harness 给的 git status 快照)后分两档处置:
    • 本任务会碰下方枢纽文件 → 主动开 worktree 并告知用户(挡路径 A + B)。
    • 即使不碰枢纽文件、文件域完全不相交 → 仍受路径 B 威胁,必须启用工作树级护栏:每完成一个可编译的小步就提交,不把改动长时间攥在未提交状态。判断责任在 Claude,不在用户。
  • 局限:机制②依赖"对方已产生脏树/worktree 信号",两个全新会话同时起手改同一新文件有短暂盲窗——所以能用机制①就优先用①。(更强的 harness 层强制可选:PreToolUse hook 挂 Edit/Write,改 hub 文件时自动查 git 并警告/拦截;连 Claude 记不记得都不依赖,需单独配置。)

按碰撞风险分级判定(同 §"修法范围按影响校准"哲学)。判定看会不会撞枢纽文件

src/lib/commands.ts(每个 Tauri 命令)、src-tauri/src/lib.rs(每个新命令注册)、src/lib/strings/**(每个 UI 文案)、commands/*/mod.rsdb/migrations.rssync/ ——这些几乎每个功能都改,并行功能实现撞它们是大概率而非罕见

并行场景方案
功能实现 / 会碰上述枢纽文件 / 文件足迹说不清各会话开独立 worktree(自动附带独立分支)
纯文档 / 规划 / 明确不相交的独立模块,且能预先说清各自文件域共用树 + 廉价护栏(见下)——注意:本行只豁免路径 A,路径 B / C 照样打得到你。护栏里的"勤提交"对本行是硬要求不是建议;提交一律用 git commit <pathspec>(防路径 C)

git 约束:同一分支不能被两个 worktree 同时签出,EnterWorktree 默认建新分支——所以开 worktree 就自动得到独立分支,分支隔离免费附送,不是额外要做的一件事。分支的独立价值在合入环节(§10.2/10.5:merge 单元 + 合完即删),不在并行隔离。

三层保障(worktree 场景,缺第一层后两层没机会介入)

机制挡什么
① worktree 物理隔离各会话独立目录同文件并发写的静默覆盖
git merge合入时三方合并文本冲突——强制暴露、绝不静默丢失(不同行自动合并;同行冲突标记逼裁决)
/build-check + CI合入后编译/测试语义冲突(改不同文件但逻辑互破,如改函数签名 vs 调旧签名——git 合并无冲突却坏了)

关键心智:worktree 不保证"不发生冲突",而是保证"冲突一定被看见"。共用工作树把这层保护整个绕过。

worktree 操作 + 成本压缩

  • 开会话即 EnterWorktree(或 git worktree add .claude/worktrees/<task> -b <branch>);完成后合入 maingit merge --ff-only,后合入者先 rebase,见 §10.2 合入序)→ 跑 /build-check → 删 worktree + 分支。
  • worktree 分叉基准 = 本地 HEAD.claude/settings.json 已设 worktree.baseRef: "head")。EnterWorktree 默认从 origin/main 分叉,但本仓单机开发本地 main 才是真相源、origin 只是备份镜像——本地领先未推时从 origin 分叉会基于旧代码,故显式改为 head。
  • 成本缓解让 ROI 站得住:pnpm 共享 store(默认已是)→ node_modules 近乎秒装(硬链);共享 CARGO_TARGET_DIR → 依赖只编译一次(省最大头,代价见下)。纯文档/规划任务本就无需 build,成本≈0。
    • ① 两 worktree 代码差异大时增量缓存互相失效;
    • ② cargo 对 target 目录持文件锁,两 worktree 同时 build/check 会串行等锁——并行会话里 cargo 长时间无输出的第一嫌疑就是它,不是卡死;
    • ③ 🔴 worktree 删掉之后,主树的下一次 build 可能还带着指向那棵已删树的 build script。build.rsenv!("CARGO_MANIFEST_DIR") 定位 .env.local,而那个值是编译 build script 时 烤进二进制的;共享 target 时 target 里那份二进制可能是为另一棵 worktree 编的。那棵树一删, 它就去一个不存在的目录找 .env.local → 主树 cargo checkMissing build-time config RB_SUPABASE_URL,而 .env.local 明明就在 src-tauri/ 下。 ⚠️ 报错完全指不到真因:它说的是「缺配置」,实际是「缺的是另一棵树」,照它的指引去 cp .env.example .env.local 只会覆盖掉本来好好的文件。解药是 touch src-tauri/build.rs 逼 cargo 重编 build script。2026-08-27 拆 sync/pull 时实测踩到。 (同理:worktree 里首次 build 需要把主树的 src-tauri/.env.local 复制过去——它是 gitignored 的, git worktree add 不会带。)

共用树的廉价护栏(低风险场景,或确实无法开 worktree 时):

  • 勤提交(对路径 B 是唯一护栏,故为硬要求):未提交的改动 = 暴露在 reset --hard 下的资产,攥得越久风险越大。每完成一个可编译的小步就提交,别攒一整轮再一次性交。对方一旦提交,你再改就基于其版本——路径 A 的丢失窗口也随之缩到"两边同时攥着未提交的同一文件"这一小段。
  • 显式路径 git add <path> git add -A,避免卷入对方未提交改动。(注意:这条只防"误提交别人的",不防别人清掉你的。)
  • 禁止无差别破坏工作树git reset --hardgit checkout . / git restore .git stash push(无 pathspec)、git clean -fd —— 它们作用于整棵共用树,会连带抹掉其他会话未提交的改动,且受害方毫无提示。确有需要时:先 git status 逐条确认没有不是自己改的文件,再带 pathspec 精确作用git checkout -- <你自己的路径>)。

把单文件拆成模块目录时:git add -A <dir> 不会暂存同名文件的删除

reading.rsreading/{analysis,view,...}.rs 这类拆分,git add -A src-tauri/src/commands/reading 只暂存那个目录不含兄弟文件 reading.rs 的删除。于是提交出来的 commit 里两者并存, Rust 报 file for module 'reading' found at both reading.rs and reading/mod.rs —— 该 commit 单独 checkout 编译不过

🔴 本地毫无感觉:工作树里旧文件确实已删,所以 cargo test 全绿、git status 事后也干净, 只有别人(或 git bisect)checkout 到那一条时才炸。 实录:c57c9fb(reading.rs 拆 5 块)正是这样一条 commit,删除在下一条 95b12d9 才被 commit -a 顺带补上;两条合起来是对的,中间那条对 bisect 有害。

做法:拆完提交前 git status --short 确认有 D <旧文件> 那一行;或把 pathspec 提到 父目录(git add -A src-tauri/src/commands/)。提交后 git show --stat 再核一眼有没有 delete mode

第二次实录(2026-08-27,sync/pull.rs 1804 行拆成 sync/pull/{mod,vocab,library,prefs}.rs: 这一次按本条走了——用 git rm src-tauri/src/commands/sync/pull.rs 让删除在写新文件之前就进暂存区 (比事后核 git status 更早一步,git add -A <新目录> 那个坑根本没有机会形成), 提交后又 git worktree add /tmp/verify-split HEAD 单独 checkout 跑了一遍 cargo check

  • 禁止切换 HEAD 分支git checkout <branch> / switch)——会夺走对方脚下的分支,令其 commit 落错。注意 reset --hard HEAD 不移动 HEAD 引用,不在本条覆盖范围内,它归上一条。
  • 开工前 git status + git worktree list 探测:若有自己没碰过的文件正被改动 → 有活跃并行会话,涉枢纽文件则停手改开 worktree;不涉也要把"勤提交"当硬要求执行
  • 怀疑丢过东西时git reflogreset: / checkout: 条目定位破坏时点。⚠️ 未提交的改动不在 reflog 里、也不可恢复(除非编辑器本地历史有留),reflog 只能告诉你"何时被谁清的"。

10.4 版本发布 = tag,不是分支

/release skill 编排。

  • 版本唯一真相源 = src-tauri/tauri.conf.jsonversion(决定 app 版本 + tag)。package.jsonversion 是死字段(长期 0.0.0),勿据它判断版本。
  • 递进: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(有迁移链,基本用不到)。
  • tag 推 github remote 触发 Actions 构建(Win + macOS);origin(Gitee)为镜像。详见 /release skill。

10.5 分支卫生

  • merge 后立即删本地 + 远程分支(git push <remote> --delete <branch>)。
  • 定期查僵尸分支:git rev-list --left-right --count main...<branch>0 ahead = 已合并可删。
  • 定期 git remote prune github / git remote prune origin 清理已删远程分支的本地引用。
  • 本地 main 定期 git push github main + git push origin main 双推备份——单机开发 = 未推的 commit 无备份