以 Spec 为先的 AI 辅助开发工作流:cal.diy 仓库的 Spec-First Development 实践指南
2026/9/10 3:11:31 网站建设 项目流程

以 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定义了每次功能开发必须遵守的五步流程:

  1. 实现功能之前,先创建一个 spec 目录,写入设计文档;
  2. Claude 在写任何代码之前,先完整阅读设计;
  3. 进度记录在implementation.md,保证会话间的连续性;
  4. 决策记录在decisions.md(即 ADR,架构决策记录),供未来参考;
  5. 功能完成时生成配套文档,附带开发过程中捕获的截图。

从源码结构看,这套流程还得到了仓库根目录 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.mdAGENTS.md内容一致,包含项目上下文(Project Context)、开工前检查清单(Before Starting Work,要求先读 design.md、核对 implementation.md、参考相关目录既有模式)、代码模式(Code Patterns)以及明确的 "Don't" 清单(如"不要添加 design.md 之外的功能""不要跳过测试")。

仓库中已经有一个完整落地的真实案例——specs/cancellation-reason-requirement/,其目录结构严格遵循上述模板:CLAUDE.mddesign.mdimplementation.mddecisions.mdfuture-work.mddocs/一应俱全,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)、CompletedIn ProgressBlockedNext StepsSession 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 将依次执行:

  1. 在浏览器中打开该功能;
  2. 捕获关键 UI 状态的截图;
  3. 保存到specs/{feature}/docs/screenshots/
  4. 更新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 将:

  1. 把内容拷贝到docs/{feature}.mdx(Mintlify 格式);
  2. 将截图移动到docs/images/{feature}/
  3. 更新docs/mint.json导航;
  4. 将语言调整为面向客户的表述(去除内部细节)。

在 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),与既有的disableCancellingdisableRescheduling等核心开关并列存放。对应的迁移文件 20260115111819_add_cancellation_reason_require/migration.sql 内容简洁明了:创建CancellationReasonRequirement枚举类型,并为EventType表添加带默认值的列。这正是设计文档中"数据库变更"章节的直接产物。

决策层:为什么用列而不是 metadata JSON

decisions.md记录了 ADR-001:在"新增枚举数据库列"与"metadata JSON 字段"之间,最终选择前者。理由包括:这是核心预订流程设置(与disableCancellingrequiresConfirmation同级);数据库层类型安全;取消校验逻辑中查询更干净;与同类设置存储方式保持一致。同时记录了代价:需要数据库迁移。这份 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 的严格对应:settingnull/undefined时回退到MANDATORY_HOST_ONLY(对应"空列值默认行为");OPTIONAL_BOTHMANDATORY_BOTH不区分身份直接返回固定值。此函数被服务端与客户端双端复用

  • 服务端 handleCancelBooking.ts 先判断取消者是否为 host(bookingToDelete.userId === userId || bookingToDelete.user.email === cancelledBy),再调用isCancellationReasonRequired,当!platformClientId && !cancellationReason?.trim() && isReasonRequired && !skipCancellationReasonValidation时抛出 400 错误 "Cancellation reason is required"。注意platformClientIdskipCancellationReasonValidation两个豁免条件,正好对应设计文档中"平台用户应遵守设置"与 API 调用方可选跳过校验的两类边界;
  • 客户端 CancelBooking.tsx 同样先判定isCancellationUserHostprops.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 的ControllerdefaultValueeventType.requiresCancellationReason ?? MANDATORY_HOST_ONLY(再次落实空值回退),四个选项分别映射mandatory_for_bothmandatory_for_host_onlymandatory_for_attendee_onlyoptional_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询