发现于 #5074 实施途中(observation-class:今天没有用户能撞到,但盲点是真的,且会随下一个 preprocess 根注册复发)。
事实
packages/spec/scripts/build-schemas.ts:687:
if ( def . type === 'pipe' && def . in instanceof z . ZodType ) return zodShapeOf ( def . in , depth + 1 ) ;
管道有两种,授权面在相反的一侧:
a.transform(fn) —— 授权面是 IN (a 是被接受的输入形状);
z.preprocess(fn, schema) —— IN 是那个 transform ,授权面是 OUT 。
这里无条件取 def.in,所以对 preprocess 根拿到的是 transform,zodShapeOf 返回 null。
这个盲点在本仓已经有过一次实例并被修过 :scripts/liveness/check-liveness.mts:196 的 unwrap 带着 #4488 的注释,说的正是同一件事 —— TranslationItemSchema 的 retired-dialect preprocess 守卫(#3778 )让 translation 一度「walked to no shape,ungovernable」。那次只修了 liveness 一处;build-schemas.ts 这处没跟上。
#5074 又在两处碰到同一个盲点(kernel/metadata-authoring-lint.ts、system/metadata-form-zod-reconciliation.test.ts),都在 #5074 的 PR 里按 #4488 的写法修了。build-schemas.ts 故意留着没动 ,因为它的行为改变会移动生成基线(见下),超出 #5074 的范围。
为什么归为 observation-class 而不是缺陷
zodShapeOf 在这里只喂 computeSurfaceReachability 的 derived-clone 桥 ,而:
BFS 的 visited 闭包走的是 zodChildSchemas,它递归遍历 def 的所有值(含 def.out),所以可达性本身不受影响 ;
取不到 shape 时该函数 return 'root-graph'(fail closed,宁可多要一个 tombstone 也不静默放宽)。
所以当前表现是保守 ,不是错答。今天没有已知的错误产物。
但值得修
它是「同一个盲点、第二次出现」—— 活性账本覆盖 worklist:9 个已注册 metadata type 仍未治理(#4487 建立闸门后的剩余债务) #4488 的教训只落在一个文件里,没有落成规则。三处独立复发(liveness/活性账本覆盖 worklist:9 个已注册 metadata type 仍未治理(#4487 建立闸门后的剩余债务) #4488 、authoring-lint 与 form-reconciliation/ViewItemSchema 同时是授权形状和 Studio 往返的 wire 成员 —— 拆成两个 schema 还是保持宽松?(挡住 #4001 批 18 最后 2 站点) #5074 )说明这是模式,不是巧合。
translation 今天就是 preprocess 根注册,所以这条分支现在就在走错方向 ,只是后果被 fail-closed 兜住了。
修的时候注意
改成 #4488 的写法(IN 侧解开后若是 transform 则改走 OUT)会让 translation 这类 preprocess 根第一次拿到真实 shape ,derived-clone 桥因此可能新增 bridge 项,authorable-surface.json / 可达性判定有可能移动。所以这不是一行改动就完事:要跑 pnpm --filter @objectstack/spec check:generated,并逐条解释产生的 diff(尤其是任何从 root-graph 变成 derived-clone 的条目 —— 那正是 #5056 提醒过的、会把死形状标成可达的桥)。
参考实现:packages/spec/scripts/liveness/check-liveness.mts:191-205。
同一 PR 里可以顺手加一条断言,把「preprocess 根能被解出 shape」钉住,免得第四次复发。
发现于 #5074 实施途中(observation-class:今天没有用户能撞到,但盲点是真的,且会随下一个 preprocess 根注册复发)。
事实
packages/spec/scripts/build-schemas.ts:687:管道有两种,授权面在相反的一侧:
a.transform(fn)—— 授权面是 IN(a是被接受的输入形状);z.preprocess(fn, schema)—— IN 是那个 transform,授权面是 OUT。这里无条件取
def.in,所以对 preprocess 根拿到的是 transform,zodShapeOf返回null。这个盲点在本仓已经有过一次实例并被修过:
scripts/liveness/check-liveness.mts:196的unwrap带着 #4488 的注释,说的正是同一件事 ——TranslationItemSchema的 retired-dialect preprocess 守卫(#3778)让translation一度「walked to no shape,ungovernable」。那次只修了 liveness 一处;build-schemas.ts这处没跟上。#5074 又在两处碰到同一个盲点(
kernel/metadata-authoring-lint.ts、system/metadata-form-zod-reconciliation.test.ts),都在 #5074 的 PR 里按 #4488 的写法修了。build-schemas.ts故意留着没动,因为它的行为改变会移动生成基线(见下),超出 #5074 的范围。为什么归为 observation-class 而不是缺陷
zodShapeOf在这里只喂computeSurfaceReachability的 derived-clone 桥,而:visited闭包走的是zodChildSchemas,它递归遍历 def 的所有值(含def.out),所以可达性本身不受影响;return 'root-graph'(fail closed,宁可多要一个 tombstone 也不静默放宽)。所以当前表现是保守,不是错答。今天没有已知的错误产物。
但值得修
ViewItemSchema同时是授权形状和 Studio 往返的 wire 成员 —— 拆成两个 schema 还是保持宽松?(挡住 #4001 批 18 最后 2 站点) #5074)说明这是模式,不是巧合。translation今天就是 preprocess 根注册,所以这条分支现在就在走错方向,只是后果被 fail-closed 兜住了。修的时候注意
改成 #4488 的写法(IN 侧解开后若是
transform则改走 OUT)会让translation这类 preprocess 根第一次拿到真实 shape,derived-clone 桥因此可能新增 bridge 项,authorable-surface.json/ 可达性判定有可能移动。所以这不是一行改动就完事:要跑pnpm --filter @objectstack/spec check:generated,并逐条解释产生的 diff(尤其是任何从root-graph变成derived-clone的条目 —— 那正是 #5056 提醒过的、会把死形状标成可达的桥)。参考实现:
packages/spec/scripts/liveness/check-liveness.mts:191-205。同一 PR 里可以顺手加一条断言,把「preprocess 根能被解出 shape」钉住,免得第四次复发。