get-shit-done CJS↔SDK 硬接缝迁移:Shared-Module 单一事实源架构实践
导读
本文以 docs/prd/3524-cjs-sdk-hard-seam.md(CJS↔SDK hard seam PRD)为骨架,完整讲解 get-shit-done 项目中 CJS 工具层(get-shit-done/bin/lib/*.cjs)与 SDK 层(sdk/src/**/*.ts)之间结构性的配置漂移(drift)问题,以及通过"每个 Shared Module 只有一个手写事实源(source of truth)+ 生成物 + 新鲜度检查 + 门禁 lint"六阶段迁移方案根治该问题类的完整落地路径。读完本文,你将掌握:如何复用仓库中 gen-command-aliases 既有先例,把一对一手写同步(hand-synced)的 .cjs/.ts 文件替换为"TS 源 → 双端生成物 → Adapter"的硬接缝架构,以及配套的逐阶段验收标准、回滚策略、风险矩阵与性能约束。
背景:CJS↔SDK 边界为什么"结构上可渗透"
get-shit-done 存在两个运行时:CJS 工具层(shell 脚本回兼容所需的 gsd-tools)与 SDK(TypeScript 查询层,gsd-sdk query)。两者之间共享的多个 Shared Module——STATE.md Document Module、Workstream Inventory Module 等——历史上以"字符级一致的手写同步对"形式存在,即同一份逻辑在 get-shit-done/bin/lib/*.cjs 和 sdk/src/**/*.ts 各写一遍。常量(CONFIG_DEFAULTS、VALID_CONFIG_KEYS)同样被定义两次。
此前的边界防线只有两道,均只能捕获部分漂移:
- 命名对等测试 tests/config-schema-sdk-parity.test.cjs;
- 只读 handler 的输出对等 golden 测试 sdk/src/golden/read-only-parity.integration.test.ts。
这两道防线漏掉了四类漂移,PRD 中逐一给出:
| 漂移类别 | 具体表现 |
|---|---|
| 默认值结构漂移 | #3523:顶层 branching_strategy 在 CJS 侧返回 'none',SDK 侧返回 'phase' |
| 告警/报错文案漂移 | #3523:CJS 侧误报告警,SDK 侧静默移植(silently grafts) |
| 变更路径漂移 | 两侧各自单测,缺少跨侧变更夹具(cross-side mutation fixture) |
| 新增 Module 漂移 | 一侧新增常量另一侧不知情,完全不可见 |
该问题类反复出现,历史 bug 清单包括 #1535、#1542、#2047/#2052、#2638/#2655、#2653/#2670、#2687/#2706、#2798/#2816、#3055/#3116、#3523——每一例都是"修复只落在了一侧"。仓库内 docs/agents/cjs-sdk-seam.md 的 Phase 6 回顾部分记录了 15 个此类配置漂移 bug 的逐条档案(如 #2638:loadConfig 把 sub_repos 写到顶层后被标记为未知键;#2047:intel.enabled 已文档化却不在 VALID_CONFIG_KEYS 导致 config-set 拒绝)。
既有先例:command-aliases 生成器模式
修复方案并非凭空设计,仓库中已经存在可工作的先例:命令别名(Command-Alias)模块。
sdk/scripts/gen-command-aliases.ts 是一个构建期生成器,它从 sdk/src/query/command-manifest.{state,verify,init,phase,phases,validate,roadmap,non-family}.ts 读取唯一的命令清单,一次生成两个产物:
sdk/src/query/command-aliases.generated.ts(SDK 侧的 TS 类型化别名表);get-shit-done/bin/lib/command-aliases.generated.cjs(CJS 侧的纯 JS 别名表,通过JSON.stringify序列化并导出module.exports)。
生成文件顶部自动插入 "GENERATED FILE — Source: …" 横幅,禁止手改。
配套的 sdk/scripts/check-command-aliases-fresh.mjs 是 CI 新鲜度门禁:它重新计算两侧期望值(TS 侧从编译产物 dist/query/*.js 导入 manifest,CJS 侧通过 require 加载已提交的生成文件),再与已提交产物逐项 JSON.stringify 比较,任何不一致都会抛出 "drift detected" 并令 CI 失败。PRD 明确将这一"一个 TS 源 → 双端生成物 → 两侧纯 Adapter 消费"模式推广到所有 Shared Module,并禁止继续手写同步对。
六步通用迁移配方
对每个待迁移的 Shared Module,PRD 规定如下机械式流程(以 TS 侧为事实源,因为其天然携带类型):
- 提升一侧为事实源:采用 TS 源(携带类型);
- 编写生成器
sdk/scripts/gen-<module>.ts,同时产出.generated.ts与.generated.cjs; - 编写新鲜度检查
sdk/scripts/check-<module>-fresh.mjs,仿照check-command-aliases-fresh.mjs; - 把手写 CJS 文件替换为薄 re-export:仅从生成文件转发导出,保留原文件名以维持调用方(如
workstream-inventory.cjs对state-document的require)不变; - 把新鲜度检查接入 CI;
- 稳定一个发布周期后,清理不再被引用的手写内容(移除死 re-export)。
另有常驻 CI lint scripts/lint-shared-module-handsync.cjs(Phase 6 引入)扫描 get-shit-done/bin/lib/<name>.cjs 与 sdk/src/query/<name>.ts(或 sdk/src/<name>.ts)中"两侧都非 *.generated.* 且不在显式 allow-list 中"的对,直接阻断任何新手写同步对合并。
Phase 1 的真实实现印证
Phase 1(STATE.md Document Module)已经落地,其产物可以在仓库中直接核对:
- 事实源
sdk/src/query/state-document.ts(纯变换模块,文件头注明 "does not read the filesystem and does not own persistence or locking"); - 生成器 sdk/scripts/gen-state-document.ts:加载编译后的
sdk/dist/query/state-document.js,通过Function.prototype.toString()提取导出函数体、通过源码文本扫描(花括号配平)提取内部非导出 helper(escapeRegex、toFiniteNumber、existingProgressExceedsDerived),拼接出get-shit-done/bin/lib/state-document.generated.cjs; - 新鲜度检查 sdk/scripts/check-state-document-fresh.mjs:在内存中重新生成期望内容并与已提交文件逐字节比对,退出码 0/1 表示新鲜/过期;
- CJS Adapter get-shit-done/bin/lib/state-document.cjs:正文只有一行
module.exports = require('./state-document.generated.cjs');,使state.cjs、workstream-inventory.cjs、init.cjs及测试的既有require('./state-document')无需改动。
这印证了 PRD 的判断:"删除测试在接触时即通过(deletion test passes on contact)"——两侧本已字符级一致,一侧成为生成物后另一侧立即可替换为 re-export,且无任何外部消费者受损。
六阶段迁移计划
Phase 1 — STATE.md Document Module(最小可行证明)
为何最先做:bin/lib/state-document.cjs 与 sdk/src/query/state-document.ts 已是字符级一致的手写同步对,且是纯变换模块(不读文件系统、不持有持久化或锁)。这是最安全的起步,也证明了生成器模式不仅适用于别名表,还适用于可执行逻辑。
范围:提升 TS 侧为事实源;编写 gen-state-document.ts 与 check-state-document-fresh.mjs;把 CJS 侧替换为薄 re-export(保留 state-document.cjs 文件名);把新鲜度检查接入 CI(与 check-command-aliases-fresh.mjs 并列)。
验收标准:
bin/lib/state-document.cjs只含对state-document.generated.cjs的 re-export;check-state-document-fresh.mjs在 CI 通过,且在故意失同步时失败;- 既有调用点(CJS:
state.cjs、workstream-inventory.cjs;SDK:state-mutation.ts、state-project-load.ts等)原样工作; - 两侧既有 STATE.md 单测全部通过;
- CONTEXT.md "STATE.md Document Module" 条目补充一句事实源路径说明。
回滚:直接 revert 分支;从 git 历史重新导入被删除的 CJS 文件内容即恢复原手写同步形态,无外部消费者受损。
Phase 2 — Configuration Module(关闭 #3523 问题类)
为何第二:这是触发本次工作的问题模块,是杠杆率最高的漂移面,同时考验模式能否从"纯变换模块"扩展到"消费数据清单的模块"。
范围:
- 先在 CONTEXT.md 增加 Configuration Module 条目,定义:"Module owning config load, legacy-key normalization, defaults merge, and explicit on-disk migration for
.planning/config.json",接口与不变量见 ADR §6; - 把
CONFIG_DEFAULTS、VALID_CONFIG_KEYS、DYNAMIC_KEY_PATTERNS、RUNTIME_STATE_KEYS抽取为两份数据清单sdk/shared/config-schema.manifest.json与sdk/shared/config-defaults.manifest.json(先例:sdk/shared/model-catalog.json); - 编写模块源
sdk/src/configuration/index.ts(导入两份清单,导出loadConfig、normalizeLegacyKeys、mergeDefaults、migrateOnDisk); - 编写
gen-configuration.ts与check-configuration-fresh.mjs; - 把
bin/lib/core.cjs:loadConfig(约 220–243、434–449、485 行)与bin/lib/config.cjs校验面替换为薄 Adapter,删除内联CONFIG_DEFAULTS、core.cjs:444-449的误报警告与重复的_deepMergeConfig; sdk/src/config.ts:mergeDefaults(192–218 行)改为 re-export;- 为四种 legacy-key 归一化(顶层
branching_strategy、顶层sub_repos、multiRepo: true、顶层depth)扩展read-only-parity.integration.test.ts的夹具矩阵。
验收标准:CONTEXT.md 含接口契约;CJS 两侧不再有本地 CONFIG_DEFAULTS/VALID_CONFIG_KEYS 字面量;#3523 夹具矩阵在 CJS 与 SDK 双路径通过且误报警告消失;golden parity 矩阵四形状全绿;#3523 以指向本阶段的 back-reference 关闭。
回滚:revert 分支后内联实现自 git 历史恢复,manifest 文件保持未引用即可。
当前仓库中 sdk/scripts/check-configuration-fresh.mjs 已存在,其实现完全遵循先例:动态导入 ./gen-configuration.mjs 的 buildConfigurationCjs() 在内存中重算期望内容,与 get-shit-done/bin/lib/configuration.generated.cjs 比对,并提供 cd sdk && npm run gen:configuration 的再生成指引。
Phase 3 — Workstream Inventory Builder 与剩余手写同步对
为何第三:Phase 1 证明了纯变换,Phase 2 证明了数据清单支撑的逻辑,Phase 3 则推广到其余同步对,其中 Workstream Inventory Module 是重点,因为它要求 Builder/Reader 拆分——投影逻辑是纯且可共享的,而目录遍历在 CJS(同步 fs)与 SDK(异步 I/O)两侧是合理不同的。
范围:
- 编写 Builder 源
sdk/src/workstream-inventory/builder.ts:纯函数,接收目录条目列表 + 每个 workstream 的 STATE.md 文本 + plan-scan 结果,返回类型化的WorkstreamPhaseInventory/WorkstreamInventory投影,不做任何 fs 读取; gen-workstream-inventory-builder.ts产出 CJS 与 SDK 双侧生成物;check-workstream-inventory-builder-fresh.mjs把关;bin/lib/workstream-inventory.cjs重构为同步 Reader Adapter(fs.readdirSync+readFileSync读 STATE.md 后调用 Builder);sdk/src/query/workstream-inventory.ts重构为异步 Reader Adapter(同形态、异步 I/O、调用 Builder);- 审计其余疑似对(
frontmatter.cjs↔frontmatter-mutation.ts、plan-scan.cjs↔plan-scan SDK 等价物),可共享者套用 Builder 模式;结构性重复者(路由表、不同返回形态的 sync/async 等)在阶段 issue 中记录决策并推迟。
验收标准:两侧不再共享投影逻辑而都调用生成 Builder;CONTEXT.md 反映拆分;两侧 workstream golden 测试通过;范围内每个模块都有独立新鲜度检查;未迁移模块在阶段 issue 中留下段落级推迟说明。
Phase 4 — Project-Root Resolution Module
范围:CONTEXT.md 新增条目,接口为 findProjectRoot(startDir)、findEffectiveRoot(startDir, options);事实源 sdk/src/project-root/index.ts(纯函数,注入 fs probe 或直接用 node:fs,两运行时均同步可用);生成器与新鲜度检查;bin/lib/core.cjs:74-140 与 sdk/src/helpers.ts:497-630 替换为薄 Adapter;对独立项目、含 planning.sub_repos 的 monorepo、legacy multiRepo: true、深层嵌套四种配置扩展对等测试。
验收标准:findProjectRoot 在源码形态上只定义一次;两侧都导入生成模块;四种配置的对等测试全部通过。
Phase 5 — CJS Command Router Adapter:委托给 SDK runtime bridge
为何第五:Phase 1–4 消灭了"共享逻辑"的漂移,Phase 5 则消灭"并行逻辑"的漂移——即 CJS 侧各命令族的 state/verify/init/phase/roadmap/validate handler 实现。落地后,经 gsd-tools 运行的每个 canonical 命令与 gsd-sdk query 执行同一个 SDK handler,进程内执行、无子进程跳转,接缝成为真正的墙。
范围:
- 修订 CONTEXT.md "CJS Command Router Adapter Module" 条目,记录 runtime-bridge 委托;
- 为 CJS 调用方暴露同步友好的入口:
QueryRuntimeBridge.execute()本为异步,新增executeForCjs(input) → { exitCode, stdoutChunks, stderrLines }同步包装(在deasync或受控runUntil语义下运行 dispatch;若同步桥接不可行,退回 Worker 通道上的Atomics.wait,绝不引入gsd-sdk子进程); - 把
bin/lib/*-command-router.cjs中各 canonical 族的handlersmap 替换为生成式委托发射器:每子命令调用executeForCjs({ canonical, argv, env, cwd })并经由既有 CJS 输出 Adapter 写出; - 按顺序逐族迁移:
state.*、verify.*、phase.*、phases.*、validate.*、roadmap.*、init.*、frontmatter.*、config.*,加上 sdk/src/query/command-manifest.non-family.ts 中列出的非族命令,一族一个子 PR,合入前运行 golden parity 矩阵; - CJS-only 模块(
graphify、gsd2-import、schema-detect、fallow-runner、intel、drift、installer-migrations)保留进程内 CJS 实现,不进入 canonical 族注册表; - 扩展 sdk/src/golden/golden.integration.test.ts,对 manifest 中每个 canonical 命令验证
gsd-tools与gsd-sdk query的退出码 + stdout 块 + stderr 行完全一致。
验收标准:executeForCjs 落地;command-manifest.*.ts 中每个 canonical 族经 executeForCjs 路由,CJS-only 命令继续走既有 CJS handler;每个族的 CJS handler 文件不再含命令特定逻辑(仅剩委托接线或被删除);golden parity 矩阵对全部 canonical 命令验证两侧输出等价;gsd-tools 每次调用的子进程开销不增加(桥接在进程内)。
逐族回滚:每个族的 PR 可独立 revert;未迁移族的手写 handler 留在 git 历史中,委托回退时从历史恢复。
Phase 6 — 执行加固与回顾
范围:
- 编写 scripts/lint-shared-module-handsync.cjs:扫描
get-shit-done/bin/lib/<name>.cjs与sdk/src/query/<name>.ts(或sdk/src/<name>.ts)中两侧均非*.generated.*且不在显式 allow-list 的配对,allow-list 记录"协作兄弟"例外(如实现结构性不同的路由文件); - 核对 Phase 1–4 每个 Shared Module 都有接入 CI 的新鲜度检查;
- 核对 Phase 5 的 golden parity 矩阵覆盖每个 canonical 族;
- 为
sdk/src/<module>/**、sdk/shared/*.manifest.json与sdk/src/query-runtime-bridge.ts增加 CODEOWNERS 规则(架构组评审); - 回顾式遍历反复 bug 清单(#1535 … #3523),在
docs/agents/cjs-sdk-seam.md中记录每个 bug 会被哪层执行机制拦截(handsync lint、新鲜度检查、清单数据隔离、逐模块漂移 lint、runtime-bridge 委托); - 把 docs/agents/cjs-sdk-seam.md 写成面向 CONTRIBUTING 的指南,说明如何新增 Shared Module 与新增 canonical 命令。
验收标准:handsync lint 进 CI 并演示能拦截故意的回归 PR;Phase 1–4 全部模块进入新鲜度检查工作流;Phase 5 矩阵在触碰 bin/lib/* 或 sdk/src/query/* 的每个 PR 上运行;CODEOWNERS 就位;回顾文档提交;任何重新引入 #3523 反模式或绕过 runtime-bridge 委托的 PR 无法落地。
值得对照的是 docs/agents/cjs-sdk-seam.md 已记录六个阶段全部落地(#3531/#3540/#3548/#3554/#3558/#3574/#3577),并给出 Phase 6 的 15 个历史漂移 bug 回顾矩阵——例如 #1535(loadConfig 静默忽略未知顶层键)会被 handsync lint 拦截、#2638(sub_repos 写错位置)会被逐模块漂移 lint 拦截,每一条都映射到五层执行机制之一。
跨阶段关注点
向后兼容
CJS 公共 CLI 表面(gsd-tools <subcommand>)不变:标志位、退出码、stdout 形态全部保留。每一阶段都在既有 Module 接口之后替换内部实现,外部契约由既有 golden parity 套件加新增夹具矩阵固定。
性能
全程无子进程开销:生成的 .cjs 是可直接 require 的 CommonJS 模块,SDK 直接消费 TS 源;跨全部阶段合计每次 require 的模块加载成本 ≤ 10 ms。Phase 5 特别保持进程内模型——executeForCjs 与 CJS 分发器同 Node 进程运行 SDK handler,不调用 gsd-sdk 子进程,同步桥接相对此前的直接 CJS handler 调用每次仅增加至多几微秒,主导成本仍是既有 dispatch policy 开销。
构建/安装管线影响
- 生成器只在开发机构建期(以及 CI 中的新鲜度检查)运行,无运行时生成;
- 已发布的
get-shit-done-cc包同时包含get-shit-done/bin/与sdk/dist/;生成的.cjs文件(与今日的command-aliases.generated.cjs一样)提交进仓库,安装流程不变——不做安装期代码生成; npm run build:sdk维持原行为;生成器沿用npm run gen:<module>既有先例调用。
风险矩阵
| 风险 | 可能性 | 缓解 |
|---|---|---|
| 提交间生成物与源漂移 | 中 | 每模块 check-<module>-fresh.mjs 在 PR 期拦截;先例已在 command-aliases 使用 |
| TS 源使用了 CJS 输出无法表达的特性 | 低 | 生成器只产出 CJS 兼容子集(源中禁 ESM-only 语法);既有 gen-command-aliases.ts 模板已覆盖 |
Phase 2 删除内联 _deepMergeConfig 改变微妙合并语义 |
中 | golden parity 矩阵是测试:夹具不一致则矩阵失败,合入前修订新模块 |
migrateOnDisk 上线静默改变升级可见行为 |
中 | migrateOnDisk 显式且 opt-in,安装器在下次升级调用一次并附 release-note;提供独立命令 gsd-tools migrate-config 手动调用 |
| CODEOWNERS 拖慢架构组响应 | 中 | 仅对事实源目录与清单应用 CODEOWNERS;Adapter 与 .generated.* 保持开放;架构组承诺 ≤ 24 h SLA |
| Phase 3 审计暴露超出预期的配对,范围蔓延 | 中 | 非 Phase 1/2 模块在阶段 issue 中做范围核查,不契合者记录理由后推迟 |
Phase 5 同步桥接机制没有干净形态(deasync 绑定 C++、Atomics.wait 需要 Worker、全量改同步 SDK handler 巨大) |
高 | Phase 5 spike 在任何族迁移前解决;若无干净机制,Phase 5 降级为只迁移 SDK handler 本就同步的族,其余转入后续增强 |
| Phase 5 族迁移回退可见 CJS 输出(退出码、stdout/stderr 形态) | 中 | 每族 golden parity 矩阵是门槛,矩阵在族内所有 canonical 命令全绿前不得合入 |
| Phase 5 因 runtime bridge 提前加载更多 handler 改变启动时间 | 低 | 桥后惰性加载 handler(SDK 已有该模型);迁移前后测量 time gsd-tools state load,中位延迟回退 >20 ms 则打回该族 PR |
待决开放问题(依赖它的阶段开始前解决)
- Phase 1 事实源位置:
sdk/src/state-document/index.ts(迁移)vssdk/src/query/state-document.ts(原地)——在 Phase 1 PR 起草时定; - Phase 2 清单格式:JSON vs JSONC vs TypeScript-as-source——Phase 2 决定,若无注释记录不变量需求则 JSON 胜出;
- Phase 3 兄弟模块审计:Builder 拆分 vs 推迟的精确配对清单——作为 Phase 3 spike 的交付物;
- Phase 5 同步桥接机制:
executeForCjs实现策略(deasync原生模块 / Worker 通道Atomics.wait/ 每个异步 handler 暴露同步入口)——在任何族迁移前的 spike issue 中定; - Phase 5 族迁移顺序:推荐最小只读族先行(大概率
frontmatter.*或config.*读路径)作为模式证明,再按复杂度递增迁移 state/verify/phase/roadmap/validate/init; - Phase 6 回顾格式:表格 vs 散文——起草回顾文档时定。
完成标准
#3524 的关闭条件是:六个阶段全部上线,每阶段有各自合并的 PR 关闭各自阶段 issue,且 Phase 6 回顾确认反复 bug 清单中的每个历史漂移 bug 都能被五层执行机制之一(handsync lint、新鲜度检查、清单数据隔离、逐模块漂移 lint、runtime-bridge 委托)拦截。
架构约束与设计要点总结
从 docs/adr/3524-cjs-sdk-hard-seam.md 及其 PRD 可以提炼出本方案区别于一般"去重重构"的四个关键约束:
- 以 Module 为单位,而非新增层标签:接缝词汇保持在既有 CONTEXT.md/LANGUAGE.md 框架内,不引入"shared core""shared data"等新层名,接缝所有权单位就是仓库处处在用的 Module。
- 不引入新构建工具:生成器就是既有
gen-command-aliases.ts形态;不做双 CJS+ESM bundler、不改package.jsonexports子路径、不做tsup/rollup决策。 - 每侧 I/O Adapter 合理不同:状态 Adapter、verify Adapter 等不进 Shared Module 表——CJS 用同步 fs/exec,SDK 用异步 I/O 与观测装饰器;只有其背后的纯变换(解析、投影、归一化)被抽进 Shared Module,golden parity 测试钉住跨接缝可观测行为。
- 事实源即所有权:有行为的模块事实源在
sdk/src/<module-name>/(TypeScript),纯数据的在sdk/shared/<module-name>.manifest.json;CJS 侧生成物get-shit-done/bin/lib/<module-name>.generated.cjs永不手编;defer给既有 ADR(0001/0003/0004/0006/0009/0011)的模块不在本方案范围内重复定义。
这套"一个事实源 + 双端生成物 + 逐模块新鲜度门禁 + 手写同步对禁用 lint + golden parity 输出等价"的组合,将原本靠人工纪律维持的跨运行时一致性,变成了靠 CI 机械强制的不变量——这正是该 PRD 对反复出现九次的漂移 bug 类给出的根治性答案。
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 StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00