产品设计

Replit:回到一个检查点,先说清哪些东西要跟着回去

Replit 是把对话、应用开发与发布放在一起的 AI 工具。检查点帮助用户在改动出问题时找回先前的项目和上下文,并明确是否连同开发数据一起恢复。

先尝试拆解问题、设计和实现,再展开思路,检查状态、边界与取舍,并设计验证用例。思路提供分析路径,不给出完整实现。

01
代码思考进阶

恢复快照前,怎样避免只恢复了一半?

练习场景

为自己的项目工具设计一个“恢复预检器”。输入当前快照和目标快照,先检查目标是否完整,再返回一份新状态及文件差异;任何错误都不能改变当前状态。只讨论内存中的纯函数,不执行磁盘写入、数据库请求、网络恢复、并发修改或崩溃恢复。

输入与术语

  1. 目标快照顶层必须是非 null 对象;快照包含 files 数组和 devDatabaseId。files 中每项为非 null 对象,含 path 与 blobId 两个非空字符串。blobId 是内容仓库里的不透明ID,不是文件名;仓库保证同一ID始终对应相同内容,相同文件内容只使用同一个ID。仓库及当前快照已经校验且只读;目标快照不可信。忽略其他字段。
  2. path 是区分大小写的相对路径,用 / 分隔。每段只允许ASCII字母、数字、点、下划线、短横线,但整段不能是 . 或 ..;不允许空段、首尾斜杠、反斜杠。所有项目都是文件,目录由路径隐含。同一快照不能重复path,也不能同时有文件 src 与 src/a.js。src 与 src-old/a.js 不冲突。
  3. 每个快照最多100,000个文件,两个快照的路径总字符数至多5,000,000。目标 files 缺失、不是数组或包含不合法条目均报错;空数组合法,表示目标没有任何文件。目标 devDatabaseId 必须是字符串或 null。restoreDatabase 必须是布尔值。
  4. 所有目标文件的blobId必须存在于只读仓库。restoreDatabase=true 时,目标 devDatabaseId 必须非null且仓库内存在,成功后采用该ID;false时不要求目标数据库ID可取回,只保留当前ID。仓库已按上游协议核验内容与类型,本题不重新算哈希或执行数据库语义检查。
  5. 成功返回新快照与 changes 数组。仅目标存在的path为 added,仅当前存在为 removed,两边都有且blobId不同为 modified,其余不输出。按path的ASCII字典序排列,条目含path、kind、beforeId、afterId;不存在的一侧用null。数据库结果另列,不混入文件差异。禁止修改任一输入或仓库。
  6. 按错误层级返回定位信息:顶层错误用 target;files 容器、数量上限或路径总长度错误用相应对象/字段路径及错误原因;devDatabaseId 或 restoreDatabase 错误用各自字段名。这些错误没有对应文件项,不返回虚构下标。单个文件条目错误须给出真实数组下标与字段(如 files[2].path);重复路径及祖先/后代冲突须给出双方真实下标。只需报告第一个发现的错误,不规定不同错误的优先级。失败不返回可提交的新快照或部分差异,当前文件与数据库引用均保持原样。

从这个例子开始

当前文件为 src/page.js → A、src/data/seed.json → B,当前开发库为D2。目标文件为 src/page.js → C、src/data → D,目标开发库为D1;A、B、C、D、D1、D2均存在。分别列出 restoreDatabase=false 和 true 的文件差异及数据库结果。注意:目标将一个旧目录替换成文件,这本身合法,因为当前和目标不能混成同一棵树校验。

再分别尝试:给目标追加 src/data/more.json → A;把C换成不存在的Z;把目标数据库ID设为null。说明各变体在两种restoreDatabase选项下是否成功,失败时哪些数据必须保持不变。另将目标设为 null,或仅把 restoreDatabase 设为字符串 "false",分别写出错误定位。数组下标从0开始:追加的第三项与原第二项路径冲突,应能指出下标2和1。

交付物

提出校验顺序、路径冲突检测结构、差异计算与一次性交付新状态的思路;逐步推演例子,给出至少五个不变量和反例。用n表示两边文件总数、L表示路径总长度,分析时间和额外空间,另说明ID读写/比较的成本。可以讨论JS Map或Rust集合,但不写完整代码和逐行伪代码。

查看思路
  1. 先在独立工作区校验目标。 检查顶层、数组、条目、路径格式、ID和选项。顶层/容器/选项错误按对象或字段路径定位,文件条目错误才附真实下标;路径冲突保留双方下标。仓库只读;没有完成全部必要校验前,不向调用者暴露新状态或部分差异。false时仍检查devDatabaseId的字符串或null类型,但不要求它存在于仓库。
  2. 按路径段检测同一目标内的冲突。 可以使用字典树:经过已标为文件的节点,或在已有子节点处标记文件,都说明祖先/后代冲突;重复终点是重复路径。保留相关输入下标,避免用普通字符串前缀误判 src-old。不要将当前旧文件插进目标树制造跨版本假冲突。
  3. 把内容差异与数据库选项分开。 建立当前/目标path索引,根据存在性与规范化内容ID判断added、removed、modified;选true采用目标数据库ID,false保留当前ID。先准备全部返回对象,再作为一个成功结果交付。这里的原子性是纯函数无部分输出,不声称能直接实现分布式恢复。
  4. 推演替换目录的例子。 按字典序返回 src/data added(null→D)、src/data/seed.json removed(B→null)、src/page.js modified(A→C)。false结果库D2,true结果库D1。追加子路径会与目标中的文件 src/data 冲突;Z缺失使两种选项都失败;目标库null时false成功并保留D2,true失败。目标为null时定位target;字符串选项定位restoreDatabase;追加子路径定位files[2].path与files[1].path。所有失败都不能改动原输入。
  5. 检查不变量和成本。 成功后目标文件集合完整、路径无祖先文件冲突;差异每path至多一条且顺序确定;false数据库不变;相同快照文件差异为空;任意失败与输入只读保证一致。每个反例单独运行,还应测试空目标和反斜杠。字典树校验O(L),通过平均常数索引比较可作O(n)次集合操作,但路径哈希/比较计入字符成本。若用普通比较排序输出k条差异,需要O(k log k)次比较,每次还可能扫描公共前缀;不能直接把它写成与路径长度无关的O(n log n)。额外空间包含O(L+n)的路径/索引与结果引用;不复制底层不可变blob内容,另计实际ID字符成本。