
get-shit-done 修复 #3599 深度解析roadmap get-phase 如何正确命中 project-code 前缀阶段 ID【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-done本文基于仓库中的变更记录 .changeset/3599-roadmap-get-phase-project-code-prefix.md讲清 get-shit-doneGSD 元提示与规格驱动开发系统中roadmap get-phase子命令的一个正则匹配缺陷及其修复方案当阶段 ID 带有项目代号前缀如PROJ-42时命令曾查不到 ROADMAP.md 中真实的### Phase PROJ-42:标题。读完本文你能理解该命令背后的“两遍查找two-pass”设计、phaseMarkdownRegexSourceExact()新辅助函数的职责边界以及它与历史修复 #3537、#2391 之间如何互相保约而不互相破坏。背景GSD 的阶段 ID 体系与 ROADMAP 散文解析get-shit-done 以.planning/ROADMAP.md作为项目规划的单一事实来源阶段以 Markdown 标题形式书写形如### Phase 2.7: 功能名。解析阶段标题的入口是 get-shit-done/bin/lib/roadmap.cjs 中的cmdRoadmapGetPhase它把「调用方传入的阶段 ID」转换成一段正则片段再拼进#{2,4}\s*Phase\s片段:\s*([^\n])这样的标题匹配模式中。问题在于阶段 ID 存在多种合法形态。从 核心库 的normalizePhaseName()可以看出系统支持的输入标准数字阶段1、01、12A、12.1整数部分可补零、可带字母后缀、可带小數位带 project-code 前缀的目录名形态CK-01-nameproject_code配置会出现在阶段目录名中自定义阶段 IDPROJ-42、AUTH-101normalizePhaseName对这类非数字 ID 原样返回。其中两类形态之间天然存在「错位」补零错位#3537 / #2391skill 或 CLI 解析后传入补零形态02.7而人类手写的 ROADMAP 惯用未补零的### Phase 2.7:。为此core.cjs 提供了phaseMarkdownRegexSource()先剥掉 project-code 前缀再把整数部分的 leading zero 去掉、重新以0*前缀输出使片段同时匹配2.7与02.7。其源码注释明确记录了动机function phaseMarkdownRegexSource(phaseNum) { const stripped String(phaseNum).replace(/^[A-Z]{1,6}-(?\d)/i, ); const match stripped.match(/^0*(\d)([A-Z])?((?:\.\d)*)$/i); if (!match) return escapeRegex(phaseNum); const integer match[1].replace(/^0/, ) || 0; const letter match[2] ? escapeRegex(match[2]) : ; const decimal match[3] ? escapeRegex(match[3]) : ; return 0*${escapeRegex(integer)}${letter}${decimal}; }前缀错位本次 #3599PROJ-42这类 ID 本身就是一个自定义阶段 IDROADMAP 里真实写的是### Phase PROJ-42:。但上面的phaseMarkdownRegexSource()会先剥掉PROJ-前缀再生成0*42——于是roadmap get-phase PROJ-42实际生成的正则是0*42。缺陷机理剥前缀后的正则丢失了「精确身份」用0*42这段片段去匹配 ROADMAP 时会出现两种坏结果回归测试文件 tests/bug-3599-roadmap-get-phase-project-code-prefix.test.cjs 头部注释对此有完整描述若 ROADMAP 里只有### Phase PROJ-42:而没有### Phase 42:0*42匹配不到任何标题命令返回found: false——变更记录里描述的行为就是「roadmap get-phase PROJ-42now matches### Phase PROJ-42:instead of returningfound: false」若 ROADMAP 里恰好还有一个同尾数的裸数字标题### Phase 42:0*42会交叉命中它把属于42的阶段内容错配给PROJ-42查询。还有一个微妙的实现陷阱phaseMarkdownRegexSource()的 docstring 承诺「对非数字 ID 回退到escapeRegex(phaseNum)」但剥前缀之后PROJ-42变成了纯数字42if (!match)这个回退分支对项目代号前缀 数字的 ID 而言永远不可达——承诺的 fallback 恰好覆盖不到最需要它的输入。修复方案新增精确形态辅助函数 调用点两遍查找修复分两层且刻意把「选哪种正则」的决策放在调用点而非正则内部。第一层phaseMarkdownRegexSourceExact()——只回答「有没有前缀」新辅助函数位于 core.cjs实现极短function phaseMarkdownRegexSourceExact(phaseNum) { const raw String(phaseNum); if (!/^[A-Z]{1,6}-(?\d)/i.test(raw)) return null; return escapeRegex(raw); }语义输入形如PROJ-421~6 位大写字母 连字符 至少一位数字开头时返回整体转义后的精确片段PROJ\-42用于匹配### Phase PROJ-42:这一保留前缀的标题输入不带前缀如42、02.7、12A.1时返回null表示调用方只需要既有的phaseMarkdownRegexSource()数值形态即可。这样把「是否需要精确前缀匹配」的判定与「数值补零容忍」的片段构造解耦phaseMarkdownRegexSource()的 #3537 契约保持原样所有既有调用方零改动。第二层cmdRoadmapGetPhase的两遍查找修复后的命令实现见 roadmap.cjs。查找顺序是精确前缀遍若phaseMarkdownRegexSourceExact(phaseNum)非 null先用精确片段搜索「当前里程碑切片」extractCurrentMilestone产出的内容未命中再搜索剥离已交付里程碑后的全量 ROADMAPstripShippedMilestones补零容忍遍#3537只有精确遍落空才用phaseMarkdownRegexSource(phaseNum)生成0*42形态片段按同样的「当前里程碑优先、全量兜底」策略搜索。源码中的注释解释了为什么不在正则内部用「PROJ\-42|0*42」这样的选择式一步完成Doing this at the call site (instead of inside phaseMarkdownRegexSource) avoids the alternation-order ambiguity where a bare### Phase 42:heading in the same document would intercept the match for aPROJ-42query.即选择式正则的匹配顺序会让同文档中的裸### Phase 42:标题拦截掉PROJ-42查询两遍查找则保证「先精确、后宽松」的优先级在逻辑上成立同时不破坏CK-01目录名形态映射到### Phase 1:散文的既有契约。命中之后的输出结构无论哪一遍命中最终都交给 searchPhaseInContent() 解析出结构化结果匹配######级标题并截取到下一个阶段标题之间的区块提取**Goal:**、**Mode:**统一小写后规范化与**Success Criteria**编号列表返回{ found: true, phase_number: phaseNum, // 标题中「如所写」的规范 token phase_name, // 标题冒号后的名称 goal, mode, success_criteria, // 字符串数组 section, // 完整章节原文 }另有两条兜底路径标题缺失但概要清单里存在- [ ] **Phase X:** ...时返回error: malformed_roadmap提示 ROADMAP 需要「清单 详情节」双格式ROADMAP.md 不存在时返回{ found: false, error: ROADMAP.md not found }。加--json参数即输出上述 JSON payload这正是回归测试断言的字段found、phase_name、goal。SDK 侧的镜像实现该仓库同时维护一套 TypeScript 的 SDK 查询层roadmap.get-phase在 sdk/src/query/roadmap.ts 中作为roadmapGetPhase处理器存在是cmdRoadmapGetPhase的移植版本源码注释标注 Port of cmdRoadmapGetPhase from roadmap.cjs lines 75-113。#3599 的修复在 SDK 侧保持了逐行对等parityphaseMarkdownRegexSourceExact() 与 core.cjs 版本逐语句一致注释直接写明「parity with core.cjs phaseMarkdownRegexSourceExact, lines 691-708」roadmapGetPhase内部同样先计算exactEscaped精确前缀片段与numericEscapedphaseMarkdownRegexSource片段先试精确片段、落空再走补零容忍片段且都遵循「当前里程碑切片 → 全量内容」的两级搜索。另外CLI 侧的子命令路由在 roadmap-command-router.cjs 中把get-phase转发到 SDK 处理器roadmap.get-phase——也就是说路由层与底层解析层的两条实现在同一个语义下工作修复对两条链路同时生效。回归测试四个用例钉死四种行为tests/bug-3599-roadmap-get-phase-project-code-prefix.test.cjs 基于node:test通过runGsdTools()在临时项目中真实执行roadmap get-phase ... --json四个用例分别覆盖正向命中ROADMAP 只含### Phase PROJ-42: Custom phase查询PROJ-42必须found: true且phase_name/goal与该标题一致反向不交叉查询裸42时绝不能命中### Phase PROJ-42:防止修成「双向交叉匹配」#3537 契约保持查询CK-01project-code 前缀 补零时仍须解析到未补零的### Phase 1:散文——证明新修复没有弄丢旧契约双形态共存消歧同一 ROADMAP 同时存在### Phase 42:与### Phase PROJ-42:时两个查询各自命中自己那条。这四个用例合起来构成一个完整的「不回归、不交叉、能消歧」矩阵在本地可用node --test tests/bug-3599-roadmap-get-phase-project-code-prefix.test.cjs直接复验。修复的定位与适用前提契约关系#3599 是在 #3537phaseMarkdownRegexSource()补齐到全部 8 个正则构建点见 变更记录的基础上把「前缀保留」这一维度从正则内部剥离到调用点两条契约前缀 ID 精确匹配 / 目录补零形态映射数字散文各自独立可验证适用前提该修复在仓库中以待发布的 changeset 形式存在type: Fixed, issue: 3599适用对象是配置了project_code、ROADMAP 中使用### Phase PROJ-42:这类带前缀标题的项目对纯数字阶段与无 project-code 的项目行为不变phaseMarkdownRegexSourceExact()对无输入返回 null直接走原路径相关变更同批 changeset 中的 3600-milestone-phase-filter-project-code.md 处理的是 milestone 阶段过滤侧的同类前缀问题与本篇的get-phase查找侧互为补充。小结#3599 修复看似只涉及一个正则片段实际示范了一套处理「多形态 ID 命名空间」的可复用模式用一个廉价的判别函数phaseMarkdownRegexSourceExact()仅判断前缀存在性把「精确形态」与「宽松形态」的正则构造拆开再由调用点按「先精确、后宽松」的顺序做两遍查找从而同时满足新契约PROJ-42→### Phase PROJ-42:、旧契约CK-01→### Phase 1:和消歧要求42与PROJ-42互不串扰并在 CLI 与 SDK 两套实现中保持逐行对等。对于同样以 Markdown 散文承载结构化状态ROADMAP、STATE 等的规格驱动工具链这种「判别 两遍查找 双向回归用例」的做法值得直接借鉴。【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-done创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考