☰
gsd-core 里程碑相位过滤修复:project-code 前缀目录(CK-01-name)如何正确匹配数字 Phase 标题
2026/9/26 10:47:32 网站建设 项目流程

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载

导读

本篇文章讲解 gsd-core(Git. Ship. Done - Core)中一次典型的"目录命名约定与标题解析不一致"缺陷修复(issue #3600):当项目启用了project_code(如CK)后,相位目录被命名为CK-01-discovery这样的带前缀形式,而 ROADMAP.md 中的标题仍是数字形式### Phase 1: Discovery,此时里程碑相位过滤器getMilestonePhaseFilter会漏掉所有带前缀目录,导致init.new-milestone统计的phase_dir_count、phase complete的完成比例、verify-work与validate-health的校验结果全部失真。阅读本文后,你将掌握getMilestonePhaseFilter的完整匹配链、strip-and-retry修复路径的底层实现,以及如何通过回归测试复现与验证该行为。

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

1.1 触发场景

gsd-core 的.planning/phases/目录中,相位目录名允许携带项目代码前缀。典型的项目布局如下:

.planning/ ├── config.json # 含 "project_code": "CK" ├── STATE.md # 声明 milestone: v1.0.0 ├── ROADMAP.md # 使用数字 Phase 标题 └── phases/ ├── CK-01-discovery # 项目代码前缀 + 相位号 └── CK-02-build

而 ROADMAP.md 中的标题是数字形式:

## Current Milestone: v1.0.0 - Test ### Phase 1: Discovery **Goal:** GoalOne ### Phase 2: Build **Goal:** GoalTwo

在修复前,getMilestonePhaseFilter面对CK-01-discovery这类目录时判定其"不属于当前里程碑",表现为:

  • init.new-milestone输出的phase_dir_count为 0(实际磁盘上有 2 个相位目录);
  • phase complete/verify-work/validate-health中所有依赖该过滤器的统计(如 completed_phases、percent)随之失真。

1.2 根因:两条既有匹配路径都无法命中

从源码(src/roadmap-parser.cts)可以还原修复前的判断逻辑,失败原因有两处:

  1. 数字匹配器要求目录名以数字开头。numericRe的正则是^0*(\d+[A-Za-z]?(?:\.\d+)*)(无连字符约定下),CK-01-discovery以字母C开头,直接匹配失败;
  2. 自定义 ID 匹配器把完整的前缀名与裸 token 比较。该路径要求目录名以字母开头,并将整个CK-01-discovery与 ROADMAP 中声明的相位 ID(如01)做等值/前缀比较,同样失败——它试图匹配的其实是CK-01这种完整自定义 ID,而不是剥离前缀后的数字。

两条路径都失败后,目录被判定为"不属于当前里程碑",而milestonePhaseNums集合本身非空(标题侧扫描正常),因此不会触发 pass-all 退化,统计数字就"静默地"错了——这正是 ADR-3180 中描述的"well-formed、plausible 但错误"的失败模式。

二、修复方案:strip-and-retry 前缀剥离重试路径

2.1 changeset 记录的核心改动

归档 changeset(3600-milestone-phase-filter-project-code.md)记录的修复要点是:在isDirInMilestone中新增一条strip-and-retry路径——先剥离目录名开头与normalizePhaseName识别规则一致的项目代码前缀,再对剥离结果重试数字匹配。

该修复同时落地于两处实现:

  • CJS 运行时:get-shit-done/bin/lib/core.cjs:isDirInMilestone(changeset 归档时路径,当前仓库的源码真相为 src/roadmap-parser.cts);
  • SDK twin:sdk/src/query/state.ts:isDirInMilestone。

修复对所有getMilestonePhaseFilter调用方生效,包括init.new-milestone、phase complete、verify-work、validate-health。

2.2 前缀剥离规则:与 normalizePhaseName 共用同一正则

前缀识别的权威正则定义在 src/phase-id.cts:

const PROJECT_CODE_PREFIX_STRIP_RE = /^[A-Z][A-Z0-9_]*-(?=\d)/; const PROJECT_CODE_PREFIX_STRIP_RE_I = /^[A-Z][A-Z0-9_]*-(?=\d)/i;

要点:

  • 项目代码以大写字母开头(如PROJ、APP_CODE),前导下划线不是合法的 project code(见 src/phase-id.cts 注释);
  • 前缀后必须紧跟连字符,且连字符后必须是数字((?=\d)前瞻保证);
  • stripProjectCodePrefix(src/phase-id.cts)默认按大小写不敏感剥离。

normalizePhaseName(src/phase-id.cts)在修复前就已使用该规则:先stripProjectCodePrefix(str, false)将CK-01归一化为01,再按数字相位补零。本次修复让isDirInMilestone的目录侧匹配与标题侧归一化采用同一识别规则,消除了两侧约定不一致的缺陷。

三、源码级实现:isDirInMilestone 的完整匹配链

修复后的isDirInMilestone(src/roadmap-parser.cts)按顺序尝试五条匹配路径,任中即返回true:

function isDirInMilestone(dirName: string): boolean { // ① bracket 约定:对照 milestone-qualified ID(GSD.01-01-xxx)匹配 if (headingConvention === 'bracket') { for (const qualified of milestoneQualifiedIds) { if (phaseTokenMatches(dirName, qualified, 'bracket')) return true; } } // ② 数字匹配:dirName 需以数字开头 const m2 = dirName.match(numericRe); if (m2 && normalized.has(normalizePhaseIdSegments(m2[1]).toLowerCase())) return true; // ③ 字母开头的自定义 ID:segment-boundary 前缀匹配(longest-first) if (/^[A-Za-z]/.test(dirName)) { const lowerDir = dirName.toLowerCase(); for (const id of normalizedIdsLongestFirst) { if (lowerDir === id || lowerDir.startsWith(id + '-')) return true; } } // ④ #3600 新增:strip-and-retry —— 剥离 project-code 前缀后重试数字匹配 const stripped = stripProjectCodePrefix(dirName); if (stripped !== dirName) { const sm = stripped.match(numericRe); if (sm && normalized.has(normalizePhaseIdSegments(sm[1]).toLowerCase())) return true; } // ⑤ #3185 兜底:委托给规范相位 token 提取器 extractPhaseToken const token = extractPhaseToken(dirName); if (token && normalized.has(normalizePhaseIdSegments(String(token)).toLowerCase())) return true; return false; }

3.1 各路径的分工与边界

  • 路径 ①:仅在phase_id_convention === 'bracket'时生效,用phaseTokenMatches对照GSD.01-01-xxx这类 qualified ID,解决 ADR-612 定义的 READING-B 读取约定下GSD.01-01-old-one与GSD.02-01-one共享 token01的歧义;
  • 路径 ②:numericRe是数字目录的唯一所有者。无连字符约定下为^0*(\d+[A-Za-z]?(?:\.\d+)*);当 ROADMAP 出现连字符 ID 时切换为带 continuation 段(PHASE_CONTINUATION_SEGMENT_SOURCE,宽度恰为 2)的变体(src/roadmap-parser.cts);
  • 路径 ③:segment-boundary 匹配按最长 ID 优先排序(#3213),避免proj误收本属于proj-42的目录;
  • 路径 ④:即本次 #3600 的修复核心,纯增量——只有前三条路径全部失败且目录确实带 project-code 前缀时才执行,不会排除任何已被前面路径接受的目录;
  • 路径 ⑤:解决P0.0-foundation这种字母前缀 + 小数相位的形式(#1325/#3185),同样是只增不减的兜底。

3.2 数字侧与目录侧的归一化一致

milestonePhaseNums在收集标题侧 ID 后经normalizePhaseIdSegments(剥离前导零、转小写)构建normalized集合(src/roadmap-parser.cts)。目录侧的每个命中(②③④⑤)都会把提取出的 token 走同一归一化函数再查集合,保证01、1、CK-01归一化后指向同一相位。

四、getMilestonePhaseFilter 的整体结构与退化策略

getMilestonePhaseFilter(cwd, versionOverride?, phaseIdConvention?, ws?)(src/roadmap-parser.cts)返回的不是普通布尔函数,而是带元数据的MilestonePhaseFilter:

附加属性含义
phaseCount当前里程碑窗口内识别到的相位 ID 数量
missingExplicitVersion存在版本化里程碑但versionOverride未命中时置真
versionScoped窗口是否被versionOverride成功限定
versionSectionFoundROADMAP 中是否找到版本区段
scope窗口可读性分类(COMPLETE / TRUNCATED / UNREADABLE 等)

其工作流程:

  1. 读取planningDir(cwd, ws)/ROADMAP.md;
  2. 经extractCurrentMilestoneScoped/sliceMilestoneWindow划定当前里程碑窗口(两者共用 ADR-3180 的单一所有者computeSectionEnd,避免重复推导漂移);
  3. 对milestone-prefixed约定 + 无版本化标题的组合发出弃用警告(src/roadmap-parser.cts);
  4. scanMilestonePhaseIdSets扫描窗口内全部相位标题,填入milestonePhaseNums与milestoneQualifiedIds;
  5. 若集合为空,退化为 pass-all 过滤器(() => true),但保留scope作为破坏性消费者的拒绝信号(ADR-3180 Decision 3 的两层策略)——这是"宁可多算、不可漏算"的安全设计(src/roadmap-parser.cts)。

五、配置上下文:project_code 与 phase_id_convention

5.1 project_code

.planning/config.json中的project_code字段(如"CK"、"PROJ")用于给相位目录加前缀。值得注意的是,目录侧匹配是否走 strip-and-retry 路径只取决于目录名本身是否带前缀,不直接读project_code配置——stripProjectCodePrefix是纯字符串函数。但测试表明该场景通常伴随project_code配置出现(见下文测试代码)。

5.2 phase_id_convention

phase_id_convention经resolvePhaseIdConvention(src/planning-workspace.cts)以 workstream→root 的联邦方式解析。当前存在三种取值状态(详见 ADR-612):

  • null(未迁移):纯数字/自定义 ID 形态;
  • milestone-prefixed(M-NN,如2-01):当前唯一的迁移器目标(roadmap upgrade --convention milestone-prefixed),且触发getMilestonePhaseFilter中的弃用警告与verify的 W021 检查;
  • bracket([GSD.02] Phase 02-01:):terminal 约定,需要project_code存在,否则迁移器硬拒绝。

本次 #3600 修复针对的是null/milestone-prefixed读取形态下"目录带 project-code 前缀、标题为数字"的组合,与 bracket 约定正交。

六、回归测试:如何验证修复

修复的回归测试位于 tests/milestone-archive.test.cjs,describe 块名为bug #3600: milestone phase filter understands project-code-prefixed directories,包含四个测试:

6.1 正向:init.new-milestone 计数带前缀目录

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'], tmpDir); assert.strictEqual(JSON.parse(r.output).phase_dir_count, 2);

phase_dir_count在 src/init.cts 中由cmdInitNewMilestone经listMilestonePhaseDirs(phasesDir, { cwd })计算得出(该枚举器在 src/phase-locator.cts 内部调用getMilestonePhaseFilter,并将filter.scope作为窗口分类返回)。

6.2 既有契约:无前缀目录继续计数

不带project_code配置、目录名为01-first时,phase_dir_count仍为 1,证明修复未破坏 #3537 既有的纯数字目录匹配。

6.3 自定义 ID 路径不回归

Phase PROJ-42: Custom标题 +PROJ-42目录仍通过路径③(自定义 ID segment-boundary 匹配)命中,phase_dir_count为 1——strip-and-retry 是增量路径,不影响原有的自定义 ID 匹配。

6.4 反向:非当前里程碑的带前缀目录必须被排除

ensurePhaseDir(tmpDir, 'CK-01-first'); ensurePhaseDir(tmpDir, 'CK-99-backlog'); ensurePhaseDir(tmpDir, 'CK-100-future'); // 断言 phase_dir_count === 1,仅 CK-01-first 匹配 Phase 1

CK-99与CK-100剥离前缀后分别归一化为99、100,不在normalized集合中,必须被排除——这验证了 strip-and-retry 不会变成"看到前缀就通过"的宽松匹配。

七、影响面与调用方全景

getMilestonePhaseFilter被以下核心路径共享,修复一经合入即全部受益:

调用方位置用途
cmdInitNewMilestonesrc/init.cts通过listMilestonePhaseDirs统计phase_dir_count
listMilestonePhaseDirssrc/phase-locator.cts里程碑窗口相位目录枚举的单一所有者
cmdMilestoneCompletesrc/milestone.cts归档当前里程碑时按窗口筛选要移动的目录
inspectWorkstreamsrc/workstream-inventory.ctsworkstream 当前版本窗口过滤
健康诊断roadmap-disk-consistencysrc/health-diagnostic-rules/roadmap-disk-consistency.cts校验 ROADMAP 声明与磁盘目录的一致性(validate-health)
规划快照src/planning-snapshot.ctsphaseDirs快照按窗口过滤

除此之外,src/state.cts的state sync/state update-progress等写路径也经由listMilestonePhaseDirs间接消费同一过滤器(src/state.cts 注释),因此带 project-code 前缀的项目在修复后,其完成比例(percent)、归档目录集、健康校验结果将首次与磁盘真实状态一致。

八、小结

issue #3600 的修复体现了 gsd-core 在相位标识解析上"单一所有者、增量兼容、退化安全"的一贯原则(对应 ADR-2121 与 ADR-3180):

  • 增量修复:strip-and-retry 只在既有路径全部失败时生效,只增不减,保证历史目录形态的读取行为不变;
  • 共用规则:目录侧剥离与标题侧归一化复用同一PROJECT_CODE_PREFIX_STRIP_RE,从机制上杜绝两侧约定漂移;
  • 测试完备:四向回归测试覆盖正向计数、既有契约、自定义 ID 路径与反向排除,防止修复退化为宽松匹配。

对于使用project_code前缀目录的项目,升级到包含此修复的版本后,init.new-milestone的phase_dir_count、phase complete的窗口归档、verify-work与validate-health的校验结果均会恢复正常。若需复现原始缺陷,可在.planning/config.json中设置"project_code": "CK"、创建CK-01-discovery目录并运行gsd-tools init new-milestone,观察修复前后phase_dir_count的差异。

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载

相关推荐

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

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

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

立即咨询