【免费下载链接】gsd-core
Git. Ship. Done - Core
本文为 gsd-core(GSD Core,一个面向 AI 编码代理的元提示与上下文工程系统)的运行时类型化工程实践解读:围绕 changeset 记录 migration-batch-3-ts.md 描述的第三批次迁移,说明 10 个gsd-core/bin/lib运行时模块如何从手写.cjs转为src/*.cts严格 TypeScript 源码、由tsc在发布前编译为 gitignored 的.cjs产物,并解释“行为逐字节等价(byte-for-behaviour)”的工程约束、tsconfig.build.json的编译配置,以及本地构建与测试时如何触发编译。读完后你能掌握该项目“源码为真、产物不入库”的完整迁移与构建机制,并能在本地复现npm run build:lib的编译链路。
一、这一批次做了什么:changeset 原文的完整解读
migration-batch-3-ts.md 是一个 changeset 变更记录,frontmatter 标记type: Changed、pr: 537,正文记录了第三批次(batch 3)迁移的完整范围。原文明言:
Migrate 10 more
get-shit-done/bin/libruntime modules to TypeScript sources of truth (ADR-457 build-at-publish, batch 3): event, workstream-inventory-builder, plan-scan, fallow-runner, project-root, installer-migration-authoring, update-context, 000-first-time-baseline, runtime-homes, model-catalog.
即本批次将以下 10 个运行时模块迁移为 TypeScript 源码为真(source of truth):
| 模块(原文列名) | 迁移后的源码位置 |
|---|---|
| event | src/observability/event.cts |
| workstream-inventory-builder | src/workstream-inventory-builder.cts |
| plan-scan | src/plan-scan.cts |
| fallow-runner | src/fallow-runner.cts |
| project-root | src/project-root.cts |
| installer-migration-authoring | src/installer-migration-authoring.cts |
| update-context | src/update-context.cts |
| 000-first-time-baseline | src/installer-migrations/000-first-time-baseline.cts |
| runtime-homes | src/runtime-homes.cts |
| model-catalog | src/model-catalog.cts |
上述 10 个.cts源文件均已确认存在于当前仓库的src/目录中(其中event位于src/observability/子目录、000-first-time-baseline位于src/installer-migrations/子目录,说明源码树按职责保留了子目录分层)。
changeset 正文的后半句是本次迁移的核心约束:
Each moves to
src/*.cts(strict TS), compiled bytscto a gitignored.cjsat the samerequire()path; behaviour preserved byte-for-behaviour.
拆解为三点工程承诺:
- 迁移目标:每个模块进入
src/并以.cts扩展名承载严格 TypeScript(strict: true); - 构建方式:由
tsc编译为.cjs,且产物被 gitignore,不进入版本库; - 落点不变:编译产物输出到与原手写文件相同的
require()路径(即gsd-core/bin/lib/下同名.cjs),因此所有通过require()消费这些模块的调用方完全无感; - 行为等价:“byte-for-behaviour” 表示迁移前后运行时行为必须逐点保持一致——这是一次纯源码形态的重构,而非功能变更。
changeset 末尾还附有一条docs-exempt注释,说明理由:这是 ADR-457 build-at-publish 的内部源码迁移,产物在相同require()路径上行为等价、且不入库,对用户无任何可见变化(no user-facing change),因此豁免用户文档更新要求。这一自我标注本身就体现了该仓库对“哪些变更需要对外说明”的严格分级。
二、决策背景:ADR-457 的 build-at-publish 模型
本批次是 ADR-457(Generation model for bin/lib/*.cjs type safety,状态 Accepted)落地过程中的一环。理解 ADR-457 的决策逻辑,才能理解 changeset 中“gitignored .cjs”“同一 require() 路径”这些措辞的由来。
2.1 问题起点:类型安全是“二等公民”
ADR-457 的 Context 部分核实了当时的仓库真实现状:gsd-core/bin/lib/下有 84 个.cjs文件,其中只有 1 个带// @generated头(package-identity.cjs,且它是由脚本“烘焙值”生成的,不是tsc产物);没有 TS 源码树、没有 TS→CJS 转译管线。手写运行时表面的类型错误只能以 lint 发现项的形式(若被发现)浮现,而不是编译错误。
2.2 关键辨析:两种都被叫作“生成”的技术
ADR-457 指出决策的分水岭在于区分两种技术:
- 值烘焙(value baking,已存在且是被迫的):
package-identity.cjs必须生成,因为安装后的目录树里不存在携带.name的package.json,运行时根本读不到这些值,只能在构建期烘焙进 CJS 模块。删除生成器,复杂度会在每个消费方重新出现——这是一个“深接缝”(deep seam)。 - 转译(transpilation,本迁移采用的):把
bin/lib逻辑用 TS 编写、经tsc输出.cjs。删除它不会让任何复杂度回归——手写.cjs与tsc输出的.cjs在运行时行为相同,它的价值全部在于编写期与 CI 的类型检查。
因此package-identity不构成转译工作的前例;两者是不同的技术、不同的强制因素。
2.3 三种生成模型的取舍与最终决策
ADR-457 围绕“生成的.cjs是否入库”给出三个模型:
- 源码与产物双双入库——制造“两份必须一致”的永久不变量,需要 parity 测试、双提交、预提交/CI 漂移门禁;对转译而言没有任何东西迫使其成立,代价最大。
- Build at publish(推荐,被采纳):
bin/lib/*.cjs成为 gitignored 构建产物,由tsc从 TSsrc/树输出;npm 发布构建后的输出。ADR 特别论证了可行性:package.json的files数组本就携带gsd-core与scripts,且已有prepublishOnly预发布构建步骤,.cjs输出挂入同一链路即可;npm pack包含磁盘上的产物,与.gitignore无关。 - Build at install——被拒:跨 Node 版本与平台脆弱(CONTEXT.md 记录了 Windows / Node 24 的隐患),且拖慢每次安装。
最终决策要点(见 docs/adr/457-generated-cjs-single-source.md 的 Decision 节):
- 通过 TS
src/树 +tsc编译追求类型安全,采用模型 2(build at publish):TS 源码是唯一真源,.cjs是 gitignored 产物——“这消解了漂移治理机制,而不是建立它”; - 值烘焙保持独立:
package-identity.cjs继续以提交入库的烘焙产物形式存在; - 渐进迁移,从耦合最低的模块开始,先以一个试点 PR 建立
src/树与构建接线,再做批量迁移; - 先把 lint 配置与现实对齐(12 条
GENERATED_CJS_IGNORES名单实际指向手写文件,属“谎言”),再逐步接入类型感知 lint。
本 changeset 所属的“batch 3”正是第 3 条“渐进迁移”策略推进到第三批的产物:每一批继续“10 more”,源码与产物按模块逐一换轨。
2.4 ADR 的代价与后果(Consequences)
ADR-457 明确记录的正面/负面后果,直接解释了仓库现状:
- 正面:迁移后的运行时代码可应用类型感知的
typescript-eslint规则;因为不提交生成的.cjs,没有漂移不变量、没有parity 测试要维护;新bin/lib代码有了唯一被强制的答案(写 TS)。 - 负面:编辑
src/*.cts与运行bin/lib/*.cjs之间现在隔着一个构建步骤,本地开发与 CI 都必须在运行前构建;迁移会触及大量文件,可能暴露潜在类型错误;今天直接读取 checkout 中bin/lib/*.cjs的工具必须先构建。 - 对测试:导入
bin/lib/*.cjs的测试只有在构建已运行时才有效,因此测试命令必须依赖构建——这是相对“.cjs恒在树中”的旧状态最主要的行为变化。
这一条“测试依赖构建”正是当前仓库package.json中pretest脚本存在的直接原因(见下一节)。
三、构建管线实证:编译配置、npm 脚本与 gitignore
3.1tsconfig.build.json:发布编译的完整配置
编译产物由 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 | 源码树与产物目录的映射关系,保证编译输出落在原require()路径 |
module/moduleResolution | nodenext | 按 Node 的模块解析规则处理 ESM/CJS 互操作 |
target/lib | ES2022(含ES2025.RegExp) | 运行时目标与正则能力上限 |
strict | true | 严格 TypeScript,对应 changeset 中的 “strict TS” |
esModuleInterop | true | 解决 ADR 开放问题中提到的 CJS 互操作(__importDefaultshim) |
noEmitOnError | true | 类型错误时拒绝产出,防止带错产物进入发布包 |
declaration/sourceMap | false | 不产出类型声明与 sourcemap,保持产物面最小 |
incremental/tsBuildInfoFile | true/tsconfig.build.tsbuildinfo | 增量编译,加速重复构建 |
include | src/**/*.cts | 只编译.cts源文件 |
其中.cts扩展名是一个值得注意的细节:在 Node 的nodenext语义下,.cts文件按 CommonJS 解释,tsc对其原生输出.cjs(src/a/b.cts→ 产物a/b.cjs),无需任何重命名步骤即可维持“同一require()路径”。
3.2 编辑器/CI 类型检查与发布构建的双 tsconfig 分工
tsconfig.json 只有 8 行,通过extends复用tsconfig.build.json,并覆盖noEmit: true,include同为src/**/*.cts。其头部注释说明了分工:
Default editor/CI typecheck config. The emitting publish build stays in tsconfig.build.json.
也就是说:日常在编辑器或 CI 中做类型检查时用tsconfig.json(只检查、不产出),真正发布时产出.cjs的编译留在tsconfig.build.json。这正对应 ADR-457 决策第 5 条“随着模块转为 TS,把类型感知 lint 接入真实的tsconfig.json”。
3.3 npm 脚本链路:构建何时触发
package.json(package.json)中的相关脚本构成完整的触发链:
build:lib(第 104 行):tsc -p tsconfig.build.json—— 编译src/*.cts到gsd-core/bin/lib/*.cjs的核心命令;prepare(第 116 行):npm run build:lib——npm install后自动构建,保证从源码安装也能得到产物;pretest(第 119 行):npm run build:lib && npm run lint:skill-deps——测试前强制先构建,落实 ADR-457 “测试命令必须依赖构建”的后果条款;prepublishOnly(第 118 行):npm run build:lib && npm run build:hooks—— 发布前编译产物,npm 打包时npm pack会包含磁盘上这些未被 git 跟踪的.cjs文件;build(第 102 行):聚合命令,首段generate:identity对应的是 ADR-457 保持独立的值烘焙链路(node scripts/generate-package-identity.cjs),后接build:lib与各生成器。
3.4.gitignore:产物清单随迁移批次增长
.gitignore 中有一整段 ADR-457 专用注释与逐条条目:
ADR-457 build-at-publish: TS-generated runtime artifacts (compiled from src/*.cts by
npm run build:lib). Source of truth is src/; these are emitted, never edited. Published via prepublishOnly; built before test via pretest. Grows as modules migrate.
其下逐个列出gsd-core/bin/lib/*.cjs条目(如mcp-server.cjs、state-io.cjs、model-adapter.cjs等),并保留了带 issue 编号的迁移注记(如# #2657: ADR-457 migration gap — these seven … never got a .gitignore entry when their modules moved into src/*.cts)。这段注释“Grows as modules migrate”直接印证:每完成一批模块迁移,就在.gitignore增加对应产物条目——第三批次(本 changeset 所记)的 10 个模块即属于此增长过程。产物文件因此处于“磁盘存在、git 不跟踪”的状态,与 changeset 中 “gitignored.cjs” 的表述完全一致。
四、“同一 require() 路径”与“行为等价”意味着什么
这两点是本批次对下游零影响的根基,可从仓库结构直接验证:
- 路径映射由
rootDir/outDir保证:src为根、gsd-core/bin/lib为输出,意味着src/model-catalog.cts编译为gsd-core/bin/lib/model-catalog.cjs、src/installer-migrations/000-first-time-baseline.cts编译为gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs。目录层级与文件名在编译中一一对应,消费方require('../lib/model-catalog')之类的调用无需任何改动。 - 行为等价是验收标准而非口号:changeset 以 “behaviour preserved byte-for-behaviour” 收尾,与 ADR-457 对转译的定性一致(“手写
.cjs与tsc输出的.cjs在运行时行为相同”)。因此本批次不需要新的用户文档(docs-exempt注释),也不需要 parity 测试——ADR 明确拒绝了模型 1 那种“双份必须一致”的机制,类型检查与构建成功本身就是守门人。 - 严格 TS 带来的增量价值:迁入
src/的模块从此受strict: true约束(tsconfig.build.json),并可通过noEmitOnError: true在构建期阻断类型错误进入发布包——这正是 ADR-457 所述“编写期与 CI 类型检查”价值的具体兑现。
从源码结构看,src/目录现已容纳大量.cts模块(state.cts、phase.cts、install-engine.cts等),且.gitignore的产物条目远多于本批次的 10 个,可以推断第三批次前后还有多批模块相继完成迁移,src/树正在逐步取代手写的bin/lib/*.cjs,直至 ADR-457 规划的“最后一个手写.cjs消失”时退役tsconfig.lint.json。
五、本地复现:如何查看、构建与验证这批迁移产物
当前仓库运行环境要求 Node>=24.0.0、npm>=10.0.0(见 package.json 的engines字段)。在源码 checkout 下:
# 1. 仅类型检查(不产出文件,使用 tsconfig.json 的 noEmit 配置) npx tsc -p tsconfig.json --noEmit # 2. 编译 src/*.cts -> gsd-core/bin/lib/*.cjs(使用发布构建配置) npm run build:lib # 等价于:tsc -p tsconfig.build.json # 3. 直接跑测试——pretest 会先自动执行 build:lib npm test验证本批次成果的具体做法:
- 确认 10 个源码文件存在于
src/(逐一对照 src/observability/event.cts、src/workstream-inventory-builder.cts、src/plan-scan.cts、src/fallow-runner.cts、src/project-root.cts、src/installer-migration-authoring.cts、src/update-context.cts、src/installer-migrations/000-first-time-baseline.cts、src/runtime-homes.cts、src/model-catalog.cts); - 执行
npm run build:lib后,检查gsd-core/bin/lib/下是否生成同名.cjs(如gsd-core/bin/lib/model-catalog.cjs),并确认这些产物不在 git 跟踪中(对应 .gitignore 的 ADR-457 条目段); - 检查
tsconfig.build.tsbuildinfo生成(增量编译标记),它同样被 gitignore 忽略。
需要注意的限制:由于.cjs是构建产物而非提交文件,直接克隆仓库而未执行构建时,gsd-core/bin/lib/下的这批.cjs可能不存在;任何依赖它们的工具(测试、本地脚本)必须先跑npm run build:lib(或依赖prepare/pretest钩子自动构建)。这正是 ADR-457 “Consequences → For testing” 一节预判的唯一主要行为变化。
六、小结
migration-batch-3-ts.md 这份简短的 changeset 背后是 gsd-core 一项系统性的工程决策:按 ADR-457 的 build-at-publish 模型,将gsd-core/bin/lib手写运行时模块渐进迁往src/*.cts严格 TypeScript 源码,tsc按 tsconfig.build.json 的rootDir: src→outDir: gsd-core/bin/lib映射产出 gitignored 的.cjs,require()路径与运行时行为保持不变。第三批次完成的 10 个模块(event、workstream-inventory-builder、plan-scan、fallow-runner、project-root、installer-migration-authoring、update-context、000-first-time-baseline、runtime-homes、model-catalog)是该策略“最低耦合模块先行、逐批推进”执行方式的一个可验证切片;而pretest/prepublishOnly脚本链与.gitignore中随批次增长的产物条目,则是该策略在仓库中的持久化证据。对贡献者而言,实践规则只有一条:新运行时逻辑写进src/并以.cts承载,编译与发布交给npm run build:lib链路,不再手写bin/lib下的.cjs。
【免费下载链接】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 TypeScript 源码迁移实录:10 个运行时模块从手写 CommonJS 到 tsc 构建产物
gsd core ADR 457 TypeScript 源码迁移实录:10 个运行时模块从手写 CommonJS 到 tsc 构建产物 本篇以归档 change
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考