【免费下载链接】gsd-core
Git. Ship. Done - Core
导读
本文围绕 gsd-core 中一份编号为 PR #156 的修复记录(.changeset/archived/graceful-jays-hop.md),解析一次典型的CJS 工具层与 SDK 校验层代码漂移(drift)的修复过程:sdk/src/query/validate.ts与gsd-core/bin/lib/verify.cjs之间因手工维护双份实现而造成的 W006/W007 误报,最终通过gen-validate.mjs生成器模式收敛为「单一手写源 + 机械生成产物」。读完本文,你将掌握 gsd-core 的生成器防漂移机制、phaseVariants()等纯函数被提取与复用的原理,以及 W005/W006/W007/I001 四条健康诊断规则在双运行时下的行为契约。
修复记录定位:一次典型的跨运行时漂移修复
.changeset/archived/graceful-jays-hop.md是 gsd-core 的一份归档 changeset,全文如下:
--- type: Fixed pr: 156 --- Fix W007/phaseVariants/W006 drift between SDK validate.ts and CJS verify.cjs via generator pattern (gen-validate.mjs + validate.generated.cjs). Per ADR-3524 generator framework introduced by PR #154 (issue #4).它记录的是一个bug 类别(bug class)而非单点缺陷:gsd-core 同时维护两套执行运行时——同步的 CJS 工具层(gsd-core/bin/lib/*.cjs)与 SDK/TypeScript 层。历史上,同一份校验逻辑被分别手写在validate.ts(SDK 侧)和verify.cjs(CJS 侧),导致同一修复只落在其中一侧,另一侧随即再次产生误报。ADR-3524 的正文将这一反复出现的缺陷类别列为:#1535、#1542、#2047/#2052、#2638/#2655、#2653/#2670、#2687/#2706、#2798/#2816、#3055/#3116、#3523,其共同特征都是「一个修复只落在一侧,另一侧没跟上」。
本次 PR #156(issue #6)正是用 ADR-3524 引入的generator pattern(生成器模式)解决其中三个具体漂移项:W007 的activeDiskPhases、phaseVariants()归一化、W006 的未开始阶段变体跳过逻辑。
背景:CJS/SDK 硬接缝与 Shared-Module 源策略
要理解这次修复,必须先理解 ADR-3524:CJS↔SDK hard seam(docs/adr/3524-cjs-sdk-hard-seam.md)。该 ADR 提出Shared-Module Source Policy(共享模块源策略):
- 每个共享模块恰好一个手写源——有行为的模块以 TypeScript 形式放在
sdk/src/<module>/,纯数据模块以 manifest 形式放在sdk/shared/; - 产物只允许由生成器机械产生——CJS 侧产物形如
gsd-core/bin/lib/<module>.generated.cjs,禁止手改; - 每个模块配一个 freshness 检查脚本——CI 重新运行生成器,若与已提交产物不一致则构建失败(先例:
check-command-aliases-fresh.mjs); - 禁止手工同步成对文件(hand-synced pairs)——
lint-shared-module-handsync.cjs会拦截bin/lib/<name>.cjs与sdk/src/<name>.ts的非生成器成对实现。
该 ADR 还特别强调(第 4 节):两侧的 I/O 适配器(Adapter)允许合理不同——CJS 调用方使用同步 fs/exec,SDK 调用方使用异步 I/O;只有其背后的纯转换逻辑(解析、投影、归一化)才被提取为共享模块。validate.ts与verify.cjs中的健康校验正属于「同一份纯逻辑、两个运行时」的场景,是生成器模式的天然适用对象。
生成器模式的仓库内先例是command-aliases:sdk/scripts/gen-command-aliases.ts从单一 TS 源同时产出sdk/src/query/command-aliases.generated.ts与gsd-core/bin/lib/command-aliases.generated.cjs。PR #154(issue #4)率先把它推广到 phase 生命周期(gen-phase.mjs、gen-phase-lifecycle.mjs、gen-phase-lifecycle-policy.mjs),PR #156 则把同一模式应用到 validate/verify 对。
注意:
gen-validate.mjs与validate.generated.cjs属于历史归档形态。在当前仓库中,这份逻辑已按 ADR-457(docs/adr/457-generated-cjs-single-source.md) 的「build-at-publish(发布时构建)」方向进一步演进:手写源收敛为 TypeScript 的 src/validate.cts,产物为构建产物而非提交物。但 ADR-3524 的「单一源 + 机械生成 + freshness 门禁」原则仍然是当前实现的直接前身,src/validate.cts的头部注释也明确标注了它的来源(ADR-457、ADR-3524 §4、issue #6、issue #26、PR #154、PR #156)。
三个漂移项逐一拆解:误报是如何产生的
ADR-3524 的 2026-05-23 修订记录(issue #6)把本次修复拆成三个具体漂移项。tests/health-validation.test.cjs(tests/health-validation.test.cjs)中保留了针对每一项的回归测试与详尽的注释(见文件第 480–696 行附近的「Drift item 1/2/3」段落),是理解误报机理的第一手材料。
漂移项 1:W007 的activeDiskPhases
- 症状:
verify.cjs的 Check 8 在跑 W007(「ROADMAP 中有、磁盘上没有」的警告)时,迭代的是diskPhases——它通过forEachArchivedPhaseToken把已归档里程碑的阶段也纳入了磁盘阶段集合。若某个归档阶段在当前 ROADMAP 中已不存在,就会产生假 W007 警告。 - 修复:W007 改迭代
activeDiskPhases(仅来自collectDiskPhases()的活动阶段,不再包含forEachArchivedPhaseToken的归档项),与validate.tsCheck 8 的行为对齐。 - 回归测试:构造两个里程碑归档(v1.0 旧、v1.1 活动),v1.0 的阶段不在当前 ROADMAP 中——修复前
verify.cjs对「1」误报 W007,修复后不再误报(tests/health-validation.test.cjs 第 539–620 行)。
漂移项 2:phaseVariants()归一化
- 症状:
verify.cjsCheck 8 用parseInt(p).padStart(2,'0')做磁盘存在性与 ROADMAP 成员检查,这一做法会丢弃字母后缀:例如"3B"被归一化成"03"而不是"03B"。于是带字母后缀补零的阶段目录(ROADMAP 写3B、磁盘上是03B-foo)会产生假 W006 和假 W007。 - 修复:两处检查都改用生成的
phaseVariants(p)。该函数返回包含原始形式、去零形式、补零形式、字母后缀形式的完整归一化集合,因此"01A"与"1A"可以正确互相匹配。 - 回归测试:ROADMAP
01A+ 磁盘1A-foo的补零错位场景,修复前 W006/W007 双双误报,修复后均不再误报(tests/health-validation.test.cjs 第 641–696 行)。
漂移项 3:W006 未开始阶段变体跳过
- 症状:
verify.cjsCheck 8 构造notStartedPhases时只使用原始形式与parseInt补零形式(同样丢字母后缀),导致「3B」无法正确抑制「03B」(反之亦然)的 W006 检查。 - 修复:改用
phaseVariants()构造未开始阶段集合,使带后缀变体互相抑制。 - 回归测试:ROADMAP
3B+ 磁盘03B-foo的补零字母后缀错位场景(tests/health-validation.test.cjs Drift 3 段落)。
生成器如何提取纯函数:phaseVariants的源码级细节
phaseVariants在 SDK 编译产物中是validateHealth内部的闭包,而不是模块级导出。ADR-3524 修订记录明确指出,gen-validate.mjs采用brace-balanced source-text parsing(括号配平的源码文本解析)从sdk/dist/query/validate.js中把它提取出来——这与gen-phase-lifecycle-policy.mjs提取escapeRegex的技术相同。提取的前提是该函数确定性且纯:不闭包外部状态、无副作用。
当前仓库 src/validate.cts 中该函数的实现如下(第 195–200 行起):
export function phaseVariants(phase: string): Set<string> { const variants = new Set([phase]); const dotIdx = phase.indexOf('.'); const head = dotIdx === -1 ? phase : phase.slice(0, dotIdx); const tail = dotIdx === -1 ? '' : phase.slice(dotIdx); // ... 依据头部数值前缀生成去零/补零等变体后加入集合 }同模块还导出了配套的buildRoadmapPhaseVariants()与buildNotStartedPhaseVariants()(见src/validate.cts头部注释第 10–15 行):前者替代 W007 循环中的原始roadmapPhases集合,后者替代 W006 跳过逻辑中的原始 + 补零集合。从源码结构看,这三个函数构成了 W006/W007 的统一变体归一化层:任何一侧的检查都消费同一套集合,从根上杜绝了「一侧改、一侧漏」的漂移。
issue #26 的扩展:W005/W006-archived/I001 的生成器迁移
PR #156 解决的只是 issue #6 的三个漂移项;ADR-3524 修订记录还记录了 issue #26 对生成器覆盖范围的扩展(PR #3479 修复了三类误报,PR #3806 手工移植到verify.cjs但未走生成器,于是仍可能再漂移)。issue #26 把gen-validate.mjs进一步扩展为额外导出四项:
phaseDirNameRe(W005)——阶段目录命名正则,原来内联在verify.cjsCheck 6。PHASE_DIR_NAME_RE(/^\d{2,}(?:\.\d+)*-[\w-]+$/)被提升为validate.ts的具名导出,再由生成器提取。复现路径:mkdir -p .planning/phases/999.1-foo应产生零 W005。PHASE_TOKEN_FROM_DIR_RE(W006-archived)——原来内联在verify.cjs的forEachArchivedPhaseToken()与collectDiskPhases(),改为从模块级常量提取。MILESTONE_ARCHIVE_DIR_RE(W006-archived)——原来内联在listMilestoneArchiveDirs(),同样被提取。两者共同保证归档目录遍历使用与validate.ts完全相同的模式。canonicalPlanStem(I001)——原来内联在verify.cjsCheck 7,通过extractTopLevelFunction()(括号配平解析器)提取。修复效果:68-01-scaffolding-PLAN.md能与68-01-SUMMARY.md正确匹配(都归约到68-01),不再误报 I001。
提取方法上,gen-validate.mjs新增了两个原语:extractConstRegExp()(处理const/export const的单行正则赋值,用于前三项)与extractTopLevelFunction()(用于顶层命名函数声明,用于canonicalPlanStem)。对应地把PHASE_DIR_NAME_RE从内联匿名正则提升为export const,正是为了让它在编译后的 ESM 产物中成为可提取的标识符。
ADR-3524 修订记录对 W006-archived 有一个重要澄清:issue #26 描述的 W006-archived「与 PR #156 的 W006 修复相关但不同」——调查确认两个修复其实都已在verify.cjs(来自 PR #3806),真正的缺口是生成器覆盖:forEachArchivedPhaseToken使用的正则常量仍是无人保护的内联副本。因此该扩展的交付物不是新的行为修复,而是生成器模式的覆盖补齐。
双运行时下的 W005/W006/W007/I001 行为契约
src/verify.cts中明确说明当前的健康诊断采用规则表复用:W006/W007 等诊断由validate.health评估的同一套Rule对象驱动,且当前所有 C0NN/W006/W007 诊断都是SEVERITY.WARNING。这四条诊断的语义可归纳如下:
| 诊断码 | 触发条件 | 修复相关的关键点 |
|---|---|---|
| W005 | 阶段目录名不符合规范命名(如两位数字起头等) | phaseDirNameRe统一;999.1-foo不再误报 |
| W006 | 磁盘上有阶段目录,但 ROADMAP 中未开始(含归档阶段目录遍历) | phaseVariants()归一化 + 归档正则统一 |
| W007 | ROADMAP 中声明的阶段在磁盘上缺失 | activeDiskPhases排除已归档阶段 |
| I001 | PLAN 文件与 SUMMARY 文件 stem 不匹配 | canonicalPlanStem()归约长 stem |
phaseVariants()的归一化集合行为在测试中有明确的断言,例如canonicalPlanStem('68-01-scaffolding') === '68-01'、canonicalPlanStem('3A-01-feature') === '3A-01',同时保证46-6-rs-...这类「阶段号后跟单位数字 slug 词」不被误吸收(#2043),14-2026-photos-...这类「≥3 位数字 slug 词(年份)」不被误当作连续段(#2232)——见 tests/health-validation.test.cjs 中canonicalPlanStem的专项测试段落。
防漂移机制的三层门禁与演进方向
ADR-3524 第 5 节把漂移拦截设计为三层,每一层都有仓库内先例:
- 每模块 freshness 检查——
sdk/scripts/check-<module>-fresh.mjs,重跑生成器,产物与已提交版本不一致即失败(先例check-command-aliases-fresh.mjs); - 每模块漂移 lint——当不变式不是纯文件等值时使用,如
scripts/lint-shell-command-projection-drift.cjs; - 手工同步对 lint——
scripts/lint-shared-module-handsync.cjs在 PR 阶段拒绝任何既非生成产物、又不在显式允许清单中的成对文件,从源头堵死 #3523 反模式。
生成器还会在产物文件顶部自动插入「GENERATED FILE — Source: …」横幅,沿用command-aliases.generated.*的既有样式。
需要强调的是,这个框架本身也在持续演进:ADR-457 已接受「build-at-publish」方向(TypeScript 源为规范、.cjs为 gitignore 的构建产物),这从机制上消解了「两份副本必须一致」的漂移治理负担。因此阅读本 changeset 时,gen-validate.mjs + validate.generated.cjs应理解为该演进路径上的中间形态;其核心原则——单一手写源、机械生成产物、每模块 freshness 门禁——在今天的src/validate.cts与健康诊断规则表中依然清晰可辨。
参考路径速查
- 归档 changeset:.changeset/archived/graceful-jays-hop.md
- 决策记录:docs/adr/3524-cjs-sdk-hard-seam.md(含 issue #6 / #26 修订)、docs/adr/457-generated-cjs-single-source.md
- 当前实现:src/validate.cts、src/verify.cts
- 回归测试:tests/health-validation.test.cjs(Drift 1/2/3 段落及 W005/W006-archived/I001 专项)
这份 changeset 的价值不在于它修了三个告警,而在于它示范了 gsd-core 治理「双运行时共享逻辑」的标准答案:凡是纯函数,就用生成器从单一源机械产出;凡是 I/O,就保留两侧适配器;凡是手工同步的成对文件,就在 PR 阶段拒绝合入。
【免费下载链接】gsd-core
Git. Ship. Done - Core
相关推荐
gsd-core 里程碑归档目录解析修复:getActiveMilestoneArchiveDir 的 null 语义与 W007 误报消除
gsd core 里程碑归档目录解析修复:getActiveMilestoneArchiveDir 的 null 语义与 W007 误报消除 本文聚焦 gsd
gsd-core `validate health` 误报修复实录:W005/W006/I001 三类回归的根因与移植(PR 3806)
gsd core validate health 误报修复实录:W005/W006/I001 三类回归的根因与移植(PR 3806) 导读 gsd core v
gsd-core 修复实践:平面 ` Phase Details` 导致的 Milestone 阶段泄漏与 W007 误报
gsd core 修复实践:平面 Phase Details 导致的 Milestone 阶段泄漏与 W007 误报 导读 在 gsd core 的规划工作流中
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考