☰
gsd-core 的 ADR-457 构建即发布实践:commands 与 state 枢纽模块的 TypeScript 迁移(Batch 14)
2026/9/25 10:10:54 网站建设 项目流程

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载

本文以 gsd-core 仓库中已归档的变更集.changeset/archived/migration-batch-14-ts.md为主体,讲解 ADR-457 “TypeScript 源码为唯一事实源、.cjs为发布期构建产物”这一迁移模型在一次真实批次(PR #537)中的落地:commands与state两个枢纽模块如何从手写 CommonJS 迁入src/TS 树、构建管线如何保证require()路径不变且行为逐字节一致,以及迁移对本地开发与 CI 的具体影响。读完本文,你可以完整理解 gsd-core 的 CJS→TS 迁移机制,并知道如何验证迁移产物的正确性。

变更集说了什么:Batch 14 的迁移内容

本次迁移的原始变更记录(migration-batch-14-ts.md)内容如下,它是理解整件事的骨架:

--- type: Changed pr: 537 --- Migrate 2 hub modules from hand-written CommonJS to TypeScript source of truth per ADR-457 (#537). Modules migrated: `commands` (~1305 LOC, 17 exported functions including `cmdCommit`, `cmdStats`, `cmdWebsearch`, `cmdEffortSync`, etc.) and `state` (~2074 LOC, 28 exported functions including `readModifyWriteStateMd`, `acquireStateLock`, `cmdStateBeginPhase`, `cmdStateSync`, etc.). Each `src/<m>.cts` compiles to a gitignored `get-shit-done/bin/lib/<m>.cjs` with behaviour preserved byte-for-behaviour; only strict types are added. <!-- docs-exempt: Internal ADR-457 build-at-publish source migration; behaviourally-identical gitignored artifacts at same require() paths; no user-facing change. -->

从中可以提取出四个关键事实:

  1. 迁移对象是两个“枢纽(hub)”模块:commands(约 1305 行,17 个导出函数,含cmdCommit、cmdStats、cmdWebsearch、cmdEffortSync等)与state(约 2074 行,28 个导出函数,含readModifyWriteStateMd、acquireStateLock、cmdStateBeginPhase、cmdStateSync等)。之所以称“枢纽”,是因为这两个模块是 CLI 命令路由与状态机读写的主干,依赖面广、出错影响面大。
  2. 迁移方向:从bin/lib/下手写.cjs移到src/下的 TypeScript 事实源(.cts扩展名),由tsc在构建期编译回同名.cjs。
  3. 行为契约:产物在require()路径上不变、行为逐字节一致(byte-for-behaviour),唯一新增的是 strict 类型标注——这对一个被多运行时 hook 大量require的运行时层至关重要。
  4. docs-exempt 声明:变更集尾部显式声明这是内部构建迁移,无用户可见变化,因此豁免了“每个变更必须更新文档”的检查。

值得注意的一个命名细节:变更集里写的是get-shit-done/bin/lib/<m>.cjs,这是该包的旧名称;当前仓库的构建输出目录已随包名重命名为gsd-core/bin/lib(见下文构建管线)。阅读归档变更集时需要以当前tsconfig.build.json的outDir为准。

背景:ADR-457 为什么选择“构建即发布”

Batch 14 并非孤立动作,而是 ADR 457: Generation model forbin/lib/*.cjstype safety(已接受)的落地批次之一。该 ADR 在决策前先用“删除测试”厘清了一个常被混淆的概念——仓库里存在两种完全不同的“生成”:

  • 值烘焙(value baking,已存在且被迫):package-identity.cjs必须由 generate-package-identity.cjs 在构建期把package.json的坐标值“烤”进 CJS 模块,因为安装树里根本没有可读取的package.json(name为undefined或MODULE_NOT_FOUND,对应 bug #378)。删除生成器,复杂度会散布到每个消费者——这是真正的深缝(deep seam)。
  • 转译(transpilation,本批次所属):把bin/lib逻辑用 TS 编写、由tsc发射.cjs。删除它之后没有任何东西重新出现——手写.cjs与tsc发射的.cjs在运行时行为上完全等价。它的价值全部在于编写期与 CI 的类型检查。

基于这个区分,ADR 457 在三种模型中选择了模型 2——build-at-publish(构建即发布):

模型做法ADR 457 的结论
1. 双份入库.ts源码与生成的.cjs同时提交拒绝:制造“两份必须一致”的漂移不变量,需要 parity 测试、双份提交、CI 漂移门禁,换来的运行时收益为零
2. 构建即发布.cjs变为 gitignored 构建产物,由src/经tsc发射,npm 发布构建输出采纳:漂移治理机制整体消失,而非被构建出来
3. 安装期构建在用户安装时编译拒绝:跨 Node 版本/平台脆弱(CONTEXT.md 记录了 Windows / Node 24 风险),且拖慢每次安装

ADR 还给出了两条与 Batch 14 直接相关的决策:值烘焙保持独立(package-identity.cjs继续作为入库的烘焙产物,不受此 ADR 影响);逐模块增量迁移,先从耦合最低的模块试点,再成批推进。.changeset/archived/目录下的migration-batch-10-ts.md至migration-batch-15-ts.md等一系列同 PR(#537)变更集,正是这种增量节奏的存档证据——例如 migration-batch-13-ts.md 迁移了template、uat、workstream、roadmap、audit五个模块,migration-batch-15-ts.md 迁移了phase、verify、init并为永久手写的package-identity.cjs补了src/package-identity.d.cts声明文件(让 strict.cts源码能在 nodenext 模块解析下导入它)。Batch 14 恰好位于这个序列中间,承担的是“枢纽模块”这一最难的一环。

构建管线:.cts如何变成 gitignored 的.cjs

迁移后的构建管线在仓库里有完整可查的实现证据。核心配置是 tsconfig.build.json:

{ "//": "ADR-457 build-at-publish: compile TS runtime sources in src/ to gitignored .cjs artifacts under gsd-core/bin/lib/. Source uses the .cts extension so tsc emits .cjs natively. As modules migrate, they move from hand-written bin/lib/*.cjs into src/*.cts here.", "compilerOptions": { "rootDir": "src", "outDir": "gsd-core/bin/lib", "module": "nodenext", "moduleResolution": "nodenext", "target": "ES2022", "lib": ["ES2022", "ES2025.RegExp"], "types": ["node"], "strict": true, "declaration": false, "sourceMap": false, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "noEmitOnError": true, "skipLibCheck": true, "incremental": true, "tsBuildInfoFile": "tsconfig.build.tsbuildinfo" }, "include": ["src/**/*.cts"] }

逐项对照变更集中的承诺,可以看到管线如何兑现它们:

  • rootDir: src+outDir: gsd-core/bin/lib:源码文件名与产物文件名一一对应。src/commands.cts→gsd-core/bin/lib/commands.cjs,src/state.cts→gsd-core/bin/lib/state.cjs。.cts扩展名被 tsc 原生识别为 CommonJS,直接发射.cjs,无需额外后缀改写——这正是文件头注释强调的“Source uses the .cts extension so tsc emits .cjs natively”。
  • strict: true:对应变更集里“only strict types are added”——迁移唯一的行为面变化就是把这两个模块纳入了严格类型检查。
  • noEmitOnError: true:类型错误会阻断产物发射,保证不会把编译失败的旧.cjs当成本次构建输出,这与 ADR 457 “把类型错误变成编译错误而非 lint 发现”的目标一致。
  • module/moduleResolution: nodenext:匹配运行时实际的require()解析语义。ADR 457 的开放问题之一——tscCJS interop(esModuleInterop、__importDefaultshim)对相互require的模块的影响——在此以esModuleInterop: true落地。

发布侧的接线在 package.json 中:

"prepublishOnly": "npm run build:lib && npm run build:hooks", "build:lib": "tsc -p tsconfig.build.json"

prepublishOnly钩子保证npm publish之前一定先跑tsc -p tsconfig.build.json;而package.json的files数组负责把构建输出(包括gsd-core/bin/lib/下的.cjs)打进 tarball。这里恰好实现了 ADR 457 里那句关键判断:“npm packincludes on-disk artifacts regardless of.gitignore”——.gitignore挡得住 git,挡不住 pack,所以“产物不入库”与“发布物完整”可以兼得。

“产物不入库”本身也有直接证据:.gitignore 中明确列有

/gsd-core/bin/lib/commands.cjs /gsd-core/bin/lib/state.cjs

即这两个文件在源码仓库里是被忽略的构建输出,而对应的入库事实源是 src/commands.cts 与 src/state.cts。

行为保持:为什么“byte-for-behaviour”是可验证的契约

变更集承诺的“behaviour preserved byte-for-behaviour”落在三个层面:

  1. require()路径不变。所有既有消费者(CLI 入口、hook 脚本、测试)继续require('.../bin/lib/commands.cjs'),没有任何调用方需要改动。ADR 457 的 Consequences 部分把“对裸 checkout 直接读bin/lib/*.cjs的工具”列为迁移代价,缓解方式就是“先构建或改读src/”。
  2. 导出面不变。commands的 17 个导出函数、state的 28 个导出函数在迁移后一一对应。以state为例,acquireStateLock(状态锁获取,含 #3772 系列修复的非 EEXIST 处理与重试白名单)、readModifyWriteStateMd(STATE.md 读改写,自身持有锁并带 #948 no-op 守卫)、cmdStateBeginPhase/cmdStateSync(阶段开始与状态同步命令)都在 src/state.cts 中以带 JSDoc 与类型标注的函数形态存在;commands侧的cmdEffortSync同样在 src/commands.cts 中可见其完整实现与对cmdEffortSyncCodex/cmdEffortSyncOpencode两个子路径的分发。
  3. 测试即回归网。ADR 457 明确写了测试侧的唯一行为变化:“Tests importingbin/lib/*.cjskeep workingonly ifthe build has run; the test command must depend on the build.” 这正是迁移后npm run build脚本链(generate:identity → build:lib → gen:section-manifest → gen:context-index → gen:plugin-skills → gen:loop-host-contract → gen:capability-registry → build:hooks)存在的意义:CI 必须先构建再跑测试,测试套件中对commands/state既有行为的全部断言(锁竞争、状态读写、命令路由)天然构成迁移等价性的验证。

从源码结构看,迁移后.cts文件行数(src/commands.cts约 4364 行、src/state.cts约 6870 行)远大于变更集记载的迁移时点(约 1305/2074 行),说明这两个枢纽模块在 Batch 14 之后仍持续演化——增量迁移的模型允许模块在迁入 TS 树后以 strict 类型约束继续演进,这正是 ADR 457 相比“双份入库”模型的核心收益:后续只维护一份事实源。

对开发者的实际影响与验证方式

综合变更集、ADR 与构建配置,Batch 14 迁移对使用者与贡献者意味着:

  • 终端用户零感知。npm 安装/更新拿到的仍是gsd-core/bin/lib/commands.cjs与state.cjs,require()路径与 CLI 行为完全不变——这就是 docs-exempt 注释中 “no user-facing change” 的技术含义。

  • 本地开发多一步构建。在源码仓库中运行依赖bin/lib/*.cjs的行为前,需要先执行npm run build:lib(或完整的npm run build);tsconfig.json(noEmit: true,继承tsconfig.build.json)则为编辑器/CI 提供不发射产物的类型检查配置。

  • 验证迁移等价性的三条路径:

    1. 查 .gitignore 确认产物确实未被提交(/gsd-core/bin/lib/commands.cjs、/gsd-core/bin/lib/state.cjs均在忽略列表);
    2. 运行npm run build:lib后用tsc -p tsconfig.build.json的noEmitOnError保证 strict 类型零错误;
    3. 依赖既有测试套件对state锁语义与commands命令路由的断言做回归。
  • 架构脉络可溯源。本次批次向上承接 ADR 457 的模型选择,向下服务于 ADR 174: Retire GSD SDK package boundary 所确立的“src/为唯一手写事实源、tsc取代一切.generated.cjs生成器脚本”的单运行时收敛方向;与之相关的 ESLint 前置设施(ADR 452)则保证迁移过程中.cjs表面持续受 lint 约束。

小结

migration-batch-14-ts.md所记录的 Batch 14,是 gsd-core 把 ADR-457 “构建即发布”模型推过最难一关的存档:两个合计约 3400 行、45 个导出函数的枢纽模块,在require()路径与运行时行为完全不变的约束下,把手写 CommonJS 的整体事实源切换到了src/的 strict TypeScript 树。其机制要点可归纳为四点:.cts→.cjs的原生发射(tsconfig.build.json)、prepublishOnly+files数组保证发布物完整而产物不入库(.gitignore佐证)、strict 类型作为唯一增量(strict: true/noEmitOnError: true)、以及“测试依赖构建”作为行为保持的回归保障。理解了 Batch 14,也就理解了 gsd-core 全部migration-batch-*-ts.md系列变更集共用的同一套迁移范式。

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载
上一篇:DLSS Swapper终极指南:一键管理游戏图形增强文件,释放显卡全部性能
下一篇:终极指南:3分钟掌握Switch游戏安装的完整解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询