Skip to content

✅ spec 双源清账主单:基线 52 → 0(2026-08-03 收官)—— #4446 gate 落地后的偿还 worklist #4535

Description

@os-zhuang

✅ 已完成 —— packages/spec/dual-source-exports.baseline.json 现为 entries: []

52 → 0,历时两天、17 簇、17 个 PR。最后一簇 C7(#4741 → PR #4789)于 2026-08-03 07:5xZ 合并,门禁实报 0 accepted dual-source (baseline)

#4446 的 gate(#4506)守住「不再新增」;本单偿清了存量。这份账目从此只应是 0——任何新增都会被门禁在 PR 上拦下,不需要再开清账单。


问题是什么

同一个名字在两个入口解析到不同声明,消费者拿到哪个类型只取决于 import 路径 —— #4411 陷阱。两侧都不 .strict() 时,把一侧的文档粘到另一侧不报错,只静默剥掉全部外来键(ADR-0104 silent-strip 类)。52 条这样的名字曾同时存在于发布契约里。

最终账目

批次 条数 时间 说明
第一批 52 → 13 2026-08-02 ~ 08-03 A1/A2/A3/B/C1–C5/C8/C9/C11/C12/C14,共 39 条
第二批 13 → 0 2026-08-03 C6/C10/C13/C15/C16/C17/C7,共 13 条,维护者裁决全部纳入 v17 窗口

第二批逐簇(维护者 2026-08-03 裁决:13 条全入 v17)

名字 子 issue → PR 基线 路线
C6 Event #4658#4745 13 → 12 死侧删除:automation 侧是零引用 XState 信号孤儿,从未接线
C16 TenantPlan #4739#4752 12 → 10 ./cloud 5 值词表为唯一真源;system/provisioning.zod.ts 六 def 全族 + 两个 contracts 整文件退役(21 文件 +271/−939)。词表 3→5 拓宽如实写进 changeset
C13+C15 DataSyncConfig / ConflictResolution #4738#4760 10 → 6 整删 automation/sync.zod.ts(L1「Simple Sync」叙事层,17 导出名 / 8 def / 39 key,三仓零消费者);integration 侧改名 ConnectorConflictResolution;./ui 一字未动
C10 EnvironmentArtifact #4740#4767 6 → 3 收敛到线上 wire 形状(唯一 runtime 消费者 metadata/plugin.ts 已在解析的形状),./cloud 纯 re-export;v0 家族 16 名 / 9 def 退役。checksum 对象→字符串是 #4666 盲区内的类型变更,显式承载 + parse pin 钉住
C17 ActionLocationSchema #4737#4768 3 → 2 改名 studio 侧 ActionContributionLocationSchema + 补类型导出(顺带消掉 docs-import-surface 例外行);ui 侧一字未动
C7 PackageDependency #4741#4789 2 → 0 两侧键集完全不相交(声明形 vs 解析器形),改名 kernel 侧 ResolvedPackageDependency;4 key 全承接

沉淀下来的东西(比账目本身更值钱)

1. RENAMED_DEFS 承接表 —— def 改名的正规通道

packages/spec/scripts/lib/renamed-defs.ts,C9(PR #4695)首创、C12(PR #4710)加固。五条不变式,任一违反即红:

  1. 旧 def 名下每个 key 都必须在新 def 名下存在(否则是「披着改名外衣的删除」);
  2. target 必须被本次 build 产出;
  3. source 必须不再被产出 —— 两个都在是复制而非改名,而复制正是这张表绝不能洗白的 dual-source 形状;
  4. 两个 source 不许指向同一个 target(合并两个 def 是真变更);
  5. 链式改名(A → B → C)按名报错。

表内现有 6 条(C9 / C12 ×2 / C15 / C17 / C7)。def 改名一律走这条通道;真退役一律不许走(那张表的不变式正是「没有 key 离开契约」)。

2. #4650 删行自证门禁(PR #4726)—— 基线不能再手编

被删基线行必须在门禁内自证,merge-base 锚定(CI 不空转),三条合法路径:aged-out tombstone(≥2 majors)/ 从 24 个元数据根真 Zod 图 BFS 不可达 / 整 def 不再发出(归 manifest ratchet + check:api-surface 裁决)。另加 --check 对整文件的规范形字节比对(#4663 加固法),手编即红。

实证补充:整文件/整族删除走的都是路径 3,per-key 可达性判定不适用 —— 以门禁实跑输出为准,不要按静态推断写 PR。

3. 类型级 pin 的唯一有效形态(#4642 的解药)

tsconfig exclude 掉 test、vitest 未开 typecheck.enabled ⇒ 编译期条件类型 pin 空转。有效形态是:TypeScript compiler API 在 src/ 上做符号身份解析,从 package.json exports map 枚举全部入口(新入口无法逃逸),带防空转守卫(module symbol 必解析、表面非平凡),holders 用精确相等断言。

为什么必须精确相等:C17 与 C7 各自实证了一条对 dual-source 门禁全绿、但语义说谎的路线 —— 在错误的入口 re-export 同一声明。C7 更把它量化:用含 sabotage 的源码重建 dist 后实跑门禁,读数从 173 re-exported175,门禁结构性看不见。拦住它的是 pin 的精确相等断言,以及 RENAMED_DEFS 不变式 3 —— 后者更早在 build 阶段即拒绝。

4. 定级要据实,五种形状五种论证(不许跨簇照抄)

形状 定级 实例
FROM ≡ TO patch C11
def 改名 major,零元数据迁移 C9 / C12 / C17 / C7
已发布导出名移除 major(TS2305),零元数据迁移 C14 / C6 / C16 / C10
收敛致词表拓宽 major + changeset 显式警示 C16(3→5)
收敛致字段类型变更 major + 警示 + parse pin(#4666 盲区) C10(checksum 对象→字符串)

「major 且零迁移」对真退役不成立(#4651 那类有迁移)。谎报破坏与漏报破坏同样污染升级指南。

5. ⚠️ 入队纪律:同步 main 的三个坑(2026-08-03 全部实证)

a. 用 git merge,不用 rebase + force-push。 AGENTS.md §3 禁止 --force/--force-with-lease —— 仓规不因 PM 指令而失效(PM 曾下达错误指令,实施 agent 拒绝执行并改走 merge 是正确处置)。merge=os-regen 驱动正为此而设。

b. 静默回退有三处,第三处最隐蔽:

文件 归属 合并时表现 暴露方式
dual-source-exports.baseline.json 手管 ratchet 产生冲突 冲突标记
authorable-surface.json / docs-import-surface.baseline.json 手管 ratchet 产生冲突 冲突标记
json-schema.manifest.json merge driver 托管 无冲突标记,静默合并 只有 gen:schema 实跑暴露

C17 与 #4634 各踩一次:manifest 无声复活了 C10 蓄意删除的 9 个键 / 文本 auto-merge 复活了 46 行 authorable-surface。唯一可靠处置:四张 ratchet 一律 git checkout origin/main -- <file>只重施自己的那一处改动,再全量重跑生成器,零文本合并(#4783 做法,重放后与 main 逐字节一致)。

c. renamed-defs.ts 的合并冲突必须让两条改名条目并存,不能二选一。 C7 实证:承接表是累积台账,冲突里选一条就等于把别人已合并的改名从台账抹掉 —— 而不变式 1 只在本次 build 产出时校验,被抹掉的那条不会报红。这类静默丢失只能靠人判断拦住。

6. 判不出来就升级,不要猜

分析本身就是交付物。#4653 / #4658 / #4661 都是这样处理的。四仓零 importer ≠ 有死侧可删(#4653 实证);C7 同理:两侧各自被真实 schema 嵌入、都活着,所以是改名而非删除。


派生工单(留维护者排期,均非窗口限时)

门禁洞

# 内容 优先级建议
#4666 门禁对默认值 / 约束变更完全不可见 —— 改一个字符的 .default(),check:authorable-surface 全绿 最高:元数据即 API,默认值就是 API 的一部分。C8/C10/#4634 三次实证
#4725 manifest 删键是 #4650 那个洞上移一层
#4659 检查 (b) 按 leaf name 匹配 conversion surface,无关 conversion 即可蒙混 中(与 #4726 检查 (c) 同词表,一起修)
#4642 编译期条件类型 pin 空转 中(本单已给出有效形态,可作修复参考)
#4675 / #4676 生成物冲突 —— #4676 才是对合并队列有效的那个 排期待定

文档管道(双源的第三类受害者)

# 内容
#4696 build-docs.ts 按裸 schema 名做全局索引,同名跨 category 互相覆盖产出幽灵页 —— 本单四次实证(幽灵页随改名/删除自愈消失)
#4759 references/index.mdx 根索引被生成器「保留不重生」,烂尾无 gate
#4781 runtime-capabilities.mdx 整页描述已于 #3605 删除的 schema

其他

#4686(两份 RateLimitConfig 全仓零 runtime reader,C9 落地不使其失效)、#4782(测试 mock 写满 off-spec 键)、#4723

跨仓下游

# 内容 紧迫性
objectui#3235 C14 下游:packages/types 的 type-only 一行迁移(HttpMethodType as HttpMethod) 下个 rc 前不处理会 TS2305 断链
cloud#1027 C16 下游:手拷 5 值 plan 并集
cloud#1028 #4634 下游:TursoDriver 死位 + off-spec fallback 清理 低(不破编译,纯死重)

协作方式记录

本单第二批由 PM 会话 session_0176qgxgCXTJCUv4YFLtusP9 协调,os-dev agent 并行实施。两条关键协议:

  • 不并行入队(2026-08-02 三次生成物冲突返工的教训 → 08-03 收窄):def 文件/入口区域不相交的簇可并行实施,但入队严格串行(merge main + 四张 ratchet 取 main 版重施 + 重生成 + 复验后逐个进队)。
  • 认领 = assign + claim 评论(含 session ID 与分支名):所有 agent 共用一个 GitHub 身份,assignee 字段无法区分是谁的认领,开工前必须重读评论。

关联:#4411(先例)、#4446 / #4506(gate 本体)、#4650 / PR #4726(删行自证门禁)、#4663#4711 / PR #4724、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