以 Spec 为先的 AI 辅助开发工作流:cal.diy 仓库的 Spec-First Development 实践指南
【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy
在 cal.diy(Cal.com 系的开源调度基础设施项目)中,specs/目录承载着一套名为Spec-First Development(规范先行开发)的 AI 辅助协作流程:任何新功能在动手写代码之前,必须先落成一份结构化的设计文档,再由 Claude 等 AI 智能体读取设计、跟踪进度、记录决策并生成文档。本文以 specs/README.md 为骨架,结合仓库中真实的cancellation-reason-requirement功能案例与对应源码,完整讲解这套工作流的目录结构、启动方法、会话连续性机制、文档生成与公开推广流程,以及"每个 PR 必须 10 分钟内可审查"这一核心约束,帮助你在自己的项目中复刻一套可运行的 AI 协作开发范式。
Spec-First Development:为什么先写设计再写代码
specs/目录是 cal.diy 仓库中所有开发中功能的设计文档集散地。它的定位非常明确:Claude 等 AI 智能体通过阅读这些文档来理解"要构建什么、当前进度如何",从而在跨会话、跨提交的长期开发中保持上下文连续。
这套模式解决的问题很典型:AI 辅助开发时,模型每次会话的上下文窗口有限,而功能开发往往跨越多个会话、多个提交。如果缺少一份"单一事实来源"(source of truth),很容易出现:
- 模型重复实现已经完成的功能;
- 架构决策随会话丢失,后续改动偏离最初意图;
- 提交内容失控,产生难以审查的巨大 PR。
Spec-First Development 正是为应对这些问题而设计的:design.md 定义"做什么和怎么做",implementation.md 记录"做到哪了",decisions.md 保存"为什么这么做",三份文档各司其职,配合CLAUDE.md的功能专属指令,构成一个完整的 AI 协作闭环。
核心工作流程:五个固定步骤
specs/README.md定义了每次功能开发必须遵守的五步流程:
- 实现功能之前,先创建一个 spec 目录,写入设计文档;
- Claude 在写任何代码之前,先完整阅读设计;
- 进度记录在
implementation.md,保证会话间的连续性; - 决策记录在
decisions.md(即 ADR,架构决策记录),供未来参考; - 功能完成时生成配套文档,附带开发过程中捕获的截图。
从源码结构看,这套流程还得到了仓库根目录 SPEC-WORKFLOW.md 的强化——它将该流程标记为opt-in(可选启用),只有当开发者显式说出 "use spec-driven development" 或 "follow the spec workflow" 时才启用,避免强制约束所有开发路径。
如何启动一个新功能:模板拷贝 + 一句话指令
启动新功能的第一步是复制模板目录:
cp -r specs/_templates specs/{feature-name}命令执行后,specs/{feature-name}/下会生成完整的文档骨架(详见下一节)。随后只需告诉 Claude:
"I want to build {feature}. Here's my idea: [description]. Review the codebase and fill in specs/{feature}/design.md"Claude 收到指令后会做两件事:审查现有代码库,理解既有模式与约定;把design.md填充成一份完整的技术设计,而不是直接开始写代码。这一步是 Spec-First 的灵魂所在——设计先行,实现随后。
每个功能的标准文件结构
specs/README.md用表格明确了每个 spec 目录的职责:
| 文件/目录 | 用途 |
|---|---|
CLAUDE.md | 处理该功能时给 Claude 的专属指令(最先读取) |
design.md | 单一事实来源——要构建什么、如何构建 |
implementation.md | 进度追踪——已完成、进行中、被阻塞 |
decisions.md | 架构决策记录(ADRs) |
prompts.md | 可复用的常见任务提示词 |
future-work.md | 延期的想法与增强项 |
docs/ | 带截图的内部文档 |
docs/screenshots/ | 开发过程中捕获的截图 |
模板的实际内容见 specs/_templates/:其中CLAUDE.md与AGENTS.md内容一致,包含项目上下文(Project Context)、开工前检查清单(Before Starting Work,要求先读 design.md、核对 implementation.md、参考相关目录既有模式)、代码模式(Code Patterns)以及明确的 "Don't" 清单(如"不要添加 design.md 之外的功能""不要跳过测试")。
仓库中已经有一个完整落地的真实案例——specs/cancellation-reason-requirement/,其目录结构严格遵循上述模板:CLAUDE.md、design.md、implementation.md、decisions.md、future-work.md、docs/一应俱全,docs/下的 README.md 已写有功能概述与配置说明(截图位置暂留空,等待功能实现后补充)。
设计文档的设计:design.md 模板拆解
design.md是整套流程的核心,模板 定义了七个必须覆盖的章节:
- Overview:2-3 句话说明功能做什么;
- Problem Statement:解决什么问题、为什么值得做;
- User Stories:以 "As a {用户类型}, I want to {行为} so that {收益}" 格式描述需求;
- Technical Design:分 Database Changes / API Changes / UI Changes 三个子节,分别描述 schema 变更与迁移、新端点与 tRPC 路由及请求响应形状、组件/页面/用户流;
- Edge Cases:必须处理的边界情况;
- Out of Scope:明确本功能不做什么。
以cancellation-reason-requirement的 design.md 为例,可以直观看到模板如何被填充成可执行的技术方案:它定义了CancellationReasonRequirement枚举的四个取值、requiresCancellationReason列的默认值、API 校验逻辑的落点(handleCancelBooking.ts)、UI 下拉框的插入位置,以及数据流(EventType 存储 →getEventTypesFromDBselect → 页面 props →CancelBooking组件校验),甚至连需要穿透 props 的三个文件都逐一列出。
Out of Scope 的实践价值
值得强调的是design.md中的Out of Scope章节——它直接决定了 AI 智能体的行为边界。在cancellation-reason-requirement案例中,明确排除了"改期原因配置(独立功能)""自定义原因下拉选项""原因分析与报表"。对应地,该功能的 CLAUDE.md 在 "Don't" 清单中写有"不要修改改期原因行为(超出范围)"。这种"设计文档限定范围 → CLAUDE.md 强化禁令"的双重约束,是防止 AI 在实现过程中顺手夹带无关改动的关键机制。
会话连续性:跨会话接续开发
AI 会话是有状态的,但状态不能持久保存。Spec-First 的解法是把状态落盘:
当开启一个新的 Claude 会话时,只需说:
"Continue working on {feature}"Claude 会读取该功能的implementation.md,从记录中断处继续工作。implementation.md的 模板 包含Status(not-started / in-progress / complete)、Completed、In Progress、Blocked、Next Steps、Session Notes几个区块。
看cancellation-reason-requirement的 implementation.md,状态已标记为complete,Completed 列表详细记录了 11 项工作,从 "添加CancellationReasonRequirement枚举到 schema.prisma(第 129 行)"、"新增requiresCancellationReason列(第 269 行)"、创建迁移文件,到 UI 下拉框(EventAdvancedTab 第 691-719 行)、服务端校验、props 穿透、动态标签修复等;Next Steps 则列出了剩余验证事项;Session Notes 补充了规划阶段背景(枚举和列在规划期已加入 schema,迁移已创建)。这份文档本身就是一个典型的"进度快照"范本。
文档生成:功能完成后的截图流程
功能进入文档化阶段后,对 Claude 发出:
"Generate docs with screenshots for {feature}"Claude 将依次执行:
- 在浏览器中打开该功能;
- 捕获关键 UI 状态的截图;
- 保存到
specs/{feature}/docs/screenshots/; - 更新
specs/{feature}/docs/README.md。
模板文档 定义了内部文档的结构:Overview、How to Use(Step 1 / Step 2…,每步配一张截图)、Configuration Options 表格、Common Use Cases、FAQ。可以看到cancellation-reason-requirement/docs/README.md已按此结构填写了配置选项的四种取值说明,截图位暂空,等待实现完成后补拍——与implementation.md中 "Status: complete" 但"还需端到端测试"的中间状态完全吻合。
推广到公共文档:从内部 spec 到 Mintlify 文档
内部文档面向开发者,公共文档面向客户。当功能文档准备对外发布时,对 Claude 发出:
"Promote {feature} docs to public"Claude 将:
- 把内容拷贝到
docs/{feature}.mdx(Mintlify 格式); - 将截图移动到
docs/images/{feature}/; - 更新
docs/mint.json导航; - 将语言调整为面向客户的表述(去除内部细节)。
在 cal.diy 仓库中,apps/docs/content/下已有大量.mdx文件(如 docs/content/apps、docs/content/deployments),正是这一"spec 内部文档 → 公共 Mintlify 文档"管线的产物目录。prompts.md的 模板 也将 "Generate Docs with Screenshots" 和 "Promote Docs to Public" 两个流程固化为可复用提示词,还包含 Sync Implementation Status、Generate Tests、Code Review、Continue Feature 等常用任务,形成一套标准化的 AI 操作指令集。
最重要的规则:10 分钟可审查的 PR
Spec-First 流程的最后一条,也是 README 中被称为"The Most Important Rule"的硬性约束:
每个 PR 必须在 10 分钟内可以审查完:
- 最多改动 5-7 个文件(测试文件除外);
- 最多改动 500 行;
- 每次只做一个聚焦的变更。
若改动超出上述规模,必须拆分为多个 PR。这条规则与implementation.md的"小步实现、逐步更新"工作法(SPEC-WORKFLOW.md 中"Implement in small pieces, update implementation.md after each")形成呼应:设计文档保证方向正确,小 PR 保证变更可控,二者共同把 AI 辅助开发的产出约束在可审查、可回滚的安全范围内。
案例深潜:从 spec 到源码的完整落地
为了让读者理解 spec 是如何一步步映射到真实代码的,这里沿着cancellation-reason-requirement的设计,追踪它在仓库中的每一处实现落点。
数据库层:枚举 + 列 + 迁移
设计文档要求新增枚举与列。对应实现位于 packages/prisma/schema.prisma:
enum CancellationReasonRequirement { MANDATORY_BOTH MANDATORY_HOST_ONLY MANDATORY_ATTENDEE_ONLY OPTIONAL_BOTH }EventType模型在第 287 行新增列:requiresCancellationReason CancellationReasonRequirement? @default(MANDATORY_HOST_ONLY),与既有的disableCancelling、disableRescheduling等核心开关并列存放。对应的迁移文件 20260115111819_add_cancellation_reason_require/migration.sql 内容简洁明了:创建CancellationReasonRequirement枚举类型,并为EventType表添加带默认值的列。这正是设计文档中"数据库变更"章节的直接产物。
决策层:为什么用列而不是 metadata JSON
decisions.md记录了 ADR-001:在"新增枚举数据库列"与"metadata JSON 字段"之间,最终选择前者。理由包括:这是核心预订流程设置(与disableCancelling、requiresConfirmation同级);数据库层类型安全;取消校验逻辑中查询更干净;与同类设置存储方式保持一致。同时记录了代价:需要数据库迁移。这份 ADR 展示了decisions.md的典型用法——在多个方案之间做选择时,把背景、备选方案、决策与后果固化下来。
校验逻辑层:单一纯函数 + 双端调用
设计文档要求在handleCancelBooking.ts中根据设置与取消者身份做校验。仓库将其抽象为一个独立纯函数 packages/features/bookings/lib/cancellationReason.ts:
export function isCancellationReasonRequired( setting: CancellationReasonRequirement | null | undefined, isHost: boolean ): boolean { const requirement = setting ?? CancellationReasonRequirement.MANDATORY_HOST_ONLY; switch (requirement) { case CancellationReasonRequirement.OPTIONAL_BOTH: return false; case CancellationReasonRequirement.MANDATORY_BOTH: return true; case CancellationReasonRequirement.MANDATORY_HOST_ONLY: return isHost; case CancellationReasonRequirement.MANDATORY_ATTENDEE_ONLY: return !isHost; default: return false; } }该函数体现了几处与设计文档 Edge Cases 的严格对应:setting为null/undefined时回退到MANDATORY_HOST_ONLY(对应"空列值默认行为");OPTIONAL_BOTH与MANDATORY_BOTH不区分身份直接返回固定值。此函数被服务端与客户端双端复用:
- 服务端 handleCancelBooking.ts 先判断取消者是否为 host(
bookingToDelete.userId === userId || bookingToDelete.user.email === cancelledBy),再调用isCancellationReasonRequired,当!platformClientId && !cancellationReason?.trim() && isReasonRequired && !skipCancellationReasonValidation时抛出 400 错误 "Cancellation reason is required"。注意platformClientId与skipCancellationReasonValidation两个豁免条件,正好对应设计文档中"平台用户应遵守设置"与 API 调用方可选跳过校验的两类边界; - 客户端 CancelBooking.tsx 同样先判定
isCancellationUserHost(props.isHost || organizer.email === currentUserEmail),再用同一函数计算isReasonRequired,进而推导missingRequiredReason并禁用取消按钮、在理由为空时阻止提交。
为支撑服务端校验,getBookingToDelete的 select 在 packages/features/bookings/lib/getBookingToDelete.ts 中加入了requiresCancellationReason: true;为支撑页面渲染,getEventTypesFromDB的 select 在 apps/web/lib/booking.ts 加入同名字段,packages/prisma/zod-utils.ts的 eventTypeSelect(第 694 行)也同步引入,供表单 schema 使用。
UI 层:高级设置下拉框与 props 穿透
设计文档要求下拉框放在 Booking Questions 之后、RequiresConfirmationController 之前。实现在 apps/web/modules/event-types/components/tabs/advanced/EventAdvancedTab.tsx:使用 React Hook Form 的Controller,defaultValue取eventType.requiresCancellationReason ?? MANDATORY_HOST_ONLY(再次落实空值回退),四个选项分别映射mandatory_for_both、mandatory_for_host_only、mandatory_for_attendee_only、optional_for_both翻译键(定义于 packages/i18n/locales/en/common.json),标题文案为 "Require cancellation reason" / "Ask for a reason when someone cancels a booking"。注意代码中的!isPlatform条件,与设计文档"平台用户应遵守设置"的边界表述相呼应——平台版事件类型暂不暴露该 UI。
取值从页面到对话框的穿透链也完全符合 design.md 的规划:bookings-single-view.tsx 将eventType.requiresCancellationReason传给视图组件,CancelBookingDialog.tsx 声明requiresCancellationReason?: CancellationReasonRequirement | null并透传给CancelBooking。
从这份案例可以清楚看到 spec 管线的价值:枚举与列的默认值、空值回退行为、豁免条件、UI 插入位置、翻译键命名,全部在动手前就已由 design.md 精确锁定,implementation.md 则逐条追踪落地,最终每一行实现都能回溯到设计文档中的某句话。
可复用的实践要点
- 模板先行:所有新功能从
specs/_templates/复制骨架,保证目录结构与文档章节的一致性,避免每个开发者各写一套; - 单一事实来源:
design.md是所有实现的唯一依据,"Don't add features not in design.md" 同时出现在模板和案例的 CLAUDE.md 中; - 进度落盘:每完成一小块就更新
implementation.md,这是跨会话接续开发的唯一凭据; - 决策留痕:任何多方案取舍都记入
decisions.md,ADR 编号递增,方便未来追溯; - 文档双轨:内部
docs/README.md带截图面向开发,公开docs/{feature}.mdx面向客户,通过 "Promote" 提示词自动转换; - 小步提交:5-7 个文件、500 行、单一焦点,把 AI 生成的大改动强制拆碎,保证 10 分钟可审查。
这套流程本质上把"AI 智能体当作一名远程协作者"来管理:给它设计文档作为任务书,给它 implementation.md 作为工作日志,给它 CLAUDE.md 作为行为守则,再用小 PR 规则兜底审查质量。对于任何计划用 Claude 等智能体长期维护复杂代码库的团队,specs/这套目录与提示词体系都是一份可以直接借鉴的工程实践范本。
【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考