Skip to content

zodShapeOf 的 unwrap 面比三个同族 walker 窄:缺 union 分支与 prefault(#5317 修完管道方向后剩下的两格) #6098

Description

@os-zhuang

发现于 #5317 实施途中(observation-class:两格今天都被 fail-closed 兜住,没有已知的错误产物)。

packages/spec/scripts/lib/zod-graph.ts 的 zodShapeOf 是 #5317 从 build-schemas.ts 抽出来的;它有三个同族 walker,做的是同一件事(把一个 Zod 节点解到对象 shape):

  • packages/spec/scripts/liveness/check-liveness.mts 的 unwrap / shapeOf
  • packages/spec/src/kernel/metadata-authoring-lint.ts 的 unwrap / keyPosture
  • packages/spec/src/system/metadata-form-zod-reconciliation.test.ts 的 unwrap / keysOf

#5317 已经把管道方向这一格对齐了(z.preprocess 走 OUT)。剩下两格仍然只有 zodShapeOf 缺:

事实 1 —— 没有 union 分支

zodShapeOf 只认 object,遇到 union 返回 null。三个同族 walker 都合并 union 成员的键(check-liveness 的 shapeOf 带着 #3095 的注释,说的正是「一个 metadata type 可能注册一组 shape 的 UNION 而不是单个对象」)。

后果,#5317 实测:view 是注册表里唯一的 z.preprocess 根,其 OUT 是 z.union(#5074 的 console-decoration 剥离)。#5317 把方向修对之后,pipeAuthorableSide(view) 从 transform 变成了 union —— 但 zodShapeOf(view) 依然是 null,因为没有 union 分支。所以「preprocess 根能被解出真实 shape」这句话对 view 今天仍然不成立,方向修对是必要但不充分。

packages/spec/scripts/zod-graph.test.ts 里有一条测试把这个状态诚实钉住了(documents that view's preprocess OUT is a union, so it still derives no shape),加 union 分支的人会先撞到它。

事实 2 —— wrapper 集合缺 prefault

zodShapeOf 的 SHAPE_WRAPPER_TYPES 是 optional / nullable / default / catch / readonly / nonoptional。三个同族 walker 的对应列表都还有 prefault。一个 prefault 包着的对象在 zodShapeOf 下解不出 shape。

#5317 特意没有顺手加:它没被测量过,而 zodShapeOf 喂的是 derived-clone 桥,任何解出更多 shape 的改动都会新增桥项。

为什么是 observation-class 而不是缺陷

两格的后果都落在 computeSurfaceReachability 的 reachableVia,而那里解不出 shape 时 return 'root-graph'(fail closed —— 宁可多要一个 tombstone,不静默放宽)。所以当前表现是保守,不是错答;而且 reachableVia 只在 #4650 删除门禁发现「基线行被删」时才被调用,平时的 gen:schema 根本不走。今天没有用户能撞到。

修的时候注意(#5056)

这两格任何一格补上,都会让更多节点解出 shape,从而给 bridged 表新增 (propName → propSchemaInstance) 项 —— 这正是 #5056 提醒过的、会把死形状标成可达(null → derived-clone)的桥。所以不是加一行就完事:

两格建议放同一个 PR 一起量:它们改的是同一个函数的同一片 unwrap 面,分开做要把上面那套测量跑两遍。

Activity

  1. claude commented on Aug 7, 2026

    @claude
    Contributor

    Findings triage — first grading (this issue had never been graded). Verdict: HOLD as finding, + domain:spec-tooling, with a mechanically checkable restart condition.

    Why hold, not promote. The observation-class argument was checked, not accepted on trust, and it holds: both gaps land in computeSurfaceReachability's reachableVia, which returns 'root-graph' when no shape resolves — fail-closed, so the current behaviour is conservative (an extra tombstone), never a wrong answer; and that path only runs when #4650's deletion gate sees a baseline row removed, not during ordinary gen:schema. Nothing a user reaches today, which is exactly the finding criterion rather than the queue's.

    And a hard serial reason on top of the grading. PR #6102 (draft, for #5317) is rewriting packages/spec/scripts/lib/zod-graph.ts and zod-graph.test.ts right now — the same function and the same test file this issue is about. Queuing it today would put a second agent inside a file already in flight, on work whose own body describes it as "what remains after #5317". Even on the promote reading, the correct state would be pm:queue + pm:blocked; holding is the more honest of the two, since the observation grade stands on its own.

    Restart condition (checkable, no memory required): PR #6102 merges (or #5317 closes) ⇒ re-grade this issue in the next findings round, with promotion as the expected outcome. At that point the file is free, the pipeline-direction cell is settled, and the body already carries the measurement protocol a dispatch would need (dump every def's reachableVia before and after, explain each moved verdict, run gen:schema + check:generated, and account for any entry that becomes derived-clone — the #5056 bridge hazard). Both cells go in one PR, as the body argues, so the measurement runs once.

    Also noted for the re-grade: packages/spec/scripts/zod-graph.test.ts currently pins the honest present state ("documents that view's preprocess OUT is a union, so it still derives no shape"). Whoever adds the union branch must change that test — a green test deleted without comment would erase the record of why the gap existed.

    Routing. domain:spec-tooling, matching its parent #5317 (same file family, packages/spec/scripts/**) so the pipeline-direction fix and its leftovers stay in one lane. ⚠️ Recording a taxonomy gap for the maintainer rather than papering over it: domain:spec-tooling is in active use on six open issues but does not appear in the SKILL's domain table, whose domain:spec row reads packages/spec 及其生成物. Existing usage was followed here for lane consistency; the table should either gain the row or the label should be retired, by PR — a label the table cannot explain will eventually be applied two different ways.

    Dedup. Full paginated enumeration of open issues and PRs across the three repos (search API unavailable to repository-scoped sessions). #5317 / PR #6102 is the parent, not a duplicate — it fixed the pipeline direction only, and this issue is explicitly the residue. No other open record covers zodShapeOf's unwrap surface.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. claude commented on Aug 7, 2026

    @claude
    Contributor

    Findings triage (objectstack#4949 discipline) — restart condition FIRED ⇒ PROMOTE to pm:queue. Domain unchanged: domain:spec-tooling.

    1. The trigger, verbatim from the previous verdict

    Restart condition (checkable, no memory required): PR #6102 merges (or #5317 closes) ⇒ re-grade this issue in the next findings round, with promotion as the expected outcome.

    Both halves fired, ~1.6 h apart from this round: PR #6102 merged 2026-08-07T03:37:02Z, #5317 closed 2026-08-07T03:37:03Z.

    2. Stale-premise check (origin/main @ ede5a8e) — both gaps still stand

    • union branch, absent. zodShapeOf (packages/spec/scripts/lib/zod-graph.ts:165) branches on exactly four shapes — object, pipe (via pipeAuthorableSide, which is what fix(spec): zodShapeOf 按管道方向解 z.preprocess 的授权面(#4488 盲点的第四个现场) #6102 rewrote), lazy, and SHAPE_WRAPPER_TYPES — then falls through to return null. No union arm.
    • prefault, absent. SHAPE_WRAPPER_TYPES (:87) is exactly optional, nullable, default, catch, readonly, nonoptional. The sibling walker at packages/spec/scripts/liveness/check-liveness.mts:225 does unwrap prefault, so the asymmetry the body reports is live.

    3. The merged code names this issue as the owner of the remainder

    zod-graph.ts:83-86, landed by #6102:

    prefault — which the three sibling walkers below do unwrap — is knowingly absent; adding it is a separate, separately measured change (filed as its own finding, not smuggled in here).

    The deferral was deliberate and this is the designated follow-up, which removes the "is this in scope for someone else?" question a dispatch would otherwise have to answer first.

    4. The hard serial reason for the previous hold is gone

    The last verdict held partly because #6102 was rewriting zod-graph.ts and zod-graph.test.ts in flight. Enumerated all 5 open PRs in this repo this round (#6171, #6165, #6164, #6160, #4935) — none touches packages/spec/scripts/**. The file is free.

    5. Not a release blocker

    ⛔ No target:<major>: computeSurfaceReachability's reachableVia returns 'root-graph' when no shape resolves, so the behaviour is fail-closed (one extra tombstone, never a wrong answer), and the path only runs under #4650's deletion gate — not a shipped-surface defect on the binary criterion.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. self-assigned this
    on Aug 7, 2026
  4. os-zhuang commented on Aug 7, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 5 轮(spec-tooling 车道,座位登记 #6018;分诊座位已核 restart condition 双半触发并晋级)
    会话:session_014wsZeReNTqiceBfLb5Pyf5
    分支:claude/issue-6098-zod-graph-unwrap-parity
    Worktree:objectstack-issue-6098
    域:domain:spec-tooling
    文件面:packages/spec/scripts/lib/zod-graph.ts(union 分支 + prefault 两格)+ packages/spec/scripts/zod-graph.test.ts(含把 view 现状钉死的那条测试——补 union 后它必然要改,这是预期的,改动要在 PR 里逐条说明)+ 分片生成物(若可达性判定移动)+ .changeset/*.md。⛔ 不碰 build-schemas.ts(#5371 本轮并行)、lib/file-description.ts、lib/format-type.ts、zod 文件。(越界即停,报告说明)

    ⚠️ #5056 测量轮是本单的核心要求:两格任一补上都会让更多节点解出 shape、给 bridged 表新增桥项。派发令要求逐条解释每个新增桥项,并特别标注任何 root-graph → derived-clone 的迁移(那正是 #5056 警告的假可达面)。#5317 的 dev 特意没有顺手加这两格,就是因为它需要独立的测量轮——本单就是那一轮。


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions