Skip to content

feat(spec)!: ViewItemSchema 拆成授权门 + wire 变体,并让 wire 的开放递归生效 (#5074) - #5319

Queued
os-zhuang wants to merge 5 commits into
mainfrom
claude/issue-5074-viewitem-authoring-wire-split
Queued

feat(spec)!: ViewItemSchema 拆成授权门 + wire 变体,并让 wire 的开放递归生效 (#5074)#5319
os-zhuang wants to merge 5 commits into
mainfrom
claude/issue-5074-viewitem-authoring-wire-split

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5074

按维护者裁决 A —— 拆分(2026-08-04 03:02Z)+ 执行范围追加(05:24Z 的递归开放硬要求)实施。这也关掉了 #4001 批 18 最后 2 个站点。

前提复核(先验证再动手)

origin/main 上逐条核过,裁决的前提全部成立:

改了什么

授权/wire 拆分。 ViewItemSchema 两个 arm 收紧为 strictObject,成为授权门(defineViewItem / objectui 建视图表单)。新增 ViewItemWireSchema —— .strip() 的 wire 变体,接替 ViewMetadataSchema 的成员 1,isPinned / sortOrder 在它上面显式声明,不再靠「没人关这个成员」隐式活着。

discriminatedUnion 双变体 vs 抽 base —— 选了抽 base,理由是代价。 z.discriminatedUnion 不能 .extend(),所以两套变体只能各写一遍 arm;抽出 viewItemArmShape() 后两个姿态共用同一份 shape,差异只剩两处、且都在调用点可见(未知键姿态 + wire 多声明的两个键)。这是 #2231 的 derive-by-reference:一份契约不该有两份誊写。

递归开放 —— 这才是姿态翻转做不到的那半。 .strip().strict() 一样不递归,成员顶层重开保护不了嵌套已关块。走的是范围追加里第二条被认可的路线:声明一份装饰键词表,在 wire 门校验前剥离。

id 依然没有被声明(批 18 Q1 两轴否决在案):它是 React list key,声明它等于教 AI 作者给过滤规则发 UUID。故意没有做成第二棵并行 wire schema 树 —— 那是 PD#12 的分叉,而且下次有人给 ViewFilterRuleSchema 加载体时会静默腐化。

验收(范围追加第 3 条)

控制台列排序 PUT 与过滤器保存 PUT 的 body 直接 parse 通过,均复用了批 18 的 pin 与 #5114 的三路径探针;另加 8 个载体深度的参数化用例。两个具名地雷各有测试:z.toJSONSchema() 对新结构仍出四成员 anyOf(input / output 两个方向都断言了 —— /api/v1/meta/types/view 喂 Studio SchemaForm 靠它),lazySchema Proxy 的 ADR-0089 D3a 崩溃未复发(每个新/改 schema 都单独转换,不只经由父节点)。

反向验证的方向 —— 是反转的那一种,如实报告

对两个嵌套站点,不是常见的「改前绿、改后红」。ViewFilterRuleSchema.safeParse(consoleRow) 改前绿(shape 开着)、改后(授权门按名字拒绝)—— 这正是「关闭」本身。不该动的是 wire 门:控制台真正 PUT 的 body 改前改后都绿。所以诚实的反向检查是按门分,不是按 schema 分:

  • stripViewConsoleDecorations 从 wire 门拿掉 → 新测试 §3 红、§2 绿;
  • 反过来把 ViewFilterRuleSchema 重新开放 → §2 红、§3 绿。

view-filter-rule-wire-id.test.ts 的两条 mechanism 控制项(顶层 aux 键搭车、嵌套 emptyState 仍拒)#5114 重开与 #5074 重关都保持绿 —— 这才是它们作为控制项的价值。

一个真实副作用,同 PR 修掉

z.preprocess 落在注册根上,把两个 gate walker 送进了 #4488 已经在 check-liveness.mts 修过的同一个盲点:metadata-authoring-lint.tsmetadata-form-zod-reconciliation.test.ts 都无条件按 def.in 解管道,而 preprocess 的 IN 是 transform —— 两者都会把 view 判成「非键承载」,静默停止覆盖它。是被它们自己的覆盖率断言抓到的(lintables.length >= 1root schema is not key-bearing),这正是那两条断言存在的意义。两处都按 #4488 的写法改成「取不是 transform 的那一侧」。gate 变哑比 gate 变红更糟。

build-schemas.tszodShapeOf 有同一个盲点,故意没在本 PR 动 —— 改它会移动生成基线(translation 是 preprocess 根),超出本单范围,已单独立案 #5317

台账

#5220 格式:数字一律 pnpm gen:strictness-ledger 整体重跑;view.zod.ts 行的散文手写,B 态描述换成 A 态事实。Class cell 从 mixed · 5 authorable 变成 mixed · 1 authorable, 2 wire —— 算术本身是结论:strip 站点 5 → 3,不是「又关了两个」,而是关了四个、新增了两个(ViewItemWireSchema 的两个 arm,strip 是设计意图)。这张表量的是姿态不是意图,所以意图写在散文里。

验证

命令 结果
pnpm --filter @objectstack/spec test 309 files / 7996 passed
pnpm --filter @objectstack/spec typecheck 通过
pnpm --filter @objectstack/objectql test 117 files / 1882 passed
pnpm --filter @objectstack/metadata-protocol test 42 files / 388 passed
pnpm --filter @objectstack/metadata-core test 8 files / 103 passed
check:generated / check:strictness-ledger 全绿
scripts/check-nul-bytes.mjs + 控制字符自扫 干净

消费半径是按规则的调用方扫的、不是按被改的包:objectql 的 protocol-view-identity-overlay.test.ts / metadata-diagnostics.test.ts 各自带着 sort: [{ id, … }] 的 fixture,都经 wire 门,全部仍绿。objectui 侧只读不改,已确认其 createBuildBody 只发声明键(收紧后仍全部通过),clientValidation.viewShapes.test.ts 的 fixture 同样只用声明键。

附带发现(已立案,未在本 PR 修)

Rider:#4959 前提实测(只验证、未改 stack.zod.ts)

结论:schema 层分歧属实,端到端不成立。实测(stack.zod.ts:250,行号随 main 变动):

探针 结果
ObjectStackSchema.views ← ViewItem REJECT unrecognized_keys: ["viewKind","config"] @ views[0]
ObjectStackSchema.views ← 容器 ACCEPT
ViewMetadataSchema(saveMetaItem 门)← ViewItem ACCEPT
defineViewItem / ViewItemSchema ← ViewItem ACCEPT

所以确实存在两种「视图」形状:栈级 views: 只收容器,持久化门收 ViewItem 记录。但 #4959 那句「objectui 产出根 schema 拒绝的元数据」在端到端上不成立:objectui 从不产出栈文档,它走 client.meta.saveItem('view', name, body)saveMetaItemViewMetadataSchema,而那正是 ADR-0017 的记录门,接受 ViewItem。两条是不同的生产者、不同的门。

而且栈级的拒绝是有处方的,不是裸拒 —— defineStack({ views: [ViewItem] }) 实测报:

viewKind belongs to a single VIEW, not to the container. Wrap it: defineView({ list: { type, data, columns, … } })

即「栈级 views: 只放容器」是刻意契约,且拒绝自带改法。

两点值得 PM 在裁 #4959 走向时知道(都不是 #5074 的范围):

  1. 两个门对同一个数组的形状判断不一致:授权 lint 把 views 集合映射到 view 元数据类型 schema(即那个接受 ViewItem 的 union),而 ObjectStackSchema.views 只收容器。方向是良性的(parse 更严,且报错的是它),但意味着 lint 的预解析告警够不到这一类拒绝。
  2. 运行时比 schema 宽:objectql/engine.ts 的注册循环把 views: 数组里的每一项都注册为 view 类型,只在 isAggregatedViewContainer 为真时额外展开 —— 所以一个 ViewItem 放进 manifest 的 views: 数组在运行时能注册成功,而 defineStack 会拒。属 declared ≠ enforced 的「运行时接受得更多」方向,今天没有用户能撞到(授权门先拒,且带处方)。

建议 PM 据此重裁 #4959:按原文的「objectui 产出根 schema 拒绝的元数据」是前提不成立;若要保留,应重新聚焦到上面第 1/2 条。


🤖 Generated with Claude Code


Generated by Claude Code

claude added 3 commits August 4, 2026 18:40
…ariant (#5074)

`ViewItemSchema` carried two contracts at once: the authoring surface
`defineViewItem()` and Studio's view-create form parse, AND member 1 of the
`ViewMetadataSchema` union that `saveMetaItem` validates every persisted `view`
body against. The wire role needed Studio's round-trip keys through, so the
shape stayed open — and `defineViewItem({ …, confg: {…} })` parsed clean,
handing back a ViewItem with no view configuration at all.

Per the maintainer's ruling (option A, 2026-08-04):

- `ViewItemSchema` is strict on both arms — the authoring gate.
- `ViewItemWireSchema` is the `.strip()` wire variant and is member 1 of
  `ViewMetadataSchema`, with `isPinned` / `sortOrder` DECLARED on it instead of
  surviving because nobody closed the member. Both are built from one
  `viewItemArmShape()`, so the two postures cannot drift into two
  transcriptions (a `discriminatedUnion` cannot be `.extend()`ed).

The scope addendum's hard requirement was recursive-effective openness, which a
posture flip cannot deliver: `.strip()` re-opens a member's TOP level only, so
the console-decorated NESTED blocks were still reached at full strictness.
`stripViewConsoleDecorations` (+ the declared `VIEW_CONSOLE_ROW_DECORATIONS`
vocabulary) removes them at the wire door before the union runs — the
write-path mirror of `stripReadDecorations`. That let both blocked sites close:

- `ViewFilterRuleSchema` — #5114's hotfix was explicitly provisional; retired.
- `ListView.sort[]` — 批 18's revert (#5070); the `direction → order` alias
  (#4721, a silently REVERSED sort) comes back with it.

`id` is still not declared anywhere: it is a React list key, and declaring it
would teach an AI author to emit a UUID (批 18 Q1, rejected on record).

Two gate walkers went silent on `view` under a preprocess-rooted registration —
the exact blind spot #4488 already fixed in `check-liveness.mts` — and are
fixed the same way here. A gate that stops covering a type is worse than one
that fails.

Fixes #5074

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
The os-regen merge driver defers generated artifacts rather than text-merging
them, so `json-schema.manifest.json` came out of the merge holding this
branch's pre-merge side — which still listed `system/HttpServerConfig`,
`ui/Animation` and `ui/ZIndex`, all retired by the #5289/#5293 chain. Reset the
deferred artifacts to `origin/main`, rebuilt, and regenerated wholesale.

Asserted after regenerating, because a silent one-side drop is exactly what
this step exists to catch: the api-surface delta vs `origin/main` is exactly
this PR's four additions and ZERO removals; the manifest delta is one addition
(`ui/ViewItemWire`) and zero removals; the sibling retirements
(`HttpServerConfigSchema`, `ui/Animation`, `ui/ZIndex`) all stay removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 10:51pm

Request Review

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 2 commits August 4, 2026 22:26
Second relay merge: #5300 / #5304 / #5306 / #5308 / #5318 / #5326 / #5327.
Textually clean, but the os-regen driver defers generated artifacts rather than
text-merging them, so `json-schema.manifest.json` again came out holding this
branch's pre-merge side — this time still listing `ui/EmbedConfig` and
`ui/NotificationAction`, both retired by #5300. Reset the deferred artifacts to
`origin/main`, rebuilt from the merged tree, regenerated wholesale.

Post-regen assertions (a silent one-side drop is exactly what this catches):
api-surface delta vs `origin/main` is exactly this PR's four additions and ZERO
removals; manifest delta is one addition (`ui/ViewItemWire`) and zero removals;
every sibling retirement stays removed (`ui/EmbedConfig`, `ui/NotificationAction`,
`system/HttpServerConfig`, `ui/Animation`, `ui/ZIndex`) and every sibling
addition stays present (`FilterArray` ×7, `EmailProvider` ×2).
`check:authorable-surface` (+ its #5304 `.base.json` anchor) is green and the
anchor file is byte-identical to `origin/main` — not hand-edited.

`metadata-form-zod-reconciliation.test.ts` co-edited with #5280/#5318 and merged
SEMANTICALLY, not by taking a side: #5318 rewrote the docblock, imports, helpers
and test bodies, while this PR's only edit is `unwrap`'s `pipe` case, so the two
did not overlap textually — but they do interact, and in the direction that
matters. #5318's `isRetiredAt` / `authorableKeysOf` both route through
`unwrap`/`keysOf`, and `view`'s root is now a `z.preprocess` pipe. Measured both
ways: without this PR's #4488-style fix `unwrap(view root)` resolves to
`transform` and `keysOf` returns NULL, so #5318's brand-new tombstone assertions
would be VACUOUS on `view` (and the pre-existing key-bearing assertion would
fail outright); with it, 89 keys. Both PRs' assertions are live on every type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
@os-zhuang
os-zhuang marked this pull request as ready for review August 4, 2026 22:52
@os-zhuang
os-zhuang enabled auto-merge August 4, 2026 22:52
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 4, 2026
Any commits made after this event will not be merged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ViewItemSchema 同时是授权形状和 Studio 往返的 wire 成员 —— 拆成两个 schema 还是保持宽松?(挡住 #4001 批 18 最后 2 站点)

2 participants