Repository navigation
feat(cli,plugin-dev): an app that declares its own sys_organization default record page boots under os serve and DevPlugin; the conflict remedy names a page, not a package - #23550
Conversation
…ion default record page leaves out plugin-auth's under os serve and DevPlugin Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…h AuthPlugin roots Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…ecord page option Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…ationRecordPage as serve-only, with why Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…rve-org-record-page
📓 Docs Drift CheckThis PR changes 4 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 54 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 a2494a766d181180d84e11c745f0966212b76daa && git checkout a2494a766d181180d84e11c745f0966212b76daa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0fb55fc2fca5590a76c1827d95bf7d1bacf22d02 c2db30e78717c3237cef19631fc77bf6dc95091c && git checkout -B drift-repro 0fb55fc2fca5590a76c1827d95bf7d1bacf22d02 && git merge --no-ff c2db30e78717c3237cef19631fc77bf6dc95091c
node scripts/docs-audit/affected-docs.mjs --json 0fb55fc2fca5590a76c1827d95bf7d1bacf22d02
|
Contract reviewServed-tier: Inputs: card #23310 (body, triage Check-runs on the head (read once at review time). Completed ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named right or wrong.
Nothing in the diff is judged wrong. ② Semver level
Clause-②: yes (widening) — the accept set of ③ Boundary flags
Dev deviations, each answered:
Out-of-scope findings (two noted, not filed): judged in ①.6 as follow-ups, with a third sibling added; no must-fix. Check-runs in progress at brief time are named above; none is escalated, since every completed run is green and the brief forbids a watcher. Implemented-by: VERDICT: PASS Adopted by Generated by Claude Code |
…ude/issue-23429-metadata-family No textual conflict on this merge. The generated reference docs are regenerated over the merged ledger in the next commit, never picked from a side. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ
Fixes #23310
Clause-②: yes
What this does
Ruling A on #23015 (record
6134056653) allows oneisDefault: truerecord page per object per deployment. PR #23309 addedAuthPluginOptions.registerOrganizationRecordPageso a host that ships its ownsys_organizationdefault can leave outplugin-auth'ssys_organization_detail. The two composition roots that constructAuthPluginthemselves,os serveandDevPlugin, never passed the option. An app served by either one that ships its ownsys_organizationdefault was refused at boot withDEFAULT_RECORD_PAGE_CONFLICT, and the app had no switch to set.AuthPlugin. If the app declares anisDefault: truerecord page forsys_organization, they passregisterOrganizationRecordPage: false. Otherwise they passtrue, which is today's behaviour. There is no new stack-config key: the page the author already wrote is the switch.stackDeclaresOrganizationRecordPage(stack, plugins?)is a new export of@objectstack/core(packages/core/src/stack-organization-record-page.ts).serve.tscalls it with(config, plugins)anddev-plugin.tswith(this.options.stack). It readspagesbyresolveStackCollection's rule (the top level, or each package body for a multi-package stack) off the stack and off each app bundle already inplugins. Each page is judged bydefaultRecordPageObject(@objectstack/spec/ui), the reading the registration refusal uses.packages/objectql/src/registry.ts). "...or compose the deployment without the package that ships the other" now reads "...or compose the deployment without the other package's page". It names no option, no package and no tracker number. The code, the422status, the named pages and the first sentence are unchanged.Derive vs forward (the measurement)
new AuthPlugin(...)?serve.tshasconfigin scope and has already appendednew AppPlugin(config)toplugins(step 3, before step 5d).DevPluginhasthis.options.stack.defaultRecordPageObject(page) === 'sys_organization'over the app's pages)Derive was reachable cleanly from both roots, so forward was not built.
Where the predicate lives. In
@objectstack/core:@objectstack/cliand@objectstack/plugin-devboth depend on it, and neither can import the other. Core already hostsresolveStackCollectionand the auth-family gateresolvePlatformAuthCompositionfor the same reason. Core already depends on@objectstack/spec, so nothing points the wrong way. It is a new file rather than an addition tostack-auth.ts, because that module's header promises "importing this module loads nothing", and this one imports@objectstack/spec/ui.File surface vs the claim. The claim names
serve.ts,dev-plugin.ts,registry.ts, their tests and the changeset. Two further paths:packages/core/src/{stack-organization-record-page.ts,index.ts}and the predicate's test hold the shared predicate. The dispatch asked for one predicate shared by both roots.packages/cli/src/utils/boot-preparation-parity.test.tsgains oneSERVE_ONLYrow. That pin reds whenserve.tsuses a core function thatos migrate's one-shot boot does not. The reason recorded in the row: the migrate boot composesplugin-auth's objects throughcreateIdentityObjectsPlugin, which registers no page, so it has no organization page to leave out.Pins (each with a negative control)
packages/cli/test/serve-organization-record-page.e2e.test.ts, a realos serve. It boots adefineStackapp, signs in as the seeded dev admin and readsGET /api/v1/meta/page.orgpage_organization_detailas thesys_organizationdefault. The boot reaches its banner with no conflict, and thesys_organizationdefaults are exactly["orgpage_organization_detail of com.example.orgpage"].["sys_organization_detail of com.objectstack.plugin-auth"], so the reading in case 1 is not vacuous.sys_user=["sys_user_detail of com.objectstack.plugin-auth"]: the option removes exactly one page.e2etier, by name. Twoos serveboots take about 28 s, so per-PR CI runs the contract pin below instead.packages/cli/src/commands/serve-auth-organization-record-page.contract.test.ts(unit tier, per PR). The singlenew AuthPlugin(literal inserve.ts(comments masked, asserve-auth-app-name.contract.test.tsreads it) passesregisterOrganizationRecordPage: !stackDeclaresOrganizationRecordPage(config, plugins). The predicate reads an AppPlugin-shaped bundle, with anisDefault: falsecontrol.packages/plugins/plugin-dev/src/dev-plugin-organization-record-page.test.ts.DevPlugin.init()with a recordingAuthPlugin: the app page givesfalse. Controls: the same page withisDefault: false, no pages, and no stack each givetrue.packages/core/src/stack-organization-record-page.test.ts. 11 cases. Yes: top-level pages, an untyped page (a record page by default), a page in apackages[]body, an app plugin'sbundle. No:isDefault: falseor absent, another object, ahomepage, an empty stack, a body or bundle without the default.packages/objectql/src/registry-default-record-page.test.ts, new case:code+status(DEFAULT_RECORD_PAGE_CONFLICT/422), and the remedy sentence asserted verbatim: "Keep one default per object: setisDefault: falseon one of the two pages, or compose the deployment without the other package's page". The message contains neither "without the package" nor an option name, a package name or a#NNN.DEFAULT_RECORD_PAGE_CONFLICTrepo-wide, examples included. No test pinned the remedy text: the existing pins read only the first sentence,split('. ')[0], and stay verbatim. The other hits are prose; see Acceptance notes.Ablations (
scripts/ablation-replace.mjs, underos-verify-lock, each restored to the HEAD blob withgit diff HEADempty):serve.ts: the derived option replaced byregisterOrganizationRecordPage: true,e06e62ff1b24to6bd5bb650dd5os serve ... exited 1 before /Press Ctrl\+C to stop/, with the child printing✗ Default record page conflict: page "orgpage_organization_detail" of package "com.example.orgpage" cannot be the default record page of object "sys_organization": page "sys_organization_detail" of package "com.objectstack.plugin-auth" already is. Keep one default per object: ... or compose the deployment without the other package's page.The control stays green. 1 failed, 1 passed.dev-plugin.ts: the same mutation55c505e1c936to20335a650c35registry.ts: the old remedy text restored8fb229d5baceto7da871a01cc3serve.tsruns fromsrc/through tsx (bin/run-dev.js). The plugin-dev and objectql tests import their subject relatively. So none of the three mutated subjects resolves through adist/.Verification
Head
c2db30e787(origin/main5fd2c76d6merged in, a plain merge with no generated artifacts on either side), after a fullturbo run build(76/76 tasks). All readings are on a shared box.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 77 commands from the 12-path change set. All 77 ran, each exit code captured before any pipe, and--ranreconciled77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN.check:type-check-debtexited 3 in the battery (queue-timeout, never acquired). It was re-run under the lock as its refusal prescribes:none above its recorded number, exit 0.check-engine-split-ratiorefused on the shallow clone (exit 2). It read 98.5% afterfetch --shallow-since=2026-07-09. Thedual-buildandi18ngates exited 3 (unbuilt CLI). All three pass on the merged, built head.@objectstack/corestack-organization-record-page.test.ts: 11 passed.@objectstack/objectqldefault-record-page registry and save-door tests: 38 passed.@objectstack/plugin-devfull suite: 10 files, 89 tests passed.@objectstack/cliunit: the contract pin,boot-preparation-parity.test.tsandvitest-tiers-partition.test.ts: 61 passed.@objectstack/plugin-authorganization-record-page-option.test.ts: 3 passed.@objectstack/spectest:repo: 61 files, 1033 tests passed.7c4bce2697, before the merge. The merge changed no source file of these packages; it did changepackages/spec, whosetest:repoand the targeted suites above re-ran green on the merged head, and the full suites were not re-run):@objectstack/core: 99 files, 2452 tests passed.@objectstack/objectql: 418 files, 8109 tests passed.@objectstack/cli --project unit: 288 passed, 1 failed. The failure wasboot-preparation-parity.test.tsnamingstackDeclaresOrganizationRecordPage, fixed in891cc1736and green above.integrationtier is declared to CI; the only spawn file this diff adds is the e2e above, run locally.@objectstack/core,@objectstack/plugin-dev,@objectstack/objectqland@objectstack/cliall exit 0. Everycheck:test-typecheckis OK with no new ledger entries.eslint --no-inline-config --format jsonover the 11 changed.tsfiles reports 11 files linted, 0 errors, 0 warnings, and none reported as ignored. The repo config has noparserOptions.projectorprojectService(type-aware linting is off), so this diff cannot move the verdict on any untouched file. The fullpnpm lintis CI's.Acceptance notes
@objectstack/metadata-protocol'ssaveMetaItemrefusal (protocol.ts, theremedybuilt besideDEFAULT_RECORD_PAGE_CONFLICT) says "a page a package ships stays the default unless the deployment is composed without that package". It is the same family as this card's second half, in a file outside this claim. Its reach through a public door was not measured. Successor: none.packages/spec/src/ui/page.zod.ts,PageSchema.isDefaultTSDoc: "compose the deployment without the package that ships the other". This is author-facing prose, not runtime text, andpackages/specis outside this claim, so it is unchanged. Successor: none..changeset/pre/23015-one-default-record-page-per-object.mdkeeps the old wording. It is a pre-mode release record and not this PR's to edit.plugin-auth's README still shows the option set by hand. That stays correct for a host that composes its own kernel; underos serveandDevPluginit is now derived.packageson an option-B stack that carries no top-levelpagesmakes the predicate raise the collection reader'sINVALID_ARTIFACT_PACKAGES. That is the refusal the same boot raises when it registers that stack, and it is not swallowed here.Generated by Claude Code