feat(spec)!: ViewItemSchema 拆成授权门 + wire 变体,并让 wire 的开放递归生效 (#5074) - #5319
Queued
os-zhuang wants to merge 5 commits into
Queued
feat(spec)!: ViewItemSchema 拆成授权门 + wire 变体,并让 wire 的开放递归生效 (#5074)#5319os-zhuang wants to merge 5 commits into
os-zhuang wants to merge 5 commits into
Conversation
…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
…witem-authoring-wire-split
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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 4, 2026
Open
…witem-authoring-wire-split
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
marked this pull request as ready for review
August 4, 2026 22:52
os-zhuang
enabled auto-merge
August 4, 2026 22:52
os-zhuang
added this pull request to the merge queue
Aug 4, 2026
Any commits made after this event will not be merged.
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 #5074
按维护者裁决 A —— 拆分(2026-08-04 03:02Z)+ 执行范围追加(05:24Z 的递归开放硬要求)实施。这也关掉了 #4001 批 18 最后 2 个站点。
前提复核(先验证再动手)
在
origin/main上逐条核过,裁决的前提全部成立:ViewItemSchema两个 arm 仍是 strip(z.object无.strict()),仍是ViewMetadataSchema的成员 1;view-strictness-batch18.test.ts§5、台账ui/行)——本 PR 把三处全部换成 A 态,没有留半更新的;ViewFilterRuleSchema(控制台保存筛选条件会 422:ViewFilterRuleSchema拒绝 filter-builder 盖的id,而 wire 成员的.strip()救不到嵌套块 #5114 热修重开)与ListView.sort[](批 18 回退)都还开着;userFilters.allowAddTab,随后收紧 UserFiltersSchema (#5073) #5237 的allowAddTab收紧都没被碰(本 PR 不涉及那些站点)。改了什么
授权/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 门校验前剥离。VIEW_CONSOLE_ROW_DECORATIONS+stripViewConsoleDecorations——stripReadDecorations的写路径镜像;filter[]/sort[]在任意深度的所有载体(扁平 overlay、ViewItem 的config、tabs[]、userFilters.tabs[],以及以后新增的载体);ViewFilterRuleSchema(控制台保存筛选条件会 422:ViewFilterRuleSchema拒绝 filter-builder 盖的id,而 wire 成员的.strip()救不到嵌套块 #5114 热修本就写明是「pendingViewItemSchema同时是授权形状和 Studio 往返的 wire 成员 —— 拆成两个 schema 还是保持宽松?(挡住 #4001 批 18 最后 2 站点) #5074」的临时态,本 PR 按正确结构重关)、ListView.sort[]条目(连同 一个被静默丢弃的排序键对外部调用方仍然是静默的:direction该 400 还是继续被丢掉(#4674 第 4 项) #4721 的direction→order别名一起回来 —— 那个别名拼错会让排序静默反向)。⛔
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.ts与metadata-form-zod-reconciliation.test.ts都无条件按def.in解管道,而 preprocess 的 IN 是 transform —— 两者都会把view判成「非键承载」,静默停止覆盖它。是被它们自己的覆盖率断言抓到的(lintables.length >= 1、root schema is not key-bearing),这正是那两条断言存在的意义。两处都按 #4488 的写法改成「取不是 transform 的那一侧」。gate 变哑比 gate 变红更糟。build-schemas.ts的zodShapeOf有同一个盲点,故意没在本 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 testpnpm --filter @objectstack/spec typecheckpnpm --filter @objectstack/objectql testpnpm --filter @objectstack/metadata-protocol testpnpm --filter @objectstack/metadata-core testcheck:generated/check:strictness-ledgerscripts/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 修)
clientValidation.ts的viewloader 拿授权 schema 去判一份 stored body:给视图打过 pin 之后再进 metadata-admin 编辑器,客户端会对一份服务端接受的 body 报isPinned未识别。方向反了(客户端比服务端严)。建议编辑路改用ViewMetadataSchema/ViewItemWireSchema,创建路保持ViewItemSchema。build-schemas.ts的zodShapeOf对z.preprocess走错管道方向(#4488 已在 check-liveness 修过的同一个盲点) #5317(finding)— 上面说的build-schemas.tszodShapeOf管道盲点。Rider:#4959 前提实测(只验证、未改
stack.zod.ts)结论:schema 层分歧属实,端到端不成立。实测(
stack.zod.ts:250,行号随 main 变动):ObjectStackSchema.views← ViewItemunrecognized_keys: ["viewKind","config"]@views[0]ObjectStackSchema.views← 容器ViewMetadataSchema(saveMetaItem门)← ViewItemdefineViewItem/ViewItemSchema← ViewItem所以确实存在两种「视图」形状:栈级
views:只收容器,持久化门收 ViewItem 记录。但 #4959 那句「objectui 产出根 schema 拒绝的元数据」在端到端上不成立:objectui 从不产出栈文档,它走client.meta.saveItem('view', name, body)→saveMetaItem→ViewMetadataSchema,而那正是 ADR-0017 的记录门,接受 ViewItem。两条是不同的生产者、不同的门。而且栈级的拒绝是有处方的,不是裸拒 ——
defineStack({ views: [ViewItem] })实测报:即「栈级
views:只放容器」是刻意契约,且拒绝自带改法。两点值得 PM 在裁 #4959 走向时知道(都不是 #5074 的范围):
views集合映射到view元数据类型 schema(即那个接受 ViewItem 的 union),而ObjectStackSchema.views只收容器。方向是良性的(parse 更严,且报错的是它),但意味着 lint 的预解析告警够不到这一类拒绝。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