首页
/ GSD 里程碑阶段过滤器修复解析:让 CK-01 前缀阶段目录正确计入数字 ROADMAP 里程碑

GSD 里程碑阶段过滤器修复解析:让 CK-01 前缀阶段目录正确计入数字 ROADMAP 里程碑

2026-09-04 10:36:15作者:彭桢灵Jeremy

本篇基于 get-shit-done(GSD)仓库中的 changeset 文档 .changeset/3600-milestone-phase-filter-project-code.md,完整解析 #3600 缺陷的产生机理与修复方案:当项目启用了 project_code(阶段目录形如 CK-01-name)时,init.new-milestone 等命令的阶段计数为什么漏掉这些目录、getMilestonePhaseFilter / isDirInMilestone 的三级匹配逻辑如何演进,以及 CJS 运行时与 SDK 双实现如何保持一致、测试如何锁死该行为契约。

背景:GSD 的里程碑、ROADMAP 与阶段目录命名

GSD 是一个面向 Claude Code 的轻量级元提示(meta-prompting)、上下文工程与规格驱动开发系统。它以 .planning/ 目录下的结构化文件作为项目状态的单一事实来源,其中最关键的两个文件是:

  • ROADMAP.md:以 ## Current Milestone: vX.Y.Z - 名称 标记当前里程碑,其下用 ### Phase N: 阶段名 这样的标题列出里程碑内的各个阶段;
  • phases/ 目录:每个阶段对应一个子目录,目录名通常为 NN-名称(如 01-foundation)。

当项目配置了 project_code(例如 CK)后,阶段目录会带上项目代码前缀,形如 CK-01-foundationCK-02-api。这个前缀的识别规则集中在 normalizePhaseName 中:

function normalizePhaseName(phase) {
  const str = String(phase);
  // Strip optional project_code prefix (e.g., 'CK-01' → '01')
  const stripped = str.replace(/^[A-Z]{1,6}-(?=\d)/, '');
  // Standard numeric phases: 1, 01, 12A, 12.1
  const match = stripped.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
  ...
}

即:前缀形态为 ^[A-Z]{1,6}-(?=\d)(1~6 位大写字母 + 连字符,且紧跟数字),剥离后若剩余部分是数字阶段号(支持零填充、字母后缀如 12A、小数阶段号如 12.1),则归一化为补零两位的数字形式;否则视为自定义阶段 ID 原样返回。#3600 的修复正是复用了这一"权威"前缀识别规则,避免两套正则漂移。

缺陷表现:CK-01-name 目录被里程碑过滤器跳过

changeset 文档记录的核心问题是:getMilestonePhaseFilter 之前会把 .planning/phases/CK-01-name 这类目录直接跳过——尽管当前里程碑的 ROADMAP 里明明写着 ### Phase 1:

这个过滤器是 GSD 各里程碑相关命令判断"某阶段目录是否属于当前里程碑"的唯一入口。它读取 ROADMAP.md,抽取当前里程碑(支持 versionOverride 指定版本段落),用正则 #{2,4}\s*Phase\s+([\w][\w.-]*)\s*: 收集该里程碑下所有阶段号,归一化后放入一个 Set,然后返回一个谓词函数 isDirInMilestone(dirName),供调用方过滤 phases/ 下的子目录。

修复前 isDirInMilestone 只有两级匹配(对照 CJS 实现):

  1. 数字匹配dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/)——要求目录名以数字开头(允许零填充)。CK-01-name 以字母 C 开头,此级必然失败;
  2. 自定义 ID 匹配dirName.match(/^([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*)/) 取出整段 CK-01-name,然后拿完整名称去和里程碑 Set 中的裸 token(如 1)比对,必然不相等。

两级都不中,CK-01-name 就被判定"不属于当前里程碑"。于是所有共享该过滤器的命令全部受影响:

  • init.new-milestone:新建里程碑时统计上一里程碑遗留的阶段目录数(phase_dir_count),带前缀目录被漏计;
  • phase complete:按里程碑口径核对阶段完成情况;
  • verify-work:验证工作产物时圈定当前里程碑的阶段范围;
  • validate-health:健康检查时统计阶段/计划数量。

SDK 侧的状态构建路径 中也能看到这个过滤器的实际用法:buildStateFrontmatter 先取 getMilestonePhaseFilter 的谓词,readdirphases/ 全部子目录后 .filter(isDirInMilestone),再逐目录统计 plan/summary 数量生成 STATE.md 的进度 frontmatter——目录漏判会直接导致进度失真。

#3600 修复:strip-and-retry 第三级匹配

修复没有改前两级逻辑,而是在其后追加了第三级 strip-and-retry 路径(见 core.cjs 中 isDirInMilestoneSDK 孪生实现):

// #3600: project-code-prefixed directory (`CK-01-name`) against a
// numeric ROADMAP heading (`### Phase 1:`). Strip the same prefix
// shape `normalizePhaseName` recognises (`^[A-Z]{1,6}-(?=\d)`) and
// retry the numeric match.
const stripped = dirName.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
if (stripped !== dirName) {
  const sm = stripped.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
  if (sm && normalized.has(sm[1].toLowerCase())) return true;
}

三个设计要点值得注意:

  1. 复用 normalizePhaseName 的前缀正则。剥离用的 ^[A-Z]{1,6}-(?=\d) 与全局阶段名归一化规则完全一致,保证"目录名 → 阶段号"的映射在整套系统里只有一个权威定义;
  2. 放在自定义 ID 匹配之后执行。源码注释明确说明:如果 ROADMAP 使用 ### Phase PROJ-42: 这种带前缀标题,PROJ-42 目录仍应优先走既有的自定义 ID 路径命中;strip-and-retry 只在里程碑以裸数字形式(Phase 1:)为键时才兜底生效,不会引入误判;
  3. stripped !== dirName 守卫。只有确实剥离成功(即目录名符合前缀形态)才进入重试分支,无前缀目录的匹配行为与修复前完全一致,零回归风险。

谓词函数同时保留了两个元信息属性:isDirInMilestone.phaseCount(ROADMAP 中该里程碑声明的阶段数)与 isDirInMilestone.missingExplicitVersion(显式版本覆盖未命中时的标记),供上层命令区分"目录缺失"与"ROADMAP 未声明"两种语义。

双实现落地与调用方清单

changeset 文档强调该修复同时落入 CJS 运行时与 SDK 孪生实现

从源码结构看,getMilestonePhaseFilter 的调用方分布在 CJS 侧的 milestone.cjsinit.cjsphase.cjsstate.cjscommands.cjsuat.cjs,以及 SDK 侧的 phase-lifecycle.tsinit.tsstate-mutation.tsprogress.tsuat.ts——即 changeset 所列的 init.new-milestonephase completeverify-workvalidate-health 等命令共用这一份判定逻辑,修复一处谓词,全部调用方同时受益,无需逐个命令打补丁。

测试契约:正向计数、反向排除与跨 issue 兼容性

该行为的回归测试集中在 tests/milestone-archive.test.cjs,构成完整的正反契约:

test('init.new-milestone counts CK-NN-name dirs against numeric `Phase N:` headings', () => {
  writeConfig(tmpDir, { project_code: 'CK' });
  writeState(tmpDir, 'v1.0.0');
  writeRoadmap(tmpDir, [
    '# Roadmap', '',
    '## Current Milestone: v1.0.0 - Test', '',
    '### Phase 1: Discovery', '**Goal:** GoalOne', '',
    '### Phase 2: Build', '**Goal:** GoalTwo', '',
  ].join('\n'));
  ensurePhaseDir(tmpDir, 'CK-01-discovery');
  ensurePhaseDir(tmpDir, 'CK-02-build');

  const r = runGsdTools(['init', 'new-milestone', '--json'], tmpDir);
  assert.strictEqual(JSON.parse(r.output).phase_dir_count, 2, ...);
});

正向用例验证了 CK-01-discoveryCK-02-build 两个带前缀目录对 Phase 1: / Phase 2: 数字标题全部命中(phase_dir_count === 2);反向用例则确保边界不被放大——CK-99-backlogCK-100-future 这类目录号不在当前里程碑中的带前缀目录必须被排除(计数保持为 1),防止 strip-and-retry 退化成"凡是带前缀就算数"。

此外还有两组相邻测试守护了修复不破坏既有契约:

  • tests/phase.test.cjsfind-phase 01 能解析出 CK-01-foundation 前缀目录并抽取数字阶段号 01phases list 对带前缀目录按数字序正确排序(CK-01 < CK-02 < CK-03);
  • tests/bug-3599-roadmap-get-phase-project-code-prefix.test.cjs:守护 #3537 契约——CK-01 目录形态必须能解析到 ### Phase 1: 散文标题,同时裸数字 42 不得误匹配 ### Phase PROJ-42:,即"带前缀 ID 精确命中、跨前缀不串号"。

小结:如何验证与排查同类问题

如果在你使用 GSD 的项目中遇到"里程碑阶段数/计划数统计偏少",可以按以下路径自查:

  1. 确认 STATE.md / ROADMAP.md 的里程碑标题与 phases/ 目录命名形态(是否启用 project_code 前缀、ROADMAP 用的是数字标题还是自定义 ID 标题);
  2. 运行 init new-milestone --json 观察 phase_dir_count,对照 tests/milestone-archive.test.cjs 的构造方式手工搭建最小 .planning 结构复现;
  3. 核对本地版本是否包含 #3600 修复:检查 get-shit-done/bin/lib/core.cjssdk/src/query/state.tsisDirInMilestone 是否含有标注 #3600 的 strip-and-retry 分支。

从源码结构看,这类"目录名形态 vs ROADMAP 散文形态"的桥接问题在 GSD 中不止一处(如 #3537 的零填充容忍、#3599 的项目代码前缀),但它们的解法高度一致:把形态归一化收敛到少数几个共享的正则(前缀识别 ^[A-Z]{1,6}-(?=\d)、数字形态 ^0*(\d+[A-Za-z]?(?:\.\d+)*)),再在每个判定入口做"先精确、后兜底"的多级匹配,并以正反向测试锁死行为边界。#3600 正是这一工程模式的一次典型落地。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
981
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384