Uh oh!
There was an error while loading. Please reload this page.
feat(spec,lint,metadata-protocol): a page member on the view type enum — mount a published page on an object view - #13372
Conversation
#13216) Adds a `page` member to `ListViewSchema.type` plus a `pageName` binding, so an object list view / tab can mount an already-published custom page. Rendering is delegated to the existing page renderer; no metadata type's authorization flags are touched. Part of #13216 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
…d i18n bundles; derive the batch-closure accumulator from RuntimePendingDeclarations Part of #13216 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
…ew `page` surface block `view.form.ts`'s `page` section is gated by `visibleWhen: "data.type == 'page'"` exactly as its seven sibling surface blocks are, so the shipped-metadata-form corpus carries one more predicate (48 -> 49) and one more `==` literal comparison (41 -> 42). Both pins track the corpus, not an issue; the expectation moves in place with the reason recorded beside it. Part of #13216 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
…ew-type-page-member
os-zhuang
commented
Aug 30, 2026
PM 回复:落地姿态正确。 |
Step 4 of scripts/pm/os-regen-merge.sh: the merge commit landed first (the pre-commit hook recorded the deferral), and this commit is the regeneration on top of it — schema, reference docs, liveness counts, api-surface and the react-blocks contract, all from the merged tree. Part of #13216 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
📓 Docs Drift CheckThis PR changes 4 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 127 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ed0be873d9a4774445b9862a2e1b5b154dafe9c4 && git checkout ed0be873d9a4774445b9862a2e1b5b154dafe9c4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3f54efdf52c7c80a3ec242fa9a9e779a5873f852 90042c379c6ba05d8c044071f2c666d2f66bb044 && git checkout -B drift-repro 3f54efdf52c7c80a3ec242fa9a9e779a5873f852 && git merge --no-ff 90042c379c6ba05d8c044071f2c666d2f66bb044
node scripts/docs-audit/affected-docs.mjs --json 3f54efdf52c7c80a3ec242fa9a9e779a5873f852
|
os-zhuang
commented
Aug 30, 2026
文档漂移告警 —— 已测量。⛔ 无需改动,但给契约复审者留一条真实输入( 六个手写页里,五个是经 真正值得看的是第六行:
|
| 位置 | 性质 | 含义 |
|---|---|---|
content/docs/references/ui/app.mdx ×5 | 生成 | "Target custom page component name" |
content/docs/ui/apps.mdx:132 / :378 / :636 | 手写 | 导航项:{ id, type: 'page', label, pageName, icon } |
(正对照:content/docs/ui/** 下 17 个页面提到 view,语料可搜,所以这些命中是读数。)
⇒ 既有的 pageName 挂在导航项上,本 PR 新增的挂在 view 上 —— 拼写相同、判别值 type: 'page' 也相同、但形状不同。
⛔ 这不是一个缺陷,两者是不同的 schema,本 PR 也没有改动导航项。但它是契约复审该权衡的东西,所以摆在这里而不是替复审者裁:
- 一致性 vs 碰撞:同一个概念用同一个拼写,是好事;但在一段代码片段里,
{ type: 'page', pageName: 'x' }现在有两种读法,靠所处的父结构区分。文档里两处示例都长这样。 ⚠️ 它和被缓议的方向 2 共用这个键。裁决把「发布时注册 app 导航」推给了独立设计卡 —— 而既有的pageName正住在方向 2 要动的那个导航项形状上。⇒ 两个方向共享一个键名,方向 2 落地时会同时面对两处。apps.mdx:311还给导航项的pageName记了一条授权属性:被门禁挡下的条目「连同它的objectName/pageName/componentRef目标永不离开服务端」。本 PR 为 view 级pageName给出的授权论证是另一条机制(委托渲染 ⇒page.assignedProfiles随行)。同名、不同机制 —— 复审时值得并排看一眼。
结论
⛔ 本 PR 无需改动,也未编辑任何文档页。手写页里没有任何一句被本 diff 证伪 —— 既有的 pageName 描述的是导航项,本 PR 没碰导航项。
记在这里,是因为契约复审的问题不是「有没有写错」,而是「这个加宽在已发布契约上是否自洽」—— 而「同名键在第二个形状上复用」正是那个问题的一部分,且它不会出现在任何门禁的输出里。
本席位对本卡的工作到此为止:⛔ 保持 draft、保留 needs:contract-review。skills/**(两个生成文件,净增删 0 行),因此另有一条独立约束 —— 受治理面需维护者手工合并,即使条款②复审通过也不得自动入队。
Generated by Claude Code
Part of #13216
Direction 1 of the maintainer's ruling of 2026-08-29 (live director session, verbatim 「同意」): a
pagemember on theviewtypeenum, so an object's list view / tab can mount an already-published custom page. Direction 3 (indexing published pages in the command palette) is not in this PR — see "What is left" below. Direction 2 stays deferred to its own design card.Clause-②: yes
Both limbs fire.
needs:contract-review照挂」 and 「枚举加宽 = 已发布契约加法 ⇒ 条款②契约复审」.packages/spec/src/**.Landing posture, mirroring PR #13287: this PR parks at draft and carries
needs:contract-reviewfrom creation. It has not been flipped ready, auto-merge has not been armed, and it has not been enqueued — the contract-review chain owns enqueue.Zero new authorization surface — the ruling's stated basis, checked
The ruling's basis is that this goes through a door the platform deliberately left open. Measured on this tree rather than assumed:
viewisallowOrgOverride: true+allowRuntimeCreate: trueinDEFAULT_METADATA_TYPE_REGISTRY;pageisallowRuntimeCreate: true. Both writes already exist for a runtime author, and this change touches neither flag, norapp.allowOrgOverride, on any type.pageview holds a NAME. The page itself is still fetched through the metadata read path it already had, and rendering is delegated to the existing page renderer — sopage.assignedProfiles, the page's own audience gate, rides along unchanged. Delegation is what preserves it: writing a second renderer is what would have introduced a second gate.The stop condition in the dispatch (report if direction 1 turns out to need an authorization decision reversed) did not fire.
What lands
1. The enum member and its binding (
packages/spec/src/ui/view.zod.ts)ListViewSchema.typegainspage, and the shape gainspageName, declared withSnakeCaseIdentifierSchema— the same grammarPageSchema.namecarries, so the set of accepted values is exactly the set of strings that could name a page.checkListViewPageMountrefuses, at parse, in both directions:type: 'page'with nopageNamepageNameon any other view typeVIEW_HISTORYexists to recordtype: 'page'with non-emptycolumnscolumnsis the one required key on a list view and the only truthful value here is[]; leaving it unchecked would mean forcing an author to write a value and then ignoring itThe check is attached at all three list-view doors —
ListViewSchema,ObjectListViewSchema(objects[].listViews.*), and the flattened runtime overlay behindPUT /api/v1/meta/view— because zod 4 refuses.omit()and key-overwriting.extend()on a refined object, so the derived shapes are built from an unrefined module-private shape and re-attach it.viewDoorsCarryingPageMountCheckinview.test.tsfails if any attachment is dropped; the ablation below proves it does.⛔
columnsis deliberately not relaxed to spare the author the[]. The union tells its list and form overlay members apart by exactly "no requiredcolumns, disjointtypeenum", with the list arm tried first, so an optionalcolumnswould let a flattened FORM overlay match the list arm and have itssectionssilently stripped. There is a test pinning that property.pageis also not added toVisualizationTypeSchema: the visualization switcher offers alternative ways to draw the same rows, and a page draws none.2. Existence of the target page, answered where the collection is visible
defineStack'svalidateCrossReferences(packages/spec/src/stack.zod.ts) refuses at build time, under the identicalpageNames.size > 0policy the two other page references in that same function already use — one reference kind, one policy, not three.@objectstack/lintrule,view-page-unresolved(validateViewPageRefs), joins the reference-integrity suite, so it speaks onos validate/os lint/os compileand at the runtime publish gate. Advisory, not gating, for its nav twin's reason: with no curated cross-package page registry, "unresolved here" cannot be told apart from "provided by a package this stack cannot see".3. The runtime publish gate reaches the mount — the door #13100 actually measured
The mount is authored by an agent that publishes a page and then writes a view through
PUT /api/v1/meta/view. No CLI is on that path, so a build-time-only rule would never speak to its author. Crossing the suite member ontoviewsnapshots needed the per-write snapshot to carry pages:RuntimeStackContext.pagesis threaded throughevaluateRuntimeAuthoringGateand read off the live registry insaveMetaItem's gate call. That is the one-key wideningRuntimeStackContextdocuments — made when a rule that reads the collection crossed the wall, never in advance.Two consequences that came with it, both load-bearing:
pagesjoinedNAME_KEYED_STACK_KEYS. A collection that is both context-filled and write-targeted must have its finding paths name-keyed (finding: a publish refusal'sissues[].pathcarries an array index into the gate's private snapshot (objects[417].sharingModel) that no caller can resolve #10064), andvalidatePresetComparandsalready emits paths into this one onpagewrites.CLOSURE_CONTEXT_KEY_BY_TYPEgainedpage, and the batch-closure accumulator inprotocol.tsis now typed as a mapped type overRuntimePendingDeclarationswith-?instead of a hand-listed literal. That literal was the one place the collection set could fall behind silently; it now fails to compile.4. Studio form, liveness ledger, generated artifacts.
view.form.tsgains apagesection;liveness/view.jsongains apageNameentry with in-repo evidence pointers;authorable-surface/ui.json,content/docs/references/**,api-surface/, the react-blocks contract and the i18n bundles are regenerated by their own generators.Boundary note — two generated files under
skills/The dispatch listed
skills/**as untouched. Two files there changed, both generated, both by the gate's own prescribed repair (pnpm --filter @objectstack/spec gen:react-blocks), becausecheck:react-blocksderives the ListView react-block contract fromListViewSchemaand goes red otherwise. Readings, as the standing clause requires:skills/objectstack-ui/contracts/react-blocks.contract.jsonskills/objectstack-ui/references/react-blocks.mdskills/treeSKILL.mdNet zero lines: one line replaced in each file, both adding
'page'to a rendered union. NoSKILL.mdchanged. Flagging it rather than deciding it — if the reviewer would rather this PR left the contract stale, say so and I will drop the two files and recordcheck:react-blocksas a known red.Verification
All numbers below are from HEAD
90042c37, and the union was re-run on that commit after the last change.origin/mainwas merged in and the os-regen artifacts regenerated from the merged tree (scripts/pm/os-regen-merge.sh— the merge is committed FIRST, so the authorable-surface anchor cannot roll back to the old fork point); everything below is the run after that, and the diff againstorigin/maincarries only this branch's changes (36 files, +1173/-54) with nothing of main's reverted.Tests —
@objectstack/spec443 files / 11803 tests passed;@objectstack/lint84 files / 2355 tests passed;@objectstack/metadata-protocol145 files / 2017 passed, 10 skipped.Whole-workspace typecheck —
turbo run typecheck --concurrency=2: 130 successful, 130 total. This is the downstream-consumer sweep and then some: it is every package in the workspace, which strictly contains every consumer of@objectstack/spec.Whole-repo ESLint —
pnpm lint(eslint . --no-inline-config, the whole tree, no narrowing): exit 0.Gate union, all green at
90042c37— derived withnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackand re-derived against the actual change set:check:authorable-surface,check:liveness,check:strictness-ledger,check:spec-changes,check:upgrade-guide,check:adr-0087-registration,check:generated,check:docs,check:empty-state,check:variant-docs,check:api-surface,check:react-blocks,check:skill-refs,check:skill-docs,check:skill-frame-sync,check:skill-compatibility,check:llms-txt,check:yaml-examples,check:i18n,check:i18n-coverage,check:i18n-stale-fill,check:app-nav-i18n,check:type-check-coverage,check:type-check-debt(--re-measure: 30 ledger entries, 1558 raw errors, none above its recorded number),check:dual-build-cjs-loads,check:cross-package-test-inputs,check:engine-double-contract,check:where-matcher,check:query-options-erasure,check:test-source-alias,check:objectql-double-limit,check:type-source-resolution,check:page-declaration-shape,check:slot-lookup,check:stack-collection-maps,check:spec-parsed-alias,check:doc-authoring,check:doc-formula-expressions,check:durability-log-level,check:logger-receiver-detach,check:filter-alias-parity,check:dispatcher-error-vocabulary,check:published-files,check:nul-bytes,check:merge-driver,check:changeset-gate-self-tests,check:empty-changeset,check:objectui-changeset,check:partof-closing-keyword, andcheck-reference-carrier-shape(run directly —Lint & Repo Gatesinvokes it by path, and the path derivation does not name it; this is the gate that cost PR #13322 a CI cycle, filed as #13333).NOT MEASURED, quoted rather than counted as a pass —
check:react-declaration-parity:Ablation 1 — the three-door attachment is not vacuous. Dropped
.superRefine(checkListViewPageMount)from the flattened-overlay door only. Mutation proved on disk before reading anything (injected line count 1, deleted line count 0,git hash-objectmoved0e25f8fe…toaa992a9c…). Predicted direction: red on the overlay rows only. Observed: exactly the 3 overlay cases failed, theListViewSchemaandObjectListViewSchemarows stayed green —Tests 3 failed | 295 passed (298). Restore verified by hash against the HEAD blob (0e25f8fe…, equal) and an emptygit diff HEAD, not by an exit code. No rebuild leg was needed and none is claimed:view.test.tsimports./view.zodby relative source path, so the mutation is what the test resolves.Ablation 2 — the
pagessnapshot widening is load-bearing. RemovedpagesfromCONTEXT_STACK_KEYSwhile leaving the rule crossed ontoviewwrites. Mutation proved on disk the same way (2f6f621e…to7d4c6f78…). Predicted direction: red on the two "WITH live pages" cases, green on the "WITHOUT live pages" case, which already measures the un-widened state. Observed exactly that —Tests 2 failed | 3 passed (5). Restore hash-verified equal to the HEAD blob. Source-resolved for the same reason.Reverse verification for the cross-package type change. From a downstream consumer (
@objectstack/lint, which depends on@objectstack/spec) against the rebuilt.d.ts, so a stale-dist false green is excluded. Accept probe —const accepted: ListView = { type: 'page', pageName: 'sales_dashboard', columns: [] }— compiles, which the pre-change declaration could not do. Reject probe —type: 'pagey'— still fails, and the error enumerates the widened union rather than degrading toany:Two expectations inverted in place, never re-baselined, each with the reason recorded beside it: the reference-integrity membership list and the
view-crossing member list gained the new member, and the two shipped-metadata-form census pins invalidate-predicate-path-refs.test.tsmoved 48 to 49 predicates and 41 to 42==literal comparisons —view.form.ts's newpagesection is gated byvisibleWhen: "data.type == 'page'", exactly as its seven sibling surface blocks are.What is left
Direction 3 — indexing published pages in the command palette — is not here. The ruling allows it in the same PR or an immediately following one, and it is a separate contract change:
searchAllsweeps object RECORDS, so surfacing pages means a new hit kind on the published/api/v1/searchresponse shape. Folding it in would have compounded two contract-review surfaces in one draft and compressed the verification each deserves. It should be dispatched as its own PR, alsoPart of #13216.The known limitation the ruling accepts on the record stands untouched: a page belonging to no object still has no browse-to entry, only search-to. That is direction 2's motivation and is left to it.
app.allowOrgOverrideis not touched.Generated by Claude Code
Generated by Claude Code