☰
使用 /openspec-apply 落地已批准的 OpenSpec 变更:Midway 仓库的规范驱动开发实施指南
2026/9/27 8:18:54 网站建设 项目流程
  • 后端
  • 微服务
  • 云原生

【免费下载链接】midway

🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈

项目地址:https://gitcode.com/gh_mirrors/mi/midway
点击查看免费下载

本文围绕 Midway 仓库内置的 Cursor 命令 .cursor/commands/openspec-apply.md,完整讲解"将已批准的 OpenSpec 提案变为真实代码、并保持任务清单同步"的标准流程。读者将掌握openspec-apply的执行步骤、守护规则、配套校验命令,并能结合仓库中真实的add-functional-web-routing-api变更案例,理解从proposal.md到packages/core/src/functional/api.ts实现的完整落地链路。

一、openspec-apply 是什么:一个规范驱动开发的落地命令

在 Midway 仓库的.cursor/commands/目录下,有三个相互配套的 OpenSpec 命令,openspec-apply是其中负责"实现阶段"的入口:

命令文件职责阶段
openspec-proposal.md起草变更提案并严格校验(Stage 1)
openspec-apply.md实现已批准的变更、同步任务清单(Stage 2)
openspec-archive.md变更部署后归档、更新 specs(Stage 3)

openspec-apply.md本身采用 Cursor 命令的标准格式:YAML frontmatter 声明命令元数据(name: /openspec-apply、id、category: OpenSpec、description),正文被包裹在<!-- OPENSPEC:START -->与<!-- OPENSPEC:END -->标记中,便于工具链识别。命令正文由三部分组成:Guardrails(守护规则)、Steps(执行步骤)与Reference(参考命令)。

该命令的核心定位是:只做实现,不做设计。它要求先读取变更目录下的提案与任务文档确认范围,再按任务清单逐项、最小化地修改代码,全部完成后才把清单中的每一项勾选为- [x],确保tasks.md始终反映真实进度。

二、前置背景:OpenSpec 三阶段工作流

openspec-apply不是孤立存在的,它属于 OpenSpec 规范驱动开发工作流的中间一环。仓库根目录的 openspec/AGENTS.md 对完整流程给出了权威定义:

  1. Stage 1:创建变更(Creating Changes)——当需要新增功能、破坏性变更、架构调整、性能优化或安全模式更新时,创建proposal.md、tasks.md、可选的design.md以及每个受影响 capability 的 delta specs。此阶段"不写任何代码"(openspec-proposal 命令明确要求Do not write any code during the proposal stage)。
  2. Stage 2:实现变更(Implementing Changes)——提案获批后,按tasks.md顺序实现,这就是/openspec-apply的战场。
  3. Stage 3:归档变更(Archiving Changes)——部署后把changes/[name]/移入changes/archive/YYYY-MM-DD-[name]/,并同步更新specs/。

三个阶段的目录状态可概括为:changes/是"提案中(未构建)",specs/是"已构建并部署",archive/是"已完成"。Specs 是真相,Changes 是提案,二者必须保持同步——这正是openspec-apply存在的意义。

三、/openspec-apply 的守护规则与执行步骤

3.1 Guardrails:实现阶段的行为底线

命令开篇给出了两条硬性守护规则,与 openspec/AGENTS.md 中"Simplicity First"最佳实践一脉相承:

  • 优先做直白、最小化的实现,只有被明确要求或确有需要时才增加复杂度。仓库侧的配套建议是:默认不超过 100 行新代码、单文件实现直到被证明不够用、避免无明确理由引入框架、选择经过验证的成熟模式。
  • 变更范围严格收敛到被请求的结果,不得顺手重构无关模块。
  • 需要更多 OpenSpec 约定时,查阅openspec/目录内的AGENTS.md(若看不到该文件,可运行ls openspec或openspec update生成)。

3.2 Steps:五步落地流程

/openspec-apply要求将以下步骤作为 TODO 跟踪并逐一完成:

  1. 读文档确认范围:读取changes/<id>/proposal.md、design.md(若存在)与tasks.md,确认变更范围和验收标准。
  2. 按序实现:逐个完成任务清单中的条目,编辑保持最小化,聚焦于被请求的变更。
  3. 确认完成再更新状态:更新状态前必须确保tasks.md中每一项都已完成——不要"先勾选、后补实现"。
  4. 同步清单:所有工作结束后更新 checklist,使每个任务标记为- [x]并反映真实情况。
  5. 按需补充上下文:需要额外上下文时,引用openspec list或openspec show <item>。

3.3 Reference:实现过程中的辅助命令

命令同时给出两个参考命令:

  • openspec show <id> --json --deltas-only:实现过程中需要从提案补充上下文时使用,可精确输出该变更的 delta 定义,便于核对需求措辞。
  • openspec/AGENTS.md 中还提供了完整的 CLI 速查:openspec list(列出活动变更)、openspec list --specs(列出既有 capability)、openspec show [item](查看详情)、openspec validate [item](校验)、openspec archive <change-id> [--yes|-y](归档),以及调试组合openspec show [change] --json --deltas-only与openspec validate [change] --strict --no-interactive。

四、apply 阶段读取的三份关键文档

/openspec-apply第 1 步要求读取的文档各有分工,仓库中的真实变更 openspec/changes/add-functional-web-routing-api/ 是理解它们的最佳标本:

4.1 proposal.md:为什么改、改什么、影响什么

