gsd-core init.* 阶段查询支持 `--phase` 参数:让 `gsd-sdk query init.*` 不再把 flag 当成阶段名
gsd-core init.* 阶段查询支持 --phase 参数:让 gsd-sdk query init.* 不再把 flag 当成阶段名
本篇技术指南聚焦 gsd-core 中 gsd-sdk query init.*(即 init.* 命令族)的阶段性查询参数修复:从 changeSet sdk-init-phase-flags.md 记录的 PR #3389 修复出发,深入解析 --phase <N> 与 --phase=N 两种 flag 形式的底层实现(normalizePhaseAlias)、受影响子命令范围、严格参数校验(ADR-3473)语境,以及 init.test.cjs 中 #3865 测试组的验证方式。读完本文,你将掌握在 init.execute-phase、init.plan-phase、init.verify-work、init.code-review 等查询命令中正确使用 --phase 参数,并理解其与位置参数形式的等价关系与边界行为。
一、背景:flag 形式查询曾把 --phase 当作字面阶段名
归档的 changeSet 记录了一次典型的"参数歧义"缺陷修复:
---
type: Fixed
pr: 3389
---
**`gsd-sdk query init.*` phase-scoped handlers now accept `--phase <N>` and `--phase=N`**
- flag-style init queries no longer search for a phase literally named `--phase`.
在修复之前,以 flag 形式调用阶段作用域的 init 查询(例如 gsd-tools query init.execute-phase --phase 03),底层会把 --phase 当作位置参数位上的阶段名去搜索——也就是说,它不是在"读取 --phase 的值 03",而是在磁盘上查找一个名字就叫 --phase 的阶段目录。其后果是静默的:查询不会报错,而是返回 phase_found: false、plan_count: 0。源码注释中记录了一起真实事故——某阶段磁盘上明明有 7 份计划,查询结果却是 0("the reported incident: 7 plans read as 0",见 init-command-router.cts 第 133-135 行附近注释)。
这一缺陷在源码与测试中以 issue #3865 追踪(changeSet 以 PR #3389 记录)。它之所以危险,正是因为"静默错误":工作流层(如 execute-phase.md、plan-phase.md)拿到 phase_found:false 后可能走错误分支,而用户从 JSON 输出上很难第一时间察觉查询参数没有生效。
二、核心修复:normalizePhaseAlias 统一入口
修复的实现位于 src/init-command-router.cts 的 normalizePhaseAlias 函数(第 82-112 行)。该路由器是 manifest-backed 的 init 子命令路由器:所有 init.* 子命令都有 SDK 等价物,并通过 executeForCjs(同步桥)派发,CJS 回退仅在 GSD_WORKSTREAM 激活或 SDK 构建缺失时保留。
normalizePhaseAlias 的核心思想是:把 flag 形式规约(normalize)成位置参数形式,再放入"调用方自己读取的 args[2] 槽位",从而使下游 handler 和严格 flag 校验看到的 argv 与位置参数形式完全一致。函数对输入分四种情况处理:
输入(args[2] 位置) |
处理结果 | 说明 |
|---|---|---|
--phase <N> |
将 args[2], args[3] 折叠为单个值 <N> |
经典双 token 形式,等价于位置参数 <N> |
--phase=<N> |
截取 = 后的值替换 args[2] |
单 token 内联值形式 |
--phase(无值) |
usage error,直接报错退出 | 明确提示 --phase requires a value: use --phase <N> (or the positional form <N>),绝不静默 |
其他以 -- 开头的 flag |
规约为 undefined |
等价于"未给位置参数"的输入(见下节) |
| 普通 token | 原样作为阶段名 | 位置参数形式,行为不变 |
其中两个关键细节值得注意:
- 无值
--phase是硬错误而非软失败:函数在报错后还会throw,作为 fail-closed 的兜底(第 91-92 行、第 103-104 行注释),确保即使error()回调异常返回,也不会继续执行后面的 splice 逻辑把坏值写进 argv。 - 其他 flag 形状的
args[2]会规约为undefined:例如init execute-phase --tdd这种写法,--tdd不再被当作阶段名传给下游,而是走"无位置参数"路径——execute-phase/plan-phase/verify-work会给出 "phase required" 用法错误,基于 find 的查询返回phase_found:false,todos则丢弃区域过滤。
isFlagToken 谓词(command-arg-projection.cts 第 114-116 行,tok.startsWith('--'))在这里被复用为 flag 形状判定,与严格参数解析器保持同一个判定标准。
三、受影响的 init.* 阶段作用域子命令
在 command-aliases.cts 的 INIT_COMMAND_ALIASES(第 276-492 行区域)中,INIT_SUBCOMMANDS 共登记了 27 个 init 子命令。其中读取 args[2] 位置槽位作为阶段 token 的"阶段作用域查询",都通过 normalizePhaseAlias 获得 --phase 支持:
| 子命令 | 路由处理(见 init-command-router.cts) | 额外 flag |
|---|---|---|
execute-phase |
normalizePhaseAlias + 严格解析(第 136-149 行) |
--validate、--tdd(布尔)、--wave [value] |
plan-phase |
normalizePhaseAlias + 严格解析(第 150-171 行) |
--granularity、--prd、--ingest、--research-phase(值)、--validate、--tdd、--reviews、--chunked(布尔) |
verify-work |
normalizePhaseAlias + 仅位置校验(第 233-237 行) |
无(--ws 在到达此路由器前已被上层剥离) |
phase-op |
normalizePhaseAlias + 仅位置校验(第 238-242 行) |
无 |
code-review |
normalizePhaseAlias + 严格解析(第 243-247 行) |
--fix(布尔) |
review |
normalizePhaseAlias + 仅位置校验(第 248-252 行) |
无 |
discuss-phase-assumptions |
normalizePhaseAlias + 严格解析(第 253-257 行) |
--auto(布尔) |
todos |
normalizePhaseAlias + 仅位置校验(第 258-262 行) |
无 |
其中 execute-phase 路由还额外说明:--wave 属于 optionalValueFlags(值归属于 workflow 层消费,CLI 层只关心 token 出现与否),这是 ADR-3473 Bucket-A 修正后的独立机制,与 --phase 的规约逻辑互不干扰。
这一设计也与 phase list-plans 保持一致——后者本来就同时接受位置参数与 --phase 两种形式(见 init-command-router.cts 第 128-130 行注释)。修复的实质是把这一"双形式等价"契约推广到整个 init 命令族。
四、为什么必须走"规约"而非"直读":严格参数校验语境
修复采用"先规约再解析"而非"直接读取 --phase 值",根因在于 gsd-core 在 ADR-3473 §8.4("failure is a value") 之后推行严格参数校验:parseNamedArgs 不再宽容地接受未声明的 flag 或游离位置参数,而是返回带类型的 Result;parseNamedArgsOrExit(command-arg-projection.cts 第 231-242 行)在失败时调用 error() 并以 ERROR_REASON.USAGE 退出。
如果直接在 handler 里读取 --phase 的值而不做规约,就会遇到两个问题:
- 裸
--phase 60留下的60会成为"被拒绝的游离位置参数"——严格校验会直接报 usage error,而不是按用户意图解析阶段号; - 若绕过校验(旧版宽容解析路径),则会回到"把
--phase当阶段名"的静默错误——这正是 pre-#3884 时期的问题:--phase 01-stub会静默传下去当阶段名搜索(见 adr857-core-without-capabilities.test.cjs 第 285-288 行注释)。
因此 normalizePhaseAlias 在解析器之前完成规约:把 --phase <N> / --phase=N 折叠成调用方槽位 args[2] 中的位置值,让后面的 parseNamedArgsOrExit 看到与位置参数形式逐字节相同的 argv。这样既保留了严格校验的 fail-closed 语义,又实现了 flag 形式与位置形式的完全等价。
五、测试验证:#3865 测试组的完整覆盖
修复配套的回归测试位于 tests/init.test.cjs 第 924-986 行,以 #3865 为分组标记,覆盖六个场景:
| 测试 | 命令 | 断言 |
|---|---|---|
--phase <N> 形式 |
init execute-phase --phase 03 |
phase_found === true 且 plan_count === 1(直指"7 plans read as 0"事故) |
--phase=N 形式 |
init execute-phase --phase=03 |
phase_found === true |
| 位置参数控制组 | init execute-phase 03 |
phase_found === true(确认旧形式未回归) |
plan-phase |
init plan-phase --phase 03 |
phase_found === true |
verify-work |
init verify-work --phase 03 |
phase_found === true |
code-review |
init code-review --phase 03 |
phase_found === true |
无值 --phase |
init execute-phase --phase |
success === false(非零退出),且诊断消息必须包含 --phase 字样 |
最后一条尤为重要:它锁定了"无值的 --phase 必须报错,绝不静默返回 phase_found:false"这一行为契约——即用户少传了值,也要得到明确的用法诊断,而不是一个看似成功实则空结果的 JSON 输出。
六、实操示例:在 SDK 查询与工作流中的用法
修复后,gsd-sdk query init.* 阶段作用域查询的三种合法写法等价:
# 形式一:双 token flag(本次修复新增)
gsd-tools query init.execute-phase --phase 03
# 形式二:单 token 内联值(本次修复新增)
gsd-tools query init.execute-phase --phase=03
# 形式三:位置参数(原有行为,保持不变)
gsd-tools query init.execute-phase 03
同样的规则适用于 init.plan-phase、init.verify-work、init.code-review、init.review、init.phase-op、init.discuss-phase-assumptions、init.todos。
实际工作流中,这些查询常以 --pick 形式被上层使用。例如 resolve-verify-command-path-findings.md 第 33 行展示了从前一阶段继承已验证命令的惯用法:
gsd-tools query init.plan-phase N --pick prior_verify_commands
此时 N 即阶段号,可安全替换为 --phase N 或 --phase=N 形式,行为完全一致——这使脚本化调用(尤其是阶段号来自变量、需要避免位置参数拼接歧义的场景)更加健壮。
七、小结
- 问题本质:flag 形式的 init 阶段查询曾把字面
--phase当作阶段名搜索,静默返回phase_found:false/plan_count:0,曾造成"磁盘 7 份计划被读成 0"的事故。 - 修复方案:
init-command-router.cts的normalizePhaseAlias在严格解析前把--phase <N>/--phase=N规约到位置槽位args[2],使两种形式与位置参数形式严格等价;无值--phase报错退出(fail-closed)。 - 适用范围:8 个阶段作用域 init 子命令全部生效,与
phase list-plans的双形式契约对齐。 - 验证保障:init.test.cjs 的 #3865 测试组覆盖两种新形式、位置参数控制组、3 个附加子命令以及无值报错分支,防止回归到"静默空结果"。
如果希望深入底层,可继续阅读 init-command-router.cts(路由器与规约函数)、command-arg-projection.cts(严格参数解析与 isFlagToken)、command-aliases.cts(init 子命令登记表),以及 init.cts(cmdInitExecutePhase、cmdInitPlanPhase、cmdInitVerifyWork 等下游实现)。