首页
/ get-shit-done 硬接缝工程实践:PR 3577 出界模块清理、配置校验移植与单一默认值源落地解析

get-shit-done 硬接缝工程实践:PR 3577 出界模块清理、配置校验移植与单一默认值源落地解析

2026-09-07 20:28:48作者:翟江哲Frasier

导读:本文围绕 get-shit-done 仓库中 .changeset/3577-adr-violations-and-validation-port.md 这份「Fixed」类型变更记录展开,深度还原一次发生在 CJS 工具层(gsd-tools)与 TypeScript SDK(gsd-sdk)硬接缝(Hard Seam)边界上的系统性修复。你将看到:一个因违反 ADR 出界(out-of-seam)清单而在 64 GiB Docker 主机上 fork 出数百个 node 进程的无限递归事故是如何被定位与根治的;config-ensure-section 如何通过「目录重绑定」而非 CJS 回退来恢复旧契约;configNewProject 默认值为何必须取自规范化 Manifest;以及 configSet 的枚举/形状校验、init 处理器 --tdd、STATE.md 自动修剪、GSDError 语义等一批 SDK 侧行为为何需要与 CJS 逐项对齐。

背景:ADR-3524 与「CJS↔SDK 硬接缝」的单一来源政策

get-shit-done(gsd)采用双运行时架构:

  • CJS 工具层get-shit-done/bin/lib/*.cjsbin/gsd-tools.cjs,命令路由、同步 fs/exec;
  • SDK 层sdk/src/**/*.ts,异步 I/O、gsd-sdk query <canonical> 统一查询入口。

为了让两个运行时共享的每个模块都“只有一个手写真相源”,仓库用 ADR 记录了决策 docs/adr/3524-cjs-sdk-hard-seam.md,其配套 PRD 在 docs/prd/3524-cjs-sdk-hard-seam.md。核心规则(ADR §1)是:

  1. 每个 Shared Module 有恰一个手写真相源(行为模块在 sdk/src/<module>/ 的 TypeScript,纯数据模块在 sdk/shared/<module>.manifest.json);
  2. CJS 侧只允许机械生成的产物bin/lib/<module>.generated.cjs),绝不手工编辑;
  3. 每个模块配 freshness 检查(sdk/scripts/check-<module>-fresh.mjs)与漂移 lint;
  4. 手写成对复制(hand-synced pair)被禁止,由 scripts/lint-shared-module-handsync.cjs 在 PR 阶段拦截。

更重要的是 ADR §3「出界模块(Out-of-seam Modules)」:部分模块按决策保持 CJS-only,因为“没有 SDK 对应物”:

graphifygsd2-importschema-detectfallow-runnerinteldriftinstaller-migrations——这些模块在 CJS 侧保留进程内实现。

PRD 的 Phase 5(docs/prd/3524-cjs-sdk-hard-seam.md L160)明确写到:“CJS-only Module handlers(…inteldrift…)keep their in-process CJS implementations. They are not in the canonical family registry and do not route through the SDK runtime bridge.” 而 PR #3577 要解决的,正是 Phase 6 实施期间出现的一批 违反该政策的绑定(violation) 以及 SDK 侧缺失的行为移植(validation port)。

事故还原:verify.codebase-drift 的双向桥接无限递归

事故链路

变更记录的第一条披露了当时最严重的问题:verify.codebase-drift 与八个 intel.* 动词被错误地绑定进了 SDK catalog / manifest,直接违反 ADR §3。

错误绑定带来的是一段灾难性的调用链:

  1. SDK 侧存在一个 verifyCodebaseDrift 桩(stub);
  2. 这个桩通过 execFileSync 回执到 gsd-tools verify codebase-drift
  3. 而 CJS 侧的 verify 命令路由(command router)此时已按 Phase 5 切到 SDK runtime bridge,于是又把调用桥接回 SDK
  4. SDK → CLI → SDK 形成闭环,在远端 64 GiB Docker 主机上 fork 出数百个 node 进程,直到被人工 kill。

修复方式

  • 把所有错误绑定的条目从 SDK catalog 中移除
  • CJS 路由器与 gsd-tools.cjs 本来就有针对这些动词的直连 CJS 分发路径,现在它成为唯一路径。

当前仓库源码完整保留了这条决策的“现场注释”,可作为最直接的源码级证据。在 sdk/src/query/verify.ts 顶部有明确说明:

verify.codebase-drift handler intentionally NOT exported from the SDK. drift (bin/lib/drift.cjs) is out-of-seam, CJS-only per ADR/PRD … Previous Phase 6 stub (which execFileSync'd back to gsd-tools) created an infinite SDK→CLI→SDK recursion … observed forking hundreds of node processes on a 64 GiB host. The router now dispatches verify codebase-drift direct to verify.cmdVerifyCodebaseDrift, which is the canonical implementation.

同类型的不绑定声明还出现在:

需要说明的是,仓库中仍保留着 intel.* 的 SDK 侧实现文件 sdk/src/query/intel.ts(标注为从 bin/lib/intel.cjs 移植而来),本次变更的核心是把这些动词从“会经过 SDK 路由/catalog 绑定”的集合中移除,让它们只走 CJS 进程内分发——避免任何经 runtime bridge 绕一圈又回到 CJS 的路径存在。这也是该变更所在 changeset 命名为 “ADR violations and validation port” 的原因:前者清理绑定违例,后者把 CJS 已实现、SDK 缺失的校验逐项移植过去。

从这条事故中提炼的工程原则

  1. 禁止“自我回环”桥接:任何 adapter 如果通过子进程调用一个最终会路由回自己的命令,就是无限递归的温床。SDK↔CJS 之间的桥只允许“每侧一个入口、单方向完成”。
  2. 出界即不绑定:CJS-only 模块不应出现在 SDK catalog / manifest / family handlers 的任何注册表里,只留物理文件与测试引用是不够的——注册表才是“会路由它”的判定依据。
  3. 源码注释即决策日志verify.tscommand-family-handlers.ts 里保留的注释,让后续维护者无需回溯 git log 即可知道“为什么不 export / 为什么省略”,这正是仓库工程文档化的做法。

config-ensure-section:用 catalog 重绑定替代 CJS 回退

第二个修复涉及一个“旧契约回归”。历史上 CLI 的无参调用(例如 gsd-tools config-ensure-section 或初始化流程)会生成一份完整默认值 config.json(对应旧 CJS 的 ensureConfigFile → buildNewProjectConfig 链路)。Phase 6 最初把 catalog 里的 'config-ensure-section' 条目绑到了新的 configEnsureSection 处理器上——但该处理器是单节确保语义,必须要求 args[0]=sectionName。结果所有 CLI 调用方都调用的是无参形式,全部被破坏。

修复方案是在 catalog 里把该条目重绑定到 configNewProject,而不是试图给 CJS 侧打补丁回退:

  • configNewProject 的无参分支产出的形状与旧 ensureConfigFile → buildNewProjectConfig 链条完全一致;
  • SDK 路径恢复“无参 → 全量默认 config.json 初始化”契约。

sdk/src/query/config-mutation.ts 可以确认 configNewProject 的幂等语义:如果 .planning/config.json 已存在,直接返回 { created: false, reason: 'already_exists' },不会覆盖已有配置;只有缺失时才基于默认值构造并原子写入(返回 { created: true, path: '.planning/config.json' })。

实现细节还展示了它与旧行为对齐的努力(见 config-mutation.ts L588-L622 的注释):

  • 顶层中 resolve_model_idscontext_windowmodeplanninggraphify 被从 init 输出中省略——它们要么有独立的解析路径,要么属于用户另行配置的 opt-in 特性;
  • git.base_branch 也被省略:保留 null 会抑制 origin/HEAD 自动探测,从而破坏 ship-ready 预检;
  • 运行时通过环境变量(BRAVE_API_KEY / FIRECRAWL_API_KEY / EXA_API_KEY~/.gsd/ 下的 key 文件)探测三个搜索服务是否可用,并把 manifest 中的 false 默认值覆盖为探测结果。

与这条修复配套的分支 changeset 是 .changeset/3577-config-ensure-section-parity.md,说明仓库内部对“单节确保”与“全量初始化”两个语义做了显式区隔记录。

configNewProject 默认值:从硬编码漂移到 Manifest 单一来源

漂移的代价

修复前,configNewProject 里有一份硬编码重复的 defaults 对象。这份副本与 Manifest 脱节后,产生了大量“两边行为不一致”的隐性缺陷——变更记录明确点名了缺失的键族:

  • workflow.* 下的 ai_integration_phasetdd_modehuman_verify_modepattern_mapperplan_bounce*auto_prune_statesubagent_timeoutsecurity_*post_planning_gaps
  • git.create_tagclaude_md_path
  • planning.*graphify.*moderesolve_model_idscontext_window

修复:派生自 Configuration Module Manifest

修复把硬编码副本替换为从 Configuration Module 的 canonical manifest 派生:默认值定义在 sdk/shared/config-defaults.manifest.json,由 sdk/src/configuration/index.ts 导出为 CONFIG_DEFAULTS,再被 configNewProject 消费(见 config-mutation.ts 中的 import { CONFIG_DEFAULTS } from '../configuration/index.js')。

这份 Manifest 本身就是仓库“数据即真相源”的样本,值得展开看几个关键默认值(节选):

路径 默认值 说明
model_profile balanced 默认模型档位
context_window 200000 上下文窗口(CJS 起源的顶层键)
mode interactive 运行模式(CJS 起源顶层键)
git.branching_strategy none 分支策略(旧 CJS 顶层 branching_strategy 归一化后所在)
git.create_tag true 打标签开关
workflow.tdd_mode false TDD 模式
workflow.human_verify_mode end-of-phase 人工验证时机
workflow.subagent_timeout 300000 子代理超时(毫秒)
workflow.auto_prune_state false STATE.md 自动修剪开关
workflow.security_enforcement true 安全门强制
planning.sub_repos [] 子仓库列表
brave_search / firecrawl / exa_search false 运行时按 API key 覆盖

Manifest 顶部的 _comment 字段还记录了归一化决策:例如 CJS 扁平键 plan_checker 与 canonical 嵌套 workflow.plan_check 的差异已解决(canonical 名为 workflow.plan_check),安全相关键(security_enforcementsecurity_asvs_levelsecurity_block_on)与 post_planning_gaps 的 canonical 位置在 workflow.*。生产代码在落盘前会剥离 _comment 元数据(config-mutation.ts L585-L586)。

这次修复的价值在于它关闭了 ADR 所要预防的 DEFECT.PORT-DRIFT.cjs-sdk 缺陷族——当某键只在 CJS 或只在 SDK 生效时,两个运行时的行为就无法再由黄金测试锁住。

configSet 枚举与形状校验移植:把 CJS 已经强制的规则带到 SDK

第三个移植批次把 CJS cmdConfigSet 强制执行的枚举/形状校验器补齐到 SDK 的 configSet。逐项实现在 sdk/src/query/config-mutation.ts 都有对应代码与 CJS 行号注释,整理如下:

配置键 校验规则 合法值
workflow.drift_action 枚举 warnauto-remap
workflow.drift_threshold 正整数 >= 1 的整数
workflow.human_verify_mode 枚举 mid-flightend-of-phase
statusline.context_position 枚举 frontend
code_quality.fallow.scope 枚举 phaserepo
code_quality.fallow.profile 枚举 minimalstandardstrict
review.default_reviewers 数组形状 slug 必须匹配 ^[a-zA-Z0-9_-]+$,归一化为小写唯一,并以归一化值落盘

review.default_reviewers 是最复杂的形状校验:拒绝空数组、拒绝非字符串元素、拒绝含非法字符的 slug,去重后小写归一化,并且 parsedValue 被原地改写,持久化的是归一化形式config-mutation.ts L378-L420),保证 CJS 与 SDK 写入磁盘的值一致。

