个人开源项目最容易停在一个很舒服的位置。

代码能跑,测试是绿的,README 也大概写了。没有产品经理追排期,没有测试团队问验收,也没有发布工程师盯着制品。作者既负责实现,也有权宣布「差不多完成了」。

这个自由很好,也很容易让一个功能永远停在只有作者电脑上成立的状态。

我维护多个小工具以后,开始给「完成」加一个更严格的条件。不是所有检查都要做一遍,而是项目对外承诺了哪些表面,每一个表面都应该拿出与声明相匹配的证据。

先别跑测试,先写清楚用户到底会拿到什么

不同项目的交付物差别很大。@anys/url-join 面向 npm 包和交互演示,nice-use-modal 还有生命周期文档,Sticker UI 同时提供 npm、源码 registry 与组件预览,CodexUsage 则是要下载、安装和更新的 macOS 应用。

如果不先列出这些表面,验证很容易只覆盖最熟悉的源码仓库。测试通过以后,我们会下意识觉得 npm、Pages、ZIP 和 DMG 应该也没问题。

可用户并不安装源码仓库。他可能拿到一个 tarball,打开一个公开页面,或者把应用从 DMG 拖进 Applications。完成线应该画在他真正接触的位置,而不是画在开发者最方便检查的位置。

我现在会先写一份交付清单,再决定需要什么验证。项目小,清单也可以很短,但不能靠脑子里一句「就那些东西」。

完成不是一个点,而是一条有层次的线

我大致把它分成五层。

行为完成,说明核心功能在真实路径里工作,边界和失败状态有明确表现。契约完成,检查类型、导出、配置、格式与兼容范围是不是和文档一致。制品完成,确认 tarball、静态站点、扩展 ZIP、DMG 或应用 ZIP 里真的包含正确文件。

再往外是公开完成。远端 commit、tag、CI、Release、npm、Pages 或应用商店已经更新,并且能从公开入口回读目标版本。生命周期完成则关心 README、CHANGELOG、LICENSE、安全渠道、贡献说明和升级路径是否跟得上当前产品。

前一层是后一层的基础,不是替身。

一个功能可以行为正确,却因为声明文件没进 tarball 而无法被 TypeScript 用户安装;应用可以成功打包,却因为 Release 资产命名不符合更新契约而无法自动升级;公开版本能用,安全问题却没有私密入口,也仍然留下维护缺口。

每份证据都应该只说自己有资格说的话

我会反复问一句,这份证据究竟能证明哪句话?

lint 通过证明静态规则通过,单元测试证明覆盖到的逻辑,浏览器冒烟证明那条页面路径能够交互。HTTP 200 不能证明脚本和样式正常,压缩包结构正确不能证明应用能启动,推送命令没有报错也最好再回读远端 SHA。

这听起来有点较真,但它能阻止「全部通过」吞掉那些其实没有验证的部分。

证据强度要和声明匹配。声称支持多个 Vite 主版本,就在对应组合里启动和交互;声称零依赖,就检查最终包的依赖,而不是只搜源码 import;声称已经发布,就从公开 registry 或 Release 下载,不拿本地产物代替。

当结论变具体,遗漏也会变得具体。我们可以清楚地说还缺公开回读,而不是在一个模糊的完成状态里继续猜。

权限边界也属于交付质量

修改文件、创建 commit、推送分支、部署 Pages、发布 npm 和提交应用商店,是不同级别的动作。技术上可以把它们串进一条自动化,不代表用户说「修一下」就天然授权了整条链路。

我会明确当前任务停在哪一层。只改代码,就报告源码和本地验证;授权发布,才继续处理 tag、registry 和公开回读。

这不是流程上的拘谨。错误发布会影响真实用户,也可能占用不可复用的版本号。自动化应该减少机械操作,不应该把每一个外部动作都藏进同一个按钮里。

对维护者只有一个人的小项目,权限边界更重要。没有第二个人替你踩刹车时,流程本身得知道在哪里停。

失败路径决定下一次还能不能从容继续

发布流程不能只设计最顺利的那一次。tag 推上去了,Release 创建失败怎么办;npm 已经发布,文档部署却挂了怎么办;扩展进入审核以后,下一次怎样查询和继续。

幂等工作流、唯一版本来源、可重新运行的远端检查和非破坏性回滚,会让一次网络波动停留在某个明确阶段,而不是变成需要手工猜测状态的事故。

我很看重「可恢复」这件事。成功时少点几次按钮当然很好,失败时知道哪些动作已经发生、哪些可以安全重试,才真正减少维护成本。

小项目认真完成以后,下一次才不用考古

小工具用户不多、代码也不大,并不意味着契约和交付可以省略。恰恰因为维护者常常只有一个人,几个月后回来时,那份明确完成线就是写给未来自己的说明。

我喜欢做小项目,是因为一个具体问题可以被收得很清楚。API 不含糊,制品能检查,公开入口可以使用,失败也有恢复方法。

「真的完成」当然不表示以后永远不改。它只表示这一刻对外说过的话已经兑现,而且我能指出证据在哪里。

下一次迭代可以从可靠基线继续,而不是重新问一句。

上次……应该发布过了吧?