拆出去,行为一个字都不许改
九月二十六日,KernLab.Loom 建仓。第一笔提交里没有一行生成器代码——只有三条基线:.NET 8、Roslyn 4.8.0 统一钉版、MIT。六个文件,175 行。
Loom 是家族的编译期层:把协议编解码、显式内存布局、端点、DI 装配这些代码在编译期”织”出来,把分层引用、禁用模式这类架构纪律在编译期拦成编译错误(名字的来源就是织机——把线织成布的机器)。但它不是新写的东西,它是从 Tier 的仓库里整个拆出来的:此前它一直只是 Tier 写底层时的附带产物——八个源生成器、正确的 analyzer 打包形态、黄金测试基座都在,只是从没按产品级对待过。
这篇讲那次拆分:为什么”只搬迁、不改行为”是唯一纪律、黄金快照为什么是唯一可信的验收仪器、一条反直觉的依赖纪律——以及搬家当天最大的发现:Tier 的家族规则,原来一直没在执法。
拆什么:通用面走,域专属面留
拆分的第一刀切在”通用与专属”之间。抽出的源料是 Tier 的 CodeGen 家族:八个增量生成器(WireMessage / WireArray / BinaryLayout / Command / KvStore / RingKey / TierFs / ConstantRegistry)、能正确加载的 analyzer 打包形态、现成的黄金测试基座。
切分结果:
- 通用面走——Wire(消息族线编码)、Layout(二进制布局)、Cli(命令面)、Analyzers(规则)、Abstractions(属性契约)、Sdk(生成器开发包)进 Loom,面向全家族;
- 域专属面留——TierFs / KvStore / RingKey / ConstantRegistry 四个生成器留在 Tier 仓,但改用 Loom 的
Sdk编写——“公开面保持通用,域专属面同样拿到产品级待遇,且’与某某同款’式的手抄骨架绝迹”。
独立成仓后按产品级运营:自己的版本线与兼容性承诺(属性命名空间、配置键、生成产物形状都是下游的编译期 API)、自己的 CI 矩阵、自己的测试仪器、随包发布的文档面。
Tier 则成了 0 号消费者:通用面改成包消费、域专属生成器留在仓内经 Sdk 织入、配置键与诊断 ID 翻成 loom_* / KLSG*。消费矩阵上还有一队排队者:Traffic、Synod、Cell、Cohort——它们以后都按同一形态切换。
节奏很快:建仓当天首批四面落地(122 个文件、7484 行插入,测试全绿);第二天,Cli(36 测试全绿)、Sdk、Endpoints(42 文件、4910 行)、Di(28 文件、2387 行)全数到位。
纪律一:只做搬迁与命名,行为零变更
拆分的验收仪器是黄金测试:GeneratorDriver 直跑生成器,产物与 Golden/*.g.cs 逐字节比对;重新生成要显式设 GOLDEN_DUMP=1。纪律原文只有一句:
迁移期只做搬迁与命名、行为零变更——黄金输出是拆分唯一可信的验收仪器;同时改行为则仪器失效。
这行字背后是有账的:验收前先做行尾归一(CRLF→LF),因为此前有一次”单文件 CRLF 污染让全量 golden 假红”的战役——仪器本身必须先可信。
搬迁里最危险的不是搬,是改名。诊断 ID 换了前缀但编号不动(TCSG130-139 → KLSG130-139),配置键整族换名(tier_layer.* → loom_layer.*),FQN 识别串、模板里的品牌 FQN 逐串更新——第一批四面落地时,“FQN 识别串改 KernLab.Loom.*”直接标注为静默失效项清单①:改错一个字符串,生成器不报错,只是不产码。所以改名不是”重构”,是一张逐串核对的清单。
行为变更则必须单独走,__result 死变量那次是个标准判例:生成器侧裁撤了 void 端点里的死赋值(消费方零警告 CS0219 缺陷修复),黄金快照随之补拍——先改生成器,再补拍基线,而且判例写着”黄金多套并行时,重生成必须逐套机检——人工挑套必漏”。Tier 侧还有一次补拍是 CI 咬出来的:本地跑测覆盖不到 CodeGen 测试套,基线漏同步,pve-ci 门禁红,用官方 GOLDEN_DUMP=1 重生成收口。仪器的价值就在于它会咬人。
纪律二:两条反直觉的依赖纪律
写生成器的人第一批会犯的错,Loom 的纪律里逐条禁掉了:
生成器不得引用它为之生成代码的运行时库,且不得依赖消费方磁盘文件(模板随包 EmbeddedResource 分发)。生成物自足——只依赖 BCL,不依赖消费方
ImplicitUsings。
第一条的道理:生成器是寄生在别人编译过程里的组件,多一个运行时引用,就多一种加载失败的姿势、少一类能消费它的世界(比如 AOT/裁剪)。识别标注不靠类型引用,靠 FQN 字符串常量。
第二条落实在分发上:所有模板以 <EmbeddedResource> 打进 analyzer dll。连”生成器开发 SDK”本身也是源码包——Templating 模板底座与黄金测试基座以源码随包分发,消费方设一个属性就织入。为什么不能是二进制引用?设计注释写得很直白:“TemplateSet 按自程序集读内嵌模板,二进制共享破坏该模型”。
analyzer 包形态本身也有一套配方(netstandard2.0 + IsRoslynComponent + IncludeBuildOutput=false + dll 手工搬进 analyzers/dotnet/cs + AnalyzerReleases.*.md 随包分发)——这些不是审美,改错任何一条,包在消费方那里就是个”装了但没用的包”。
搬家当天最大的发现:Tier 的规则一直没在执法
0 号消费者切换那天,配置键要按家族惯例从 .editorconfig 搬进 .globalconfig。搬的理由,是切换过程揪出来的一个陈年真相:Tier 的家族规则一直”静默不执法”——键写在 .editorconfig 任何节头之前,而 Roslyn 对”无节键”的态度是:静默忽略。隔离项目实测:无节键 0 命中,同样的键移进 [*] 节,2 命中。
规则真正生效的那一刻,立刻暴露 9 处受控违规——全部早已有 #pragma 豁免与理由,只是 ID 没随改名同步(TCSG137/138 → KLSG137/138)。22 个 pragma 文件更新完,验收还加了一条行为断言:故意在 src 里留一个反射调用,探针必须命中 KLSG136——“规则真执法”要能被证明,不能靠”看起来配好了”。
静默的失败不会喊疼,它只会在某次”碰巧检查”时一起兑付。这一族事故我们踩了一串,值得单独写一篇——下一篇专门讲它。
收尾工程:把发射文本收进模板
搬迁还顺手做了一件立法级的事:全部生成器面的文本发射统一进共享模板底座 src/_shared/Templating(.sbn 模板 + 严格模式,C# 只做编排和 token 值)。判例原文:
底层扩展模板能力全家族同享,禁止回退手拼字符串。
Cli 面是重灾区——审计基线:CommandEmitCli 139 处手拼、CommandEmitHttp 52 处、CommandEmitJson 87 处。一次清零,新增 39 个模板,发射文本 100% 出模板;验收还是老仪器:黄金快照逐字节不变,171 个测试直接绿。
模板化也带来新的小判例:模板 token 命名必须与发射侧逐一核对({{HB}} / {{HEARTBEAT}} 双 token 名会触发严格模式 fail-fast,把全部诊断吞掉);以及一条容易上当的:模板是 EmbeddedResource,改动不触发增量重编——换模板后必须强重建,消费端 bin 里的旧 dll 会伪装成”模板没生效”。
收尾:一条可迁移的判据
如果只能留一句给未来的自己:
搬迁和改造不能同车——搬迁的验收仪器是”新旧逐字节一致”,任何行为变更都会让这台仪器失效。所以搬运时一个字都不许改;想改,等搬完,另外开一趟。
半个月后回头看,这台织机从”附带产物”变成了产品:自己的版本线(alpha.1 到 alpha.12 已在 nuget 上)、自己的 CI 矩阵、51 个黄金快照、随包文档。而墙上有幅有趣的旧画——Tier 仓 CodeGen 的 README 还列着已经迁出的四个生成器和早已不存在的 analyzer 路径。搬家之后,旧清单还挂在墙上;文档的漂移速度,永远比代码快一步。
本文事实来自 KernLab.Loom 的建仓与首批抽取提交、GoldenTestBase 与黄金快照测试族、Tier 仓 0 号消费者切换记录(含”无节键静默不执法”隔离实证)、模板底座统一两次重构的手拼清除审计,与依赖纪律的 README 原文。
评论