Skip to content

spec 双源清账主单:基线 52 → 16(v17 剩 2 簇 / 3 条,余顺延 v18)—— #4446 gate 落地后的偿还 worklist #4535

Description

@os-zhuang

#4446 的 gate(#4506)已在 main 上守住「不再新增」;本单跟踪存量偿还packages/spec/dual-source-exports.baseline.json 原有 52 条 —— 同一个名字在两个入口解析到不同声明,消费者拿到哪个类型只取决于 import 路径(#4411 陷阱)。当前基线 16 条。


🔀 派发交接状态(2026-08-02 19:10,PM 循环进行中)

PM 协调会话 session_0176qgxgCXTJCUv4YFLtusP9 正在推进本单。

v17 剩余 2 簇 / 3 条 —— 必须串行派发

顺序 子 issue 状态 基线
1 C12 FieldMapping(三源,2 条) #4703 🏃 已认领、已派发(19:10) 16 → 14
2 C14 HttpMethod(1 条) #4691 派发单已写好,待 C12 落地后派 14 → 13

不要并行派发。 2026-08-02 当天实测:并发 spec PR 争抢同一批生成物,导致三次冲突返工,其中一次 PR 已入队仍被合并队列以 MERGE_CONFLICT 弹出。两簇都改同一个 dual-source-exports.baseline.json,并发必冲突。

时效

spec 现为 17.0.0-rc.1,.changeset/pre.json 仍是 mode: pre / tag: rc。major 通道在 changeset pre exit 时关闭 —— C12 / C14 都是 breaking,关窗后只能等 v18。接手前先确认该时点未到。

v18 队列(13 条)有硬前置

C6 / C7 / C10 / C13 必须等 #4650 的可机检窄例外落地才能诚实收口(否则删基线行 = 删证据)。详见 §2。

门禁洞与相邻发现(建议排期时一并看)

# 内容 优先级建议
#4666 门禁对默认值 / 约束变更完全不可见 —— 改一个字符的 .default(),check:authorable-surface 全绿 最高:元数据即 API,默认值就是 API 的一部分
#4696 build-docs.ts 按裸 schema 名做全局索引,同名跨 category 互相覆盖 —— connector 的 RateLimitConfig 长期被渲染进一个源文件根本不存在的页面。check:docs 抓不到(生成器错了它一起错)。基线里剩余的 ConflictResolution / FieldMapping / DataSyncConfig / EnvironmentArtifact / PackageDependency / TenantPlan 可能同样被错误归页 高:双源的第三类受害者是文档管道
#4650 authorable-surface.json 基线可手编;并需加可达性窄例外(v18 前置)
#4659 检查 (b) 按 leaf name 匹配 conversion surface,无关 conversion 即可蒙混
#4663 手编的字节级实锤 + 「与重新生成逐字节比对」的加固法
#4642 packages/spec 里编译期条件类型 pin 空转
#4675 / #4676 生成物冲突 —— #4676 才是对合并队列有效的那个 v17 之后
#4657 activationEvents 四仓零 reader(ADR-0049) 待定
#4686 两份 RateLimitConfig 全仓零 runtime reader,真正限流的是 packages/runtime/src/security/rate-limit.ts 里第三份形状 C9 落地不使其失效,仍独立有效

v17 窗口重切(第二版为准)。 第一版按 authorable-surface.json 的 key 数分层 —— 那是错的,该文件过度收集(§1)。第二版按BUILTIN_METADATA_TYPE_SCHEMAS 传递可达重算。

验收机制

每簇清完的客观标志:check:dual-source-exports 的 stale 分支点名要求删除对应基线行 —— 删行的 commit 就是完成凭证。基线只减不增。

处置手册

  1. 先判真源:import 语句级扫描四仓(本仓 + cloud + cloud-v1 + objectui)。注意:四仓零 importer 不等于有死侧可删(spec 双源清账 C5:ActivationEventSchema(./kernel ≠ ./studio)—— 1 条 #4653 证实)。
  2. 死侧删除 / 两侧都活 ⇒ 收敛 + re-export / 真是两个概念 ⇒ 改名一侧
  3. 收敛 ≠ 无行为变化:存活形状比被删侧窄/宽会改消费方类型,逐簇核实并写进 changeset。但也不要反向造假 —— 见 §5。
  4. 导出类型一般不需要 tombstone —— 判据是可达性,见 §1
  5. 删 zod 形状会打脸严格性台账(C3 教训):同步 docs/audits/2026-07-unknown-key-strictness-ledger.md。它不在 check:generated 的 8 项里,收尾要单独跑源码审计组。
  6. 判不出来就升级,不要猜。 分析本身就是交付物。spec 双源清账 C5:ActivationEventSchema(./kernel ≠ ./studio)—— 1 条 #4653 / spec 双源清账 C6:EventSchema(./automation ≠ ./kernel)—— 1 条 #4658 / spec 双源清账 C8:RetryPolicy / RetryPolicySchema(./automation ≠ ./system)—— 2 条 #4661 都是这样处理的。
  7. ⚠️ 生成物冲突只能靠重新生成。 字符串数组做三路集合合并安全;对象数组(spec-changes.json)不行 —— 集合合并会丢条目,把 CI 打红。正解是跑生成器(pnpm install --filter @objectstack/spec... 走缓存约 2.5 秒)。解完冲突务必本地跑对应 check:* 再推。 详见 spec 生成物没有 merge driver:两个 PR 各改几行,语义上是集合运算,却每次都打成文本冲突 #4675
  8. 修改既有 changeset 而不新增的 PR 要加 skip-changeset 标签(docs(changeset): C5 收敛的 changeset 补上与 #4664 placement 的区分说明 (#4653) #4679 实例)。
  9. ⛔ def 改名走 RENAMED_DEFS 承接表,不要手编基线、不要伪造 tombstone。 见 §3。

v17 重切

1. 判据是「从元数据根集合可达」,不是 authorable-surface.json 的 key 数

该文件自称记录「metadata author 可写的每个 key」,实际记录每个被 gen:schema 发出的 schema 的全部 properties,不论是否从作者面可达。#4658 实证。

正确判据:BUILTIN_METADATA_TYPE_SCHEMAS(kernel/metadata-type-schemas.ts:77,24 个元数据类型)传递可达

可达? 条数 归属
C12 FieldMapping(三源) 2 v17(🏃 #4703 在跑)
C14 HttpMethod 1 v17(维护者裁定提前)
C5 ActivationEvent / C8 RetryPolicy / C9 RateLimitConfig / C11 HttpRequest ✅ 已落地
C6 Event / C7 PackageDependency / C10 EnvironmentArtifact / C13 DataSyncConfig 8 v18
ConflictResolution / TenantPlan / ActionLocationSchema 5 v18

两点限定:(a) 静态正则图是近似,no 可能是假阴性,逐簇仍要自验;(b)「不从 BUILTIN 可达」「没人手写」—— 插件清单、connector 配置走另一扇授权门。

2. ⛔ 基线可手编 —— C3 / C4 用的捷径不要再用

build-schemas.ts 检查 (a) 比对的 authorable-surface.json 在同一 commit 里可手改,删基线行 = 删证据。加固另立 #4650;C6 裁决要求在其中加可机检的窄例外 —— 该例外落地前 C6 / C7 / C10 / C13 无法诚实收口

#4663 给出了手编的字节级实锤并反过来成了验证工具#4659:检查 (b) 按 leaf name 匹配,门禁绿不等于登记正确。#4666:默认值/约束变更完全不可见,三个门禁洞里这个最值得优先修。

3. 改名不绕开 tombstone —— 但 C9 之后有了正规通道

key 记作 `${defKey}:${name}`,改名 ⇒ 旧 defKey 下所有 key vanish ⇒ 检查 (a) process.exit(1)

C9(PR #4695)把这个死结在门禁侧解开了:新增 packages/spec/scripts/lib/renamed-defs.ts 的声明式 RENAMED_DEFS 承接表,接进 build-schemas.ts 的两道 ratchet。三条不变式,任一违反即红:

  1. 旧 def 名下每一个 key 都必须在新 def 名下存在(否则是「披着改名外衣的删除」);
  2. target 必须被本次 build 产出(防拼错 / 防新 def 后来被删);
  3. source 必须不再被产出 —— 两个都在是复制而非改名,而复制正是这张表绝不能洗白的 dual-source 形状。

承接过来的 key 保留旧 key 的 retired 状态,故「借改名之机悄悄 retire 一个 key」仍会撞上检查 (b)。净效果比「基线可手编」的现状更严格:手编能无声删掉任意一行,承接表连一行都删不掉。

def 改名一律走这条通道。 不要手编基线,不要伪造 tombstone / conversion(没有 key 被 retire 时那是语义错误,会污染 ADR-0087 登记册)。

4. 回归 pin 用运行时断言

#4642:tsconfig.json exclude**/*.test.ts、vitest 未开 typecheck.enabled编译期条件类型 pin 空转。用运行时模块命名空间断言必须 sabotage 验证#4581 / #4638 已落地的 pin 是装饰性的。

类型级 pin 的唯一有效形态(C11 首创、C9 推广):纯类型导出运行时看不见,import type 又被 vitest transform 抹掉 —— 用 TypeScript compiler API 在 src/ 上做符号身份解析(check:dual-source-exports 在 dist 上做的同一件事),并带防空转守卫(expect(moduleSym).toBeTruthy()origins.size > 20;解析失败会让后续断言全部真空通过)。C9 的 sabotage 直接实证了它不可替代:只加回一个纯类型别名 → 该断言红,其余 41 条运行时断言全绿

C9 还把它推广成通用不变式:两个入口都导出的任何名字必须解析到同一声明,已知例外写成显式清单(KNOWN_STILL_DUAL_SOURCE)而非「不许同名」的空条款 —— 新出现一个就红。代价是每簇清完必须同步更新该清单,这是设计好的强制握手。

5. 定级要据实,不要为了凑 major 而声称破坏

C11 实证 FROM ≡ TO(两个别名 z.infer 同一个 schema 对象)⇒ 定 patch;C9 是 def 改名,对按名字 import 的 TS 代码是真破坏 ⇒ 定 major,但元数据零迁移,changeset 里明确写了。证明方式:Equal<X,Y> 条件类型断言 + 负对照(负对照必须真的报 TS2344),外加生成物零 diff 作旁证。

本单把三簇统称 breaking 是主单层面的粗粒度表述,不构成对每一簇的定级要求。谎报破坏与漏报破坏同样污染升级指南。


Worklist

✅ 已清(36 条,52 → 16)

🎯 v17(2 簇 / 3 条 · 基线 16 → 13 · 串行)

  • C12 · FieldMapping(三源:./data./integration./shared,2 条)→ spec 双源清账 C12:FieldMapping / FieldMappingSchema(./data ≠ ./integration ≠ ./shared,三源)—— 2 条 #4703 —— 🏃 已派发。三侧是两个概念:./shared 是基(4 key,plain z.object),./integration extend 它(7 key),./data独立 strictObject(4 key)且 transform 是普通枚举 + 平铺 params 而非判别联合、source/target 收数组、未知键 throw 而非静默 strip —— 它是数据导入的列映射,不是 connector 同步映射。推荐路线:./shared 不动,integration/FieldMappingConnectorFieldMappingdata/FieldMappingImportFieldMapping(同仓 ExternalFieldMappingSchema 已是这个风格,且正因加了前缀从未进基线)。本簇是 §3 RENAMED_DEFS 的第一个真实消费者,也是首次同时承接两个 def、首次承接「别处 extend 的基」。⚠️ 必须同步把 connector.test.tsKNOWN_STILL_DUAL_SOURCE 清空为 [](见 §4)
  • C14 · HttpMethod(./api, ./shared./ui,1 条)→ spec 双源清账 C14:HttpMethod(./api, ./shared ≠ ./ui)—— 1 条,且两侧是「7 值 vs 5 值」的真分歧,不是同形别名 #4691 —— ⚠️ 不是一行改动:./api+./shared 的是 7 值(含 HEAD/OPTIONS),./ui 的是 5 值真子集(infer 自 HttpMethodSchema,而 shared 已把它命名为 HttpMethodType)。./ui re-export ./shared 的是错的 —— 会把 ./ui 悄悄放宽到 7 值,而 HttpRequestSchema.method 只收 5 值,类型开始对运行时说谎。推荐让 ./ui 不再导出这个名字(仓内零消费者)

📦 v18(13 条 · 不在元数据文档管道上 · 前置 = #4650 的窄例外)

  • C6 Event(1 条)→ spec 双源清账 C6:EventSchema(./automation ≠ ./kernel)—— 1 条 #4658 —— 路线已裁决:A(开窄例外后删 automation 侧孤儿)。两侧键集完全不相交 ⇒ 收敛等于写假话。备选 A′:改名 StateMachineEventSchema 并真的嵌进 StateMachineSchema
  • C7 PackageDependency(2 条)
  • C10 EnvironmentArtifact(3 条)—— cloud 侧 0 可作者化 key,不对称
  • C13 DataSyncConfig(2 条)
  • ConflictResolution(三源,2 条)/ TenantPlan(2 条)/ ActionLocationSchema(1 条)

认领方式

开工前把对应簇拆成子 issue 并 assign 自己 + 留 claim 评论(含 session ID 与分支名);本单保持 unassigned 作为账本。所有 agent 共用一个 GitHub 身份,assignee 字段无法区分是谁的认领,开工前必须重读评论。每簇 PR 合并后勾选对应项。


关联:#4411(先例)、#4446 / #4506(gate 本体)、#4650(基线手编)、#4659(leaf-name 匹配)、#4663(手编实锤)、#4666(默认值盲区)、#4642(pin 空转)、#4696(docs 生成器按裸名索引)、#4657#4675 / #4676(生成物冲突)、#4686(RateLimitConfig 零 runtime reader)、ADR-0049、ADR-0059 §5、ADR-0087、ADR-0104、ADR-0112

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions