Skip to content

feat: SQLite 统一持久化 + 项目/PR 合并导航(#67 #70) - #71

Closed
ghbvf wants to merge 2 commits into
developfrom
Feature/67-70-sqlite-nav
Closed

ghbvf wants to merge 2 commits into
developfrom
Feature/67-70-sqlite-nav

Conversation

@ghbvf

@ghbvf ghbvf commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

引入 SQLite 作为统一本地持久化(config / PR / ledger 迁入 + 新增会话元数据·历史内容·webhook-PR),并把主页面合并为「项目 → PR」单列导航、会话面板独立支持回看历史内容。

Why / 背景

  • 使用 SQLite 持久化项目、PR、会话和历史会话内容 #70:现状持久化全走 tauri-plugin-store 的 JSON;review 会话只在内存 SessionRegistry、流式 deltas emit 后即丢。后果:gh 不可用时 webhook PR 无法稳妥暂存;重启后会话 ID 丢失;历史会话「点击前已产生的内容」无法回看。引入 SQLite 作为统一存储并提供清晰的 schema + 迁移/初始化逻辑。
  • 调整主页面为项目/PR 两级导航并按 PR 关联会话 #67:现状 ProjectSwitcher(边栏1) + PrList(边栏2) 两条分离边栏 + 全局会话列表。改为「项目作为一级 H2 分组、PR 嵌入其下」,消除分离边栏结构。

Refs

Closes #67
Closes #70

ref: src-tauri/src/db.rs(横切 SQLite 句柄 + 迁移 runner + 全部 DDL)
ref: src-tauri/src/review/history_store.rs(会话/历史持久化)
ref: src/pr/ProjectNav.vue(合并 ProjectSwitcher + PrList)

Risk / 兼容性

  • 数据迁移(高关注):新增一次性 legacy JSON→SQLite 导入(lib::import_legacy_stores,meta('legacyImported') 守卫只跑一次,插入与守卫同一事务可重跑)。旧 config.json/prs.json/ledger.json 保留不删(可回退/降级,导入守卫令其惰性失效)。config 以原始值入 config_blob,load 仍跑 migrate_value,已测。
  • 持久化后端切换:config/service、pr/registry、pr/ledger 的 load/save 由 store 改 SQLite;保留 migrate_value/validate、WRITE_LOCK/LEDGER_WRITE_LOCK 语义与全部既有 serde golden / 行为测试(已移植为 SQLite 往返测试)。
  • 跨切片契约:新增切片私有 wire 类型 HistoryItem(= 前端 StreamItem 同形,serde golden 锁 camelCase);新增命令 get_session_history / get_pr_sessions 注册于 lib.rs。model.rs↔types.ts 共享契约未变。
  • 前端结构:删除 ProjectSwitcher.vue / PrList.vue(合并入 ProjectNav.vue);useReviewStore.focus 改为 async(从历史水合);ReviewSessions 改为按选中 PR 取 durable 会话。切片边界(slice-boundary.test.ts)保持。
  • 新依赖 rusqlite(bundled 特性,编译期需 C 编译器;无系统 sqlite 依赖)。

Test plan

  • pnpm build 本地通过(vue-tsc 类型检查 + vite build)
  • cargo build --manifest-path src-tauri/Cargo.toml --locked 本地通过
  • cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --locked -- -D warnings 0 告警
  • cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check 干净
  • cargo test --manifest-path src-tauri/Cargo.toml --locked 通过(238 通过)
  • pnpm test(vitest)通过(114 通过,含 slice-boundary、useReviewStore 历史水合)

🤖 Generated with Claude Code

ghbvf and others added 2 commits June 20, 2026 14:09
#70(后端):引入 rusqlite(bundled)作为统一本地存储。
- 新增横切 db.rs:Database 句柄(Mutex<Connection>)、PRAGMA user_version 迁移 runner、
  全部 DDL(config_blob / tracked_pr / dispatch_key|event / review_session|history_item)、
  with_conn/with_tx;setup 内 manage 为 tauri::State。
- config/pr-registry/pr-ledger 的 load/save 由 tauri-plugin-store 改 SQLite(保留
  migrate_value/validate 与 WRITE_LOCK 语义、serde golden)。
- 新增 review/history_store:会话元数据 + 历史内容持久化;pump emit 后 best-effort
  落库(按 item_id 合并 delta),状态转移镜像到 review_session。
- 新命令 get_session_history / get_pr_sessions(注册于 lib.rs)。
- 一次性 legacy JSON→SQLite 导入(meta 守卫,旧文件保留可回退)。
- webhook PR 经现有 ingest 落 tracked_pr,gh 不可用时仍可存可查。

#67(前端):合并 ProjectSwitcher + PrList 为单列 ProjectNav(项目 H2 一级分组、
活动项目展开其 PR),消除两条分离边栏;会话面板保持独立。
- useReviewStore.focus 改为从持久历史水合(打开历史会话看之前内容)。
- ReviewSessions 改为按选中 PR 展示其会话(durable getPrSessions + 实时叠加)。
- api/types 增 getSessionHistory / getPrSessions。

Closes #67
Closes #70

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- F2 可靠性:去掉 review_history_item → review_session 的 FK 约束(best-effort
  历史捕获不应因 session 行缺失而 FK 违反静默丢整段历史;孤儿行可读优于丢失)。
- F1 产品:focus 水合历史改为「合并」而非覆写——await 窗口内到达的 live deltas
  (新 itemId)不再被 history 覆盖丢弃。
- F3 安全:history_store::status_wire 的 unwrap_or_default 改 expect fail-fast。
- F4 产品:ReviewSessions 切 PR 时同步清空旧 durable(消除 stale flash)+ 增加
  loading / 加载失败 错误态(避免静默空列表)。
- F5 测试:补 get_pr_sessions 排序、SessionStatus 存储往返(锁 Deserialize 路径)、
  corrupt labels_json 降级、去 FK 后孤儿 append 等测试。
- F6 DX:批量订正迁移后过时注释(prs.json/ledger.json/Store::set/key-prefix),
  修正 config/model.rs 中 id 校验的过时技术理由(project_id 现为列非键前缀)。

遗留(large / 需人工决策,见 pm:ship 评论):会话/历史无界增长保留策略、
u64→i64 类型、persist 失败 UI 可见性、会话按时间排序(需 wire 加 createdAt)、
import_legacy_stores 跨切片集成测试。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ghbvf

ghbvf commented Jun 20, 2026

Copy link
Copy Markdown
Owner Author

🛠 ship review + fix

reviewer 6(六维度并行,diff 2351 行 ≥1500 → 6 档)· Findings 11(已修 small 6 · 遗留 large 5 · OUT_OF_SCOPE 0)

  • F1 [P1·small·产品] src/review/useReviewStore.ts:329 — focus 水合历史覆写丢失 await 窗口内 live deltas → ✅ 已修
  • F2 [P2·small·可靠性] src-tauri/src/db.rs:222 — review_history_item FK + best-effort 持久化冲突,session 行缺失时 FK 违反静默丢整段历史 → ✅ 已修
  • F3 [P2·small·安全] src-tauri/src/review/history_store.rs:48 — status_wire unwrap_or_default 静默写空串 → ✅ 已修
  • F4 [P2·small·产品] src/review/ReviewSessions.vue:23 — 切 PR 时旧 durable 未清(stale flash) + 缺 loading/错误态 → ✅ 已修
  • F5 [P2·small·测试] history_store.rs / registry.rs — 缺 get_pr_sessions 排序、SessionStatus 反序列化往返、corrupt labels_json 降级测试 → ✅ 已修
  • F6 [P2·small·DX] 多文件(8 处) — 迁移后过时注释(prs.json/Store::set/key-prefix) + config/model.rs 错误的 id 校验理由 → ✅ 已修
  • F7 [P2·large·可靠性] src-tauri/src/db.rs:201 — review_session/review_history_item 无清理→无界增长 → ⏸ 遗留(需保留策略)
  • F8 [P2·large·安全] registry/ledger/history 多处 — u64 as i64 无检查转换(现实 PR 号/epoch 安全) → ⏸ 遗留
  • F9 [P2·large·安全] src-tauri/src/review/session.rs:519 — persist 失败仅 eprintln,桌面用户不可见 → ⏸ 遗留(产品决策)
  • F10 [P3·large·产品] src/review/ReviewSessions.vue:51 — 会话按 threadId 排序无时间语义(需 wire 加 createdAt) → ⏸ 遗留
  • F11 [P2·large·测试] src-tauri/src/lib.rs:148 — import_legacy_stores 跨切片组装层无集成测试 → ⏸ 遗留
完整详表(根因 + 证据 + 建议 + 方案种子,/fix 读此)

F1 [P1·small·产品] src/review/useReviewStore.ts focus()

  • 证据:旧实现 items.value = [] → await getSessionHistory → items.value = history.map(...),await 窗口内 applyEvent 追加的 live deltas 被整体覆写丢弃。
  • 处置:✅ 已修——改为合并:history 为前缀,await 期间到达的「新 itemId」live item 保留追加;同 itemId 以 history 为准(其文本更全,下一条 delta 再续)。守卫 activeThreadId !== threadId → return 防焦点切换误写。

F2 [P2·small·可靠性] src-tauri/src/db.rs schema

  • 证据:review_history_item 有 FOREIGN KEY (thread_id) REFERENCES review_session(thread_id) + foreign_keys=ON;但 persist_session 是 best-effort,若 session 行 upsert 失败/竞态缺失,pump 的 append_item 会 FK 违反并被吞 → 整段历史丢失。
  • 处置:✅ 已修——去掉 FK(best-effort 语义不应携刚性约束;孤儿历史行仍可按 thread_id 读,优于丢失)。补 append_item_without_session_row_is_orphan_not_error 测试。

F3 [P2·small·安全] history_store.rs status_wire

  • 证据:serde_json::to_value(status).ok()...unwrap_or_default() 失败路径静默返回 "",status_from_wire("") 再降级为 Failed,难诊断。
  • 处置:✅ 已修——SessionStatus 是 unit enum,序列化不可能失败,改 expect fail-fast。

F4 [P2·small·产品] src/review/ReviewSessions.vue loadDurable

  • 证据:切 PR 时旧 durable 在 getPrSessions 返回前仍展示(stale flash);失败仅 console.error → 用户看到「暂无会话」无法区分真空与加载失败。
  • 处置:✅ 已修——PR/项目切换时同步清空 durable + 显示 loading;后台(sessions 变化)刷新不清空(防闪);新增 loadError 错误态展示。

F5 [P2·small·测试] history_store.rs / registry.rs

  • 证据:get_pr_sessions 的 ORDER BY created_at DESC、status_from_wire 的 Deserialize 路径、corrupt labels_json 降级均无测试。
  • 处置:✅ 已修——补 get_pr_sessions_orders_newest_first、session_status_round_trips_through_storage(锁 Deserialize funnel 下游)、load_db_degrades_corrupt_labels_json_to_empty。后端测试 238→242。

F6 [P2·small·DX] scheduler/commands/registry/ledger/config 多处

  • 证据:注释仍引 prs.json/ledger.json/Store::set/Store::save/tracked:{pid}/dispatched:{pid};config/model.rs id 校验理由「: 会 split partition」在 SQLite 列模型下已不成立。
  • 处置:✅ 已修——comment-only 订正为 SQLite 表/列/WHERE project_id=?1 语义;id 校验理由改为「project_id 作列值 + 保 legacy 导入 key-prefix 解析无歧义」,Medium 载体不变。

F7 [P2·large·可靠性] db.rs — review_session/review_history_item 无 prune

  • 证据:grep "DELETE FROM review_" → 无。tracked_pr 有 prune(500)、dispatch_* 全量替换有上限,但会话/历史只增不减。
  • 三级方案种子:最小=按 PR 保留最近 N 个 session 定期删;彻底=保留策略 + ON DELETE CASCADE 级联删 history;重构=独立保留/归档子系统。
  • 处置:⏸ 遗留(需保留策略 + schema v2,跨 history_store/session/commands)。

F8 [P2·large·安全] registry/ledger/history as i64

  • 证据:pr.number as i64 / epoch as i64 等 10+ 处无检查位转换。现实 GitHub PR 号与 epoch 远在 i64 正区间,无实际溢出。
  • 三级方案种子:最小=i64::try_from(...).unwrap_or(i64::MAX);彻底=Candidate/TrackedPr 的 number 改 i32/i64;重构=统一 newtype。
  • 处置:⏸ 遗留(改类型触及跨切片契约)。

F9 [P2·large·安全] session.rs persist_* best-effort

  • 证据:persist_session/status/delta 失败仅 eprintln!,桌面用户不可见,历史无法恢复时难诊断。
  • 三级方案种子:最小=失败计数 + 一次性 review:event Error 通知;彻底=结构化持久化健康指示;重构=持久化层独立错误通道。
  • 处置:⏸ 遗留(产品决策:best-effort 静默 vs 可见)。

F10 [P3·large·产品] ReviewSessions.vue 排序

  • 证据:visibleSessions 按 threadId.localeCompare(UUID 无时间语义),覆盖了后端 created_at DESC,用户看不出最近会话。
  • 三级方案种子:最小=SessionInfo/ReviewSession 加 createdAtEpoch(Rust 已有列) + 前端按其降序;彻底=同时给会话行展示时间。
  • 处置:⏸ 遗留(需改 wire 契约 + golden)。

F11 [P2·large·测试] lib.rs import_legacy_stores

  • 证据:各切片 import_legacy_* 有独立 in-memory DB 测试,但跨切片组装(key 前缀解析 + 多切片同事务 + guard)无测试。
  • 三级方案种子:最小=提取键前缀解析单测;彻底=端到端:构造 mock store 值驱动组装、断言 legacy_imported() + 各切片读回;重构=可注入 store 抽象。
  • 处置:⏸ 遗留(跨切片测试基建)。

下一步:切 pr-status/needs-review-again(待再审:codex / /pr-review #71;large 遗留有需做再 /fix #71)。


🤖 PR #71 · Generated with Claude Code · branch Feature/67-70-sqlite-nav · worktree worktrees/Feature/67-70-sqlite-nav · session —

@ghbvf ghbvf added the pr-status/needs-review-again ship 交接后首审一次(review changes-requested 后转 needs-fix) label Jun 20, 2026
@ghbvf

ghbvf commented Jun 20, 2026

Copy link
Copy Markdown
Owner Author

CI 检查结果

状态:通过(已通过 2 / 共 2 个检查)

  • Frontend (vue-tsc + build) — pass (16s)
  • Rust (fmt + clippy + build) — pass (1m36s)

🤖 PR #71 · Generated with Claude Code · branch Feature/67-70-sqlite-nav · worktree worktrees/Feature/67-70-sqlite-nav · session —

@ghbvf ghbvf closed this Jul 4, 2026
@ghbvf
ghbvf deleted the Feature/67-70-sqlite-nav branch July 4, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-status/needs-review-again ship 交接后首审一次(review changes-requested 后转 needs-fix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

使用 SQLite 持久化项目、PR、会话和历史会话内容 调整主页面为项目/PR 两级导航并按 PR 关联会话

1 participant