feat(spec)!: retire the type: 'page' list-view mount and its pageName binding - #17298
feat(spec)!: retire the type: 'page' list-view mount and its pageName binding#17298os-bill wants to merge 2 commits into
type: 'page' list-view mount and its pageName binding#17298Conversation
…ame` binding A list view could declare `type: 'page'` and name a published page in `pageName`, delegating its whole rendering to the page renderer. Only the spec half of that was ever built (#13216 direction 1, PR #13372). No renderer ever routed the member — objectui's list-view switch shares its `default:` arm with `case 'grid'`, and `isListViewVisualization('page')` is false — so a page view drew an empty table where the page belonged, and the three parse refusals policing the binding (`checkListViewPageMount`) policed a mount that never mounted anything. ADR-0049 enforce-or-remove; maintainer ruling 2026-09-09, decision batch #107 item 1. - `pageName` becomes a `retiredKey()` tombstone on both list-view doors. - `'page'` leaves the `type` enum; the enum's own error map carries the prescription, keyed on `issue.input` (the `exportOptions` 'pdf' precedent — an enum-VALUE narrowing has no tombstone to hang one on). - `checkListViewPageMount`, its three refusal messages, the `validateCrossReferences` page branch, `@objectstack/lint`'s `validateViewPageRefs` / `VIEW_PAGE_UNRESOLVED`, and `view.form.ts`'s `page` section are removed with the mount. - `RuntimeStackContext.pages` and the `page` row of `CLOSURE_CONTEXT_KEY_BY_TYPE` leave with the only crossed rule that read them: a view publish no longer gathers a live page universe. - ADR-0087: D2 conversion `view-page-mount-removed` (protocol 18) strips both keys from stored rows and `os migrate meta --from 17` output; `type` is stripped rather than rewritten, since the schema defaults it to `grid` — exactly what the row already rendered. The surviving page mount is the app navigation item (`PageNavItem.pageName`), untouched. Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
…tirement `view.form.ts`'s `page` section and its `pageName` input are gone (#17063), so the extracted metadata-form labels drop with them: the `page` section label/description and the `pageName` label/helpText, across en/es-ES/ja-JP/zh-CN plus the three source-hash bundles. Pure deletion — merge mode adds and translates, it does not remove, so this is `check-i18n-bundles.mjs --write` rewriting the default locale from source and dropping the orphaned keys. Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
📓 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:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 135 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 07b767880d2be58bbb7dc50bd7e1d03417b34700 && git checkout 07b767880d2be58bbb7dc50bd7e1d03417b34700
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a2509d720ecc77ba34410e0ee62dd5eaacbd4460 1e937111c14c19fd4fc28cfe7872e28786d9b18a && git checkout -B drift-repro a2509d720ecc77ba34410e0ee62dd5eaacbd4460 && git merge --no-ff 1e937111c14c19fd4fc28cfe7872e28786d9b18a
node scripts/docs-audit/affected-docs.mjs --json a2509d720ecc77ba34410e0ee62dd5eaacbd4460
|
Fixes #17063
Clause-②: no
⛔ This PR is GOVERNED (
check-governed-merges --teston every diff path returns exit 3). No seat flips it ready, enqueues it, or arms auto-merge — a human merge is the review record. The dispatch predicted exit 0 / NOT governed; that prediction was false, and the reason is below.What is retired
A list view could declare
type: 'page'and name a published page inpageName, rendering nothing of its own and delegating to the page renderer (#13216 direction 1, spec half PR #13372). Only the spec half was ever built. Measured on objectui at the pinned.objectui-sha53ded82band at objectuimain3fbdd4a2d:packages/plugin-list/src/ListView.tsx— the view-typeswitchshares itsdefault:arm withcase 'grid'("an unrecognized viewType must degrade to a working table"), so'page'has always drawn an empty grid;@object-ui/core'sisListViewVisualization('page') === false(normalize-list-view.test.ts:647), with a residual test named for objectui#8429.So the three parse refusals that policed the binding policed a mount that never mounted anything. ADR-0049 enforce-or-remove; maintainer ruling 2026-09-09, decision batch #107 item 1, verbatim 「撤」, on 「已发布零消费的能力不因沉没成本获得豁免」.
Route, and why each half took the route it did
pageNameretiredKey()tombstone onListViewSchema+ObjectListViewSchematsctypes itneverand the parse raises the prescription instead of a bare unrecognized-key report.strict-object.tscalls a tombstone "strictly stronger than aguidanceentry", and this same shape already carriesvirtualScroll: retiredKey(…).'page'typeenum's ownerrormap, keyed onissue.inputLIST_VIEW_EXPORT_PDF_RETIRED(#8010), andHookBodyCapability/object.managedBy: 'system'before it.Liveness-ledger disposition follows the route, as the playbook's table demands: the
pageNamerow stays, flipped tostatus: "dead"with averifiedAtand a REMOVED note, becauseretiredKey()keeps the key in the walked shape (therls.priorityprecedent). Deleting it would report UNCLASSIFIED.The four surfaces the dispatch front-loaded
1 — a
sys_metadatarow already carryingtype: 'page'/ apageName. The read path does re-validate:applyConversionsToStoredItemreplays the full ADR-0087 chain on every rehydration seam, and the loader skips only'flow'(packages/metadata/src/loaders/database-loader.ts:824— theif (singular === 'flow') return payloadguard, one line above the call at:825), neverview. The new D2 conversionview-page-mount-removedtherefore fires, and the row is served canonical. Measured, with a lit control:What an operator sees is a warn, not a 400 and not silence — and there are three emitters, one per rehydration seam, which do not carry the same string. Grep
carries a pre-protocol shape:[DatabaseLoader]packages/metadata/src/loaders/database-loader.ts:831stored view/NAME carries a pre-protocol shape; NOTICE[ObjectQLPlugin]packages/objectql/src/plugin.ts:2134stored view/NAME carries a pre-protocol shape; NOTICE[Protocol]packages/metadata-protocol/src/protocol.ts:4511The row itself is unchanged — re-save it (Studio edit → save, or run "os migrate meta --stored --apply") to persist the canonical shape.A write through
PUT /api/v1/meta/viewis where the refusal fires instead: the tombstone and the enum error map both answer at parse, and that door heals the row on the next save.stack.views[]in all three persisted spellings (mapViewPayloads); it does not reachobjects[].listViews.*, which no conversion in this registry reaches — theview-export-options-pdf-removedprecedent has the identical reach. An object body still carrying a mount is refused at its own door with the prescription rather than converted. Both populations measured at zero (below).2 — the ADR-0087 disposition.
registered view-page-mount-removed, a real D2.not-required (no-migration-prescription)was not available: ADR-0087's own text (docs/adr/0087…md:596) refuses it for a body carrying a migration prescription, and this changeset carries a FROM → TO table because AGENTS.md requires one. A D3semanticentry was considered and is not what this needed: a D2 is possible here and therefore owed. The conversion stripstyperather than rewriting it to'grid'—ListViewSchema.typecarries.default('grid'), so deleting the key lands the row on exactly what it already rendered, declared in one place instead of guessed in the registry, andstripKeys-shaped deletion is idempotent by construction. Registered under protocol 18 (RETIRED_KEYS_BY_MAJOR[18],MIGRATIONS_BY_MAJOR[18].conversionIds,step18.rationaleextended), matching every in-flight retirement on the 17.x line.check-adr-0087-registrationrun after the commit — it reads changeset bodies from git, not the working tree:3 — the tombstone kit. Per the
spec-property-retirementplaybook. Both prescriptions carry the pinnedos migrate meta --from 17house sentence, andpageName's is aretiredKey(). TwoRETIRED_KEYS_BY_MAJOR[18]entries as new files underentries/retired-keys/(18.ui__ListView__pageName.ts,18.ui__ObjectListView__pageName.ts) plusgen:migration-registry; nothing was hand-edited inside the generated markers. The build gate named the two exact key spellings itself before they were registered, which is how they were derived rather than guessed. NoRETIRED_DEFS_BY_MAJORrow is owed: no def is deleted.4 — the three refusal messages.
checkListViewPageMountexisted only to police this mount, so the check, its three message constants and the export all go with it. They were module-private string constants, not error codes — noERROR_CODE_LEDGERrow exists for any of them (VIEW_PAGE_MOUNT_NEEDS_PAGE_NAME/VIEW_PAGE_NAME_NEEDS_PAGE_TYPE/VIEW_PAGE_MOUNT_HAS_COLUMNSappear nowhere butview.zod.ts), so nothing is owed there.@objectstack/lint'sVIEW_PAGE_UNRESOLVEDis a lint rule id, not an ADR-0112 code, and leaves withvalidateViewPageRefs.What else left with it, and what deliberately did not
validateCrossReferences' page branch instack.zod.ts. The surviving three page references in that function (an app nav item'spageName, a modal action'stargetat two rungs) andpageNamesitself are untouched.@objectstack/lint'svalidateViewPageRefs/VIEW_PAGE_UNRESOLVED. Its nav twinvalidateNavTargetRefsis untouched.RuntimeStackContext.pagesand thepagerow ofCLOSURE_CONTEXT_KEY_BY_TYPE.validateViewPageRefswas the only member whoseruntimeTypesreachedstack.pages(every other page-reading rule takes the frozenflowdefault, andvalidatePageVisualizationBindingssays in its own comment that it is not crossed for exactly this reason). The runtime-gate docblock states the widening rule — "made when a rule that reads the collection actually crosses the wall, never in advance" — so the symmetric retirement is owed, not optional: leaving it would create a carried collection no rule reads, which is the ADR-0049 shape this card exists to remove. A view publish also stops paying asys_metadataround trip for it.view.form.ts'spagesection (its one input waspageName) and the four i18n metadata-form bundles it fed — a form input for an unwritable key is the false-compliant UI half of a retirement.pageview type on objects, or nav registration at publish time #13216 (command-palette page indexing, PR feat(spec,metadata-protocol): a page hit kind on GET /api/v1/search — command palette indexes published pages #14249) and the nav-itemtype: 'page'/PageNavItem.pageNamesurface, which is the page mount that has always rendered.Zero usage, re-measured with a lit control
Query:
git grep -n "type: *'VALUE'"overexamples/**,packages/platform-objects/**,packages/mcp/**,packages/cloud-connection/**,packages/create-objectstack/**, tests excluded.pageitems:array undernavigation, carryingid/label/icon(25 inexamples/app-showcase/src/ui/apps/index.ts, pluscloud-connection-ui.ts:69,marketplace-ui.ts:99,connect-ui.ts:72;studio.app.ts:187is an actionparamsbag). Zero list views.kanbantypevaluestreegalleryA zero whose control was never inspected survives review while saying nothing; each of the four rows above was read, not counted.
Ablation — the old spelling now fails, proven both ways
Each leg: mutate → prove it landed on disk (anchor count + injected-marker count +
git hash-objectmoved) → run → restore → prove the restore by state. The spec unit suite imports./view.zodas source (nodisthop), so no build leg is owed; the restore isgit checkout HEAD -- PATH, never a baregit checkout --.'page'in thetypeenum34f111fc→c3c5d06bREFUSES type: 'page'casespageName: SnakeCaseIdentifierSchema.optional()34f111fc→6447f4e3Restore, both legs:
git hash-objectreturned34f111fcef274c7ade5543b195a884fe6abde7c4, byte-equal to theHEAD:packages/spec/src/ui/view.zod.tsblob, andgit diff HEAD -- PATHwas empty. Not read from an exit code. A trap onEXIT INT TERMwith absolute paths carried the crash path.grep -cFwith a multi-line anchor, which counts lines rather than occurrences; the guard mis-fired, the run was declared VOID and restored without being read. Leg 1 above is the re-run with a single-line anchor. The mutation had in fact landed — that is exactly why a landing check that cannot be trusted has to void the reading rather than be talked past.Verification
Every exit code captured to disk before any pipe. Scripts under
scripts/pm/were run from anorigin/mainmirror (git archive origin/main scripts | tar -x,origin/main=a256f189), each byte-verified:check-widening-tells.mjs949e94fd,check-governed-merges.mjs081df968,check-clause2-carriers.mjs60b7562c— all three identical togit rev-parse origin/main:PATH.pnpm --filter @objectstack/spec buildpnpm --filter @objectstack/spec test(13 185 tests)pnpm --filter @objectstack/spec check:generated(15 artifacts)pnpm --filter @objectstack/lint typecheck && … test(3 688 tests)pnpm --filter @objectstack/metadata-protocol typecheck && … test(2 482)pnpm --filter @objectstack/platform-objects test(561)check:liveness·check:empty-state·check:variant-docs·check:skill-examples·check:skill-refs·check:skill-docs·check:exported-any·check:dual-source-exports·check:duration-unit-keys·check:entry-nameability·check:yaml-examplespnpm check:i18n--write; the diff is 44 pure deletions)check:doc-authoring·check:nul-bytes·check:cross-package-test-inputs·check:test-source-alias·check:stack-collection-maps·check:quick-reference-counts·check:corpus-claim-drift·check:published-files·check:type-check-coverage·check:i18n-stale-fill·check:objectui-changeset·check:pm-changeset-deadline-censuscheck-adr-0087-registration --self-test/--base(post-commit)check-changeset-no-major --self-test/--basecheck-empty-changeset·check-reference-carrier-shape(+self-tests) ·check-skills-token-ratchet·check-skill-line-ratchet·check-spec-docblock-symbol-anchors·check-comment-mask-adoption·check-comment-mask-corpus·check-closing-keyword-paritynode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwas re-derived after the i18n commit; it grew six entries (the changeset/i18n/release-rehearsal family), all of which were then run. The remaining families in its 121-line list are declared to CI as a narrowing, not claimed green.ESLint — a measured narrowing, not a skipped run. ① The population is read from eslint's own config, not guessed:
pnpm exec eslint --no-inline-config --format json .enumerates 6 465 files. ② The narrowed run linted 32 files (every surviving.ts/.tsx/.mjs/.mtspath in the diff; the three deleted paths cannot be linted), count read from--format json: 0 errors, 0 warnings, exit 0. ③ Invariance: this repo runs oneeslint.config.mjsand it never enables type-aware linting for any file — noparserOptions.project, no typed@typescript-eslintrules, stated and positively controlled ateslint.config.mjs:327-335— so nothing in this diff can move the verdict on a file it does not touch. Measured at1e937111c, this branch's final commit.Clause ② — measured, three readings
Run on the finished diff (
git diff MERGE_BASE...HEAD), from theorigin/mainmirror:check-widening-tells --declaration nocheck-widening-tells --declaration yesyesalready routes to contract review, so a tell on top decides nothingcheck-clause2-carriers --pair THIS_PR_NUMBERThe three T2 tells, by number and line:
packages/spec/src/migrations/registry.ts:5443—+ 'view-page-mount-removed',(the D3 chain step'sconversionIds)packages/spec/src/migrations/registry.ts:12757—+ 'ui/ListView:pageName',packages/spec/src/migrations/registry.ts:12791—+ 'ui/ObjectListView:pageName',registry.ts's os-generated markers;registry.ts:5443is NOT — the nearest marker above it is the CLOSING</os-generated semantic:17>, so it is the hand-maintainedstep18.conversionIds. The argument below is unchanged. Originally written as: all three are generated rows produced bygen:migration-registryfrom the three new files undersrc/migrations/entries/. They are the retirement LEDGER growing — the mechanical record that two keys are now unwritable — and every one of them is a consequence of the accept set shrinking. But that argument is the seat's to accept or reject: the dispatch ordered me to measure rather than agree, and to stop and report on disagreement. The declaration in this body staysno, exactly as claimed in comment5612101866. ⛔ I have not moved it.Governance — the dispatch's prediction was false
check-governed-merges.mjs --testover all 48 diff paths returns exit 3, GOVERNED:skills/**×2 hits the register —skills/objectstack-ui/SKILL.md(the retired list-viewtyperow, a surface the retirement playbook requires) andskills/objectstack-ui/references/react-blocks.md(regenerated bygen:react-blocks; the #11705 generated-surface exception did not lift it, because the generator declares no readable output manifest, so it fails closed). The dispatch's pre-flight readpackages/spec/**andpackages/lint/**only and concluded "⛔ No … ordinary queue landing applies". One hit governs the whole PR. ⛔ Dropping the skills edit to dodge governance is not an option — a published SKILL.md teaching a key that now refuses is the thing being retired.Changeset
minorfor@objectstack/spec,@objectstack/lint,@objectstack/metadata-protocol, with the**BREAKING**banner. Level derived, not inherited:check-changeset-no-majorrefusesmajorin the launch window, existing pending changesets already put@objectstack/specatminor(next release 17.5.0, which the two prescriptions name), and all three packages publish removed surface —@objectstack/lintloses two exports and@objectstack/metadata-protocolnarrows a published context type, so neither is apatchandskip-changesetis plainly wrong.维护者速读(草稿)
改了什么 — 列表视图的
type: 'page'挂载与它的pageName绑定被整体退役:枚举值、键、三条拒绝消息、checkListViewPageMount导出、defineStack的页面交叉引用分支、@objectstack/lint的validateViewPageRefs、以及只为那一条规则而存在的RuntimeStackContext.pages实时页面全集。表单区块与四份 i18n 词条随之删除。应用导航项的type: 'page'是另一个键,原样保留。为什么改 — 2026-09-09 裁决「撤」,依据 ADR-0049 enforce-or-remove 与「已发布零消费的能力不因沉没成本获得豁免」。spec 侧半边落了地,objectui 的渲染半边从未建成:实测 objectui 的
switch用default:与grid共用分支,所以page视图一直画的是空表格,而三条解析拒绝守的是一个从未挂载过任何东西的挂载点。风险与代价(含回滚) — 已存⚠️
sys_metadata行不会炸:ADR-0087 的 D2 转换在每个再水化接缝上重放,剥掉两个键,行按规范服务;type是剥除不是改写,靠 schema 自己的grid默认值落到它本来就渲染的样子。运营者看到的是三个接缝各一条 warn(字符串见上表),不是 400。已测编写面用量为零(带亮对照)。objects[].listViews.*不在任何转换的行走范围内 —— 那条路上的旧行在它自己的门上被带处方拒绝,而不是被转换。回滚 = 还原本 PR;墓碑与转换都是新增,不改动任何现存行为。席位意见 — 达档契约复核返回 PASS WITH FINDINGS,无 must-fix;席位判定可以合。转录保险丝:120 个 harness 打戳的⚠️ 独立性说实话:复核者是本席位派出的子代理、继承本 session id ⇒ 席位内达档复核,⛔ 不是跨席位独立。⛔ 人工合并即审核记录,但 ⛔ 不替代契约复核 —— 上面那份就是那一道。
claude-fable-5-1,零残留,对照词点亮。⭐ 两件请你在 diff 里特别看一眼的事。
一、这不是普通的收窄,它是一次真 D2 —— 但覆盖有边界,而边界的代价被测出来了。 存量 view 行会被自动改写(探针:
type:'page'视图 → 2 条view-page-mount-removednotice;grid 对照 → 0 条;容器视图 → 3 条)。⛔ 但转换只走stack.views[],够不到objects[].listViews.*。复核实测了那条边界的实际代价:一条带 page mount 的存量 object 行会原样穿过转换链(0 条 notice),在自己的门被ObjectSchema拒绝,却仍然被注册,每个进程打一条[metadata_spec_invalid]—— 供着,不治;而两处 prescription 都叫作者去跑一个够不到它的os migrate meta。今天两个人群实测都是零生产者,所以不挡落地,但它是 ACCEPT 时要立的卡(view-export-options-pdf-removed先例有同样的够不到)。二、clause ② 的机械读数与本 PR 的声明冲突,我裁维持
no。 三条 T2 tell 全部落在退役台账的行上 —— 它们存在的原因恰恰是接受集缩小了。改判yes会让 clause ② 轴对语义上最明确的收窄报「扩大」,并成为此后每一次退役的先例。工具侧另立 #17300。registry.ts:5443不在任何os-generated区域内(它上面最近的标记是闭合标记</os-generated semantic:17>),它是手工维护的step18.conversionIds。复核逮到,我自己在 head 上重测确认。已在裁决评论和 #17300 上就地更正 —— 因为 #17300 原本提的修法只按生成区域来判,会只覆盖三分之二的 tell,而每一次 D2 退役都会注册一个 conversion。复核另纠了三处报告口径,都不动字节: runtime 读者的「唯一」少算了一个(
validateVisibilityPredicates也读stack.pages,但collectionEntries(undefined)返回[],无害,措辞应为「不把引用解析进stack.pages」);消融计数 3/6/13 实为 4/4/14(方向对、守卫有效,只是记录要写真数);以及 #11705 的生成物例外其实抬得起react-blocks.md,dev 读到的「fail closed」是未构建闭包导致的环境依赖结果。受管面归属复核已重跑确认:exit 3,由
skills/objectstack-ui/SKILL.md命中。 ⛔ 我当初的预检说「走普通队列」是错的 —— 我把检查跑在了卡片点名的路径上,而不是一次退役实际需要碰的路径上,而退役手册本身就要求动已发布的 skills 目录。⛔ 把 skills 拆出去躲开受管面这条路,dev 在我之前就拒了,理由对:一份教着「现在会在 parse 处被拒的键」的 SKILL.md,正是被退役的东西本身。你要做的 — ① 判定 clause-② 的分歧:机械读数给出三条 T2(全部是退役台账的生成行),语义方向仍是收窄;移动声明是席位的事。② 本 PR 受治理(
skills/**),需人工合并。③ ACCEPT 时按规则 3 在 objectui 立跨仓跟进:objectui#8429 的前提被本次退役消灭。pm:blocked— it is the mount half that was never built, and this retirement kills its premise. ⛔ Nothing in objectui was filed or touched by this PR; that cross-repo follow-up is the seat's at ACCEPT.Generated by Claude Code