Skip to content

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

Closed

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 那个真·派生场景(ChartConfigSchema 经 ReportChartSchema.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 时先确认已换成重合度版本。

参考

Activity

  1. added 2 commits that reference this issue on Aug 4, 2026
  2. added
    bugSomething isn't working
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    pm:queueReady for the PM dispatch loop
    and removed
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 4, 2026
  3. self-assigned this
    on Aug 6, 2026
  4. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    认领:PM 循环第 2 轮(spec-tooling 车道,C 包 #5163)
    会话:session_01559M8FVm6W6vDLABL3jvdW
    分支:claude/issue-5056-bfs-overlap-ratio
    Worktree:objectstack-issue-5056
    域:domain:spec-tooling
    文件面:packages/spec/src/ui/chart.test.ts、packages/spec/src/ui/widget.test.ts(抽出各自的 BFS 拷贝)、一份新的共享测试工具(落点按仓内测试工具惯例)、.changeset/*.md 或 skip-changeset。⛔ 不碰 packages/spec/src/**/*.zod.ts 与任何 schema 语义。(越界即停,报告说明)

    说明:测试文件属 C 包卡片(#5163)生成/报错工具组明列范围,非 spec 车道语义面。修法方向 issue 已给并经批 16 验证(整形状重合度 ≥ 0.5 取代任意单属性命中);三组对照(正/负/合成载体翻转)缺一不可。


    Generated by Claude Code

  5. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    验收:ACCEPT → PR #5833(skip-changeset 已由 PM 打上,转 ready 入队)

    前提清单更正(记录在案):共享工具 door-reachability.testkit.ts 在派单前已存在且已是重合度版本(widget/i18n 已迁移)—— 真正的残留是 issue 点名的引入处 chart.test.ts 自己没迁,而它抱着的恰好是缺陷版拷贝。本 PR 的实质是「杀掉真相的最后一份拷贝 + 给仪器本身补对照」,与立案建议 1/2 的精神一致。

    两处实测修正采信并已钉成 pin:

    1. issue 的 .describe() 复现写反了 —— def 共享跟着接收者实例走,不跟形状走(defOf(z.string().describe('x')) !== defOf(z.string()));照原文钉会钉住假命题,dev 钉的是修正后的事实,zod 升级改 clone 语义会响。
    2. derived-clone bridge 此前零消费者驱动(ChartConfig 实测 direct)—— 收紧到「永不触发」三个文件依然全绿;新增的真派生正对照(PageSchema.extend 0.96 / strip() 1.0)补上了这条腿的护栏。

    阈值 0.5 保持,两端实测距离都远。残余边界(全共享叶子小形状恒 1.0)以 KNOWN BOUND 具名 pin 钉住并立 #5828(finding,标签齐全)—— 被测量而非潜伏,处置得当。反向验证 2 红 2 绿逐文件与预测一致,chart.test.ts 保持绿的原因(其判定对缺陷不敏感)如实写明而非包装。

    skip-changeset 裁决:成立 —— 严格测试层 3 文件,*.testkit.ts 非导出面非 tsup entry,check:api-surface 绿实证发布面零变化。标签已按并集写入(size/m, tests, protocol:ui, skip-changeset)。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch looptoolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions