#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 就是完成凭证。基线只减不增。
处置手册
先判真源 :import 语句级扫描四仓(本仓 + cloud + cloud-v1 + objectui)。注意 :四仓零 importer 不等于 有死侧可删(spec 双源清账 C5:ActivationEventSchema(./kernel ≠ ./studio)—— 1 条 #4653 证实)。
死侧删除 / 两侧都活 ⇒ 收敛 + re-export / 真是两个概念 ⇒ 改名一侧 。
收敛 ≠ 无行为变化 :存活形状比被删侧窄/宽会改消费方类型,逐簇核实并写进 changeset。但也不要反向造假 —— 见 §5。
导出类型一般不需要 tombstone —— 判据是可达性,见 §1 。
删 zod 形状会打脸严格性台账 (C3 教训):同步 docs/audits/2026-07-unknown-key-strictness-ledger.md。它不在 check:generated 的 8 项里,收尾要单独跑源码审计组。
判不出来就升级,不要猜。 分析本身就是交付物。spec 双源清账 C5:ActivationEventSchema(./kernel ≠ ./studio)—— 1 条 #4653 / spec 双源清账 C6:EventSchema(./automation ≠ ./kernel)—— 1 条 #4658 / spec 双源清账 C8:RetryPolicy / RetryPolicySchema(./automation ≠ ./system)—— 2 条 #4661 都是这样处理的。
⚠️ 生成物冲突只能靠重新生成。 字符串数组做三路集合合并安全;对象数组(spec-changes.json)不行 —— 集合合并会丢条目,把 CI 打红。正解是跑生成器(pnpm install --filter @objectstack/spec... 走缓存约 2.5 秒)。解完冲突务必本地跑对应 check:* 再推。 详见 spec 生成物没有 merge driver:两个 PR 各改几行,语义上是集合运算,却每次都打成文本冲突 #4675 。
修改既有 changeset 而不新增的 PR 要加 skip-changeset 标签 (docs(changeset): C5 收敛的 changeset 补上与 #4664 placement 的区分说明 (#4653) #4679 实例)。
⛔ 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。三条不变式,任一违反即红:
旧 def 名下每一个 key 都必须在新 def 名下存在 (否则是「披着改名外衣的删除」);
target 必须被本次 build 产出(防拼错 / 防新 def 后来被删);
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)
A1 MetadataWatchEvent.type 收紧三值 —— [#4535·A1] MetadataWatchEvent.type 收紧到 'added'|'changed'|'deleted' —— add/change/unlink 零生产者 #4536 → PR feat(spec)!: MetadataWatchEvent.type carries only the values the runtime emits (#4536) #4545 (docs 修正 docs(releases): v17 upgrade checklist — the raw watcher values are gone from MetadataWatchEvent.type (#4536) #4549 )
A2 MetadataFormat + CacheStrategy —— [#4535·A2] MetadataFormat + CacheStrategy:./shared ≠ ./system 的枚举分歧收敛 #4537 → PR feat(spec)!: converge the dual-source MetadataFormat and CacheStrategy enum declarations (#4537) #4557 ;52 → 49
A3 contracts 手写 interface vs 域内 zod(11 名)—— [#4535·A3] contracts 手写 interface 与域内 zod 推导类型收敛(3 簇 9 条) #4538 → PR feat(spec)!: contracts 手写 interface 与域内 zod 推导类型收敛(3 簇 11 名,#4535·A3) #4568 ;49 → 38;附带 defineJob 解析后的 cron schedule 是表达式信封,CronJobAdapter 直接喂给 croner —— 声明式 cron job 静默调度失败 #4567 / docs(releases): v17 checklist — Metadata*Options now come from contracts, not system (#4538) #4569
B ShareRecipientType / TransformType / suggestFieldType —— [#4535·B] 跨形态同名三条:ShareRecipientType(type≠const)、TransformType(const≠type)、suggestFieldType(双实现 function) #4539 → PR feat(spec)!: 跨形态同名三条收敛 — ShareRecipientType / TransformType / suggestFieldType (#4539) #4571 ;38 → 35;附带 build-docs.ts 用「剥掉 Schema 后缀」推导 import type 示例,类型别名不存在时生成的文档引用无法编译 #4570
C1 WebhookConfig / WebhookEvent —— spec 双源 C1:WebhookConfig / WebhookEvent —— ./api ≠ ./integration(4 条,#4535 C 组) #4572 → PR feat(spec)!: 双源 C1 收敛 — WebhookConfig / WebhookEvent 归 ./integration,./api 侧死删 + 改名 OpenApiWebhookEvent (#4572) #4581 ;35 → 31;附带 RestServerConfig.openApi31(OpenApi31Extensions / Callback / OpenApiWebhookEvent)declared ≠ enforced:没有任何运行时读取它 —— ADR-0049 enforce-or-remove 候选 #4579
C2 MetadataEvent / MetadataBulkRegisterRequestSchema —— spec 双源 C2:MetadataEvent / MetadataBulkRegisterRequestSchema —— ./api ≠ ./kernel(3 条,#4535 C 组) #4587 → PR feat(spec)!: 双源 C2 收敛 — MetadataEvent / MetadataBulkRegisterRequest 归 ./api,./kernel 侧死删 (#4587) #4603 ;31 → 28;附带 client realtime declared ≠ enforced:subscribeMetadata 回调把 RealtimeEventPayload 硬铸成 MetadataEvent,声明的顶层字段运行时是 undefined #4602 (已由 fix(metadata,client): subscribeMetadata 交付真正的 MetadataEvent——生产者履约 + 边界校验 (#4602) #4628 修)
C3 Notification ×2 + NotificationConfig ×2 —— spec 双源清账 C3:通知语汇 Notification(Schema)(./api ≠ ./ui)+ NotificationConfig(Schema)(./system ≠ ./ui)—— 4 条,单 PR #4610 → PR feat(spec)!: 双源 C3 收敛 — 通知语汇归 ./api,./ui 与 ./system 侧死删 (#4610) #4638 ;28 → 24。⚠️ 用了 §2 捷径
C4 Session —— spec 双源清账 C4:Session / SessionSchema(./api ≠ ./identity)—— 2 条 #4641 → PR feat(spec)!: 双源 C4 收敛 — Session 归 ./api,./identity 侧死删 (#4641) #4643 ;24 → 22。附带 packages/spec 的「编译期 pin」是失效的:tsconfig 排除了 *.test.ts,vitest 也不做类型检查 #4642 。⚠️ 用了 §2 捷径
C5 ActivationEvent —— spec 双源清账 C5:ActivationEventSchema(./kernel ≠ ./studio)—— 1 条 #4653 → PR feat(spec)!: 双源 C5 收敛 — ActivationEventSchema 归 ./kernel 结构化形状,./studio re-export (#4653) #4662 (+ changeset 跟进 PR docs(changeset): C5 收敛的 changeset 补上与 #4664 placement 的区分说明 (#4653) #4679 );22 → 21 。裁决路线 A :收敛到 kernel 结构化 {type, pattern},studio re-export;枚举取两侧词表并集(9 值);['*'] → {type:'onStartup',pattern:'*'};零 key vanish、零 tombstone ;conversion walker 经验证不可达 ,故写手工迁移而非伪造 conversion。附带 ADR-0049:两份 activationEvents 声明四仓零 reader —— declared-but-unenforced,且 studio 侧 z.string() 零校验 #4657 / authorable-surface.json 在 main 上不是 gen:schema 的输出 —— 基线手编的字节级实锤(#4650 加固建议) #4663 。
C8 RetryPolicy —— spec 双源清账 C8:RetryPolicy / RetryPolicySchema(./automation ≠ ./system)—— 2 条 #4661 → PR feat(spec)!: converge RetryPolicy onto one declaration (#4661, C8) #4670 ;21 → 19 。裁决路线 C1 :收敛为能力并集,单一声明落 shared/retry-policy.zod.ts 由两个入口 re-export;conversion 显式物化 pre-17 默认值进存量 job.retryPolicy ⇒ 已部署栈行为不变;唯一 vanish 的 retryDelayMs 走 retiredKey()。maxRetryDelayMs / jitter 在 runWithPolicy 接上了执行 而非仅声明。附带 spec 门禁盲区:可作者化 key 的「默认值 / 约束」变更不被任何 gate、tombstone 或 conversion 记录(#4650 / #4659 同族) #4666 。
C11 HttpRequest —— spec 双源清账 C11:HttpRequest(./shared ≠ ./ui)—— 1 条 #4688 → PR refactor(spec): 双源 C11 收敛 — HttpRequest 类型别名改为 re-export ./shared 的唯一声明 (#4688) #4689 ;19 → 18 。HttpRequestSchema 从来只有一份声明 ,唯一双源点是 ui/view.zod.ts 底部的本地类型别名 ,改为 re-export 即收敛。定级 patch —— FROM ≡ TO 经编译器验证(含负对照),三份生成物零 diff(见 §5)。类型级符号身份 pin 首例 (见 §4)。未顺手清掉紧邻的 HttpMethod,后另立 spec 双源清账 C14:HttpMethod(./api, ./shared ≠ ./ui)—— 1 条,且两侧是「7 值 vs 5 值」的真分歧,不是同形别名 #4691 。
C9 RateLimitConfig —— spec 双源清账 C9:RateLimitConfig / RateLimitConfigSchema(./integration ≠ ./shared)—— 2 条 #4684 → PR refactor(spec)!: 双源清账 C9 — connector 侧 RateLimitConfig 改名 + ratchet 学会承接 def 改名 (#4684) #4695 ;18 → 16 。裁决路线 A (维护者裁定在 v17 窗口内做):两侧是方向相反的两个概念 (入站 API 限流 vs 出站 connector 节流,windowMs vs windowSeconds、可选 vs 必填),按 ADR-0112 D9(a) 改 connector 侧的名为 ConnectorRateLimitConfig,不留兼容别名 (别名会是同名的第三个声明)。本簇真正的交付是门禁 :新建 RENAMED_DEFS 承接表解开 def 改名死结(见 §3),四组 sabotage 全部贴了实测输出。6 个 authorable key 一一对应、零 tombstone、零 conversion 、major 但元数据零迁移。顺带证实双源也骗了文档生成器 —— content/docs/references/integration/http.mdx 是这个 bug 的产物,改名后被 gen:docs 自动删除,直接催生 build-docs.ts 的 schema→页面索引按「裸名字」全局建表,同名跨 category 的 schema 会被归到错误的页面 #4696 。
🎯 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/FieldMapping → ConnectorFieldMapping、data/FieldMapping → ImportFieldMapping(同仓 ExternalFieldMappingSchema 已是这个风格,且正因加了前缀从未进基线)。本簇是 §3 RENAMED_DEFS 的第一个真实消费者 ,也是首次同时承接两个 def、首次承接「别处 extend 的基」。⚠️ 必须同步把 connector.test.ts 的 KNOWN_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 的窄例外)
认领方式
开工前把对应簇拆成子 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
#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 条 —— 必须串行派发
FieldMapping(三源,2 条)HttpMethod(1 条)⛔ 不要并行派发。 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。
门禁洞与相邻发现(建议排期时一并看)
.default(),check:authorable-surface全绿build-docs.ts按裸 schema 名做全局索引,同名跨 category 互相覆盖 —— connector 的RateLimitConfig长期被渲染进一个源文件根本不存在的页面。check:docs抓不到(生成器错了它一起错)。基线里剩余的ConflictResolution/FieldMapping/DataSyncConfig/EnvironmentArtifact/PackageDependency/TenantPlan可能同样被错误归页authorable-surface.json基线可手编;并需加可达性窄例外(v18 前置)packages/spec里编译期条件类型 pin 空转activationEvents四仓零 reader(ADR-0049)RateLimitConfig全仓零 runtime reader,真正限流的是packages/runtime/src/security/rate-limit.ts里第三份形状验收机制
每簇清完的客观标志:
check:dual-source-exports的 stale 分支点名要求删除对应基线行 —— 删行的 commit 就是完成凭证。基线只减不增。处置手册
cloud+cloud-v1+objectui)。注意:四仓零 importer 不等于有死侧可删(spec 双源清账 C5:ActivationEventSchema(./kernel ≠ ./studio)—— 1 条 #4653 证实)。导出类型一般不需要 tombstone—— 判据是可达性,见 §1。docs/audits/2026-07-unknown-key-strictness-ledger.md。它不在check:generated的 8 项里,收尾要单独跑源码审计组。spec-changes.json)不行 —— 集合合并会丢条目,把 CI 打红。正解是跑生成器(pnpm install --filter @objectstack/spec...走缓存约 2.5 秒)。解完冲突务必本地跑对应check:*再推。 详见 spec 生成物没有 merge driver:两个 PR 各改几行,语义上是集合运算,却每次都打成文本冲突 #4675。skip-changeset标签(docs(changeset): C5 收敛的 changeset 补上与 #4664placement的区分说明 (#4653) #4679 实例)。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 个元数据类型)传递可达。FieldMapping(三源)HttpMethodC5/ActivationEventC8/RetryPolicyC9/RateLimitConfigC11HttpRequestEvent/ C7PackageDependency/ C10EnvironmentArtifact/ C13DataSyncConfigConflictResolution/TenantPlan/ActionLocationSchema两点限定:(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。三条不变式,任一违反即红:承接过来的 key 保留旧 key 的 retired 状态,故「借改名之机悄悄 retire 一个 key」仍会撞上检查 (b)。净效果比「基线可手编」的现状更严格:手编能无声删掉任意一行,承接表连一行都删不掉。
def 改名一律走这条通道。 不要手编基线,不要伪造 tombstone / conversion(没有 key 被 retire 时那是语义错误,会污染 ADR-0087 登记册)。
4. 回归 pin 用运行时断言
#4642:
tsconfig.jsonexclude掉**/*.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)
MetadataWatchEvent.type收紧三值 —— [#4535·A1] MetadataWatchEvent.type 收紧到 'added'|'changed'|'deleted' —— add/change/unlink 零生产者 #4536 → PR feat(spec)!: MetadataWatchEvent.type carries only the values the runtime emits (#4536) #4545(docs 修正 docs(releases): v17 upgrade checklist — the raw watcher values are gone from MetadataWatchEvent.type (#4536) #4549)MetadataFormat+CacheStrategy—— [#4535·A2] MetadataFormat + CacheStrategy:./shared ≠ ./system 的枚举分歧收敛 #4537 → PR feat(spec)!: converge the dual-source MetadataFormat and CacheStrategy enum declarations (#4537) #4557;52 → 49ShareRecipientType/TransformType/suggestFieldType—— [#4535·B] 跨形态同名三条:ShareRecipientType(type≠const)、TransformType(const≠type)、suggestFieldType(双实现 function) #4539 → PR feat(spec)!: 跨形态同名三条收敛 — ShareRecipientType / TransformType / suggestFieldType (#4539) #4571;38 → 35;附带 build-docs.ts 用「剥掉 Schema 后缀」推导 import type 示例,类型别名不存在时生成的文档引用无法编译 #4570WebhookConfig/WebhookEvent—— spec 双源 C1:WebhookConfig / WebhookEvent —— ./api ≠ ./integration(4 条,#4535 C 组) #4572 → PR feat(spec)!: 双源 C1 收敛 — WebhookConfig / WebhookEvent 归 ./integration,./api 侧死删 + 改名 OpenApiWebhookEvent (#4572) #4581;35 → 31;附带 RestServerConfig.openApi31(OpenApi31Extensions / Callback / OpenApiWebhookEvent)declared ≠ enforced:没有任何运行时读取它 —— ADR-0049 enforce-or-remove 候选 #4579MetadataEvent/MetadataBulkRegisterRequestSchema—— spec 双源 C2:MetadataEvent / MetadataBulkRegisterRequestSchema —— ./api ≠ ./kernel(3 条,#4535 C 组) #4587 → PR feat(spec)!: 双源 C2 收敛 — MetadataEvent / MetadataBulkRegisterRequest 归 ./api,./kernel 侧死删 (#4587) #4603;31 → 28;附带 client realtime declared ≠ enforced:subscribeMetadata 回调把 RealtimeEventPayload 硬铸成 MetadataEvent,声明的顶层字段运行时是 undefined #4602(已由 fix(metadata,client): subscribeMetadata 交付真正的 MetadataEvent——生产者履约 + 边界校验 (#4602) #4628 修)Notification×2 +NotificationConfig×2 —— spec 双源清账 C3:通知语汇 Notification(Schema)(./api ≠ ./ui)+ NotificationConfig(Schema)(./system ≠ ./ui)—— 4 条,单 PR #4610 → PR feat(spec)!: 双源 C3 收敛 — 通知语汇归 ./api,./ui 与 ./system 侧死删 (#4610) #4638;28 → 24。Session—— spec 双源清账 C4:Session / SessionSchema(./api ≠ ./identity)—— 2 条 #4641 → PR feat(spec)!: 双源 C4 收敛 — Session 归 ./api,./identity 侧死删 (#4641) #4643;24 → 22。附带 packages/spec 的「编译期 pin」是失效的:tsconfig 排除了 *.test.ts,vitest 也不做类型检查 #4642。ActivationEvent—— spec 双源清账 C5:ActivationEventSchema(./kernel ≠ ./studio)—— 1 条 #4653 → PR feat(spec)!: 双源 C5 收敛 — ActivationEventSchema 归 ./kernel 结构化形状,./studio re-export (#4653) #4662(+ changeset 跟进 PR docs(changeset): C5 收敛的 changeset 补上与 #4664placement的区分说明 (#4653) #4679);22 → 21。裁决路线 A:收敛到 kernel 结构化{type, pattern},studio re-export;枚举取两侧词表并集(9 值);['*']→{type:'onStartup',pattern:'*'};零 key vanish、零 tombstone;conversion walker 经验证不可达,故写手工迁移而非伪造 conversion。附带 ADR-0049:两份activationEvents声明四仓零 reader —— declared-but-unenforced,且 studio 侧z.string()零校验 #4657 /authorable-surface.json在 main 上不是gen:schema的输出 —— 基线手编的字节级实锤(#4650 加固建议) #4663。RetryPolicy—— spec 双源清账 C8:RetryPolicy / RetryPolicySchema(./automation ≠ ./system)—— 2 条 #4661 → PR feat(spec)!: converge RetryPolicy onto one declaration (#4661, C8) #4670;21 → 19。裁决路线 C1:收敛为能力并集,单一声明落shared/retry-policy.zod.ts由两个入口 re-export;conversion 显式物化 pre-17 默认值进存量job.retryPolicy⇒ 已部署栈行为不变;唯一 vanish 的retryDelayMs走retiredKey()。maxRetryDelayMs/jitter在runWithPolicy接上了执行而非仅声明。附带 spec 门禁盲区:可作者化 key 的「默认值 / 约束」变更不被任何 gate、tombstone 或 conversion 记录(#4650 / #4659 同族) #4666。HttpRequest—— spec 双源清账 C11:HttpRequest(./shared ≠ ./ui)—— 1 条 #4688 → PR refactor(spec): 双源 C11 收敛 — HttpRequest 类型别名改为 re-export ./shared 的唯一声明 (#4688) #4689;19 → 18。HttpRequestSchema从来只有一份声明,唯一双源点是ui/view.zod.ts底部的本地类型别名,改为 re-export 即收敛。定级patch—— FROM ≡ TO 经编译器验证(含负对照),三份生成物零 diff(见 §5)。类型级符号身份 pin 首例(见 §4)。未顺手清掉紧邻的HttpMethod,后另立 spec 双源清账 C14:HttpMethod(./api, ./shared ≠ ./ui)—— 1 条,且两侧是「7 值 vs 5 值」的真分歧,不是同形别名 #4691。RateLimitConfig—— spec 双源清账 C9:RateLimitConfig / RateLimitConfigSchema(./integration ≠ ./shared)—— 2 条 #4684 → PR refactor(spec)!: 双源清账 C9 — connector 侧 RateLimitConfig 改名 + ratchet 学会承接 def 改名 (#4684) #4695;18 → 16。裁决路线 A(维护者裁定在 v17 窗口内做):两侧是方向相反的两个概念(入站 API 限流 vs 出站 connector 节流,windowMsvswindowSeconds、可选 vs 必填),按 ADR-0112 D9(a) 改 connector 侧的名为ConnectorRateLimitConfig,不留兼容别名(别名会是同名的第三个声明)。本簇真正的交付是门禁:新建RENAMED_DEFS承接表解开 def 改名死结(见 §3),四组 sabotage 全部贴了实测输出。6 个 authorable key 一一对应、零 tombstone、零 conversion、major但元数据零迁移。顺带证实双源也骗了文档生成器 ——content/docs/references/integration/http.mdx是这个 bug 的产物,改名后被gen:docs自动删除,直接催生 build-docs.ts 的 schema→页面索引按「裸名字」全局建表,同名跨 category 的 schema 会被归到错误的页面 #4696。🎯 v17(2 簇 / 3 条 · 基线 16 → 13 · 串行)
FieldMapping(三源:./data≠./integration≠./shared,2 条)→ spec 双源清账 C12:FieldMapping / FieldMappingSchema(./data ≠ ./integration ≠ ./shared,三源)—— 2 条 #4703 —— 🏃 已派发。三侧是两个概念:./shared是基(4 key,plainz.object),./integrationextend它(7 key),./data是独立strictObject(4 key)且transform是普通枚举 + 平铺params而非判别联合、source/target收数组、未知键 throw 而非静默 strip —— 它是数据导入的列映射,不是 connector 同步映射。推荐路线:./shared不动,integration/FieldMapping→ConnectorFieldMapping、data/FieldMapping→ImportFieldMapping(同仓ExternalFieldMappingSchema已是这个风格,且正因加了前缀从未进基线)。本簇是 §3RENAMED_DEFS的第一个真实消费者,也是首次同时承接两个 def、首次承接「别处 extend 的基」。connector.test.ts的KNOWN_STILL_DUAL_SOURCE清空为[](见 §4)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)。让./uire-export./shared的是错的 —— 会把./ui悄悄放宽到 7 值,而HttpRequestSchema.method只收 5 值,类型开始对运行时说谎。推荐让./ui不再导出这个名字(仓内零消费者)📦 v18(13 条 · 不在元数据文档管道上 · 前置 = #4650 的窄例外)
Event(1 条)→ spec 双源清账 C6:EventSchema(./automation ≠ ./kernel)—— 1 条 #4658 —— 路线已裁决:A(开窄例外后删 automation 侧孤儿)。两侧键集完全不相交 ⇒ 收敛等于写假话。备选 A′:改名StateMachineEventSchema并真的嵌进StateMachineSchema。PackageDependency(2 条)EnvironmentArtifact(3 条)—— cloud 侧 0 可作者化 key,不对称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