Repository navigation
feat(spec): a record page can name the apps whose routes open it (PageSchema.apps) - #29437
Conversation
… at the three default-record-page doors Claude-Session: https://claude.ai/code/session_01ADHkmSGMLCTCPxZfV3GqHr Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmSGMLCTCPxZfV3GqHr Co-authored-by: Claude <noreply@anthropic.com>
…k and composeStacks Claude-Session: https://claude.ai/code/session_01ADHkmSGMLCTCPxZfV3GqHr Co-authored-by: Claude <noreply@anthropic.com>
…or and item seam Claude-Session: https://claude.ai/code/session_01ADHkmSGMLCTCPxZfV3GqHr Co-authored-by: Claude <noreply@anthropic.com>
…ave door Claude-Session: https://claude.ai/code/session_01ADHkmSGMLCTCPxZfV3GqHr Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmSGMLCTCPxZfV3GqHr Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmSGMLCTCPxZfV3GqHr Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmSGMLCTCPxZfV3GqHr Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 171 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 08c89576d6173d6caee899f91a3260470d52fb03 && git checkout 08c89576d6173d6caee899f91a3260470d52fb03
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 208d3e2efb47d21dc05793cae500d1d76ea25a21 b634bbb5d6f14cca29e900ea58461b71722f68a3 && git checkout -B drift-repro 208d3e2efb47d21dc05793cae500d1d76ea25a21 && git merge --no-ff b634bbb5d6f14cca29e900ea58461b71722f68a3
node scripts/docs-audit/affected-docs.mjs --json 208d3e2efb47d21dc05793cae500d1d76ea25a21
|
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Head-sha: 维护者速读
席位意见:可落地,等契约复审。 席位核对 — ACCEPT ·
|
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Contract-review-claim: session_01ADHkmPi25XXKx92Xmg4viS · head 0b4d782 · 2026-11-02T14:59Z |
…-0hebo5 # Conflicts: # packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts
Claude-Session: https://claude.ai/code/session_01ADHkmSGMLCTCPxZfV3GqHr Co-authored-by: Claude <noreply@anthropic.com>
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Merge-only head The merge queue removed the PR 2026-11-02T16:02Z on a conflict with main. The dev merged origin/main
There is no semantic change, so the seat carries the CONTRACT_REVIEW_TIER PASS on |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 41495583355 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Two things before this PR re-enters the merge queue. ⛔ Not a claim; triage is not taking this review.
(The Generated by Claude Code |
Claude-Session: https://claude.ai/code/session_01ADHkmSGMLCTCPxZfV3GqHr Co-authored-by: Claude <noreply@anthropic.com>
The merged body holds 998 sites; the three-way merge kept the 993 header both sides had written. Claude-Session: https://claude.ai/code/session_01ADHkmSGMLCTCPxZfV3GqHr Co-authored-by: Claude <noreply@anthropic.com>
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Merge-only head The merge queue removed the PR 2026-11-02T18:18Z. Spec test
There is no semantic change, so the seat carries the CONTRACT_REVIEW_TIER PASS on |
Requested by Steve · project thread
Fixes #28979
Clause-②: yes (widening)
Metadata-protocol: added
维护者速读
apps: string[]。在列出的应用里,该对象的记录视图打开这张页;其它地方(含 Setup)仍打开唯一的isDefault默认页。同一对象在同一应用里只能有一张:第二张在 defineStack / composeStacks、包注册、元数据保存三道门被拒,点名两张页。sys_userrecord page the one a person opens to: on 18.x the console opens plugin-auth'ssys_user_detail, and no declarable route reaches a second record page #28979,记录 6573557633)。duly 这类应用无法让自己的sys_user记录页在自己的路由下打开,而改成全局默认会让 Setup → Users 丢掉管理页。apps时行为完全不变)。三道门共用一个读法defaultRecordPageObject(page, app),错误码沿用DEFAULT_RECORD_PAGE_CONFLICT,无新公开名。回滚即 revert 本 PR。控制台尚未读apps(ui 后续卡),所以合入后界面暂无变化。packages/spec/src/ui/default-record-page.ts(唯一读法)、packages/objectql/src/registry.ts(注册门按"槽位"计账,默认槽键不变)、packages/metadata-protocol/src/protocol-save-door-refusals.ts(保存门逐槽判)。pnpm --filter @objectstack/spec exec vitest run src/stack-app-record-page.test.ts,应见 27 个用例全绿(在 main 上 22 红)。What changes
The key.
PageSchemagains an optionalapps: string[](app names: lowercase snake_case, at least one, each once; a repeat is refused at the repeat). On a record page bound to anobject, it names the apps whose routes open this page for that object (/apps/APP/OBJECT/record/ID). Everywhere else, Setup included, the object's oneisDefaultrecord page still opens. Ruling A on #23015 (one default per object per deployment) is unchanged and no precedence ladder is added: the deployment default and an app's page are two slots, and one page may hold both. The describe carries[PLANNED — not yet rendered]until the console reads it.Placement, the way
isDefaultis gated.appsis refused on any page that is not a record page bound to an object. The one sentence ismisplacedDefaultPageRefusal's (it now coversappsafterisDefault), so the page parse (checkPageDefaultPlacement, which now reports each key at its own path) and the registry's package door print the same refusal.One reading, no new public name.
defaultRecordPageObject(page, app?)gains an optional second argument: with an app, it answers the object a record page is the page of inside that app (appslists it). With one argument it is byte-for-byte the old reading. Every door asks it once with no app and once per string entry of the page'sapps.The three refusal doors, one level down. Each door keeps its existing mechanism and keys it by slot (an object's default, or an object inside one app) instead of by object alone:
defineStack/composeStacks(stack.zod.ts,duplicateDefaultRecordPageFindings):Object 'X' has two record pages for app 'A' in pages: … Remove 'A' from apps on all but one of them.(STACK_CROSS_REFERENCE_INVALID).objectqlregistry.ts+default-record-page.ts): the claims map, the holders reading, the package door and the item seam are keyed byrecordPageSlotKey(the object alone for the default, so default keys are unchanged).DefaultRecordPageConflictErrorkeeps its code (DEFAULT_RECORD_PAGE_CONFLICT, 422); a conflict carriesappwhen the slot is an app's, and the message names the app with its own remedy.defaultRecordPageHoldersOf(object, app?)answers an app's slot.metadata-protocoldefaultRecordPageSaveRefusal): judges each slot the saved page holds against the stored rows and the registry's holders; the refusal names both pages and the app and carriesapp. Saving a page under its own name stays an overlay, as for the default.Why
An app could not make its own record page over a platform object the page its routes open: plugin-auth's
sys_user_detailholdssys_user's one default, a second default is refused, and making the app's page the default would take Setup → Users' admin page away from the whole deployment (#28979). Ruling A (director 6573557633, maintainer 「同意」) chose a page-level app scope.Tests
packages/spec/src/stack-app-record-page.test.ts(new, 27): the key (accepted alone and besideisDefault; empty, repeated and non-snake entries refused; refused on a home page, an app page and an unbound record page in the registry door's sentence; both misplaced keys reported);defaultRecordPageObject(page, app);defineStack(same app refused naming both; different apps accepted; a page scoped to two apps; default + app page coexist; one page both default and scoped; a second default still reported as a default);composeStacks(merged andpreserve, with controls).packages/objectql/src/registry-app-record-page.test.ts(new, 10): package door across packages and within one manifest, two-app page, install claim and uninstall release, item seam, environment holder, default and app page side by side, second default still the default's refusal, misplacedappsat the package door; each refusal with a control.packages/objectql/src/default-record-page-save-door.test.ts(+6 per topology, 12): refused over a shipped page and over a stored page for the same app, controls for another app, beside the package default and an overlay by own name, andappson a home page refused by the parse.packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts(pageserves 27 → 28 top-level properties:apps);packages/spec/src/system/metadata-form-zod-reconciliation.test.ts(aplannedomit row forpage.apps, therecord/printprecedent).Acceptance notes
usePageAssignment/appObjectScope.declaredRecordPageread the route's app first, then the default, then declaration order, with the ruling's two pins (/apps/THAT_APP/sys_user/record/IDopens the scoped page; the Setup app's route still openssys_user_detail); the liveness row forpage.appsflips tolivethere. duly#113: delete the URL-action stopgap once the ui half ships.appsentries are app names (AppSchema.name). The Setup app's name issetup;com.objectstack.setupis its package id. The ui card maps the route's app segment to the app name.authorable-surface/ui.json,content/docs/references/ui/page.mdx,liveness/state-counts/page.md; ledgers:dropped-refinements.baseline.json(+5 sites for the array refine, 988 → 993),liveness/page.json(apps:planned).Verification
Red on main, green after
The six implementation files plus the two ledgers restored to main (
2f7977cd), proven by blob hash; spec and metadata-protocol rebuilt; restored afterwards with an emptygit diff HEAD.src/stack-app-record-page.test.ts: mainTests 22 failed | 5 passed (27); branch27 passed (27).registry-app-record-page.test.ts+default-record-page-save-door.test.ts: mainTests 20 failed | 38 passed (58); branch58 passed.Package suites
All on the merged head
0b4d7827(maindfbe34d8merged withos-regen-merge.sh; the authorable surface regenerated on the merged tree; branch delta vs main unchanged, 17 files), each underos-verify-lock,VERDICT command-exit 0:pnpm --filter @objectstack/spec test:Test Files 792 passed (792),Tests 23478 passed | 1 todo.typecheck: exit 0.check:generated: exit 0.pnpm --filter @objectstack/metadata-protocol test:Test Files 304 passed | 4 skipped (308),Tests 22155 passed | 26 skipped.typecheck: exit 0.@objectstack/objectql: the four default-record-page filesTests 93 passed (93);typecheck: exit 0. The full suite ran on the pre-merge head4d11203c:Test Files 531 passed (531),Tests 10300 passed(the merge moved no objectql file).defaultRecordPageObject(corestack-organization-record-page.ts, service-print, plugin-auth's test) call it with one argument, which reads exactly as before; they compile in the workspace build (turbo run build --filter=./packages/**, exit 0).!in the title and no(narrowing)arm.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon0b4d7827: 110 families, all run after the last commit, after a workspace build.dispatch-gates --ran:110 derived famil(ies) accounted for — 110 run, 0 NOT-MEASURED (a DERIVED zero).4d11203c: 110 exit 0.check-changeset-no-major.mjs --base origin/main --eventwith this body's two lines:LEVEL AXIS: … clause-② yes (widening) and metadata-protocol added, exit 0.check:skill-frame-freshnessexit 0;check:dev-prereqsexit 1 on machine state (35ui/packages have no dist in this container), not on this diff.Human Minimum
packages/specholds no runtime logic.pnpm lint(repo-wide, CI-owned) not run locally.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01ADHkmSGMLCTCPxZfV3GqHr
Generated by Claude Code