proposal.md 采用固定三段式结构:

  • Why:说明问题与机会。该提案指出 Midway 的 Web 入口仍以@Controller、@Get、@Post等类/方法装饰器为核心,在 React/Vue 前端工程化场景中,用户更习惯函数式声明与跨运行时共享模块,因此需要与defineConfiguration对齐的 Functional 路由形态。
  • What Changes:逐条列出变更内容,破坏性变更需标注BREAKING。该提案的核心是新增functional-web-routingcapability,首选defineApi('/prefix', api => ({ ... }))链式 DSL,并导出到@midwayjs/core/functional。
  • Impact:列出受影响的 specs 与代码、兼容性说明。该提案明确"向后兼容,装饰器 API 保持不变,Functional API 作为增量能力引入",并给出预期的实施路径(packages/core/src/functional/*、webRouterService.ts、packages/react/*等)。

4.2 tasks.md:可勾选、可验证的实施清单

tasks.md 把工作拆成"小步、可验证"的条目,例如1.1 冻结按协议分别导出的入口签名与命名(HTTP: defineApi,WS: defineWebSocketApi,等)、2.1 定义 FunctionalControllerOptions、FunctionalRouteDefinition、FunctionalRouteOptions 类型。清单按阶段分组(API 设计冻结、路由定义协议与类型、与核心路由系统对齐、用户文档与示例、验证与验收),每组内部有序号化子项,方便逐项跟踪。该文件的全部条目均已勾选- [x],正是/openspec-apply第 4 步"让清单反映现实"的产出形态。

4.3 design.md:技术决策的记录(按需创建)

design.md 不是必须文件,openspec/AGENTS.md 给出了创建判据:跨模块/新架构模式、新增外部依赖或重大数据模型变更、安全/性能/迁移复杂度、或需要在编码前澄清歧义。它采用Context → Goals/Non-Goals → Decisions → Risks/Trade-offs → Migration Plan → Open Questions的最小骨架。

该提案的 design.md 给出了极具参考价值的架构分层(Definition Layer → Compile Layer → Runtime Adapter Layer → Service Bridge Layer)和装饰器演进矩阵:

现有装饰器族当前代表装饰器Functional 草案客户端草案
Web HTTP@Controller+@Get/@Post/...defineApi(HTTP)httpApiClient(fetch/axios)
WebSocket@WSController+@OnWSMessagedefineWebSocketApiwsApiClient
Socket.IO@WSController+ socket 事件defineSocketIOApisocketIoApiClient
gRPC / 微服务@Provider/@Consumer、@KafkaListener、@RabbitMQListenerdefineRpcApi/defineMessageApigrpcApiClient/messageApiClient
Serverless Trigger@ServerlessTriggerdefineServerlessApi(后续阶段)functionInvokeClient(后续阶段)
Task / Queue@Queue、@TaskLocal、@ScheduledefineTaskApitaskClient(调度/入队)

矩阵明确:不采用defineApi({ protocol })统一入口,而是按协议分别导出对应 define API,演进维度与现有装饰器族一一对应。

五、实例纵深:defineApi 在 core 中的真实实现

规范文本定义的是"用户怎么写",而 packages/core/src/functional/api.ts 展示的是/openspec-apply落地后的"运行时怎么实现"。读这份源码能验证提案中"复用现有装饰器元数据、不引入平行元数据体系"的承诺。

5.1 链式 DSL 的实现机制

defineApi(prefix, factory, controllerOptions?)接收前缀与工厂函数,工厂内暴露get/post/put/delete/patch/options/head/all八个方法(默认 path 均为/),每个方法返回一个RouteBuilder。RouteBuilder通过input()/output()/middleware()/meta()/handle()链式累积定义,其中handle(fn)是终止操作,调用__build()返回完整的FunctionalRouteDefinition(包含method、path、options、handle)。若未调用.handle(fn),__build()会抛出 "Functional route is missing handler" 错误——这是类型层面之外的另一道运行时防线。

5.2 路由注册:完全复用装饰器协议

实现的关键在于defineApi内部把函数式声明转换成等价的类装饰器声明:

  1. createNamedFunctionalController根据 prefix 与路由名生成一个内部匿名类(类名形如FunctionalApi_<prefix>_<hash>,用 sha1 哈希保证唯一性);
  2. 对每条路由,通过Object.defineProperty在类原型上定义 handler 方法;
  3. 对该方法调用RequestMapping({ path, requestMethod, routerName, middleware, summary, description, ignoreGlobalPrefix })——即复用了@Get/@Post等装饰器背后的同一元数据定义函数;
  4. 对生成的类调用Controller(prefix, controllerOptions),同样复用@Controller的元数据收集协议;
  5. 最终通过DecoratorManager.saveModule(CONTROLLER_KEY, FunctionalApiController)与MetadataManager.defineMetadata(FUNCTIONAL_API_CONTROLLER_KEY, true, ...)把生成的 controller 登记进统一路由收集流程。

从源码结构可以推断:functional 路由与装饰器路由在底层走的是同一条收集管道,因此 spec.md 中"混用不引入平行元数据体系""统一冲突检测与排序"等需求得以自然成立。FUNCTIONAL_API_MODULE_META_KEY('__midwayApiMeta')与FUNCTIONAL_API_CONTROLLER_CLASS_KEY('__midwayApiControllerClass')定义在 packages/core/src/functional/constants.ts,用于在返回的路由对象上携带 controller 级元信息(prefix、ignoreGlobalPrefix、version、versionType、versionPrefix)。

5.3 schema 校验与 IoC:两个运行时细节

  • 输入输出校验:getInputFromContext从 ctx 提取params/query/body/headers,validateInput与runSchemaValidation按safeParseAsync → safeParse → parseAsync → parse的优先级调用 schema(兼容 zod 等校验库),校验失败统一抛出MidwayCommonError,错误信息含Functional API input.params validation failed之类的定位标签。这印证了 spec 中"非法输入会触发统一的校验失败行为"。
  • hooks 风格 IoC:packages/core/src/functional/hooks.ts 提供useContext、useLogger、useInject、useInjectSync、useConfig、useApp、useMainApp、useInjectClient、useInjectDataSource等函数。其中useInject(identifier, args?)优先从请求级requestContext取实例,无请求上下文时回退到主应用的应用上下文——这就是提案中"IoC 使用体验保持连续:使用 useInject(hooks 风格)"的落地。

5.4 最小可运行样例

仓库提供了纯函数式服务的可运行样例 samples/functional-api-service,其中 src/api/health.api.ts 用defineApi('/health', api => ({ ... }))定义了ping(GET)与echo(POST)两个路由,并通过.meta({ routerName: 'healthPing' })指定路由名;src/configuration.ts 则展示了注册方式:使用defineConfiguration搭配ESModuleFileDetector显式发现 API 模块,不需要web.apis嵌套注册——正是 spec 中"直觉化注册"需求的实证。

六、任务清单同步纪律:为什么先完成、后勾选

/openspec-apply在步骤 3、4 中反复强调"完成确认先于状态更新":只有tasks.md中每一项都真正完成,才允许把条目改为- [x]。这是一条防止"虚假进度"的工程纪律,具体落地建议来自 openspec/AGENTS.md:

  • 按顺序实现任务,保持编辑最小化;
  • 每个任务都应是"小步、可验证"的,能产出用户可见的进展;
  • 更新清单应在所有工作完成之后统一进行,使清单整体反映现实;
  • 若发现上下文缺失(如某个 spec 语义不清),先读project.md、检查相关 specs、浏览近期归档,仍不清楚再提问,而不是在清单上打勾蒙混。

七、实现过程中的校验与调试命令

/openspec-apply与配套工作流中最常被引用的命令集中在 openspec/AGENTS.md 的 CLI 速查中:

# 查看当前上下文 openspec list # 列出活动变更 openspec list --specs # 列出既有 capability 规范 openspec show [item] # 查看变更或规范详情 # 实现阶段调试(Reference 中推荐) openspec show <id> --json --deltas-only # 仅输出 delta 定义,核对需求措辞 openspec validate <id> --strict --no-interactive # 严格校验变更 # 全文检索需求与场景(用 ripgrep) rg -n "Requirement:|Scenario:" openspec/specs

关键 flag 语义:--json输出机器可读结果;--strict做全面校验(应始终使用);--no-interactive禁用交互提示(适合自动化);--skip-specs仅用于纯工具型变更的归档;--yes/-y跳过归档确认。调试场景中还可配合openspec show [change] --json | jq '.deltas'检查 delta 解析结果。

八、常见校验失败与恢复策略

实现阶段最容易踩的坑集中在 delta 格式上,openspec/AGENTS.md 给出了明确的排错路径:

  • "Change must have at least one delta":检查changes/[name]/specs/目录是否存在且含.md文件,并确认文件头使用## ADDED Requirements等操作前缀。
  • "Requirement must have at least one scenario":场景必须用四个井号的#### Scenario: 名称格式,禁止用列表项或加粗充当场景标题——格式不严格会导致"静默解析失败"。
  • MODIFIED 的经典陷阱:修改既有需求时必须粘贴完整需求块(标题 + 全部场景)再编辑,否则归档时会把原有细节丢弃;若只是新增关注点而不是修改现有需求,应放在## ADDED Requirements下新增。
  • 恢复顺序:带--strict重跑 → 看 JSON 输出细节 → 核对 spec 文件格式 → 确认场景格式,必要时用openspec show [change] --json --deltas-only定位静默解析问题。

九、三个命令的边界与配合

理解openspec-apply还需要看清它与兄弟命令的边界:

  • /openspec-proposal负责 Stage 1,明确禁止写实现代码,只产出proposal.md、tasks.md、design.md与 spec deltas,并要求openspec validate <id> --strict --no-interactive全部通过后才分享提案;它还要求在动笔前用rg/ls摸清现状、识别含糊点并提问。
  • /openspec-apply只能在提案已批准后使用,只管实现与清单同步。
  • /openspec-archive负责 Stage 3:先用openspec list确认变更 ID(无法唯一确认时宁可不做),再openspec archive <id> --yes执行归档,最后openspec validate --strict --no-interactive复核。

三者环环相扣,共同构成 Midway 仓库从"想法 → 提案 → 实现 → 归档"的完整规范驱动闭环。对仓库贡献者而言,在开始任何实现前,建议先运行openspec list与openspec list --specs检查是否有冲突的活动变更,并阅读 openspec/project.md 了解 Midway 的技术栈、代码风格与测试约束,再调用/openspec-apply进入实现阶段。

  • 后端
  • 微服务
  • 云原生

【免费下载链接】midway

🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈

项目地址:https://gitcode.com/gh_mirrors/mi/midway
点击查看免费下载
上一篇:如何把洛雪音乐助手用成免费的全网音乐播放器
下一篇:innerself高级技巧:如何实现组件连接和状态订阅的终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询