同一函数中还保留了一批其他布尔/枚举守卫,可作为“SDK 行为对齐 CJS”的完整参考:

  • git.create_tagworkflow.post_planning_gaps 拒绝非布尔值(防 config-set git.create_tag maybe 之类静默写脏,Bug #3086);
  • context 只允许 dev / research / review
  • ship.pr_body_sections 走专门的结构校验 validateShipPrBodySections(字段白名单、source selector 格式、模板 token 白名单等);
  • 未知键先查 CONFIG_KEY_SUGGESTIONS 纠错映射,再做 LCP 建议(isValidConfigKeyconfig-mutation.ts L163-L196)。

此外还带上了两个工程细节:写入采用临时文件 + rename 的原子写(失败回退直接写,atomicWriteConfig),并通过 acquireStateLock/releaseStateLock 保护读-改-写(D6),响应值用 maskIfSecret 掩码密钥类键(不回显明文凭据,但磁盘上的值不掩码)。

init 处理器:--tddworkflow.subagent_timeout

initExecutePhaseinitPlanPhase 两个 init 处理器补齐了两处与 CJS 的参数契约:

  • 解析 --tdd 布尔覆盖,与 CJS 路由器里的 parseNamedArgs(args, [], ['validate', 'tdd']) 以及 CJS handler 的 options.tdd || config.tdd_mode || false 对齐;
  • initMapCodebase 读取 subagent_timeout:从 canonical 位置 workflow.subagent_timeout 取值,并使用 Manifest 规定的 300000 默认值,取代原先的 undefined 兜底。

这一批改动说明 Phase 6 的移植不是机械照抄 handler,而是连参数解析的接受集合--tdd--validate)与默认值来源都要逐一与 CJS 对齐,否则同一个命令在两条路径上会有不同的行为下限。

roadmap.analyze 按阶段暴露 mode

此前只有 roadmapGetPhase 会解析状态/计划文本中的 **Mode:** 字段。本次让 roadmap.analyze 也提取同样的字段并按阶段暴露,使消费方无论走哪个查询 handler 都能读到 MVP-mode 标记,避免两个查询对同一事实给出不一致的视图。

