fix(spec): zodShapeOf 补齐 union 分支与 prefault,并附 #5056 测量轮 (#6098) - #6222
Merged
Conversation
`zodShapeOf` was two spellings narrower than its sibling walkers after #5317 fixed the pipe direction: it had no `union` arm (so a union node derived no shape at all) and its `SHAPE_WRAPPER_TYPES` omitted `prefault`. Both cells are here, measured separately. union: a metadata type may register a UNION of shapes rather than a single object (#3095). `view` is the shipped specimen — the registry's one `z.preprocess` root, whose OUT is a 4-member union — so after #5317 it had the right side and still derived nothing. A union now resolves to the MERGE of its members' keys, recursively (member 0 is itself a union), first member wins on a name two members declare. That merging rule follows `keysOf`/`keyPosture` rather than check-liveness's first-object-member rule, because this walker feeds reachability, where a missed key can only waive a tombstone. prefault: measured as inert today — the 25 metadata-type roots reach zero `prefault` nodes and `.prefault(` has no call site in `packages/`. It is parity against five sibling walkers that all peel it, so the first author to write one does not silently lose this walker. Measured with the #5056 protocol (every def's reachableVia dumped before and after): - bridge table 1518 -> 1519 pairs. The one new entry (`children`, from ui/NavigationItem) comes from a `z.lazy` getter that mints fresh instances per call, so it matches nothing — 0 defs hit it. Widening this walker cannot widen the bridge in general: every object a resolved shape comes from is already in the BFS closure and has already contributed the same pairs. - 18 verdicts move, all on the QUERY side, all out of the `!shape` fail-closed default: 17 root-graph -> null and 1 root-graph -> derived-clone (ui/ViewItem, a genuine shared-instance bridge with the view root's own union members). Every one of the 17 has holders that already answer null, or no holder at all. Totals: root-graph 520 -> 502, null 1084 -> 1101, derived-clone 6 -> 7. - prefault alone: 0 verdict moves, 0 bridge changes. - merge order is immaterial: a last-member-wins build gives identical verdicts for all 1610 defs. - generated artifacts do not move; check:generated reports all 10 up to date. The `view` pin in zod-graph.test.ts is CONVERTED, not deleted: it documented that a corrected direction still derived no shape, and now pins the merged shape that direction leads to. Fixes #6098
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 0 changed package(s). ✅ |
Contributor
Author
|
PM 验收:ACCEPT — 已 ready + auto-merge。 验收依据取自 GitHub 侧读数,不采信报告自述:
本单真正需要盯的一格,也是我重点复核的一格:这轮唯一的放宽方向不是派发令预期的
三项额外加分,记在这里因为它们是我希望在本车道成为常规的做法:
parity 未竟的两格( Generated by Claude Code |
This was referenced Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6098
zodShapeOf在 #5317 把管道方向修对之后,仍比三个同族 walker 窄两格:没有 union 分支、wrapper 集合缺prefault。本 PR 把两格补齐,并按 #5056 要求做了完整的测量轮——两格分开量,逐条解释判定移动与桥项变化。改了什么
packages/spec/scripts/lib/zod-graph.ts:mergedUnionShape):union 解为全部成员键的合并,递归(view的 OUT union 第 0 个成员本身又是 union),同名键取第一个成员的实例。prefault加入SHAPE_WRAPPER_TYPES。该常量同时被pipeInIsTransform读,所以「藏在prefault后面的 preprocess transform」两端都能看见。合并规则为什么跟
keysOf/keyPosture(合并全部成员)而不是 check-livenessshapeOf(取第一个 object 成员):两者被问的问题不同。shapeOf治理的是「规范可编写容器」的台账,取首个 object 成员是它的设计答案;这个 walker 喂的是可达性,漏一个键只会白白豁免一个 tombstone,所以取最宽的读法。#5056 测量轮(本单的核心)
方法同 #5317:在
computeSurfaceReachability末尾临时 dump 全部 1610 个 def 的reachableVia判定 + 完整bridged表 + 每个桥项的贡献者 + 每个 def 的 holder(从全部 emitted def 出发的反向 BFS),改前/改后各跑一次gen:schema做 diff。该 instrumentation 已在提交前完全还原,本 PR 不含build-schemas.ts的任何改动。两格分开量
prefaultprefault之所以完全惰性:25 个 metadata-type 根的闭包里一个prefault节点都没有,且.prefault(在整个packages/下没有任何调用点。它是 parity 而不是 fix——五个同族 walker(check-liveness.mts、metadata-authoring-lint.ts、metadata-form-zod-reconciliation.test.ts、metadata-type-schemas.test.ts、shared/strict-object.ts)都剥prefault,只有这一个不剥;第一个写下.prefault()的人,会恰好只在这个喂删除门禁的 walker 上静默丢掉 shape。bridged表:唯一一个新增桥项,逐条解释zodChildSchemas会走 union 的options、wrapper 的innerType、pipe 的两侧和lazy的 getter——那个 object 节点本来就在 BFS 闭包里,早已贡献过同样的 (name, instance) 对。union 节点自己解出的合并 shape 只是它成员已贡献内容的子集。NavigationItemSchema = z.lazy(() => z.discriminatedUnion('type', [... .extend({ children })...])),其 getter 没有记忆化(实测getter() !== getter()),每次调用都会.extend()出全新的children实例。zodChildSchemas调一次 getter 入队,zodShapeOf又调一次拿到另一份——于是桥表里多了一个从未被 BFS 访问过的实例。.describe()共享 def 对象,任意单属性 bridge 把无关形状连起来 #5056 说的假可达桥?不是,它谁也匹配不上:实测0 个 def 命中该桥项(每次zodShapeOf都会 mint 新实例,身份永远对不上)。它是惰性的,不改变任何判定。z.lazygetter 让zodShapeOf每次解出**新实例**:桥项进得去、永远匹配不上(#6098 测量轮的副产品) #6221,不在本 PR 修):非记忆化z.lazy的 shape 实例身份不稳定。今天不产生错误答案——ui/NavigationItem自身在visited里(先于 shape 判定返回root-graph),data/FilterArray则根本解不出 shape、维持 fail-closed。判定移动:18 条,全部发生在查询侧
总量:
root-graph520 → 502、null1084 → 1101、derived-clone6 → 7。18 条移动没有一条来自新桥项——全部来自
reachableVia里if (!shape) return 'root-graph'那个 fail-closed 兜底:这些 def 自己的 shape 现在解得出来了,于是改由真实判定回答。root-graph → derived-clone(#5056 的假可达面)。实测这个方向只有 1 条,而且它不改变严格度——root-graph和derived-clone对门禁都是「可达」,同样要求 tombstone,变的只是提示语。真正需要盯的是派发令没有预期的另一个方向:17 条root-graph → null,而null是豁免 tombstone。这是本轮唯一的放宽,所以下面逐条给证据。(a) 唯一一条
root-graph → derived-cloneui/ViewItemvisited。现在解出 18 键,其中name命中的桥实例ui/ViewItemName与view根 pipe、其 OUT union、ui/ViewItemWire及两个扁平 view 成员是同一个实例;_lock等 ADR-0010 provenance 字段同理。这是真桥——ViewItem 确实通过view根 union 成员的派生克隆可编写。且该桥项改动前就已存在(由那两个 object 成员贡献),本次只是它自己的 shape 终于解得出来去命中它。(b) 17 条
root-graph → null(放宽方向,逐条 holder 证据)全部 17 个都是
union类型、visited=false(BFS 身份不可达——这点本次改动前后完全一致,visited恒为 4900 个节点),且没有任何桥命中。判定它们「不可达」的最强旁证是 holder:每一个的 holder 家族本来就全部答null,或者根本没有 holder(顶层 export,没人内嵌)。这正是 #5317 为ui/InlineAction用过的同一条论证。ai/KnowledgeSourceKindai/KnowledgeSource[null]ai/MessageContentai/ConversationMessage[null]api/DeviceTokenResponseapi/WebSocketMessagedata/DataEngineRequestdata/FileLikeValuedata/GroupByNodedata/Query[null]integration/ConnectorInstanceAuthintegration/Connector[null]、integration/DeclarativeConnectorEntry[null]kernel/ManifestPermissions[null](kernel/Manifest、kernel/InstalledPackage、kernel/UpgradeSnapshot及 9 个api/*Package*请求/响应)system/CRDTStatesystem/CRDTMergeResult[null]system/MigrationOperationsystem/ChangeSet[null]system/OTComponentsystem/OTOperation[null]、system/OTTransformResult[null]system/SpecifierHandlersystem/Specifier[null]system/TenantIsolationConfigui/ChartGroupByui/ChartAggregate[null]ui/RecordHighlightsFieldui/RecordHighlightsProps[null](react-block props 家族,#5317 记录过ui/Element*Props早已全体答 null)ui/WidgetSourceui/WidgetManifest[null]判定:这 17 条是把兜底换成测量,不是放松门禁。 它们此前答
root-graph的原因与可达性无关——只是「union 解不出 shape」这一个 walker 缺口;它们的 holder 家族早已全体答null,现在它们和自己的家族答案一致了。#5317 对ui/InlineAction做的正是同一件事(root-graph兜底 →null实测),并已随 PR #6102 合入。合并顺序(同名键取第一个成员)——已测,今天无差别
同名不同实例的碰撞在闭包的 92 个 union 节点里出现在 11 个上(
view的 4 成员 union 就碰撞 21 个键)。因为返回类型是「一个名字一个实例」,后面成员的实例不会进入记录,理论上会漏掉一个桥测试。实测:把同一函数改成 last-member-wins 重跑一遍,1610 个 def 的判定完全一致,桥项总数同为 1519。所以这个选择今天不影响任何结论;真要影响的那天,修法在消费侧的桥(name → instance 集合),而不是在这里放宽测试——这一点写进了函数注释。分片生成物:零移动
gen:schema全程没有打印任何📒 … — commit it.(该行只在分片字节真的变化时才打),改动后git status对authorable-surface/、json-schema.manifest/、api-surface/全部干净,check:generated报告 10/10 up to date。测试:那条
view钉子是转换,不是删除packages/spec/scripts/zod-graph.test.ts里documents that view's preprocess OUT is a union, so it still derives no shape钉的是「方向修对但仍解不出 shape」这一诚实现状,本 PR 恰好消除了这个限制。按要求转换为resolves view's preprocess OUT union to the MERGE of its members:pipeAuthorableSide(view)仍是union)——那是 活性账本覆盖 worklist:9 个已注册 metadata type 仍未治理(#4487 建立闸门后的剩余债务) #4488 的复发守卫,与 union 分支无关;isPinned(只有第 0 个成员有,而它本身是嵌套 union)、list(容器成员)、pagination(扁平 list 成员)、sections(扁平 form 成员)。合并一旦塌回单一成员,这条就红。另新增 7 条钉子:union 合并 / 同名取首个实例(钉实例身份)/ 嵌套 union / 无 object 成员的 union 仍返回
null(放宽不得把「没有键可贡献」变成空 shape)/ discriminated union 走同一条分支(并钉住 Zod 4.4.3 下z.discriminatedUnion的def.type就是union,所以这里故意不抄同族 walker 那条永远匹配不上的discriminated_union分支)/prefault剥离 /prefault后面的 preprocess transform。反向验证(先写预测再跑):预测「把两格删掉后,7 条新钉子红、其余 10 条绿,其中『无 object 成员的 union 返回 null』因为旧代码 union 恒返回 null 而保持绿」。实测
Tests 7 failed | 10 passed (17),红的正是预测的那 7 条。Changeset:不加,改用
skip-changesetpackages/spec的files白名单是dist / json-schema / liveness / prompts / llms.txt / README.md / src/**/*.zod.ts / CHANGELOG.md / api-surface / spec-changes.json——scripts/不在发布物里;而会发布的生成物(json-schema、api-surface、authorable-surface)本轮逐字节未变。所以这个 PR 不发布任何东西。同一文件面的父 PR #6102(#5317)也未带 changeset,先例一致。门禁
Generated by Claude Code