Skip to content

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

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23310-serve-org-record-page
Oct 14, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23310-serve-org-record-page

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23310
Clause-②: yes

What this does

Ruling A on #23015 (record 6134056653) allows one isDefault: true record page per object per deployment. PR #23309 added AuthPluginOptions.registerOrganizationRecordPage so a host that ships its own sys_organization default can leave out plugin-auth's sys_organization_detail. The two composition roots that construct AuthPlugin themselves, os serve and DevPlugin, never passed the option. An app served by either one that ships its own sys_organization default was refused at boot with DEFAULT_RECORD_PAGE_CONFLICT, and the app had no switch to set.

  • Derived, not forwarded. Both roots now read the served app's own pages before they construct AuthPlugin. If the app declares an isDefault: true record page for sys_organization, they pass registerOrganizationRecordPage: false. Otherwise they pass true, which is today's behaviour. There is no new stack-config key: the page the author already wrote is the switch.
  • One predicate, two callers. stackDeclaresOrganizationRecordPage(stack, plugins?) is a new export of @objectstack/core (packages/core/src/stack-organization-record-page.ts). serve.ts calls it with (config, plugins) and dev-plugin.ts with (this.options.stack). It reads pages by resolveStackCollection's rule (the top level, or each package body for a multi-package stack) off the stack and off each app bundle already in plugins. Each page is judged by defaultRecordPageObject (@objectstack/spec/ui), the reading the registration refusal uses.
  • The remedy sentence (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, the 422 status, the named pages and the first sentence are unchanged.
  • ⛔ No precedence ladder and ⛔ no registration-order rule. The answer is one yes/no on a declared fact and does not depend on plugin order. A second default the app or any other package brings is still refused by the registry, naming both pages.

Derive vs forward (the measurement)

question derive forward
Does the root have the app's pages before new AuthPlugin(...)? yes. serve.ts has config in scope and has already appended new AppPlugin(config) to plugins (step 3, before step 5d). DevPlugin has this.options.stack. n/a
New authorable surface none a spec key, its liveness row, docs, generated baselines
Can the author get it wrong? No. The declaration is the switch. Yes. An author who ships the page and forgets the key is still refused at boot, and a key set with no page silently drops the Members / Invitations / Teams tabs.
Is it a single yes/no on a declared fact? yes (defaultRecordPageObject(page) === 'sys_organization' over the app's pages) n/a

Derive was reachable cleanly from both roots, so forward was not built.

Where the predicate lives. In @objectstack/core: @objectstack/cli and @objectstack/plugin-dev both depend on it, and neither can import the other. Core already hosts resolveStackCollection and the auth-family gate resolvePlatformAuthComposition for the same reason. Core already depends on @objectstack/spec, so nothing points the wrong way. It is a new file rather than an addition to stack-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.ts gains one SERVE_ONLY row. That pin reds when serve.ts uses a core function that os migrate's one-shot boot does not. The reason recorded in the row: the migrate boot composes plugin-auth's objects through createIdentityObjectsPlugin, 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 real os serve. It boots a defineStack app, signs in as the seeded dev admin and reads GET /api/v1/meta/page.
    1. The app declares orgpage_organization_detail as the sys_organization default. The boot reaches its banner with no conflict, and the sys_organization defaults are exactly ["orgpage_organization_detail of com.example.orgpage"].
    2. Control: the same app without the page. The defaults are exactly ["sys_organization_detail of com.objectstack.plugin-auth"], so the reading in case 1 is not vacuous.
    • Both cases also read sys_user = ["sys_user_detail of com.objectstack.plugin-auth"]: the option removes exactly one page.
    • It is in the nightly e2e tier, by name. Two os serve boots 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 single new AuthPlugin( literal in serve.ts (comments masked, as serve-auth-app-name.contract.test.ts reads it) passes registerOrganizationRecordPage: !stackDeclaresOrganizationRecordPage(config, plugins). The predicate reads an AppPlugin-shaped bundle, with an isDefault: false control.
  • packages/plugins/plugin-dev/src/dev-plugin-organization-record-page.test.ts. DevPlugin.init() with a recording AuthPlugin: the app page gives false. Controls: the same page with isDefault: false, no pages, and no stack each give true.
  • 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 a packages[] body, an app plugin's bundle. No: isDefault: false or absent, another object, a home page, 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: set isDefault: false on 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.
  • Pin sweep. I grepped the old sentence and DEFAULT_RECORD_PAGE_CONFLICT repo-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, under os-verify-lock, each restored to the HEAD blob with git diff HEAD empty):

mutation anchor result
serve.ts: the derived option replaced by registerOrganizationRecordPage: true, x1 to x0, blob e06e62ff1b24 to 6bd5bb650dd5 e2e case 1 red: os 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 mutation x1 to x0, blob 55c505e1c936 to 20335a650c35 the app-page case red, the 2 controls green
registry.ts: the old remedy text restored x1 to x0, blob 8fb229d5bace to 7da871a01cc3 the new remedy case red, 17 others green

serve.ts runs from src/ 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 a dist/.

Verification

Head c2db30e787 (origin/main 5fd2c76d6 merged in, a plain merge with no generated artifacts on either side), after a full turbo run build (76/76 tasks). All readings are on a shared box.

  • Derived gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 77 commands from the 12-path change set. All 77 ran, each exit code captured before any pipe, and --ran reconciled 77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN.
    • check:type-check-debt exited 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.
    • Pre-merge, check-engine-split-ratio refused on the shallow clone (exit 2). It read 98.5% after fetch --shallow-since=2026-07-09. The dual-build and i18n gates exited 3 (unbuilt CLI). All three pass on the merged, built head.
  • Tests on the merged head:
    • @objectstack/core stack-organization-record-page.test.ts: 11 passed.
    • @objectstack/objectql default-record-page registry and save-door tests: 38 passed.
    • @objectstack/plugin-dev full suite: 10 files, 89 tests passed.
    • @objectstack/cli unit: the contract pin, boot-preparation-parity.test.ts and vitest-tiers-partition.test.ts: 61 passed.
    • @objectstack/plugin-auth organization-record-page-option.test.ts: 3 passed.
    • The e2e above: 2 passed, 27.9 s.
    • @objectstack/spec test:repo: 61 files, 1033 tests passed.
  • Full suites before the merge (at 7c4bce2697, before the merge. The merge changed no source file of these packages; it did change packages/spec, whose test:repo and 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 was boot-preparation-parity.test.ts naming stackDeclaresOrganizationRecordPage, fixed in 891cc1736 and green above.
    • The integration tier is declared to CI; the only spawn file this diff adds is the e2e above, run locally.
  • Typecheck: @objectstack/core, @objectstack/plugin-dev, @objectstack/objectql and @objectstack/cli all exit 0. Every check:test-typecheck is OK with no new ledger entries.
  • Lint, a proven narrowing: eslint --no-inline-config --format json over the 11 changed .ts files reports 11 files linted, 0 errors, 0 warnings, and none reported as ignored. The repo config has no parserOptions.project or projectService (type-aware linting is off), so this diff cannot move the verdict on any untouched file. The full pnpm lint is CI's.

Acceptance notes

  • The save door's remedy names the same untakeable step. @objectstack/metadata-protocol's saveMetaItem refusal (protocol.ts, the remedy built beside DEFAULT_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.
  • The spec docblock carries the old sentence too. packages/spec/src/ui/page.zod.ts, PageSchema.isDefault TSDoc: "compose the deployment without the package that ships the other". This is author-facing prose, not runtime text, and packages/spec is outside this claim, so it is unchanged. Successor: none.
  • .changeset/pre/23015-one-default-record-page-per-object.md keeps 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; under os serve and DevPlugin it is now derived.
  • A malformed packages on an option-B stack that carries no top-level pages makes the predicate raise the collection reader's INVALID_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

…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>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/core, @objectstack/objectql, @objectstack/plugin-dev, touching 2 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/core/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/services-checklist.mdx (via DevPlugin (symbol, a top-level class))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/core/src/index.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: os serve (command, 31 pages)
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 54 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 0fb55fc2fca5590a76c1827d95bf7d1bacf22d02 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a2494a766d181180d84e11c745f0966212b76daa — the merge of head c2db30e78717c3237cef19631fc77bf6dc95091c into base 0fb55fc2fca5590a76c1827d95bf7d1bacf22d02, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 0fb55fc2fca5590a76c1827d95bf7d1bacf22d02 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c2db30e78717c3237cef19631fc77bf6dc95091c
Local-runs: none

Inputs: card #23310 (body, triage 6151745744, claim 6177340429, dev report 6179980256), PR #23550 (body, 12-file list, net diff against merge-base 5fd2c76d6 of main), and the check-runs on the head, read once. Nothing built, run or re-run.

Check-runs on the head (read once at review time). Completed success: Auto Label, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · ESLint, Lint · PM dispatch-gates, Lint · scoped · query-options erasure, Lint · scoped · tooling self-tests, the four PR-claim guards, filter. Completed skipped (path-gated): Build Docs, Console Pin Gate, Dogfood Regression Gate (matrix), Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance. Still in progress at brief time: Build Core; Test Core (1/6 … 6/6); Type Check · workspace / source gates / consumer gates / debt ledger; Lint · CI, release & engine gates; Lint · docs & code guards; Lint · repo & tooling gates; Lint · scoped · slot lookup & corpus. No watcher was set; the adopting seat reads their conclusions before the queue. Nothing completed is red.

① Derived judgments

Every accept-set and public-surface change the diff implies, each named right or wrong.

  1. New @objectstack/core export stackDeclaresOrganizationRecordPage(stack: unknown, plugins: readonly unknown[] = []): boolean (packages/core/src/stack-organization-record-page.ts, re-exported from packages/core/src/index.ts). Item by item:

    • Name: names the fact it reads (the served stack declares an organization record page), not an outcome or a switch. Right.
    • Input: the stack the root serves, plus the root's own plugins list; both typed unknown, so a config module, a defineStack output, a multi-package stack and a bundle all pass. Right.
    • What it reads: pages off the stack by resolveStackCollection's rule (top-level pages when the key is carried, otherwise each package body's, ADR-0130 D4), and pages off each plugins[] entry that exposes a bundle object, by the same rule; each page judged by defaultRecordPageObject(page) === SystemObjectName.ORGANIZATION. defaultRecordPageObject (packages/spec/src/ui/default-record-page.ts) is the one reading every refusal door uses (isDefault exactly true, type record or absent, non-empty object), so the predicate cannot say yes to a page the registry would not count as a default. Right.
    • Single yes/no on a declared fact, no ordering or precedence rule (ruling A on spec(page): no stated rule decides between two isDefault record pages for one object, so the renderer cannot honour isDefault without inventing precedence (the spec half of objectui#12195) #23015 as the card and triage render it): the body is bundles.some(... .some(...)); the answer is invariant under any permutation of plugins and of pages, and no page outranks another. When it answers true, plugin-auth's page is composed out; a second default from any other source is still refused by the registry naming both. This is exactly the second spelling the card itself offered ("deriving it when the app's own stack declares a default record page for sys_organization"), and triage left the choice to the measurement. Right.
    • Failure shape: a malformed packages[] on a stack with no top-level pages raises the collection reader's INVALID_ARTIFACT_PACKAGES, the same refusal the boot raises when it registers that stack, and the predicate does not swallow it. Declared in the TSDoc @throws. Right.
    • Dependency direction: core already depends on @objectstack/spec (packages/core/package.json), ./ui and ./system are published subpaths of spec, and core already imports @objectstack/spec/system at runtime in eight modules; this is the first runtime import of @objectstack/spec/ui in core, placed in its own file rather than stack-auth.ts. Neither cli nor plugin-dev can import the other, both already import core. Right.
    • One note, not a defect: the plugins leg reads AppPlugin's bundle, which packages/runtime/src/app-plugin.ts:152 declares private bundle: any. The read is structural at runtime and works (the e2e boot and the contract pin's plain-object shape prove it), but the coupling is undeclared on AppPlugin's side. Follow-up: a public readonly accessor on AppPlugin, or a comment on the field naming this reader. Not a must-fix: on both roots the stack itself is also passed, so the config boot's answer does not depend on this leg.
  2. os serve (packages/cli/src/commands/serve.ts) — the single new AuthPlugin( construction (line ~3999, step 5d) now passes registerOrganizationRecordPage: !stackDeclaresOrganizationRecordPage(config, plugins). plugins is declared at ~3029 (stackBootPlugins(config, flags.dev)), the new AppPlugin(config) wrap is appended at ~3369 (step 3), and stackSuppliesAuthPlugin(plugins) at ~3884 still skips the construction when the host composes its own AuthPlugin, so a host that sets the option by hand keeps it. The predicate therefore runs before AuthPlugin is constructed, over the config and the app wrap(s). Right. The import is added to the existing @objectstack/core import block. Right.

  3. DevPlugin (packages/plugins/plugin-dev/src/dev-plugin.ts) — step 3 wraps this.options.stack in AppPlugin (line ~569); step 4 constructs AuthPlugin (line ~715) with registerOrganizationRecordPage: !stackDeclaresOrganizationRecordPage(this.options.stack). DevPluginOptions carries stack? and no plugin list of its own, so the one-argument call reads everything the root composes. Right. Same stack, same answer under both roots: both read the same pages by the same rule through the same function; serve's extra plugins leg only adds the wrap over that same config (or host-composed AppPlugins that DevPlugin cannot carry at all). Right.

  4. DEFAULT_RECORD_PAGE_CONFLICT remedy (packages/objectql/src/registry.ts:~1783) — "or compose the deployment without the package that ships the other" becomes "or compose the deployment without the other package's page". It names no option, no package, no tracker number (triage 6151745744); code, 422, the named pages and the first and last sentences are unchanged. The TSDoc above the class is reworded to match and says why the page, not the package, is named. Right. The card asked for "without that package's page"; "the other package's page" is the same step in the sentence's own frame of two named pages. Right.

  5. Every pin of the old sentence moved? At the head the old sentence survives only in: four CHANGELOG.md files and .changeset/pre/23015-…md (release records, not this PR's to edit), and packages/spec/src/ui/page.zod.ts:1671 (item 6b). No test at the head asserts the old registry sentence; the existing registry pins read message.split('. ')[0] and are untouched; the new case in registry-default-record-page.test.ts asserts the new sentence verbatim and the absence of without the package, #NNN, registerOrganizationRecordPage and plugin-auth. default-record-page-save-door.test.ts:191 pins the metadata-protocol sentence, which this PR does not change, so it stays green by construction. Right: nothing left half-moved.

  6. The two out-of-scope findings the dev noted, not filed:

    • (a) @objectstack/metadata-protocol saveMetaItem remedy (protocol.ts:~21275): "a page a package ships stays the default unless the deployment is composed without that package". This is a different door (metadata save) with its own holder model, and this PR changes nothing about what that door accepts. The sentence was already too narrow after PR feat(plugin-auth): registerOrganizationRecordPage: false leaves out sys_organization_detail for a host that ships its own default record page #23309 (a host can leave out the page, not the package); this PR widens the set of deployments for which it is too narrow but does not make a statement that was true false. A sibling of the old wording, a follow-up, not a must-fix. It is pinned verbatim by default-record-page-save-door.test.ts:191, so its move is a change with its own pin to carry.
    • (b) PageSchema.isDefault TSDoc (packages/spec/src/ui/page.zod.ts:1670–1672): "To change which page opens: set isDefault: false on one of the two, or compose the deployment without the package that ships the other." Author-facing prose; still a true statement of one way (a host composing its own kernel can drop a package), and it does not deny the path this PR adds. A sibling, a follow-up, not a must-fix. A third sibling the dev did not list: the source comment at packages/spec/src/api/error-code-ledger.zod.ts:919–920 ("composing the deployment without the other package"), comment-only, same follow-up.
    • Neither is a stale published statement this PR makes false; the one published runtime text this card named, the registry remedy, is moved. The dev's "carrier: none" is accepted; the adopting seat may file one follow-up card for the three siblings together.
  7. packages/cli/src/utils/boot-preparation-parity.test.ts — one SERVE_ONLY row for stackDeclaresOrganizationRecordPage, with the reason: the os migrate one-shot boot composes plugin-auth's objects through createIdentityObjectsPlugin (packages/plugins/plugin-auth/src/identity-objects-plugin.ts), which registers no page (confirmed: the module names no page), so it has no organization page to leave out. Right, and the surface widening to this file is the pin reddening on the new import, not a design choice.

  8. Pins and negative controls (the card's third ask): packages/core/src/stack-organization-record-page.test.ts (yes/no pairs over top-level pages, untyped page, package body, app bundle; controls for isDefault: false, absent, other object, home page, empty, undefined); packages/plugins/plugin-dev/src/dev-plugin-organization-record-page.test.ts (recording AuthPlugin; false with the page, true with isDefault: false, no pages, no stack); packages/cli/src/commands/serve-auth-organization-record-page.contract.test.ts (exactly one new AuthPlugin( in serve.ts, carrying the derived option; the predicate over an AppPlugin-shaped bundle with an isDefault: false control); packages/cli/test/serve-organization-record-page.e2e.test.ts (two real os serve boots, the app page is the single default vs. plugin-auth's page as the control, sys_user read in both so exactly one page moves); registry-default-record-page.test.ts new remedy case. Each pin has its negative control, as the card lists. Right.

Nothing in the diff is judged wrong.

② Semver level

.changeset/23310-serve-org-record-page.md: @objectstack/core minor (a new public export), @objectstack/cli minor (os serve now boots an app shape it refused: an app declaring its own sys_organization default), @objectstack/plugin-dev minor (same widening for DevPlugin), @objectstack/objectql patch (the remedy sentence of an existing error; code, status and named pages unchanged). All four are in the fixed group, so they version together regardless; the per-package levels still describe what each publishes correctly. The changeset body states the new export by name and signature, the derived rule, the unchanged default, and "nothing to adopt". Clause-②: yes (widening) in the changeset, Clause-②: yes in the PR body, matching the claim. Check Changeset is green on the head. Right; skip-changeset would have been wrong.

Clause-②: yes (widening) — the accept set of os serve and DevPlugin widens by one declared app shape, through a new core export; no stack-config key is added, so no spec authorable surface, no liveness row and no generated baseline move.

③ Boundary flags

open_questions: [] — nothing to answer.

Dev deviations, each answered:

  1. Surface widened to packages/core and boot-preparation-parity.test.ts. The claim's own term ("the stack-config schema in packages/spec/src/** only if the measurement picks forwarding") shows the surface was expected to follow the measurement; derive needs one shared predicate and core is the only package both roots depend on (①.1). The parity row is a pin going red on the new import (①.7). Accepted.
  2. The os serve pin is a nightly-tier e2e (two real boots, ~28 s against a 10 s per-file budget). Is the unit-tier contract pin enough per-PR guard? Per PR, the chain is covered in three unit pins: the predicate's own truth table (core), DevPlugin's recorded option (plugin-dev), the single serve.ts construction carrying !stackDeclaresOrganizationRecordPage(config, plugins) (cli contract pin), and plugin-auth already pins what false does on a real kernel (organization-record-page-option.test.ts, unchanged). What only the e2e proves is the composition of all four in one real os serve process, and that belongs in the real-boot tier by this package's own tiering rule (vitest.config.ts, OS_TEST_TIERS, ci: e2e and live tiers move to a nightly run on main; PR and queue runs keep unit, integration and conformance (maintainer-directed, part B tiering) #16455). The source-text pin is brittle only loudly: moving the construction into a helper reds it rather than silently losing the guard. Enough; accepted.
  3. Changeset levels and Clause-② spelling. Judged in ②. Accepted.
  4. Zero label writes. The dispatch named none. Accepted.
  5. Commit trailers model-free. Outside this record's surface; nothing to judge.

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: claude/issue-23310-serve-org-record-page
Reviewed-by: session_01D6F9axfoPzR7t8G1J7Ytzz

VERDICT: PASS

Adopted by domain:spec seat 3 (#18883), baozhoutao, session session_01D6F9axfoPzR7t8G1J7Ytzz, at 2026-10-14T22:39Z: rendered by an isolated read-only reviewer at CONTRACT_REVIEW_TIER. ①.1 note and ①.6: the three sibling remedy texts (metadata-protocol saveMetaItem, PageSchema.isDefault's TSDoc, the error-code-ledger comment) and AppPlugin.bundle's undeclared reader are carried on one follow-up card, filed in this round. Landing waits on the in-progress check families finishing green on this head.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 14, 2026 23:02
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 14, 2026 23:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit 086fa46 Oct 14, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23310-serve-org-record-page branch October 14, 2026 23:28
marchtian pushed a commit that referenced this pull request Oct 15, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants