diff --git a/.github/workflows/data-consistency-contract-ci.yml b/.github/workflows/data-consistency-contract-ci.yml index f738c18a3..59076aeb0 100644 --- a/.github/workflows/data-consistency-contract-ci.yml +++ b/.github/workflows/data-consistency-contract-ci.yml @@ -19,6 +19,12 @@ on: - "backend/tests/backup-restore-*.test.ts" - "backend/tests/offline-workspace-sync.test.ts" - "frontend/src/lib/mobileSync*" + - "frontend/src/lib/mobileNoteSyncReceipt.ts" + - "frontend/src/lib/noteSyncReceipt.ts" + - "frontend/src/lib/useNoteSyncReceipt.ts" + - "frontend/src/components/SyncStatusBadge.tsx" + - "frontend/src/components/EditorPane.tsx" + - "frontend/src/i18n/coverageTranslations.ts" - "frontend/src/lib/mobileLocalFirst*" - "frontend/src/lib/nativeLocal*" - "frontend/src/lib/offline*" @@ -28,6 +34,12 @@ on: - "frontend/src/lib/__tests__/syncEngine*.test.ts" - "frontend/src/lib/__tests__/noteSyncSafety*.test.ts" - "frontend/src/lib/__tests__/offlineQueue.test.ts" + - "frontend/src/lib/__tests__/noteSyncReceipt.test.ts" + - "frontend/src/lib/__tests__/mobileNoteSyncReceipt.test.ts" + - "frontend/src/lib/__tests__/noteSyncReceiptBadge.test.tsx" + - "frontend/src/lib/__tests__/mobileNativeReceiptIntegration.test.ts" + - "frontend/src/lib/__tests__/useNoteSyncReceipt.nativeWiring.test.tsx" + - "frontend/src/lib/__tests__/nativeLocalNoteList.test.ts" pull_request: paths: - "docs/architecture/data-consistency-contract*" @@ -45,6 +57,12 @@ on: - "backend/tests/backup-restore-*.test.ts" - "backend/tests/offline-workspace-sync.test.ts" - "frontend/src/lib/mobileSync*" + - "frontend/src/lib/mobileNoteSyncReceipt.ts" + - "frontend/src/lib/noteSyncReceipt.ts" + - "frontend/src/lib/useNoteSyncReceipt.ts" + - "frontend/src/components/SyncStatusBadge.tsx" + - "frontend/src/components/EditorPane.tsx" + - "frontend/src/i18n/coverageTranslations.ts" - "frontend/src/lib/mobileLocalFirst*" - "frontend/src/lib/nativeLocal*" - "frontend/src/lib/offline*" @@ -54,6 +72,12 @@ on: - "frontend/src/lib/__tests__/syncEngine*.test.ts" - "frontend/src/lib/__tests__/noteSyncSafety*.test.ts" - "frontend/src/lib/__tests__/offlineQueue.test.ts" + - "frontend/src/lib/__tests__/noteSyncReceipt.test.ts" + - "frontend/src/lib/__tests__/mobileNoteSyncReceipt.test.ts" + - "frontend/src/lib/__tests__/noteSyncReceiptBadge.test.tsx" + - "frontend/src/lib/__tests__/mobileNativeReceiptIntegration.test.ts" + - "frontend/src/lib/__tests__/useNoteSyncReceipt.nativeWiring.test.tsx" + - "frontend/src/lib/__tests__/nativeLocalNoteList.test.ts" workflow_dispatch: permissions: diff --git a/docs/architecture/data-consistency-contract.matrix.json b/docs/architecture/data-consistency-contract.matrix.json index c6b38ad3d..3e0d94936 100644 --- a/docs/architecture/data-consistency-contract.matrix.json +++ b/docs/architecture/data-consistency-contract.matrix.json @@ -183,6 +183,77 @@ "case": "preserves a stale offline edit as one pending conflict without issuing PUT" } ] + }, + { + "id": "DCC-011", + "title": "逐笔记同步收据以当前修改的版本/Mutation ACK 为依据", + "status": "covered", + "checks": [ + { + "file": "frontend/src/lib/__tests__/noteSyncReceipt.test.ts", + "case": "requires exact local revision before acknowledging a note write" + }, + { + "file": "frontend/src/lib/__tests__/noteSyncReceipt.test.ts", + "case": "preserves pending queue and conflict priority over an older ACK" + }, + { + "file": "frontend/src/lib/__tests__/noteSyncReceipt.test.ts", + "case": "isolates receipt data across account and server scopes" + }, + { + "file": "frontend/src/lib/__tests__/mobileNoteSyncReceipt.test.ts", + "case": "requires a matching native mutation receipt instead of global sync time" + }, + { + "file": "frontend/src/lib/__tests__/mobileNoteSyncReceipt.test.ts", + "case": "keeps native pending and conflicts ahead of committed ACK" + }, + { + "file": "frontend/src/lib/__tests__/offlineQueue.test.ts", + "case": "does not discard a newer queued edit after a stale in-flight HTTP 200" + }, + { + "file": "frontend/src/lib/__tests__/noteSyncReceiptBadge.test.tsx", + "case": "never uses global synced state to claim a note has cloud confirmation" + }, + { + "file": "frontend/src/lib/__tests__/noteSyncReceipt.test.ts", + "case": "does not treat an Electron local SQLite response as a cloud ACK" + }, + { + "file": "frontend/src/lib/__tests__/noteSyncReceipt.test.ts", + "case": "does not confirm a metadata-only REST response while CRDT body is unresolved" + }, + { + "file": "frontend/src/lib/__tests__/mobileNativeReceiptIntegration.test.ts", + "case": "stores the actual applied and duplicate versions in an atomic mutation ACK" + }, + { + "file": "frontend/src/lib/__tests__/mobileNativeReceiptIntegration.test.ts", + "case": "keeps conflict.serverVersion separate from successful applied.version" + }, + { + "file": "frontend/src/lib/__tests__/mobileNativeReceiptIntegration.test.ts", + "case": "ignores only the known own ACK echo with a newer queued edit, without overwriting local" + }, + { + "file": "frontend/src/lib/__tests__/mobileNativeReceiptIntegration.test.ts", + "case": "keeps real competing remote modifications as conflicts (not own echoes)" + }, + { + "file": "frontend/src/lib/__tests__/nativeLocalNoteList.test.ts", + "case": "refreshes both Markdown and rich-text notes only after new Outbox commits" + }, + { + "file": "frontend/src/lib/__tests__/useNoteSyncReceipt.nativeWiring.test.tsx", + "case": "does not repaint an obsolete confirmed query after a newer pending refresh" + }, + { + "file": "frontend/src/lib/__tests__/useNoteSyncReceipt.nativeWiring.test.tsx", + "case": "updates the real editor badge after a committed Android edit" + } + ] } ] } diff --git a/docs/architecture/data-consistency-contract.md b/docs/architecture/data-consistency-contract.md index 1505dae62..3b5a180ee 100644 --- a/docs/architecture/data-consistency-contract.md +++ b/docs/architecture/data-consistency-contract.md @@ -41,6 +41,7 @@ - **DCC-008 — 备份恢复必须先校验、失败要保护原数据。** 附件和数据库快照校验失败时不得覆盖正在使用的数据;发生中途失败,原数据库及受管文件应有回滚/保护路径。 - **DCC-009 — Native 离线与身份安全。** Native 仅此设备模式的编辑在重启/初始化后仍保留;重新连接服务器前不能偷发写入;同步成功后方可清除相应待发送 mutation。 - **DCC-010 — Web 离线并不是云端确认。** Web 端失败写入、冲突快照必须保留在已有浏览器队列/冲突存储中,不能把“排队中”渲染成“服务器最新”;浏览器离线存储的实际耐久性仍受浏览器环境限制。 +- **DCC-011 — 逐笔记同步收据须有对应证据。** Web 按账号/服务器作用域保存本地逻辑修订,只有包含完整正文快照的本次版本化写入得到服务端版本推进且无更新的本地修订,才能确认;元数据 REST ACK 不能证明 CRDT 正文已经到达云端。离线队列与冲突优先于旧 ACK。Android 仅依据同一 Profile、同一笔记的 Outbox、冲突以及与 Outbox 出队同事务写入的 mutationId ACK;Native 本地笔记 CRUD 必须在事务提交后立即刷新收据(包括旧 ACK 后的新修改),`applied/duplicate` 结果用 `version` 而非冲突专用 `serverVersion`;回拉只允许通过同一 Profile/笔记的精确 ACK 版本跳过已确认的自身回声,真实远端并发修改仍保留冲突;全局 lastSyncAt 和全局队列空不能证明单篇笔记已同步。缺失证据只能显示“待核验”。 ## 3. 关键时序约束 @@ -89,8 +90,8 @@ GitHub Actions:`.github/workflows/data-consistency-contract-ci.yml`,对同 **尚未统一实现,后续应另开 Issue/PR:** -- 为所有编辑器和轻量表格统一 `localRevision / acknowledgedRevision / conflict` 的**同步收据**(Sync Receipt)与原子状态更新;不要先造假的“已同步”标签。 -- 基于该收据统一「正在保存 / 已保存到本机 / 等待云端 / 云端已确认 / 冲突 / 失败」文案,区分服务器持久化与其他设备应用完毕。 +- 本阶段已为普通笔记编辑器实现逐笔记 Sync Receipt;轻量表格、任务、附件和全部其他保存入口尚未覆盖。需要继续统一原生端本地逻辑修订号、服务端 ACK 版本,以及多平台设备级验收。 +- 本阶段显示「正在保存 / 等待云端 / 云端已确认 / 冲突 / 失败 / 待核验」;Electron 内嵌后端的 REST 成功仅代表本地 SQLite 写入,不可据此确认云端;“已保存到本机”尚未覆盖全部 Web 本地存储失败场景,未获持久化证明时不得显示。服务端确认不等于附件或第二设备已应用。 - Native Android/iOS、桌面与 Web 的真实设备断网、切账号、重启、强杀、磁盘不足、代理异常、附件一致性全链路验收。 - 备份/恢复在 SQLite 与 PostgreSQL、对象存储等部署组合下的生产级恢复演练和附件逐项校验报告。 diff --git a/frontend/src/components/EditorPane.tsx b/frontend/src/components/EditorPane.tsx index 7fbdcd735..fe0c44224 100644 --- a/frontend/src/components/EditorPane.tsx +++ b/frontend/src/components/EditorPane.tsx @@ -2604,7 +2604,7 @@ const moveToTrash = useCallback(async () => { {compactMobileEditing ? (
- +