【免费下载链接】gsd-core
Git. Ship. Done - Core
本篇技术文章围绕 gsd-core 仓库中归档的变更集 .changeset/archived/migration-batch-8-ts.md 展开:Batch 8 将cjs-command-router-adapter、phase-command-router、surface、roadmap-upgrade四个模块从手写 CommonJS 迁移为 TypeScript 源码(src/*.cts),由tsc在发布时编译为被 git 忽略的.cjs产物。读完本文,你将掌握 ADR-457「build-at-publish」生成模型的决策依据、四个模块的迁移落点与关键源码结构,以及tsconfig.build.json中.cts → .cjs的编译链路细节。
一、变更集原文:Batch 8 迁了什么
关联文档 migration-batch-8-ts.md 是一份已归档的 changeset,完整内容如下(frontmatter + 正文 + 文档豁免注释):
type: Changed pr: 537 Migrate 4 modules from hand-written CommonJS to TypeScript source of truth per ADR-457 (#537). Modules migrated: cjs-command-router-adapter, phase-command-router, surface, and roadmap-upgrade. 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. -->可以从中提取出本批次迁移的五个关键事实:
- 变更类型为
Changed(行为保持不变的能力变更,而非 Breaking); - 迁移对象为 4 个模块:
cjs-command-router-adapter、phase-command-router、surface、roadmap-upgrade; - 单一事实源从
bin/lib/<m>.cjs(手写)转移到src/<m>.cts(TypeScript); - 行为契约是 "byte-for-behaviour"(行为逐字节等价),只增加 strict 类型,不改运行时语义;
- 对用户无感——产物落在与原
require()路径相同的位置,因此文档系统标注docs-exempt,无需更新任何用户面向文档。
从归档目录结构看,这类迁移是成系列推进的:.changeset/archived/下存在 migration-batch-2-ts.md 至 migration-batch-15-ts.md 等一批同构 changeset,例如 Batch 7 迁移的是planning-workspace、runtime-artifact-layout、command-routing-hub、drift(见 migration-batch-7-ts.md)。Batch 8 是这个逐模块增量迁移序列中的一个标准批次。
二、ADR-457:为什么选择 build-at-publish 生成模型
Batch 8 的依据是 docs/adr/457-generated-cjs-single-source.md(状态 Accepted)。理解这份 ADR 是理解整批迁移的前提,因为它解释了"为什么产物不入 git"。
2.1 两种都叫"生成"的技术,只有一种是刚需
ADR-457 首先厘清了一个容易混淆的点——仓库里"生成"一词指代两种完全不同的技术:
- 值烘焙(value baking,已存在且被迫):唯一的
@generated文件 gsd-core/bin/lib 中的package-identity.cjs由 scripts/generate-package-identity.cjs 生成——一个读取package.json并把坐标字面值"烤进" CJS 模块的纯 Node 脚本。其存在是被安装树结构强制的:安装后的目录中不存在带.name的package.json,运行时require('package.json').name要么undefined要么MODULE_NOT_FOUND,值只能在构建期烘焙。删除生成器,复杂度会散布到每个消费者,因此它是一个"深层接缝"。 - 转译(transpilation,本次提议):把
bin/lib的逻辑用 TS 编写、由tsc输出.cjs。ADR 的"删除测试"结论很尖锐:删掉转译管线后什么都不会重新出现——手写的.cjs与tsc产出的.cjs在运行时行为完全一致。转译的接缝不购买任何运行时杠杆,其全部价值在于编写期与 CI 的类型检查。
因此package-identity不构成转译工作的先例;二者是不同的技术、不同的强制函数。
2.2 三种"生成物归宿"模型与决策
ADR 把承重问题归结为:生成的.cjs是入库还是作为构建产物?
| 模型 | 做法 | 评价 |
|---|---|---|
| 1. 双入库 | .ts源码与.cjs产物都提交 git | 制造永久性"两份副本必须一致"不变量,需要 parity 测试、双提交、CI 漂移门禁;对转译而言纯属无运行时收益的摩擦,拒绝 |
| 2. 发布时构建(采纳) | bin/lib/*.cjs成为 gitignored 构建产物,由tsc从src/TS 树产出,npm 发布构建后的产物 | 无漂移不变量、无 parity 测试、无双提交;代价是本地开发与 CI 需要先构建再测试,采纳 |
| 3. 安装时构建 | 用户安装时编译 | 跨 Node 版本与平台脆弱、拖慢每次安装,拒绝 |
ADR 采纳模型 2 的可行性论证:package.json的files数组已经发布gsd-core与scripts目录,且已有prepublishOnly预发布构建步骤——.cjs输出钩挂进同一步骤即可,npm pack会包含磁盘上的产物而不管.gitignore。
决策清单共五条:以 TSsrc/树 +tsc追求类型安全(模型 2);值烘焙独立保留(package-identity.cjs维持现状);增量迁移、从耦合最低的模块开始,先以一个试点 PR 建立src/树与构建接线;先把 lint 配置与现实对齐(修正对 12 个手写文件的"已生成"误标);随模块逐个转为 TS 而接入类型感知 lint,直到最后一个手写.cjs消失才退役tsconfig.lint.json。
2.3 对测试行为的影响
ADR 明确指出主要行为变化:测试若 importbin/lib/*.cjs,只有构建已执行时才能工作——测试命令必须依赖构建步骤。这与".cjs永远在树中"的旧状态不同。这也是为什么本批次 changeset 能标注docs-exempt:require()路径未变,产物行为一致,用户侧零感知,而内部约束(先构建后测试)由 CI 接线承担。
三、Batch 8 四个模块的源码落点
四个模块迁移后均位于src/下,文件扩展名为.cts(CommonJS TypeScript),且每个文件头部都带有同一段 ADR-457 迁移说明注释,这是本系列迁移的统一标记规范:
ADR-457 build-at-publish: the hand-written
bin/lib/<m>.cjscollapsed to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour from the prior hand-written.cjs; only types are added.
3.1cjs-command-router-adapter:CJS 命令族路由适配器
源码:src/cjs-command-router-adapter.cts(共 196 行,全文可见)。
该模块为gsd-tools.cjs命令族提供兼容路由,核心导出两个函数(文件末尾export = { routeCjsCommandFamily, routeHubCommandFamily }):
routeCjsCommandFamily:以family: '__legacy_cjs_family__'委托给 Hub 路由;routeHubCommandFamily:基于CommandRoutingHub的族路由适配器,把各路由器的"查找 + 错误处理"分支收敛为 Hub 的带类型Result契约(见 src/command-routing-hub.cts)。
从源码结构看,迁移时新增的类型集中在 src/cjs-command-router-adapter.cts#L36-L64:Handler类型与RouteCjsCommandFamilyOptions/RouteHubCommandFamilyOptions两个选项接口,后者对error回调签名做了"诚实化"加宽——由单参加宽为(message: string, reason?: string) => void,以匹配io.cts中error()实际接受第二参ERROR_REASON的运行时契约。
另一个迁移中保留的精细行为细节(src/cjs-command-router-adapter.cts#L145-L152):向 Hub 注入观测日志器仅在isAuditEnabled()为真时进行,否则注入undefined让 Hub 回退到 no-op logger——这正是"byte-for-behaviour"承诺的具体体现:审计未开启时,默认分派输出(含--json-errors包络)保持逐字节静默。
3.2phase-command-router:phase 子命令的路由器
源码:src/phase-command-router.cts(共 328 行)。
文件头注释(L1-L16)说明了模块职责:清单驱动的 phase 子命令路由器,保持gsd-tools.cjs薄化且现有命令语义不变;scaffold不由本路由器处理(走顶层 scaffold 命令);mvp-mode、tdd-applicable是 CJS-only 子命令,在 Hub 之前直接分派。
迁移新增的类型包括 L33-L50 的PhaseHandlers接口——把cmdPhaseAdd、cmdPhaseInsert(含allocation?: 'nested' | 'sibling')、cmdPhaseComplete、cmdPhaseUatPassed(含policy选项)等 10 个 handler 的签名全部显式化,以及RoutePhaseCommandOptions。这些类型把此前隐藏在函数体里的参数契约(例如opts: { force: boolean })固化为可被tsc --strict检查的声明。
3.3surface:运行时能力面状态管理
源码:src/surface.cts(共 825 行,是本批次体量最大的模块)。
该模块管理运行时的启用/禁用"能力面"状态——每个运行时配置目录根(如~/.claude)中的.gsd-surface.json标记,与安装期的 profile 标记(.gsd-profile)相互独立(见文件头 L1-L29)。有效技能集的算法在头注释中一句话给出:
Effective skill set = base profile ∪ explicitAdds − disabledClusters − explicitRemoves,再经 manifest 传递闭包。
导出面为readSurface/writeSurface/resolveSurface/applySurface/listSurface/pruneSkillDirs六个函数。头注释还记录了 ADR-857 phase 4c 引入的可选registry参数:提供能力注册表对象时,capability 簇会并入有效簇映射;缺省时行为与注册表出现前完全一致。
从源码结构看,迁移后该模块依赖关系全部类型化声明:installProfiles、real-home-guard以import x = require('./...cjs')的 CJS 互操作形式引入(配@typescript-eslint/no-require-imports禁用注释),CLUSTERS与ClusterMap类型分别从./clusters.cjs按值/按类型导入——export =风格模块与import x = require()的配合,正是 ADR 中"tsc CJS 互操作细节"这一开放问题在落地代码中的直接答案。
3.4roadmap-upgrade:Phase ID 迁移工具
源码:src/roadmap-upgrade.cts(共 654 行)。
该模块负责把遗留的Phase N形式 phase ID 转换为带里程碑前缀的Phase M-NN形式。迁移后可见的核心正则(L23-L35):
LEGACY_PHASE_HEADING_RE:匹配遗留标题### Phase N: Name(含小数形式Phase 2.1:,可选[...]方括号前缀),分组捕获哈希数、阶段号与行内其余部分;MIGRATED_PHASE_HEADING_RE:匹配已迁移的### Phase M-NN: Name,用于幂等判断;MILESTONE_HEADING_RE:匹配## v1.0、## Roadmap v2.0、## ✅ v1.0等里程碑小节标题。
配套的ParsedPhaseEntry/AssignedMapping接口(L39-L50)把"哪一行、旧编号是什么、已迁移与否"解析为结构化数据。值得注意的是,这些正则并非迁移时新写——PHASE_NUMBER_TOKEN_SOURCE直接复用自 src/phase-id.cts,避免同一模式在升级工具与 ID 解析器中双写漂移。
四、构建接线:.cts如何编译为 gitignored.cjs
Batch 8 的编译行为由 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.
关键编译器选项:
| 选项 | 取值 | 作用 |
|---|---|---|
rootDir/outDir | src/gsd-core/bin/lib | TS 源树与产物目录一一对应,src/<m>.cts落到bin/lib/<m>.cjs,保持原require()路径 |
module/moduleResolution | nodenext | 按 Node 原生规则解析,.cts扩展名在此模式下天然产出.cjs(无需改扩展名) |
target/lib | ES2022/["ES2022", "ES2025.RegExp"] | 运行时基线为 ES2022;额外拉入 ES2025 正则类型(如roadmap-upgrade用到的正则特性) |
strict | true | 即 changeset 所承诺的 "only strict types are added" |
esModuleInterop | true | 支撑源码中import x = require(...)与export =互操作 |
noEmitOnError | true | 类型错误即阻断产物输出,防止带病.cjs进入发布包 |
incremental/tsBuildInfoFile | true/tsconfig.build.tsbuildinfo | 增量编译,加速逐批次迁移期间的构建 |
declaration/sourceMap | false/false | 产物最小化,不附带 d.ts 与 sourcemap |
另有 tsconfig.json 继承tsconfig.build.json但叠加"noEmit": true,作为编辑器/CI 的类型检查配置——即开发期tsc只查错不出产物,出产物仅发生在发布构建链路中。两个配置共同落实了 ADR-457 "发布时构建"模型:类型检查与产物产出分离。
一个命名细节需要说明:changeset 原文写产物路径为get-shit-done/bin/lib/<m>.cjs,而当前 tsconfig.build.json 的outDir是gsd-core/bin/lib。从归档 changeset 604-rename-get-shit-done-to-gsd-core.md 的存在可推断,该目录经历过get-shit-done→gsd-core的重命名;两者指向的是同一类产物目录,阅读旧 changeset 时应按此对应。
五、迁移纪律与行为不变式
Batch 8 的四个文件共同体现了一套可复用的迁移纪律,也是后续 batch(9–15)沿用的模式:
- 头注释契约:每个迁移文件头部的 ADR-457 注释块是审计锚点,明确记录"原手写文件已坍缩为 TS 单一事实源、行为逐字节保留、仅加类型"。搜索
ADR-457 build-at-publish即可定位全部已迁移模块。 - 类型只加不改:以
phase-command-router为例,PhaseHandlers接口是把既有运行时参数形态显式化,而非改变分派逻辑;surface的六个导出函数签名保持不变。 - lint 豁免的最小化使用:源码中
import x = require('./xxx.cjs')形式仅在被导入模块使用export =值导出时出现,并逐行附// eslint-disable-next-line @typescript-eslint/no-require-imports——豁免是行级、可审计的,不是目录级放行。 - 观测行为冻结:
isAuditEnabled()决定是否注入日志器(见 3.1 节),保证默认输出(含 stderr 行数)与迁移前一致。 - 验证闭环:产物路径不变意味着既有针对
bin/lib/*.cjs的测试在构建后继续有效;ADR-457 同时要求测试命令依赖构建步骤,这是该模型下唯一的流程变化点。 - 类型感知 lint 渐进接入:按 ADR 决策第 5 条,随着
src/*.cts模块增多,类型感知 lint 接入真实的tsconfig.json;配套的 ESLint 配置与自定义规则集见 eslint.config.mjs 及 eslint-rules/ 目录(ADR-452 是其前置,状态 Accepted)。
六、小结
.changeset/archived/migration-batch-8-ts.md 记录的不是一次孤立的"换语言",而是 ADR-457 所确立的 build-at-publish 生成模型在运行时表面上的标准推进动作:src/<m>.cts成为单一事实源,tsc(配置见 tsconfig.build.json)在发布链路中输出 gitignored 的bin/lib/<m>.cjs,require()路径与运行时行为逐字节不变。Batch 8 覆盖的cjs-command-router-adapter、phase-command-router、surface.cts、roadmap-upgrade.cts 分别对应命令族路由、phase 子命令分派、运行时能力面状态、Phase ID 升级工具四条关键链路——读者可沿文中给出的相对路径在当前仓库中逐一核对类型声明、正则契约与互操作写法,验证"只加类型、不改行为"这一迁移承诺的落地方式。
【免费下载链接】gsd-core
Git. Ship. Done - Core
相关推荐
gsd-core 的 TypeScript 单源迁移:ADR-457 下第 10 批 9 个命令路由模块从手写 CJS 到 build-at-publish 的落地
gsd core 的 TypeScript 单源迁移:ADR 457 下第 10 批 9 个命令路由模块从手写 CJS 到 build at publish 的
gsd-core 的 ADR-457 build-at-publish 迁移:从手写 CJS 到 TypeScript 单一事实源的批次化落地
gsd core 的 ADR 457 build at publish 迁移:从手写 CJS 到 TypeScript 单一事实源的批次化落地 本文基于 gsd
gsd-core ADR-457 迁移第三批次:10 个运行时模块转向严格 TypeScript 源码与 Build-at-Publish 构建流程
gsd core ADR 457 迁移第三批次:10 个运行时模块转向严格 TypeScript 源码与 Build at Publish 构建流程 本文为 g
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考