这是一个典型的“同一概念双实现造成分歧”的微观案例:ADR-3524 的动机(#1535…#3523 的一长串双端漂移 Bug)本质上都是同构的——某个事实只在某一侧被解析/被暴露。

phaseComplete:配置开启时的 STATE.md 自动修剪

SDK 的 phaseComplete 补上了 CJS cmdPhaseCompleteworkflow.auto_prune_state === true 的行为分支:以 statePrune(['--keep-recent', '3', '--silent'], …) 的形态调用状态修剪(对应 issue #2087)。

它解决的问题是真实的日常漂移:完成 Phase N 之后,STATE.md 里 [Phase 1..N-3] 的陈旧决策应该被移除,而不是永久残留。若 SDK 侧不移植该分支,则:

  • 通过 gsd-sdk 走完成的用户永远不会触发修剪;
  • 状态文档随阶段推进无限膨胀,与 CJS 侧的状态文档生命周期策略(可参考 docs/STATE-MD-LIFECYCLE.md 所在的状态文档治理体系)产生分歧。

这类“默认关闭但开启后必须一致”的配置开关,正是黄金测试最难覆盖、却最容易在跨运行时移植时被漏掉的类型。

initRemoveWorkspace:用抛出的 GSDError 表达失败

错误语义的移植同样重要。此前 SDK 的 initRemoveWorkspace 在“未提供名字”与“工作区不存在”两个分支返回 { data: { error } }——但 CLI 输出路径把“返回 data”一律当作成功处理,导致命令错误码为 0、消息也没进 stderr。

修复后两个分支改为抛出 GSDError(..., ErrorClassification.Validation),从而:

  • CLI 返回非零退出码;
  • 消息写入 stderr;
  • 与 CJS 路径的错误形态一致,可被上层脚本可靠判断。

仓库中 GSDErrorErrorClassification 定义于 sdk/src/errors.tsconfig-mutation.ts 即从该处 import)。这个案例的普适教训是:跨层移植时,“值返回”与“异常抛掷”的选择必须按 CLI 契约而非内部便利来定——一个被当成功的错误返回,比没有实现更隐蔽。

frontmatterGet:支持 --field <name> 解析

最后一个移植点很“小”但很典型。CLI 实际调用 frontmatter get <file> --field phase 时,传给 handler 的 args[file, '--field', 'phase'];而旧 handler 把 args[1] 当作字段名,于是读到的是字面量字符串 --field,拿不到真正的字段名 phase

修复让 frontmatterGet 同时支持两种形式:

  • 带标志形式:--field <name>
  • 位置参数形式:args[1] 直接作为字段名。

这个案例说明跨运行时移植时连 argv 形态假设都要对齐——SDK handler 不能假设它只会收到“纯净”的位置参数,因为 CJS 侧调用方可能已经在传 --flag value 混合形态。

治理闭环:黄金测试与出界注册表的“事后验证”

本次变更并非孤立补丁,而是整个 ADR-3524 六阶段治理的一部分。观察 sdk/src/golden/golden.integration.test.ts,其中仍保留 verify.codebase-drift 的黄金比对用例(对比 gsd-tools verify codebase-drift 与 SDK registry 分发的 JSON 输出),同时仓库还配套了 .changeset/3577-docker-test-fixup.md(Docker 测试环境修复)等同行 changeset。

这一组合体现了 get-shit-done 仓库的三层防线模型:

  1. 注册表层(catalog / manifest / family handlers):决定“哪个命令会路由到哪一侧”,本次出界清理就发生在这里;
  2. 行为层(枚举校验、默认值来源、错误语义、argv 解析):决定“同一命令在两边的可观测行为是否一致”,本次 validation port 覆盖这里;
  3. 验证层(golden parity 测试 + changeset):在 CI 和变更记录两个层面锁住边界,让违反 ADR §3 的绑定和 PORT-DRIFT 缺陷无法再次混入。

小结:一次变更,两场战役

把 PR #3577 放回 ADR-3524 的坐标系看,它其实同时打了两场战役:

  • 清理违例:把 verify.codebase-driftintel.* 等出界动词从 SDK catalog / manifest 移除,恢复 ADR §3 / PRD L160 定义的“CJS-only 直接分发”唯一路径,并借源码注释为后来者留下事故现场;
  • 移植缺口:把 CJS 侧已强制的默认值单一来源(Manifest)、枚举/形状校验、--tdd 解析、STATE.md 自动修剪、GSDError 错误语义、frontmatter --field argv 解析逐项搬到 SDK,关闭 DEFECT.PORT-DRIFT.cjs-sdk 缺陷族。

对任何维护“双运行时、双语言、共享契约”代码库的团队,这条变更记录可提炼为可直接复用的操作清单:先查注册表里有没有不该被路由的模块,再查两边对同一输入是否给出相同的校验与失败语义,最后用黄金对比测试把差异钉死在 CI 里——而不是等 64 GiB 主机上的数百个 node 进程替你做压测。

关键文件索引

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388