get-shit-done 查询分发架构升级:Command Topology 单一接缝如何统一命令解析、适配器绑定与无匹配诊断
本仓库(get-shit-done,下称 GSD)以
.changeset/目录下的变更片段(fragment)作为每次用户可见改动的发布说明载体。本文围绕.changeset/blue-stones-topology.md这条类型为Changed的片段展开,剖析它背后的真实源码改动:GSD SDK 的query命令分发被「Command Topology Module」加深重构。读完你将理解:gsd-sdk query的输入是如何经过一个单一拓扑接缝(topology seam)完成命令 token 解析、原生 handler 适配器绑定与结构化无匹配诊断的,以及这套设计如何提升局部性(locality)并抑制分发接缝漂移(dispatch seam drift)。
一、变更片段到底说了什么
.changeset/blue-stones-topology.md 全文只有一段摘要性质的条目:
---
type: Changed
---
**Query command dispatch deepened with Command Topology Module** — query dispatch
now consumes a single topology seam that resolves command tokens, binds native
handler adapters, and returns structured no-match diagnosis, improving locality
and reducing dispatch seam drift.
逐词拆解这段工程语言,可以得到四个可验证的核心论断:
- 单一拓扑接缝(single topology seam):所有「token → 命令 → 处理器」的决策集中到一个名为
CommandTopology的抽象上,而非散落在分发函数的多处分支中。 - 解析命令 token:拓扑的
resolve()负责把gsd-sdk query <tokens...>的原始输入解析成规范命令名与剩余参数。 - 绑定原生 handler 适配器:解析成功后,拓扑直接返回可直接执行的原生适配器(adapter),并附带输出模式与是否变更型命令等元数据。
- 结构化无匹配诊断:解析失败时返回的是一份携带
normalized、attempted、hints、message的结构化诊断对象,而不是一句干巴巴的报错字符串。
文章后续各节将以源码为准逐一印证并深化这四条,最后落到「locality 提升」与「dispatch seam drift 减少」这两个工程目标上。
二、前置背景:query 分发曾经存在的接缝漂移问题
要理解这次 Changed 的动机,先看改动前的接口形态。在 query-dispatch.ts 中,QueryDispatchDeps 携带了两种相互平行的原生分发方式:
export interface QueryDispatchDeps {
registry: QueryRegistry;
projectDir: string;
ws?: string;
cjsFallbackEnabled: boolean;
resolveGsdToolsPath: (projectDir: string) => string;
/** @deprecated use topology */
dispatchNative?: (cmd: string, args: string[]) => Promise<QueryResult>;
/** @deprecated use topology */
nativeAdapter?: QueryNativeDispatchAdapter;
topology: CommandTopology;
}
注意两处 /** @deprecated use topology */ 注释——这正是「接缝漂移」的历史痕迹:dispatchNative 回调与 nativeAdapter 对象曾经并存,调用方需要自行决定走哪条路,命令解析逻辑也可能在多处重复实现,任何一处漂移都会导致行为不一致。重构后的方向是:所有路径统一收敛到 topology 一个接缝上,旧的 dispatchNative / nativeAdapter 字段仅作为向后兼容的遗留接口保留。
从整体架构看,query-cli-adapter.ts 是 SDK 查询命令的 CLI 层入口,它演示了新的装配方式:
const registry = createRegistry();
const topology = createCommandTopology(registry);
const out = await runQueryDispatch({
registry,
projectDir: runtime.projectDir,
ws: runtime.ws,
cjsFallbackEnabled: queryFallbackToCjsEnabled(),
resolveGsdToolsPath,
topology,
}, input.queryArgv ?? []);
整个流程只有两个创建点:createRegistry() 提供命令注册表,createCommandTopology(registry) 把注册表包装成拓扑接缝。随后 runQueryDispatch 内部不再关心「怎么把一个命令解析出来」——那是 topology 的职责。
三、Command Topology 模块的公开契约
模块主体位于 sdk/src/query/command-topology.ts。先看类型层(对应「结构化」这一设计目标):
export type CommandTopologyOutputMode = 'json' | 'text' | 'raw';
export interface CommandTopologyMatch {
kind: 'match';
canonical: string; // 规范化后的命令名,如 state.json
args: string[]; // 剥离命令后的剩余参数
output_mode: CommandTopologyOutputMode;
mutation: boolean; // 是否为变更型命令(写磁盘等副作用)
adapter: QueryHandler; // 绑定好的原生 handler
}
export interface CommandTopologyNoMatch {
kind: 'no_match';
attempted: string[]; // 实际尝试过的命令形态
normalized?: string; // 归一化后的输入
hints: string[]; // 给用户的提示数组
message: string; // 组装好的完整错误消息
}
export type CommandTopologyResult = CommandTopologyMatch | CommandTopologyNoMatch;
export interface CommandTopology {
resolve(tokens: string[], fallbackRestricted?: boolean): CommandTopologyResult;
}
值得注意的契约要点:
- 结果用**可辨识联合(discriminated union)**表达,以
kind: 'match' | 'no_match'区分成功与失败,消费方无需异常机制即可穷尽处理两种结局。 CommandTopology接口只有一个方法resolve,入参是原始 token 数组 +fallbackRestricted布尔(用于告知 CJS 兜底是否被禁用,进而在诊断消息中提示用户),出参是联合结果。这一窄接口正是「单一接缝」的最小表面。match结果的mutation、output_mode字段是把「命令能力元数据」与「命令解析」耦合在同一处返回,供上层在真正执行前做策略判断(见第五节的安全护栏)。
从文件行文看,resolve 的空命令处理、命令解析、handler 缺失三种情况都会返回 no_match,其中命令解析失败与 handler 缺失都复用同一个 diagnoseUnknownCommand 诊断器,保证错误口径统一。
四、深度解析:resolve 内部的三步流水线
createCommandTopology(registry) 返回的 resolve 实现(sdk/src/query/command-topology.ts)可以拆成清晰的四段处理逻辑:
4.1 空命令守卫
const command = tokens[0];
const args = tokens.slice(1);
if (!command) {
return {
kind: 'no_match',
attempted: [],
hints: [],
message: 'Error: "gsd-sdk query" requires a command',
};
}
首 token 缺失直接短路,给出与上层校验完全一致的提示文案(与 query-dispatch.ts 中 validateQueryDispatchInput 的 missing_command 错误消息呼应),避免空输入继续走昂贵的注册表查询。
4.2 命令 token 解析与前缀匹配
核心解析委托给 query-command-resolution-strategy.ts,resolveQueryCommand 先做 normalizeQueryCommand 归一化,再进入 resolveQueryTokens:
export function resolveQueryCommand(command: string, args: string[], registry: QueryCommandRegistryLike): QueryCommandResolution | null {
const [normCmd, normArgs] = normalizeQueryCommand(command, args);
return resolveQueryTokens([normCmd, ...normArgs], registry);
}
归一化规则(sdk/src/query/query-command-resolution-strategy.ts)处理了大量「口语化子命令」到「点分规范命令」的映射,例如:
scaffold→phase.scaffold;- 裸的
state(无参数)→state.load; state <sub>、verify <sub>、init <sub>、phase <sub>、phases <sub>、validate <sub>、roadmap <sub>各自查对应的STATE_SUBCOMMANDS/VERIFY_SUBCOMMANDS/INIT_SUBCOMMANDS/PHASE_SUBCOMMANDS/PHASES_SUBCOMMANDS/VALIDATE_SUBCOMMANDS/ROADMAP_SUBCOMMANDS集合(这些集合由 sdk/src/query/command-aliases.generated.js 生成);- 一组
MERGE_FIRST_WITH_SUBCOMMAND命令(state、template、frontmatter、milestone、workstream、intel、uat、todo、check等)在后续参数非-开头时合并为点分形式,如milestone complete→milestone.complete。
真正的前缀匹配在 matchRegisteredPrefix(sdk/src/query/query-command-resolution-strategy.ts):从最长前缀开始,逐段把 token 组拼成 dotted(点分)与 spaced(空格分)两种形态并查注册表,命中即返回命令与剩余参数;若直接匹配失败,resolveQueryTokens 还会尝试「展开首个点分 token」(state.json → state json)后再匹配,从而同时兼容 state json 与 state.json 两种书写习惯。解析结果的 matchedBy(dotted / spaced)与 expanded 字段保留了解析来源,供诊断与调试回溯。
4.3 适配器绑定与命令能力标注
解析命中后(sdk/src/query/command-topology.ts):
const adapter = registry.getHandler(matched.cmd);
if (!adapter) {
// 走 diagnoseUnknownCommand,与解析失败同口径
}
return {
kind: 'match',
canonical: matched.cmd,
args: matched.args,
output_mode: supportsRawOutputCommand(matched.cmd) ? 'raw' : 'json',
mutation: supportsMutationCommand(matched.cmd),
adapter,
};
这一步完成了「命令名 → 可执行函数」的绑定(registry.getHandler(matched.cmd)),并顺带通过 supportsRawOutputCommand / supportsMutationCommand(位于 sdk/src/query/query-policy-capability.ts)打上能力标签:默认输出为 json,若命令声明支持 raw 输出则切换为 raw。adapter 的类型签名(见 sdk/src/query/utils.ts 的 QueryHandler)说明它需要接收 (args, projectDir, ws),由上层统一注入项目上下文。
4.4 结构化无匹配诊断
当解析失败或 handler 缺失时,diagnoseUnknownCommand(sdk/src/query/command-topology.ts)组装结构化诊断:
const noMatch = explainQueryCommandNoMatch(command, args, registry);
const normalized = [noMatch.normalized.command, ...noMatch.normalized.args].join(' ');
const attempted = noMatch.attempted.dotted.slice(0, 2);
const hints = [...UNKNOWN_COMMAND_HINTS];
const attemptedSuffix = attempted.length > 0 ? ` Attempted dotted: ${attempted.join(' | ')}.` : '';
const fallbackClause = fallbackRestricted ? `${describeFallbackDisabledPolicy()} ` : '';
const message = `Error: Unknown command: "${normalized}". ${hints[0]} ${hints[1]} ${fallbackClause}${hints[2]}${attemptedSuffix}`;
诊断数据的来源是 query-command-resolution-strategy.ts 的 explainQueryCommandNoMatch——它复用同一条匹配函数,把「实际上尝试过哪些 dotted / spaced 前缀」记录进 attempted 对象,真正做到「报错与解析共用一套逻辑,永不同步丢失」。
最终诊断消息的组成是模板化、可组合的:
hints来自独立的 sdk/src/query/query-unknown-command-hints.ts(UNKNOWN_COMMAND_HINTS),是可维护的提示文案常量;- 若
fallbackRestricted为真(即 CJS 兜底被关闭),会追加describeFallbackDisabledPolicy()(sdk/src/query/query-fallback-policy.ts)产出的策略说明; - 尝试过的点分前缀会以
Attempted dotted: a | b的形式显式列出,帮助用户在输入错误时快速推理出正确命令形态。
五、上层编排:runQueryDispatch 如何消费这个接缝
拓扑接缝的真正消费者是 query-dispatch.ts 的 planQueryDispatch,它把分发决策收敛为一个显式的 DispatchPlan:
const resolved = topology.resolve(queryArgv, !cjsFallbackEnabled);
if (resolved.kind === 'match') {
return { mode: 'native', normalized: { ... }, matched: resolved };
}
if (cjsFallbackEnabled) {
return { mode: 'cjs', normalized: { ... }, matched: null };
}
return {
mode: 'error',
normalized: { ... },
matched: null,
noMatchMessage: resolved.message,
noMatchNormalized: resolved.normalized,
noMatchAttempted: resolved.attempted,
noMatchHints: resolved.hints,
};
这个三态机(DispatchMode = 'native' | 'cjs' | 'error')揭示了完整的分发优先级:
- topology 命中原生命令 →
native模式,直接使用matched中绑定的适配器或dispatchNative/nativeAdapter执行; - topology 未命中但 CJS 兜底可用(由环境变量
GSD_QUERY_FALLBACK控制,见 query-cli-adapter.ts,设为off/never/false/0即关闭)→cjs模式,回退到传统 CLI 调用; - 两者都不可用 →
error模式,把拓扑产出的message、normalized、attempted、hints原样透传给错误分类器unknownCommandError,保证用户看到的诊断与拓扑内部分析完全一致。
runQueryDispatch(sdk/src/query/query-dispatch.ts)在 native 分支执行前还有一个值得单独说明的防误写护栏(源码注释标记 #3259):当调用携带 --help/-h 且命中命令被拓扑标注为 mutation: true(变更型命令)时,会短路返回一个非变更的 usage 帮助 stub,而不是把 --help 透传给变更型 handler。注释明确指出,这防止了例如 milestone.complete --help 意外把里程碑产物写到磁盘。这正是第四节「能力标注」字段的实战价值——mutation 元数据在真正执行前启用了 fail-closed 的安全策略。
执行阶段的最终调用有两路(sdk/src/query/query-dispatch.ts):优先使用 nativeAdapter.dispatch(旧的接缝遗留形态),否则直接调用拓扑绑定的 matched.adapter(matched.args, deps.projectDir, deps.ws)。返回数据再经 formatSuccess(json/text 两种格式化与 --pick 字段提取)统一序列化为 CLI 输出。
六、测试如何锁定契约
模块级测试位于 sdk/src/query/command-topology.test.ts,用两条用例直接钉住上文所述契约:
it('resolves native command with adapter', () => {
const registry = createRegistry();
const topology = createCommandTopology(registry);
const out = topology.resolve(['state', 'json']);
expect(out.kind).toBe('match');
expect(out.canonical).toBe('state.json'); // 归一化:state json → state.json
expect(out.args).toEqual([]);
expect(typeof out.adapter).toBe('function'); // 适配器已绑定
});
it('returns no_match with diagnosis', () => {
const topology = createCommandTopology(createRegistry());
const out = topology.resolve(['unknown-cmd'], true);
expect(out.kind).toBe('no_match');
expect(out.message).toContain('Unknown command');
expect(out.attempted.length).toBeGreaterThanOrEqual(0);
});
第一条用例验证「token 解析 + 适配器绑定」双职责(state json 被归一化为点分命令 state.json 且拿到可调用的 adapter);第二条验证 fallbackRestricted = true 时未知命令仍返回结构化 no_match 与诊断消息。此外,sdk/src/query/query-dispatch.test.ts 覆盖了从 runQueryDispatch 到 CLI 输出的端到端行为,确保拓扑的结果在计划阶段(planQueryDispatch)与执行阶段(runQueryDispatch)间稳定传递。
七、这套改造如何服务「locality」与「seam drift」两个目标
回到变更片段摘要里的两个价值主张,结合源码可以得出清晰结论:
提升局部性(improving locality)。命令解析、能力标注、错误诊断、适配器绑定这些原本可能散落在分发函数、CLI 适配器与错误分类器中的关注点,现在全部内聚到 sdk/src/query/command-topology.ts 及它直接依赖的解析策略文件中。新增一个查询命令时,开发者只需要关心注册表(registry)与能力声明文件,分发管线无需改动——这是典型的高内聚低耦合重构。
减少分发接缝漂移(reducing dispatch seam drift)。改动前 dispatchNative 与 nativeAdapter 两个平行的「接缝」并存且都被标记为 @deprecated,容易造成调用方对路径选择的分叉。引入 CommandTopology 后,planQueryDispatch 的三种模式(native / cjs / error)共享同一次 topology.resolve 结果,诊断消息与解析逻辑复用同一匹配函数(explainQueryCommandNoMatch 与 resolveQueryCommand 底层共用 matchRegisteredPrefix),从结构上杜绝了解析口径与报错口径不一致的漂移。
从变更管理角度,这条 Changed 级别的片段遵循仓库的 .changeset/README.md 约定:每个 PR 向 .changeset/ 投放一个随机命名的 fragment,发布时由 scripts/changeset/cli.cjs 的 render 命令按 type(Added / Changed / Deprecated / Removed / Fixed / Security,见 scripts/changeset/parse.cjs)分组合并进 CHANGELOG.md。因此 blue-stones-topology 这条片段不仅是本模块的权威变更记录,也是读者追踪该重构从「发布说明」回溯到「实现源码」的最佳入口。
八、如何在当前仓库中复现与验证
若想亲身体验上述行为,可在本仓库 sdk/ 目录下运行模块测试:
# 在 sdk/ 子项目内执行(依赖该目录下已配置的 vitest)
npm test -- query-dispatch command-topology
其中 sdk/src/query/command-topology.test.ts 会直接验证 resolve 的 match/no_match 契约,sdk/src/query/query-dispatch.test.ts 验证完整分发链路。阅读代码时建议按以下顺序建立心智模型:
- sdk/src/query/command-topology.ts — 拓扑接缝的类型与实现(本改动的核心交付物);
- sdk/src/query/query-command-resolution-strategy.ts — 归一化、前缀匹配、no-match 轨迹采集;
- sdk/src/query/query-dispatch.ts — 三态分发计划与执行编排;
- sdk/src/query/query-cli-adapter.ts — CLI 入口处的装配示范;
- sdk/src/query/query-dispatch.test.ts 与 sdk/src/query/command-topology.test.ts — 行为契约的回归守护。
小结
.changeset/blue-stones-topology.md 用一句话概括了一次并不简单的架构收敛:gsd-sdk query 的分发从「多接缝并存、解析与报错各写一套」演化为「单一 Command Topology 接缝」。在该接缝内,token 解析遵循一套可追溯的归一化与前后缀匹配策略,成功路径返回已绑定 adapter 并标注 output_mode 与 mutation 能力的结果,失败路径返回携带 normalized / attempted / hints / message 的结构化诊断。理解这条变更,等于同时掌握了这个仓库查询子系统的核心架构与它的变更管理方法——这也是开发者在类似规模项目中降低接缝漂移、提升模块局部性时可以借鉴的工程样本。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0634
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java01
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java00
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00