fix(spec): zodShapeOf 按管道方向解 z.preprocess 的授权面(#4488 盲点的第四个现场) - #6102
Conversation
) `a.transform(fn)` and `z.preprocess(fn, schema)` compile to the same `pipe` node with OPPOSITE authorable sides: IN for the first, OUT for the second. `zodShapeOf` read `def.in` unconditionally, so every preprocess node resolved to a transform, derived no shape, and the authorable-surface reachability computation silently fell through to its fail-closed default. This is the #4488 blind spot's fourth independent site — after scripts/liveness/check-liveness.mts (#4488), src/kernel/metadata-authoring-lint.ts and src/system/metadata-form-zod-reconciliation.test.ts (both #5074). The three earlier sites carried the lesson as a comment and it recurred anyway, so this one lands with an assertion: the walkers move to scripts/lib/zod-graph.ts (the same extraction route as schema-name #4592, format-type #4912 and def-key-collisions #5832) and scripts/zod-graph.test.ts pins the direction on both synthetic and live schemas. Measured, not assumed: generated output does not move. `gen:schema` leaves authorable-surface/ and json-schema.manifest/ byte-identical, and `check:generated` reports all 10 artifacts up to date. One reachability verdict changes — ui/InlineAction, root-graph (fail-closed) -> null (computed) — which matches its sole holder ui/ElementButtonProps and its eight ui/Element*Props siblings, all already null on main. No new derived-clone bridge (6 -> 6), so the #5056 false-reachability risk did not materialise. Fixes #5317
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckNo hand-written docs reference the 0 changed package(s). ✅ |
|
ESLint job 红归因(PM 座位,会话 Generated by Claude Code |
…shapeof-preprocess-direction
ESLint job 的红是 main 带进来的(#6100),不是本 PR第一轮 CI:26 个 check,25 绿,ESLint 红。红点不在 eslint 本身,而在同一个 job 里的
定位(两步,都可复现)
责任提交是 已有单子在跟:#6100( 合并 main 之后本 PR 的复验(全绿)值得单说的一条:合进 main 之后重跑 🤖 Generated with Claude Code https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5 Generated by Claude Code |
|
队列管家:本 PR 是链上连坐**,自身无问题 ⇒ ⛔ 未重投、无需改动** 03:12:06Z 的队列世代 该包不在本 PR 的改动面内(本 PR 仅动 链序铁证(不是推测): 现状与预期:#5827 已于 03:25:18Z 被踢出,本 PR 随之重建为 已核让行:本 PR 最近 30 分钟无车道 PM 动作。 Generated by Claude Code |
Fixes #5317
做了什么
a.transform(fn)和z.preprocess(fn, schema)编译成同一种pipe节点,但授权面在相反的一侧:前者是 IN(a是被接受的输入形状),后者的 IN 是那个 transform、授权面是 OUT。zodShapeOf无条件取def.in,所以对每一个 preprocess 节点拿到的都是 transform,解不出 shape,可达性计算就静默落到 fail-closed 默认值上。这是 #4488 那个盲点的第四个独立现场:
scripts/liveness/check-liveness.mtsTranslationItemSchema的退役方言 preprocess #3778 让translation"walked to no shape, ungovernable")src/kernel/metadata-authoring-lint.tssrc/system/metadata-form-zod-reconciliation.test.tsbuild-schemas.ts)前三处都把 #4488 的教训写成了代码注释,然后它还是复发了。所以第四处落的是断言:walker 抽到
scripts/lib/zod-graph.ts(与schema-name#4592 /format-type#4912 /def-key-collisions#5832 同一条抽取路线 ——build-schemas.ts是带副作用的顶层脚本,不抽就只能靠"跑整个生成器再读它写了什么"来断言,而"它写了什么"恰恰是静默漏走会毁掉的那份证据),scripts/zod-graph.test.ts用合成 schema + 真实图双向钉住方向。前提核验(issue 行号已过期)
issue 写的
build-schemas.ts:687/:699在今天的origin/main上已经不对 —— #5837 / PR #6069(80 文件的分片重构)今天合并,分片读写移到了lib/sharded-artifacts.ts。但盲点本身仍然成立:核验时zodShapeOf在:1000,那一行是全文件
preprocess出现 0 次。前提有效,只是坐标变了。issue 正文里另一句已经过期,一并记下:它说「
translation今天就是 preprocess 根注册」。实测不是 ——TranslationItemSchema的那个z.preprocess已经被换掉了(src/system/translation.zod.ts:263用的是过去时:"time was az.preprocessthat scanned for these ten keys before parsing")。盲点没消失,只是换了宿主:今天注册表里的 preprocess 根是view,正是 #5074 两处注释预言的那个。测量:生成物零移动
改前改后各跑一次
gen:schema,并在computeSurfaceReachability末尾临时 dump 全部 1610 个 def 的reachableVia判定做 diff(临时代码已在提交前删除)。分片生成物零 diff。 三次
gen:schema之后git status只有我自己的三个文件,authorable-surface/(14 个 category 分片)与json-schema.manifest/一个字节都没动,锚点authorable-surface.base.json也没动。check:generated10/10 全绿,含check:authorable-surface。所以"逐条解释产生的 diff"这一项的答案是:没有 diff 可解释。原因是可达性判定压根不进生成内容 ——
computeSurfaceReachability()只在 #4650 删除门禁发现「基线行被删」时调用(deletedKeys.length > 0),而zodShapeOf只被它一家使用。所以本 PR 的爆炸半径正好是门禁判定本身。唯一移动的一条判定,方向与 issue 预期相反
issue 预警的是
root-graph→derived-clone(#5056 的假可达桥)。实测没有发生:桥项 6 → 6,没有任何 def 新获得桥。真正移动的一条是反方向的:InlineActionSchema是lazySchema(() => z.preprocess(normalizeInlineAction, actionObject().pick({…}).partial({…}).refine(…)))—— 一个 OUT 是对象的活体 preprocess。改之前它的root-graph不是判断结果,是 walker 看错管道方向后掉进 fail-closed 分支的默认值。改之后它解出真实的 12 键 shape,逐项比对bridged表,一项都不匹配,于是答null。这个
null与它周围的事实一致,而不是新造的:ElementButtonPropsSchema.action(src/ui/component.zod.ts:507)对应的ui/ElementButtonProps在 main 上本来就是null;ui/Element*Props兄弟(ElementFilterProps/ElementFormProps/ElementImageProps/ElementMetadataViewerProps/ElementNumberProps/ElementRecordPickerProps/ElementTextInputProps/ElementTextProps)在 main 上也全是null;action属性的对象节点,那个action是个z.string().optional()(动作名),不是InlineActionSchema。也就是说
ui/InlineAction之前是这一族里唯一的异类,而它的异类身份完全由 walker 的 bug 制造。诚实标注方向:对删除门禁而言
null比root-graph宽(null免除 tombstone 要求)。这一格确实从"因为看不清所以从严"变成了"看清了,答案是不可达"。fail-closed 仍然是规则 —— 只是不再是 walker 的默认输出。reachableVia里那段注释已按这个事实改写。断言:第五次复发会当天红
scripts/zod-graph.test.ts,10 条。反向验证(把pipeAuthorableSide退回无条件取 IN)得到 6 红 4 绿,与逐条预测完全一致:a.transform(fn)仍取 INInlineActionSchema解出真 shapeaction不解成 transformview不解成 transformaction仍解出对象 shapeview的 OUT 是 union、仍解不出 shape其中「每个注册 pipe 根都不解成 transform」是真正的复发守卫:下一个 preprocess 根注册进来那天,如果 walker 又从错的一端读,它当场红,而不是三个 issue 之后才被人发现。
一条没有按 issue 字面写的断言
issue 建议钉「preprocess 根能被解出 shape」。这句话对
view今天不成立,所以照字面写会得到一条红测试。原因:view的 preprocess OUT 是z.union,而zodShapeOf没有 union 分支(三个同族 walker 都有,check-liveness 的那条带着 #3095 的注释)。方向修对之后pipeAuthorableSide(view)确实从transform变成了union—— 方向对了,shape 仍然是null。与其把断言掰成能过的样子,不如钉住真实成立、且同样能抓复发的那条("没有任何注册 pipe 根解成 transform"),再单独用一条测试诚实记录
view的现状。union 分支与同样缺失的prefault一起另行归档为 #6098(observation-class),因为补它们会新增桥项,需要自己那一轮测量 —— 正是 #5056 提醒的方向。Changeset:
skip-changeset(已量,非断言)packages/spec/package.json的files白名单不含scripts/;npm pack --dry-run实测 2054 个文件,scripts/命中 0。本 PR 三个文件没有一个进已发布产物。json-schema//api-surface//spec-changes.json这些确实发布的目录也没动。authorable-surface/本身就不在files里,是仓内证据而非发布内容。发布面为空 → 本 PR 不声明任何 release,走
skip-changeset标签而不是写 changeset 文件。验证
check:api-surface第一轮红过一次,是 AGENTS §9 的陈旧产物陷阱:该门禁读的是已构建的 dist,而新 worktree 没有dist/(门禁自己就打印了这条警告)。pnpm --filter @objectstack/spec build之后复跑转绿 —— 与本改动无关,scripts/根本不进 dist。同文件让行说明
build-schemas.ts上串行排在本单之后的两单,本 PR 对它们的影响:clauseMajors的.endsWith('.' + prop)匹配,在checkAuthorableDeletions内部;本 PR 从那个函数里搬走的是zodShapeOf一族(它们本来就定义在函数外),门禁主体一行未动。冲突面为零。SURFACE_BASE_DESCRIPTION或锚点验证逻辑(那段注释明说它的每个字节都是锚点规范形的一部分)。两单都既没变容易也没变难,更没变得不必要。
🤖 Generated with Claude Code
https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
Generated by Claude Code