【免费下载链接】gsd-core
Git. Ship. Done - Core
本文围绕 gsd-core 的 ADR-612「bracket 阶段 ID 约定」显示面落地(issue #3638 / PR 4111)展开:当一个项目通过
phase_id_convention: "bracket"显式 opt-in 后,progress、stats、manager init 以及两种 statusline 格式会以统一的规范形态[CODE.MM] NN渲染阶段标识;而未 opt-in 的项目(null、sequential、milestone-prefixed)则保留原有输出形状,字节级不变。同时config-set为该键新增了枚举校验,只接受三个受支持值。读完本文,你将掌握 bracket 约定的完整语法、各显示面的行为差异、底层规范解析/渲染对的实现原理,以及如何安全地配置与验证这一约定。
一、背景:ADR-612 bracket 约定到底是什么
gsd-core 的phase_id_convention配置键控制项目阶段 ID 的命名约定。在 docs/CONFIGURATION.md 中,它被定义为取值"sequential"、"milestone-prefixed"、"bracket"或null的枚举,默认值为null:
null/"sequential":沿用传统数字 ID(Phase 1、Phase 2);"milestone-prefixed":使用编码了所在 milestone 的全局唯一 ID(Phase 1-01、Phase 1-02),并且是当前发布线唯一的roadmap upgrade迁移目标;"bracket":把 milestone 前置到阶段编号之前——标题写作### [GSD.02] 05: Name,目录写作GSD.02-05-name。
"bracket"是 opt-in 的:仓库必须显式把phase_id_convention设为"bracket",显示面才会切换到新的标识形态。这一点在 gsd-core/references/phase-id-convention.md 开头直接写明:"The bracket convention is opt-in throughphase_id_convention: "bracket"."
1.1 规范的 bracket 语法卡片
该约定的紧凑语法卡片由 src/phase-id-card.cts 单一来源生成(PHASE_ID_CARD常量),渲染站点通过phaseIdCard()引入,tests/phase-id-card.test.cjs 保证生成文档与其字节一致:
[GSD.02] 05.03-01 │ │ │ │ │ │ │ │ │ └── plan 01 │ │ │ └────── subphase 03 │ │ └───────── phase 05 │ └───────────── milestone 02 └───────────────── project GSD milestone = bracket integer; dots = phase-levels; one hyphen = plan; no 'Phase' word, no vX.Y两种等价形态:
- 显示形式
[PROJECT.MM] PP[.SS][-LL]:方括号承载 project 与 milestone,点号连接阶段层级,单一连字符引入可选的 plan; - 目录形式
PROJECT.MM-PP[.SS]-slug/:同一身份不加方括号编码到目录名中,末尾是 slug。
面向人的 bracket 显示面同时省略了字面的Phase标签和传统vX.Ymilestone 标记(参见 gsd-core/references/phase-id-convention.md)。
二、约定门控:只有精确的"bracket"才改变显示
#3638 的核心设计原则是精确门控(exact gating):显示面只在phase_id_convention精确等于"bracket"时才渲染规范的[CODE.MM] NN标识;其他约定(null、sequential、milestone-prefixed,以及任何未识别值)保留原有的模式与输出形状。
这一点在 docs/CONFIGURATION.md 中有明确说明:"A project on any other value compiles the same heading patterns and retains the same output shape it did before."
从源码看,这一门控贯穿了所有相关模块。以 src/phase-id.cts 为例,多个核心辅助函数都以可选参数convention作为 ADR-2121 的「增量形态」约定——不传参会字节级等价于旧行为:
phaseHeadingPrefixSrcFor(src/phase-id.cts):if (convention !== 'bracket') return base;——非 bracket 约定编译的是站点原有的基准标题前缀源码(ANY_BRACKET或LABEL_ONLY),而不是其超集;getMilestoneFromPhaseId(src/phase-id.cts):bracket 分支从[PROJECT.MM]/{CODE}.{MM}-前缀读取 milestone(READING-B),非 bracket 路径保留传统前导整数规则(READING-A);extractPhaseToken(src/phase-id.cts):bracket 目录{CODE}.{MM}-{PP}[.{SS}]-slug→ 阶段 tokenPP[.SS],同样被convention === 'bracket'门控。
门控而非「检测」的原因是字符串层面的不可区分性(ADR-2121):bracket 目录{CODE}.{MM}-{PP}与 legacy#1324字母前缀小数族(如P0.3-2、P0.12-34)在 project code 以数字结尾时无法用纯字符串规则区分。自动检测会静默地把P0.3-2重新解释为2,造成关键辅助函数上的字节级读取回归——所以必须显式约定信号。
三、四个显示面的行为详解
3.1progress/stats:新增display_id字段
在 bracket 项目上,progress与stats的 JSON 输出在两个层面发生变化(见 docs/CLI-TOOLS.md):
- 每个阶段保持裸的 join key 在
phases[].number,同时新增规范的人类可读标签phases[].display_id,例如{"number":"05.03","display_id":"[GSD.02] 05.03"}; milestone_version和表格标题使用[GSD.02],而不是 legacy 的v2.0标记。
对应的表格渲染从| 05.03 | display slice |变为| [GSD.02] 05.03 | display slice |,且不再出现v2.0。
在非 bracket 项目上,行为由 tests/adr-612-bracket-display.property.test.cjs 钉死:number保持05.03,并且不出现display_id字段(Object.hasOwn(output.phases[0], 'display_id') === false),表格保持| 05.03 | ... |原样。
3.2 manager init
init manager在 bracket 项目上读取无标签 bracket 标题(如### [GSD.02] 05.03: Display Slice)并输出规范的display_id。测试(tests/adr-612-bracket-display.property.test.cjs)断言其输出:
{ "number": "05.03", "display_id": "[GSD.02] 05.03", "name": "Display Slice", "disk_status": "planned" }3.3 两种 statusline 格式
hooks/gsd-statusline.js在两种 statusline 格式(full 与 compact)上都实现了 bracket 门控。其配置解析入口(hooks/gsd-statusline.js)把phase_id_convention === 'bracket'解析为convention: 'bracket'传入渲染函数;渲染时(hooks/gsd-statusline.js 与 hooks/gsd-statusline.js)通过opts.convention === 'bracket'决定:
- milestone 显示用
[GSD.02]替代v2.0; - 阶段显示用 bracket 阶段标签(如
[GSD.02] 05.03)替代P05/12之类的 legacy 形态。
测试(tests/adr-612-bracket-display.property.test.cjs)同时断言两种格式在 bracket 下匹配/\[GSD\.02\]/且不匹配/v2\.0/,并在sequential下保留精确的 legacy 字符串(见同文件 tests/adr-612-bracket-display.property.test.cjs)。
3.4 一个统一的语法决策:milestone 标签 = 阶段显示前缀
renderMilestoneId(id)返回[GSD.02],renderPhaseId(id)返回`${renderMilestoneId(id)} 05.03-01`——milestone 标签被钉死为所有 bracket 阶段显示共享的前缀(见 tests/adr-612-bracket-display.property.test.cjs)。显示适配层 src/phase-id-display.cts 中的renderBracketMilestoneDisplay('v2.0', 'GSD')能把 legacy 的v2.0元数据翻译成[GSD.02]。
四、底层实现:规范解析/渲染对与显示适配层
4.1parsePhaseId/renderPhaseId/toDir:单一可信的 round-trip 模型
src/phase-id.cts 定义了唯一的可往返 bracket 模型(ADR-612 Decision 4)。PhaseId结构类型包含project、milestone(零填充)、phase(零填充)、可选subphase与可选plan(仅文件名面)。
关键实现事实:
- 规范一致性由构造保证:
parsePhaseId在解析显示形式[PROJECT.MM] PP[.SS][-LL]或目录形式{PROJECT}.{MM}-{PP}[.{SS}][-{plan|slug}]后,会重新渲染并逐字节比对输入,拒绝非填充数字([GSD.5] 5)、过度填充([GSD.005] 05)与多空格([GSD.02] 05),统一抛错而不是静默归一化; render(parse(x)) === x契约由属性测试钉死:tests/adr-612-bracket-display.property.test.cjs 用 fast-check 对任意(project, milestone, phase, subphase)组合断言renderPhaseId(parsePhaseId(display)) === display且toDir(parsePhaseId(display), 'display slice')精确命中手写目录;toDir的写入侧校验:project 必须匹配[A-Z][A-Z0-9_]*,milestone/phase/subphase 必须匹配规范的数值宽度(恰好 2 位,或 3 位以上且无前导零——由BRACKET_CANONICAL_NUMERIC_SOURCE统一持有),slug 必须清洗为非空、非全数字的 token,防止路径穿越与磁盘↔身份双射破坏。
4.2 显示适配层:只做边界归一化
src/phase-id-display.cts 的定位是「纯适配器」:STATE.md和 milestone 元数据仍暴露 legacy 的vN.0标记,显示面需要把它翻译成 bracket 身份,但不重复实现另一套渲染器。renderBracketPhaseDisplay与renderBracketMilestoneDisplay只做数值边界归一化(剥离v前缀、拆分v2.0、规范数值宽度),随后委托给 src/phase-id.cts 的parsePhaseId/renderMilestoneId/renderPhaseId规范对;元数据不完整或无效时返回null,让装饰性调用方优雅降级而不是破坏命令/statusline 渲染。
4.3 约定的一次性解析与线程化
branch 文档 docs/adr/612-bracket-phase-id-convention.md 指出,消费方遵循「解析一次、显式线程化」的纪律:例如roadmap-command-router.cts在 src/roadmap-command-router.cts 中于消费方之前一次性解析phase_id_convention(含从 ROADMAP frontmatter 回退读取),再作为参数传入各消费方;src/roadmap.cts 同样把resolvePhaseIdConvention的结果线程化到阶段目录扫描(scopeToPhase、matchPhaseDirs)等调用链,而不是在每个站点重新读取配置。
五、config-set的枚举校验:三个受支持值
#3638 的另一半是配置侧校验。此前phase_id_convention是一个未校验的魔法字面量;现在config-set只接受精确的三个值。
5.1 源码中的枚举定义
src/config.cts 定义了:
// ADR-612 PR-5: configuration accepts every convention the runtime can read. // Keep this distinct from roadmap-upgrade's supported target set: sequential // is valid project configuration but is not a migration destination. const VALID_PHASE_ID_CONVENTIONS: readonly string[] = Object.freeze([ 'sequential', 'milestone-prefixed', 'bracket', ]);设置路径(src/config.cts)通过assertEnumValue(parsedValue, val, VALID_PHASE_ID_CONVENTIONS, 'phase_id_convention')校验;null用于取消该键(config-set phase_id_convention null会移除键本身,且保留兄弟配置项,见 tests/config.test.cjs)。
5.2 测试钉死的校验行为
tests/config.test.cjs 覆盖了完整行为矩阵:
- 三个受支持值
sequential、milestone-prefixed、bracket都能成功写入并被回读; null取消键,且不影响model_profile等兄弟配置;- 不支持值(
free-form)与大小写不匹配值(Bracket)都被拒绝,错误信息包含Invalid phase_id_convention与支持集合sequential, milestone-prefixed, bracket; sequential是合法项目配置,但不是roadmap upgrade --convention的迁移目标(迁移只接受milestone-prefixed)。
5.3 配置方式
# 在项目根目录设置 bracket 约定 gsd-tools config-set phase_id_convention bracket # 取消约定(键被移除,回到 null 行为) gsd-tools config-set phase_id_convention null或直接在.planning/config.json中写入{"phase_id_convention": "bracket"}(测试夹具即以此方式构造 bracket 项目,见 tests/adr-612-bracket-display.property.test.cjs)。
六、兼容性保证与边界行为
6.1 非 bracket 项目:结构等同而非「论证等价」
这是本设计最值得注意的一点:未 opt-in 的仓库编译的标题模式恰好是基线拼写本身(BASE_ANY_BRACKET_HEADING_PREFIX_SRC/BASE_PHASE_LABEL_PREFIX_SRC),而不是其超集。原因是早期实现曾不加门控地放宽读取,并论证放宽后的形态「不可能出现在 legacy ROADMAP 中」——但### [RFC.2119] 5:、### [v1.0] 2024:、### [ADR.612] 3:都是合法 legacy 标题,放宽会把它们当作阶段,从而在没有 opt-in 的仓库上移动phase_count、total_phases与 W006。构造时选择(construction-time selection)从根上消除了这一论证负担(见 src/phase-id.cts 的注释)。
6.2 malformed bracket 目录的恢复
当目录名不规范时(如小写gsd.02-05.03-recovered-display-name,tests/adr-612-bracket-display.property.test.cjs 的夹具),progress与stats仍能识别出阶段并输出number: '05.03',但不产生display_id(hasDisplayId: false)——优雅降级,不破坏命令输出。
6.3 opt-in 的代价(必须知晓的权衡)
文档在 docs/CONFIGURATION.md 中明确给出了 opt-in 的代价:在 bracket 仓库上,bracket 后直接跟数字的标题会被当作阶段标题,因此在其他约定下合法的小节标题形态——### [RFC.2119] 5:、### [v1.0] 2024:、### [ADR.612] 3:——都会被认作阶段,移动phase_count、total_phases与 W006。bracket 仓库放弃了这一标题形态,这是 opt-in 换来的权衡;也是放宽读取必须在构造时从该配置值选择、而不是全局应用的原因。
6.4 范围限制
目前"bracket"只影响读取与显示路径,尚没有 bracket 迁移器和 bracket 写入(emit)。文档明确提示:"There is no bracket migrator and no bracket emit yet, so set it only on a project whose ROADMAP.md and phase directories already use that spelling."——只应在 ROADMAP.md 与阶段目录已使用该拼写的项目上开启。
七、验证与测试地图
围绕 #3638 的测试集中在两处:
| 测试文件 | 覆盖点 |
|---|---|
| tests/adr-612-bracket-display.property.test.cjs | progress/stats 的display_id与milestone_version;init manager 读取无标签 bracket 标题;legacy 项目保留旧对象/表格形状;malformed 目录恢复且无display_id;renderMilestoneId作为共享前缀;statusline full/compact 的约定门控;render(parse(x)) === x与toDir的 fast-check 属性测试 |
| tests/config.test.cjs | config-set phase_id_convention的枚举校验:三值接受、null取消、不支持/大小写不匹配拒绝、sequential非迁移目标 |
另有 tests/phase-id-card.test.cjs 钉死语法卡片字节与 src/phase-id-card.cts 一致,以及同族测试 tests/adr-612-bracket-coherence.test.cjs、tests/adr-612-bracket-heading-selection.test.cjs 等覆盖读取侧一致性(均在 tests 目录下)。
八、总结
#3638(PR 4111)为 ADR-612 的 bracket 约定补齐了显示面与配置面的最后一块拼图,其核心贡献可以概括为三点:
- 统一的规范形态:
progress、stats、manager init 与两种 statusline 在phase_id_convention: "bracket"下渲染同一套[CODE.MM] NN标识,progress/stats同时暴露number与display_id双字段; - 严格的约定门控:非 bracket 项目编译与渲染与其基线字节级等同,彻底消除「放宽读取被误用」的论证风险;
- 配置侧枚举校验:
config-set只接受sequential、milestone-prefixed、bracket三个精确值(或null取消),配合测试矩阵钉死行为。
如果你想在已有 ROADMAP.md 与阶段目录均使用 bracket 拼写的项目上启用它,只需gsd-tools config-set phase_id_convention bracket,并在progress/stats/init manager/statusline 输出中确认[CODE.MM]形态出现、legacyvN.0消失即可;若需深入语法细节,gsd-core/references/phase-id-convention.md 是权威的紧凑语法卡,src/phase-id.cts 与 src/phase-id-display.cts 则是规范的解析/渲染/适配实现。
【免费下载链接】gsd-core
Git. Ship. Done - Core
相关推荐
Wazuh Engine schemf 模块深度解析:Schema 字段定义与两阶段校验系统
Wazuh Engine schemf 模块深度解析:Schema 字段定义与两阶段校验系统 本篇技术指南围绕 Wazuh Engine 的 schemf (S
网络安全IDS日志分析应用安全漏洞扫描Slang 仓库 LLM 生成文档的修复阶段:`_remediate.md` 提示词契约与两阶段审校工作流解析
Slang 仓库 LLM 生成文档的修复阶段: _remediate.md 提示词契约与两阶段审校工作流解析 本文档解析 Shader Slang 仓库中由 L
编译器图形学编程语言get-shit-done 命令契约校验(ADR-0002):以双层校验与前端字段规范锁定 65 个 gsd 斜杠命令的质量基线
get shit done 命令契约校验(ADR 0002):以双层校验与前端字段规范锁定 65 个 gsd 斜杠命令的质量基线 导读 本篇文章围绕 get s
人工智能AI 应用提示工程开发工具工作流自动化AI Agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考