Skip to content

#4001 战役的门测量 BFS 会误报「可达」:zod .describe() 共享 def 对象,任意单属性 bridge 把无关形状连起来 #5056

Description

@xuyushun441-sys

批 16 的门测量在第一轮把 WidgetManifestSchema 报成 REACHABLE,而该文件实测没有任何载体键(全仓除 ui/index.ts 桶文件外无人 import)。差点据此对一个死文件做了 9 个站点的 breaking 收紧。

机制

批 15 在 packages/spec/src/ui/chart.test.ts 引入的 reachableFromMetadataRoots()(批 14 亦用同形状)在 BFS 直达之外补了一条 derived-clone bridge,用来认出 .extend() / .strip() 产生的克隆 —— 克隆与原件不共享身份,但共享逐属性的 schema 实例。判据是:

for (const [name, prop] of Object.entries(shape)) {
  const d = defOf(prop);
  if (d && bridged.get(d)?.has(name)) return true;   // ← 任意一个属性命中即判可达
}

两件事叠加就炸了:

  1. zod v4 的 .describe() 返回的克隆共享同一个 _zod.def 对象(clone(inst) 不传 def 时直接复用 inst._zod.def)。实测:defOf(z.string().describe('x')) === defOf(z.string())true(.optional() / .extend() 则为 false)。
  2. 于是 SnakeCaseIdentifierSchema.describe(…)I18nLabelSchema.describe(…) 这类共享叶子,在全仓几十个 schema 上是 def-同一 的。

结果:任何一个形状,只要有一个 name: 是 described 的 SnakeCaseIdentifierSchema、或一个 label: 是 described 的 I18nLabelSchema,就会和某个活 schema 的同名属性 bridge 上 —— 而 name / label 几乎是本仓每个可授权形状都有的两个键。WidgetManifestSchema 正是这样命中的:20 个键里只有 name,label 两个命中(重合度 11%),却足以判「可达」。

为什么这条比一般的仪器 bug 更该修

误差只朝一个方向:它只会产生假可达,不会产生假不可达。也就是说它唯一能造成的后果,是让一个批次对一个死形状做收紧 —— 花掉一次 v17 breaking,换来 #4583 说的那个东西:「a precisely validated dead slot — the more convincing lie」。这正好是本战役 no door 类目存在的理由被自己的仪器反向抵消。

批 13 的教训是「验证不等于找到门」;这条是它的补集:验证也不等于相信走图器说找到了门

已验证可用的修法(批 16 用的就是这个)

把「任意单属性命中」换成整形状重合度:某个已访问 object 节点与候选形状在同名键上共享的属性 def 占候选键总数的比例 ≥ 0.5 才算 derived clone。

  • WidgetManifestSchema:重合 11% → 正确判为不可达。
  • 批 15 那个真·派生场景(ChartConfigSchemaReportChartSchema.extend())重合度远高于阈值,判定不变。
  • 负对照 z.object({ name: z.string(), label: z.string() })(刻意长得像)判为不可达。

建议

  1. 把这条 BFS 从 chart.test.ts / widget.test.ts 各自的副本抽成一份共享测试工具(现在是两份拷贝,已经开始漂 —— 战役自己反复在记「真相的第二份拷贝」这个失效模式)。
  2. 抽出时带上正对照 + 负对照 + 合成载体翻转对照(注入载体后目标必须翻转为可达),三者缺一不可:批 15 已经记过「lazySchema 的 Proxy target 是 function,typeof v !== 'object' 会砍掉半张图」,那次是正对照救的;这次是负对照救的。
  3. 剩余批次(17-22)在用这条 BFS 时先确认已换成重合度版本。

参考

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions