工程原则
本仓库完整的议题/讨论历史(截至 v0.9.10,共 569 个议题、89 个讨论)所总结出的设计原则、约束和护栏。每项原则都会引用促成它的事件。在实现新功能和修复之前,应先依据本页面进行检查。
贯穿整个历史的最大教训是:几乎每一个严重(P0/P1)错误,都是多写入方路径中的数据完整性错误——同步与 UI、同步与自身、应用与 MCP/CLI、快照与增量保存。UI 错误数量很多,但修复成本低;写入路径错误则会让用户付出数据和信任的代价。
0. 产品理念(任何工程决策前都要检查)
Mindwtr 默认简单,需要时强大:渐进式披露(高级选项在需要时才显示)、默认做减法(更少字段、更少旋钮、更少干扰),并避免功能蔓延(清晰优先于杂乱)。我只是想骑自行车,别给我看飞机驾驶舱。
自动优于手动——推断,而非询问
如果可以根据平台、安装渠道、现有数据或上下文自动确定正确结果,应用就应该直接去做。这适用于一切,而不仅仅是设置:一个旋钮、一个确认提示、工作流中的一次额外点击,或一个必须由用户操作的 UI 控件,本质上都是同一种失败——应用在询问一个本可自行回答的问题。每增加一个手动步骤,就会把认知负担从我们身上(一次)转移给每位用户(永远)。
- **更新检查:**有用户要求增加“禁用更新检查”开关,因为应用不断向通过包管理器安装的版本推荐 GitHub 下载。解决方案并不是增加开关:应用现在会检测其安装渠道,并针对不同渠道采取正确行为——对于有规范更新源的渠道,提醒会限定在该渠道内;对于自行管理更新的渠道,则完全保持安静。最终没有添加任何设置。
- **#832:**全局快速添加快捷键的默认值遵循各平台惯例(macOS/Linux 上使用安全的组合键并默认启用;Windows 上默认关闭,因为全局热键会无提示地抢占按键),而不是让用户自己研究。
- **#833:**已完成的专注时段会自动将分钟数记入关联任务——手动输入只作为修正方式,而非工作流。而且,当某种行为确实无法推断且需求真实存在时,应先将其置于一个现有开关之后,再考虑创造新开关:只有启用现有的番茄钟及其任务关联功能时,任务时间追踪才会出现;没有添加新设置。
**护栏:**在添加设置、提示或手动工作流步骤之前,必须证明结果无法推断或预测。在创造新的设置或交互入口之前,必须证明没有现有设置已经表达同一意图。默认值应遵循平台最佳实践,而非追求中立。当自动化可行时,手动控件只能作为修正的后备方式,绝不能成为主要路径。
1. 写入与持久化
P1 — 每次写入都有修订版本保护;绝不信任到达顺序
较低 rev 的载荷绝不能覆盖较高 rev 的行。此规则必须在最底层(SQL upsert)执行,而不是由调用方保证。
- #693(P1,严重):
updateTask通过即发即弃的saveTask持久化,而经过防抖的完整快照saveData可能随后落盘并覆盖较新的行——任务 upsert 没有修订版本保护。在短生命周期的 MCP 进程中,写入甚至会完全丢失。修复:在 upsert 上添加WHERE tasks.rev IS NULL OR tasks.rev <= excluded.rev,并在flushPendingSave()中跟踪正在进行的保存。 - **#316:**同步期间所做的编辑(完成任务、切换领域)会在进行中的同步刷新落地时被还原。经历了三次修复:跳过陈旧的 fetch 结果;即使编辑发生在同一毫秒,也推进本地变更标记;最后通过诊断找出其余问题。
**护栏:**任何新的持久化路径(新实体、新保存调用、新进程)都必须回答:什么机制能阻止旧载荷覆盖新载荷? 如果答案是“时序”,那就是错误。
P2 — 删除只解除关联,绝不级联;恢复优先于墓碑记录
- **#609(P0,严重):**删除一个领域会删除其中的每项任务和项目。更糟的是,用户从备份恢复后,同步的墓碑记录会在每个同步周期不断重新删除恢复的数据。修复:删除领域现在只会清除
areaId;备份恢复会将找回的记录标记为新的本地数据,使陈旧的远程墓碑记录败出。
**护栏:**删除容器(领域/项目/分区/标签)只应解除子项的关联。任何恢复/导入路径都必须重新为数据确定作者身份(新的 rev/updatedAt),使其不会输给由用户正在恢复的那个错误所产生的墓碑记录。
P3 — 即发即弃所声称的成功是谎言;只有持久化完成后才能响应
- **#694:**MCP 写入工具从内存存储中返回了已修改的任务,但
saveData已无提示地失败——读取操作(使用不同的数据库连接)和桌面应用显示的仍是旧数据。工具在什么都没有持久化的情况下宣称“成功”。 - **#693:**根因相同:进程在未被跟踪的保存 Promise 完成前就退出了。
**护栏:**API/工具/IPC 只有在存储层确认后,才能发出声称写入成功的响应。跟踪每个保存 Promise;flushPendingSave() 必须覆盖所有待处理工作。
2. 同步收敛
P4 — 合并必须形成不动点:重新合并已同步的数据时应无事可做
- **#698:**一个已删除的旧字段(
showFutureRecurrence)仍存在于一端存储的载荷中。内容签名通过{...task}展开构建,因此未知键在每次同步中产生了 325 个永久冲突,尽管 rev 和时间戳完全相同。 - **#418:**可选字段的
null与省略形式产生不同哈希值 → 无穷无尽的假冲突,以及由陈旧实体时间戳(而非时钟)引起的虚假“3 小时时钟偏差”警告。 - **#142:**旧版的无效
revBy会使合并验证完全中止,直到该字段被清理。
护栏:
- 内容签名根据明确的字段许可列表计算,绝不使用对象展开。未知键/旧键绝不能进入比较。
- 规范化必须幂等并收敛:
normalize(normalize(x)) === normalize(x),且对于可选字段,null≡ 缺失。 - 从架构中移除字段时,要同时提供迁移/剥离步骤——旧载荷会在远程存储中存在多年。
- 每次签名变更都应在测试套件中加入“对齐数据的新同步产生零冲突”的测试。
P5 — 同步自身的写入必须对同步触发器不可见
#718/#725 的“持续同步”事件需要六次修复,每一次都是不同的自反馈循环:
- 同步持久化没有标记 SQLite 监视器忽略窗口 → 自身写入被视为外部变更;
- 同步状态/历史写入再次将数据状态标记为脏;
- 状态写入保存了陈旧的内存快照,撤销了刚刚合并的实体(从而再次将同一次合并排入队列);
- 加载时对重复领域名称的去重在每次加载时重写数据;
- 人员列表没有持久化到 SQLite → 每次加载时都回填并标记为脏;
mindwtr.db-shm文件噪声被视为外部写入。
同类的早期错误:#309(监视器将应用自身的 data.json 重新合并为外部变更——需要一个近期自写入哈希值的窗口,而不仅仅是最后一个哈希值)、#502(同步活动 UI 持续显示)。
护栏:
- 同步写入的每一个字节(数据、状态、历史、快照)都要针对可能观察到它的每个监视器标记为自身写入。
- 同步元数据(状态/历史)应写在已持久化快照之上,绝不能源自可能陈旧的 UI 状态——更理想的是,使其处于用户数据带外。
- **加载数据绝不能修改数据。**迁移/去重应是显式的一次性流程,而不是会在加载时将状态标记为脏的规范化。
- 每个自动触发器都需要终止论证:无工作 → 不运行(#725),失败 → 冷却(#718 修复 5、#133 飞行模式),成功 → 空闲。
P6 — 同步期间的用户编辑不可侵犯
- **#323:**点击“状态”下拉菜单会触发焦点/失焦同步,导致打开的编辑器中所有未保存的编辑都被还原。修复:编辑器打开时,自动同步会忽略焦点/失焦触发器。
- #316 / #128:“每项操作都会在几秒后被还原”——这是历史上最摧毁信任的症状。
**护栏:**任何新的同步触发器都必须考虑“如果用户正在编辑会怎样?”本地编辑一侧会重新排队;绝不能被覆盖。(lastDataChangeAt 快照 + LocalSyncAbort + 重新排队机制正是为此存在——新的同步工作必须经过它。)
P7 — 同步文档是一个合并单元;没有事务方案就不要拆分
- **#629(讨论):**拆分
archive.json的方案被拒绝——归档/取消归档会成为跨文件事务;文件后端会独立上传文件 → 脑裂。记录的方向是:记录级增量同步,而不是增加更多文件。 - **#113(讨论):**Syncthing 冲突在设计上就是文件级冲突;推荐使用由应用控制的后端(WebDAV/云),而不是更聪明的文件命名。
**约束:**功能不得添加第二个同步文档(或每实体文件),除非同时提供原子化的多文件提交协议。应优先在实体上添加字段,而不是添加新的顶层文档。
3. 附件(二进制 + 元数据两阶段规则)
P8 — 先上传字节,再发布元数据;404 是终止错误
- **#176:**同步将附件
uri清理为"",并在上传完成之前发布元数据 → 附件没有cloudKey,任何位置都没有文件,无法恢复。随后,“阻塞至上传完成”的修复暴露了从未运行的上传(缺少大小元数据的content://URI)→ 同步永久阻塞。总共进行了四次修复。 - **#213 / #128:**陈旧的
pendingRemoteDeletes和缺少远程文件的元数据导致无限重试循环;手动删除数据也无济于事,因为同步会将其恢复。 - **#399:**SAF 末尾带斜杠的文档 URI 使现有附件目录不可见 → 每次同步都创建所有附件的一份新完整副本。
- **#655:**远程 404 被无限重试/轮询;随后还出现了临时文件冲突导致的
EISDIR。
护栏:
- 顺序不可改变:上传字节 → 记录
cloudKey→ 发布元数据。绝不能反过来。 - 每一种远程文件失败都必须归类为可重试(有界且带退避)或终止错误(404 → 标记为不可恢复并停止)。附件代码中任何位置都不得出现无界重试。
- 身份检查使用规范 ID/键,绝不使用提供商 URI 字符串形式(SAF 末尾斜杠、
content://特性)。 - 当元数据显示没有待处理工作时,跳过附件阶段(既为了性能,也为了循环安全)。
- 已知未解决风险(2026-06 审查):
duplicateTask会让副本共享cloudKey,却没有引用计数——删除一个副本会删除另一个副本的字节。不要添加新的共享键路径。
4. 多进程访问(MCP、CLI、本地 API)
P9 — 一个规范存储、一条串行写入路径;每个写入方要么完整参与,要么只读
- #179 / #285:CLI 将任务写入
data.json——它是一个导出镜像,仅在启动时读取一次,之后会被覆盖——且缺少rev/revBy/taskMode,因此即使导入,合并也会无提示地丢弃这些任务。 - **#722:**桌面应用 + 本地 MCP 并发写入同一 SQLite → 锁冲突;危险的变通办法(两个写入方共享存储)不得不明确记录为不受支持。修复:写入锁被占用时快速失败,重新加载当前数据,然后重试整个操作——绝不能从加锁前的快照继续。
- **#650:**长生命周期的桌面端 WAL 读取快照在重启前无法看到 MCP 写入。
- **#367:**Mac App Store 沙箱改变了数据库位置;MCP 的路径解析器只知道非沙箱路径。
护栏:
- 所有实体创建都要经过核心工厂,由其标记每个同步必需字段(
rev、revBy、createdAt、updatedAt、默认值)。手工拼装 JSON 的写入方就是未来的数据丢失错误。(云服务器的POST /v1/tasks缺少 rev 标记也属于同一类问题——修复待完成。) - 外部写入方要么嵌入核心存储 + 存储适配器,要么保持只读。
- 跨进程写入协议:获取锁 →(发生冲突时)重新加载 → 重新应用 → 写入。禁止从陈旧快照继续。
- 路径解析必须枚举沙箱化的安装渠道(App Store 容器、Flatpak 等)。
5. 日期、重复和状态语义
P10 — 一个共享的“下一个实例”函数;所有字段一起推进;重新生成是幂等的
重复功能群是历史上被反复破坏最多的功能领域:
- **#140:**自定义重复推进了
dueDate,但没有推进startTime→ 已完成任务立即重新出现在“专注”中。 - **#241:**重新生成的实例丢失了
sectionId/areaId。 - **#187 → #717:**完成后重复的后续任务立即进入“下一步行动”(没有延后的
startTime);一个版本周期后,相同症状又在仅日期的开始日期和模式切换上重现。 - **#662:**完成任务时,即使已存在等效的未完成后续任务,仍会创建重复项。
- **#557:**一个小型投影功能经历了 7 次以上的后续修复:每月第 n 个工作日与每月某日、没有开始日期时的锚定、月度元数据在加载/保存/编辑往返中丢失、投影标签错误。
- **#17:**受 COUNT 限制的 RRULE 从“当前月份”展开,使 10 年前的序列复活。
护栏:
- 只能有一个核心函数计算下一个实例。它必须:协调推进开始/截止/回顾日期;保留所有上下文字段(项目、分区、领域、情境、标签、检查清单重置规则);保持仅日期值仍为仅日期;将 RRULE 展开锚定在序列开始时间,而不是“现在”;并且保持幂等(如果存在等效的未完成后续任务,则跳过)。
- 重复元数据必须在加载 → 保存 → 编辑往返中保留;每个新的重复字段都要添加往返测试。
- 任何重复变更的回归矩阵:
{strict, after-completion} × {start-only, due-only, both, neither} × {daily, weekly, monthly-day, monthly-nth-weekday, yearly, COUNT-limited} × {date-only, datetime}。
P11 — 状态优先于日期;可见性谓词只在核心中定义一次
- **#341:**WAITING 任务出现在 NEXT 中,因为开始日期规则忽略了状态。
- **#237:**桌面端和移动端对“今天”的计算不同;在一个平台可见的任务在另一个平台不可见。最终修复让两个平台都使用同一条共享规则。
- **#144:**计划中→下一步的自动提升正是在核心 fetch 维护中实现的,以确保两个应用都继承该行为。
- #591:仓库自身的派生规则:日期一致性(例如
startTime晚于dueDate)在核心规范化中集中检测为派生状态,应用于所有写入路径——同步输入也视为不可信并重新检查。
护栏:
- 决定任务出现位置的任何谓词(
isToday、isAvailable、已推迟、回顾到期)都在packages/core中定义,并由两个应用导入。即使某个视图重新实现的结果目前一致,它仍然是错误。 - 状态是主要维度;日期只能在一种状态内调节,绝不能跨越状态。
- 不要自动修改用户日期以强制一致——应改为显示派生警告(#591 的解决方案:用户可能有意保留一个已逾期但仍推迟的任务)。
P12 — 每个重复任务只能有一个有效实例,这是产品不变量
- #552 → #557:预先创建未来的真实实例被明确拒绝(“会让完成、删除、同步和重复行为变得更难推理”);被接受的设计是只读投影。
**约束:**需要显示未来内容的功能应产生派生/投影的不可编辑数据——绝不能提前创建真实记录。
P13 — 仅日期值绝不能获得隐含时间
- **#298(严重):**仅日期值被安排为午夜闹钟,在夜间唤醒用户;撤销通知权限也没有清除已经安排的闹钟,因此它们在没有可见 UI 的情况下继续触发。
- **#205:**输入的年份被强制转换为 19xx;不完整的日期片段会跨越年代。
**护栏:**除非用户设置时间,否则仅日期字段不安排任何事项。撤销权限会取消所有已安排事项。闹钟/通知安排必须幂等,重新注册时保持安静(#418 的“已经设置此闹钟”提示轰炸)。
6. 设置和非任务状态
P14 — 对每项设置分类:已同步 / 设备本地 / UI 会话——默认值绝不能在合并中胜出
- **#120:**同步将设置重置为默认值(远程默认值覆盖本地显式值)。
- **#62:**应用升级后设置丢失。
- **#316:**UI 领域筛选条件位于可被同步刷新覆盖的位置。
- **#488:**更改密度会重置文字大小(一个设置的写入方覆盖同级设置)。
**护栏:**新设置在诞生时就要声明类别。UI 会话状态(当前筛选条件、所选视图)绝不能位于已同步的设置文档中。设置应逐字段合并;显式值始终优先于默认值。设置写入经过唯一的更新器,且该更新器不能丢弃同级键。
7. 发布工程与平台沙箱
P15 — 每个分发渠道都是不同的运行时;验证渠道产物,而非代码库
- **#715/#674/#723:**v0.9.9 FOSS APK 在每位 F-Droid/Obtainium 用户启动时崩溃(仅在 FOSS 依赖集中出现的模块互操作失败)。普通 APK 没有问题——测试一个渠道无法证明另一个渠道正常。
- **#583(P0):**Flatpak 无法启动——只有 Flatpak 运行时缺少 appindicator 共享库。
- #234:AUR 反复损坏(CI 注入的 tauri 配置、bun
overrides特性,然后是check()中不稳定的测试);解决方案是一个在干净的 Arch 容器中构建 AUR 包的发布流水线门禁——这是渠道门禁的范式。 - **#539:**F-Droid JVM 目标修补器遗漏了本地 Android 模块。
- **#209:**Windows Store 崩溃:托盘/全局快捷键初始化在 MSIX 沙箱中是致命错误;已注册快捷键的冲突也可能导致启动失败。
- **#198:**Hermes 崩溃——在计时器全局对象存在之前,模块作用域就引用了
setTimeout。
护栏:
- 启动绝不能硬性依赖可选能力(托盘、全局快捷键、钥匙串、通知、模块作用域的计时器)。检测 → 降级 → 记录;绝不退出。
- 每个渠道(FOSS APK、Play、F-Droid、Flatpak、AUR、MSIX/Store、winget、homebrew、App Store、TestFlight)都要在发布前对该渠道自己的产物进行启动冒烟测试,使用 CI 所允许的、尽可能忠实于渠道的环境。
- 渠道专用依赖集(FOSS 与非 FOSS、Flatpak 锁文件)应视为独立的构建目标,各自拥有 CI。
P16 — 已标记的发布不可变
- **#682/#674:**修复 FOSS 崩溃后,从较新提交构建的修补版 APK 被上传到现有 v0.9.9 发布中。IzzyOnDroid 的可复现构建检查失败;该产物不得不撤回,并将此发布标记为已被取代。
**护栏:**修复 → 新标签 → 新发布(例如 v0.9.9.1),绝不能替换产物。将构建步骤保存在有版本控制的仓库脚本中(而不是内联的工作流 YAML),这样下游的可复现构建工具才能跟踪变更。
P17 — 沙箱会拒绝你以为存在的能力;绝不持久化一次都没成功过的配置
- **#335:**Flatpak 没有钥匙串/DBus → 安全存储失败;修复添加了本地机密回退。同一报告中,Tauri 插件 ACL/锁文件偏差还破坏了 HTTP。
- **#343:**iOS 文件夹权限在重启后消失 → 需要安全作用域书签。
- **#617:**Homebrew 构建缺少 CloudKit 权利;发布工作流现在会验证已签名产物中的权利。
- **#338:**CloudKit 架构已部署到开发环境,但未部署到生产环境——所有 App Store 用户都受影响。此外:所选后端在第一次成功同步之前就持久化到配置,造成崩溃循环。修复:在首次成功前暂存该后端。
- **#727:**添加人员实体时遗漏了 CloudKit 可查询索引 → 重置 iCloud 数据后同步失败。
护栏:
- 为每个平台/渠道建立能力矩阵:钥匙串、文件系统持久化、网络传输、推送、CloudKit——每项都要有检测和回退方案。
- 同步后端配置只有在一次成功的往返后才提交。
- 服务器端架构(CloudKit 记录类型、索引、生产部署;云服务器验证)是实体检查清单和发布检查清单的一部分。
P18 — “添加一种实体类型”是一份检查清单,而不是一个类型定义
此原则源自 #727(人员缺少 CloudKit 索引)、#718-修复-6(人员未持久化到桌面 SQLite → 永久脏循环)、#322(SQLite 保存顺序违反外键顺序,导致 FOSS Android 重启后数据被清除),以及云端 POST /v1/tasks 的 rev 标记缺口。
新实体(或新字段)必须涉及:核心类型 + 工厂标记;规范化/签名许可列表;SQLite 架构和外键安全的保存顺序;data.json 导出;同步合并 + 墓碑记录;CloudKit 记录类型 + 索引 + 生产部署;云服务器验证/标记;两个应用的存储;i18n 标签;往返和合并收敛测试。
8. 大规模数据下的性能
P19 — 交互成本为 O(可见数据),而不是 O(存储数据);由合成大数据存储预算强制保证
- **#594(讨论)+ #643–#649、#195、#224:**一个正常大小的用户数据库(仅 186 kB JSON!)就会让每次点击冻结长达一秒。找到的原因包括:每个项目行扫描完整任务列表;打开编辑器时反复重新扫描所有任务;每次修改都进行完整快照持久化;宽泛的存储订阅会因无关变更唤醒整个屏幕;每个附件都重新列出 SAF 目录;没有待处理工作时仍进入附件同步阶段;非幂等点击处理程序不断累积(排队的点击产生 15 个空白检查清单项)。
护栏:
- 新列表视图发布时必须采用虚拟化和窄选择器;屏幕中不得订阅完整数组的
useTaskStore()。 - 修改持久化是增量的;完整快照保存仅用于生命周期/同步边界。
- 每个定期/同步阶段都应在无工作时提前退出。
- 点击处理程序必须幂等(排队的重复点击不会造成影响)。
- 每当发布新的热点路径时,都要在大数据存储性能套件(
bun run test:perf,预算位于docs/performance-budgets.md)中加入相应测试用例。
9. UI、i18n 与一致性
P20 — 不得硬编码面向用户的字符串;功能落地时,键必须同时进入所有语言包
十一个独立议题(#244、#245、#246、#256、#257、#261、#287、#292、#215、#23、#593)都是“硬编码英文字符串”或“缺少键”——每一个都本可在审查时轻易避免。日期/星期格式应通过支持区域设置的 API(#375、#287)。
P21 — 桌面端/移动端一致性是功能的一部分,而非后续工作
- #237(同一任务在移动端可见、桌面端不可见)、#99、#149、#559、#314:一致性缺口反复以错误的形式暴露。
**护栏:**任何任务语义/可见性变更都要在核心中落地,并在同一版本中由两个应用使用;否则必须明确记录这种差异是有意为之(例如 #725 有意让移动端保留生命周期同步的决策)。
10. 恢复、诊断与修复工作流
P22 — 恢复机制必须能战胜它为之存在的故障
- #236:“最多 5 个快照”都来自同样的 4 分钟——对任何真实事件都毫无用处。修复:将保留内容分散到整个窗口内,并在数据无变化时跳过快照。
- **#609:**恢复的备份会被最初破坏数据的同一个同步错误再次删除,直到恢复操作学会重新确定记录作者身份。
**护栏:**应针对真实的数据丢失场景(级联删除 + 墓碑记录、远程损坏、错误合并)测试恢复路径,而不只是检查“文件存在”。
P23 — 在修补看似合理的原因前,先让不可见的决策可诊断
- #698 能够一次解决,正是因为诊断信息公开了每个冲突的
diffKeys。 - #718 浪费了三次推测性修复,后来添加的诊断才揭示两个真正原因(人员回填 +
-shm噪声)。 - #316 的最后一步是添加合并解决诊断信息(胜出端、rev、时间戳),而不是再猜一次。
**护栏:**合并/冲突路径要记录原因 + 示例字段键(经过遮盖);循环类错误要先获得触发源诊断,再进行修复。对于语义错误(重复、日期逻辑),修复前要将输入矩阵枚举为测试——#187/#717 和 #557 的反复修复,都是在多维输入空间中不断进行单一场景补丁的结果。
P24 — 复现—确认循环
在这段历史中已被证明有效且应当保留的做法:带有 status:needs-confirmation 的严重程度/优先级标签;发布修复时附上完整提交哈希;要求报告者提供结构化环境信息 + 日志(安装渠道很重要——#322 仅出现在 FOSS 构建中,#617 仅出现在 homebrew 中);迅速撤下有问题的产物,并坦率说明(#674/#682)。
新功能快速检查清单
实现之前,请回答:
- **写入方:**还有谁会写入这些数据(同步、MCP、CLI、其他设备、旧版应用)?什么机制保护顺序?(P1、P9)
- **收敛:**我的变更同步一次后,第二次同步是否产生零差异——包括与运行上一版本的设备同步时?(P4)
- **触发器:**我的代码是否写入监视器/触发器可见的内容?是否标记为自身写入?每个循环都会终止吗?(P5)
- **编辑中:**如果用户在此过程运行时正在编辑,会发生什么?(P6)
- **实体/字段:**我是否完成了实体检查清单(SQLite + 外键顺序、签名、CloudKit + 索引 + 生产部署、云端验证、工厂)?(P18)
- **日期:**状态优先?仅日期值仍为仅日期?核心谓词由两个应用共享?已覆盖重复矩阵?(P10–P13)
- **设置:**已同步、设备本地,还是会话设置?默认值是否可能战胜显式值?(P14)
- **渠道:**是否涉及启动、原生能力或依赖?哪些渠道需要冒烟测试?(P15–P17)
- **规模:**在 5,000 项任务时成本如何?是否有无工作时提前退出?(P19)
- **失败:**每次重试是否有界,每个 404 是否为终止错误,每次恢复是否重新确定作者身份?(P8、P22)