Skip to content

feat(spec,objectql,metadata-protocol)!: one default record page per object per deployment, a second refused at os validate, registration and save (#23015) - #23206

Merged
objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-23015-one-default-record-page
Oct 13, 2026
Merged

objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-23015-one-default-record-page

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23015
Clause-②: yes (narrowing: a second default record page for one object, accepted today, is refused; the public surface also grows by the new DEFAULT_RECORD_PAGE_CONFLICT code and the new exports, per contract review 6142059579)

Ruling A (record 6134056653) on #23015, quoted from the card: "A — one default record page per object per deployment, refused loudly. The spec states that at most one isDefault: true record page may exist for an object in a deployment. A second is refused at registration and at save, naming both holders, the way ADR-0048's 2026-10-08 addendum refuses a second holder of a position, permission-set or capability name. The renderer then honours isDefault with no tie-break."

This PR is the objectstack half. It changes nothing in objectui or cloud. Their seats file those legs (objectstack-ai/objectui#12195, and the cloud card the ruling asks for).

Census: every isDefault: true record page producer

Measured on objectstack 1a6e24c99 (this branch after merging origin/main e540e7ffb; the producer set is unchanged at b091cd851) and objectui 4baa8a67c. A producer is non-test source that ships a record page (type: 'record', or no type) bound to an object and declared isDefault: true.

repo object page producer
objectstack sys_organization sys_organization_detail @objectstack/plugin-auth (SysOrganizationDetailPage)
objectstack sys_user sys_user_detail @objectstack/plugin-auth (SysUserDetailPage)
objectstack sys_position sys_position_detail @objectstack/plugin-security (SysPositionDetailPage)
objectstack sys_approval_request sys_approval_request_detail @objectstack/plugin-approvals
objectstack showcase_project showcase_project_detail examples/app-showcase
objectstack showcase_task showcase_task_detail examples/app-showcase
objectstack showcase_preference showcase_settings examples/app-showcase
objectui none none 0 record-page producers. The isDefault: true hits are apps, flow edges and list views.

Every object has exactly one default, so no producer in either repository trips the refusal. The fork clause does not fire here.

isDefault: true on a page that is not a record page bound to an object is outside the rule. The one case of that is showcase_capability_map, a home page. It is unchanged.

The carried producer is the cloud leg. cloud's cloud_organization_detail (com.objectstack.service-tenant) and plugin-auth's sys_organization_detail are both defaults for sys_organization. A cloud boot on an engine that carries this PR will be refused with DEFAULT_RECORD_PAGE_CONFLICT and both pages named. The ruling orders the cloud leg to land before cloud bumps to that engine, and the cloud seat resolves it. cloud is not in this session, so this run did not re-measure it. The pair is the card's own measurement.

The cloud seat needs one measured fact. An environment save under a page that a managed package ships is refused before this rule is reached, with NOT_OVERRIDABLE 403: page is allowEnvironmentOverlay: false, which is the ADR-0126 Regime C rollback. The default-record-page-save-door.test.ts control pins this on both topologies, unchanged. So "overlay sys_organization_detail by name" is not available as an environment save. For cloud, the paths that remain are host composition (compose plugin-auth without its organization page) and not shipping a second default. The ruling's fork clause routes a lost-behaviour finding to the cloud seat, so this is reported and the ruling is not touched.

FROM → TO, per door

All three doors judge with one reading: defaultRecordPageObject(page) (@objectstack/spec/ui, default-record-page.ts). It holds the type default (PAGE_TYPE_DEFAULT) that PageSchema.type is now built from.

door FROM TO
defineStack, and so os validate, os build and a source boot two default record pages for one object in pages accepted. The boot kept both, and registration order picked the one that opened. refused, STACK_CROSS_REFERENCE_INVALID 422. The finding names the object and every page with its position: Object 'X' has two default record pages in \pages`: 'a' (stack.pages[0]) and 'b' (stack.pages[1]). Set `isDefault: false` on all but one of them. …`
composeStacks each input legal on its own, so the pair composed clean refused in the same envelope, naming each stack's page. Under manifest: 'preserve', one page name in two stacks counts as two packages' pages.
package registration (SchemaRegistry.installPackage, ahead of every mutation, and the registerItem seam) a second package, a nested plugin or a plugin's manifest.register shipping a default for an object another package holds registered silently refused, DEFAULT_RECORD_PAGE_CONFLICT 422 (new ledger code, exported DEFAULT_RECORD_PAGE_CONFLICT_CODE), with object, incoming, existing and conflicts: Default record page conflict: page "b" of package "p2" cannot be the default record page of object "X": page "a" of package "p1" already is. … The other holder can be another package's page (two packages' pages under one name are two pages), the environment's stored page, or a second page of the same manifest. No collisionPolicy downgrade.
metadata save (saveMetaItem, so PUT /api/v1/meta/page/:name) a page saved isDefault: true for an object that already had a default stored or shipped was accepted refused, DEFAULT_RECORD_PAGE_CONFLICT 422, in draft and publish mode, before anything is written. The message names both pages and gives the remedy that applies: demote the stored default, or for a shipped page, compose without the package. The holders are the environment-wide stored rows plus SchemaRegistry.defaultRecordPageHoldersOf(object), so an environment-scoped kernel (no write-through) is judged too.

What is not a second default, pinned at each door:

  • a save under the existing default's own name (an ADR-0005 overlay by name);
  • an environment item under the registered page's own name;
  • the same package again: a reload, or an upgrade that moves its default to another of its own pages;
  • a non-default record page;
  • defaults for two different objects;
  • an isDefault page that is not a record page.

Also in this diff:

  • The page form's isDefault help used to say "Set as default page for this page type", which is a rule nothing enforced. It now states this rule. The source is page.form.ts; the en bundle leaf was hand-edited to the source's string, and the zh-CN, ja-JP and es-ES leaves were hand-translated.
  • The hand-written content/docs/ui/pages.mdx row said the same thing and is corrected too.
  • PageSchema.isDefault gets its .describe(). The regenerated reference docs, api-surface/ui.json and export-origins/ui.json each add one row.
  • The D3 entry is 18.page-default-record-page-one-per-object-refused (no relevantWhen, because the surface reaches plugin code and stored rows).
  • DEFAULT_RECORD_PAGE_CONFLICT is registered under @objectstack/objectql and @objectstack/metadata-protocol. It has its own code rather than NAMESPACE_CONFLICT. No name is taken: what is held is an object's one default slot, and renaming fixes nothing.

Verification record

Final head b091cd851 is this branch plus origin/main 8323cad97. Unless a row names another head, every number below was read at b091cd851. Heavy runs went through scripts/pm/os-verify-lock.sh. The lock's wall-clock figures come from a shared box.

what result
new door pins spec/src/stack-default-record-page.test.ts 26/26 · objectql/src/registry-default-record-page.test.ts + default-record-page-save-door.test.ts 37/37 (save door on both topologies, environment-scoped and host-config)
@objectstack/spec full local suite 662 files, 20120 passed, 1 todo, at d7a8f7f34 (this branch before the last merge; spec source is unchanged since then except what main brought)
@objectstack/objectql full local suite 411 files, 8033 passed, at 1a6e24c99 (after the first merge). The two door files were re-run at b091cd851.
@objectstack/metadata-protocol full suite 232 files passed, 3 skipped; 28278 passed, 19 skipped
@objectstack/platform-objects suite 70 files, 1096 passed, at 498957f10 (its only change, the four help-text leaves, was already in)
typecheck metadata-protocol and objectql green at b091cd851. spec and platform-objects green at d7a8f7f34. Each includes check:test-typecheck.
check:generated (spec) all 14 artifacts up to date, after rebuilding the spec closure at b091cd851
derived gate union (dispatch-gates --commands, 124 families) 122 run, exit 0. NOT MEASURED: check:dual-build-cjs-loads, because it needs every package's dist/, i.e. a full workspace build, so CI runs it. UNRUN by dispatch order: check:type-check-debt. --ran reconciliation: 124 derived, 122 run, 1 NOT-MEASURED, 1 UNRUN. check:i18n and check:skill-examples first answered PREREQUISITE NOT MET. I built their prescribed closures and both then passed: i18n reported 10 packages in sync, and skill-examples reported 263 prose examples type-checked.
changeset gates check-adr-0087-registration: 1 declared-breaking changeset with its disposition, registered page-default-record-page-one-per-object-refused. check-changeset-no-major and check-empty-changeset green.
lint (narrowed) eslint --no-inline-config --format json over the 18 changed .ts files: 18 files, 0 errors, 0 warnings, and none ignored. This narrowing is a measurement. The config enables no type-aware linting (eslint.config.mjs: no parserOptions.project, no typed rules), so this diff cannot move any untouched file's verdict. The repo-wide pnpm lint is CI's.
affected closure turbo ls --affected names 82 packages, because a spec change reaches nearly all of them. The four above were measured locally and the rest are declared to CI. turbo ls reads the package graph, so it is blind to tests that read another package's source from disk, and the root package is not on that graph.

Ablations were done one per door, each red then green. The mutation went through scripts/ablation-replace.mjs (anchor hit 1→0, blob changed, restore proven to the HEAD blob with git diff HEAD empty), and the tests ran while the mutation was on disk.

door mutation red green after restore
defineStack delete the collectDuplicateDefaultRecordPageErrors push 4 failed / 26 26/26
composeStacks delete the collectComposedDefaultRecordPageErrors spread 3 failed / 26 26/26
package door delete this.refuseDefaultRecordPageConflicts(manifest, selfId) 3 failed / 17 17/17
item seam if (false && packageId && type === 'page') 1 failed / 17 17/17
metadata save the save call replaced with undefined. The objectql suite reads @objectstack/metadata-protocol through dist/, so the package was rebuilt, and ablation-dist-preflight confirmed the marker present before, absent after, and present again on restore, with the tree clean. 6 failed / 20 20/20

Acceptance notes

  • A boundary of the registry door, stated in the module doc and not closed here: sys_metadata hydration is not judged. That is the bare slot, with no package, which is the same thing ADR-0048 N.3 says for the security catalog. The save door refuses a second default before it is stored. But at a cold boot every package registers before stored pages hydrate. So a stored default saved while no package held that object's default, together with a package upgrade that later adds one, reaches the registry unrefused on restart. A hot install of that package is refused. Closing the cold-boot order would mean refusing a boot after hydration. The security catalog got that only by its own ruling ([decision] cold boot admits a package whose permission set or position name the environment catalog already holds (package registration runs before sys_metadata hydration), while a hot install of the same package is refused #22307), so it is not invented here. Carrier: none.
  • isDefault on home / app / utility / list pages keeps whatever objectui does with it today. The ruling's words are "record page for an object", and only those are judged.
  • The save door judges environment-wide rows, per ADR-0131 D6, because an organization-scoped write is refused for every type. A draft is judged twice: at its own save, and again when it is published (promoteDraftForPublish, which covers both publishMetaItem and publishPackageDrafts). Another page may have taken the default in between; a test pins that case.

Generated by Claude Code

claude added 12 commits October 13, 2026 05:02
… — stack, compose and registry doors

Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf
Co-authored-by: Claude <noreply@anthropic.com>
… one-default-per-object rule

Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf
Co-authored-by: Claude <noreply@anthropic.com>
…ault when it is published

Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/platform-objects, @objectstack/spec, touching 40 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/objectql/src/index.ts, packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

32 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 150da61aa54c6a287bdc834db3b13399d18e50d9.

⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/objectql/src/index.ts, packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json, …) — pages documenting those are invisible to this run
  • 10 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 — 143 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 150da61aa54c6a287bdc834db3b13399d18e50d9 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d4804794ade7011da75d246cb5d4ae802fd46394 — the merge of head 566d19b85869c8a444873f701b06c629d86fd5f0 into base 150da61aa54c6a287bdc834db3b13399d18e50d9, 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 d4804794ade7011da75d246cb5d4ae802fd46394 && git checkout d4804794ade7011da75d246cb5d4ae802fd46394
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 150da61aa54c6a287bdc834db3b13399d18e50d9 566d19b85869c8a444873f701b06c629d86fd5f0 && git checkout -B drift-repro 150da61aa54c6a287bdc834db3b13399d18e50d9 && git merge --no-ff 566d19b85869c8a444873f701b06c629d86fd5f0

node scripts/docs-audit/affected-docs.mjs --json 150da61aa54c6a287bdc834db3b13399d18e50d9

⚠️ 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 150da61aa54c6a287bdc834db3b13399d18e50d9 → 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: b091cd85161182e2a0871713861d695717c7aeec
Local-runs: none

Inputs read: card #23015 (body and all five comments: triage 6122995432, ruling A 6134056653, claim 6138406113, dev report 6141744005, ACCEPT 6141797380); the cross-lane declaration 6138419895 on #20966; ADR-0048's 2026-10-08 addendum with N.1 to N.4 as it stands on main; PR #23206's body, its 25-path file list and the net diff against main (merge base 8323cad97, +1648 / -16); the check-runs on the head. The pinned sibling was read at .objectui-sha 088e0f705. Nothing was built, run or re-run.

① Derived judgments

Accept-set narrowings the diff makes (each RIGHT, each judged with the one reading):

  • defineStack, and so os validate, os build and a source boot: two isDefault: true record pages for one object inside pages refuse STACK_CROSS_REFERENCE_INVALID 422, one finding per object naming every page with its position. collectDuplicateDefaultRecordPageErrors sits beside the duplicate-entry check, ahead of the object-count early return, so a platform object's pages count. composeStacks refuses the pair across its inputs in the same envelope; under manifest: 'preserve' entries are keyed holder|name, so one name in two stacks is two pages. A name repeated inside one stack is the duplicate-entry finding only, not counted here a second time. RIGHT.
  • SchemaRegistry.installPackage and registerItem('page', item, 'name', packageId): DEFAULT_RECORD_PAGE_CONFLICT 422 with object, incoming, existing and every conflict in conflicts. RIGHT.
  • saveMetaItem (so PUT /api/v1/meta/page/:name) and the draft promotion: the same code and status. RIGHT.

One reading, and it matches PageSchema exactly. defaultRecordPageObject reads isDefault === true (the key is z.boolean().default(false)); type absent or 'record', the absent case filled from PAGE_TYPE_DEFAULT, the same constant PageSchema.type's .default() is now built from, so the schema's default and the reading cannot part; and object a non-empty string (PageSchema.object is an optional string; an empty one binds no object, so treating it as unbound lets nothing through). Every door calls this function: stack.zod.ts, declaredDefaultRecordPages and the item seam in objectql, defaultRecordPageSaveRefusal in metadata-protocol. RIGHT.

No precedence ladder, no registration-order rule, no silent downgrade. Whichever side arrives second is refused, in either order (pinned); collisionPolicy: 'warn' does not soften it (pinned); the renderer is left nothing to tie-break. RIGHT.

The registry door's holder bookkeeping, read against registerItem's key derivation at the head:

  • A composite slot is stored under PACKAGE:NAME, and applyProtection(item, { packageId }) stamps _packageId with that same id, so a composite slot and the install claim of one package's page collapse to one holder key (package:ID:page), while two packages' pages under one name are two composite keys and two holders. Package ids are dotted and page names snake_case, so splitting a key at its last colon is exact. RIGHT.
  • The bare slot (a sys_metadata hydration or a write-through, registered with no package id) shadows every composite slot and claim of its name, as every ADR-0005 read does; a bare slot carrying a real package stamp (the protocol grafts a row's package_id onto an overlay body as _packageId) is named as that package's page; the 'sys_metadata' sentinel and _provenance: 'org' are excluded from "packaged". The demote-by-overlay case is pinned: the holders become empty. RIGHT.
  • Same package again: otherDefaultRecordPageHolders drops the package's own holders and the bare slot of the very page being registered; a re-install REPLACES the package's claims; a reload and an upgrade that moves the default to another of its pages are pinned. RIGHT. One edge, pre-existing and not this diff's: installPackage does not evict a package's earlier items (ADR-0048 §3.4 overwrite semantics), so a default page a package DROPS in an upgrade stays as that package's residue in the item store and still counts as a holder for every other party until a restart. That errs loud, never silent.
  • Mid-install refusal: refuseDefaultRecordPageConflicts sits after the namespace gate, the co-owned-object check and the security-catalog refusal and before const now and every mutation, and declaredDefaultRecordPages reads exactly the sources the engine's collection loop registers (pages and plugins[].pages, arrays only, one level; registerPlugin does not recurse), so a manifest's conflict is caught at the package door and never first at the item seam after a partial install. The test pins no package record and no page after a refusal. RIGHT.
  • The item seam runs before applyProtection and the store, after the security seam. RIGHT.

The save door. Judged after the schema fold and withDeclaredPageTypeDefault, before the authoring gate and every write (repo.put is the method's last act; the awaits ahead of it read the lock, the existing item and the container collision, or fold the body in memory), in draft and in publish mode (the mode is derived above it, and the draft-then-publish case is pinned), and judged again in promoteDraftForPublish on draftForGate before repo.promoteDraft, which both publishMetaItem and publishPackageDrafts run. The holders are the environment-wide active rows (readActiveOverlayRows, ADR-0131 D6) plus SchemaRegistry.defaultRecordPageHoldersOf(object) for every name no stored row holds; an environment-scoped protocol (no write-through) still reads this.engine.registry, so the shipped holders are seen, pinned on both topologies. An unprovisioned store reads as no rows; any other read failure propagates. The remedy text never prescribes overlaying a packaged page by name, which NOT_OVERRIDABLE would refuse. RIGHT.

No value a real producer writes today is refused. Measured at the head: seven objects, each with exactly one default record page. sys_organization_detail and sys_user_detail are authored in @objectstack/platform-objects/pages and registered once, by plugin-auth's manifest; sys_position_detail the same, registered once by plugin-security; sys_approval_request_detail by plugin-approvals; showcase_project_detail, showcase_task_detail and showcase_settings by examples/app-showcase, where no other isDefault: true page binds those objects. showcase_capability_map is type: 'home', outside the rule. app-crm and app-todo ship no isDefault: true record page. objectui at the pin 088e0f705: usePageAssignment selects by type === 'record', object and declaration order and never reads isDefault; nothing there produces a record page. The old row in content/docs/ui/pages.mdx stated the retired "for its type" rule and is corrected. The green Dogfood Regression Gate on the head corroborates the showcase boot. RIGHT. The cloud pair is the ruling's own carried case, routed in ③.

Public-surface additions the diff makes (named here; their declaration is judged in ②): defaultRecordPageObject on @objectstack/spec/ui (api-surface/ui.json and export-origins/ui.json each +1, regenerated); PageSchema.isDefault's .describe() (reference docs +1 row, no accept-set change); DEFAULT_RECORD_PAGE_CONFLICT in ERROR_CODE_LEDGER under @objectstack/objectql and @objectstack/metadata-protocol (regenerated contract.mdx and error-code-ledger.mdx); DEFAULT_RECORD_PAGE_CONFLICT_CODE and the public method SchemaRegistry.defaultRecordPageHoldersOf, whose return type makes DefaultRecordPageHolder reachable, on @objectstack/objectql's root entry. DefaultRecordPageConflictError is NOT exported from the barrel, which keeps every refusal class unexported by design, so the ACCEPT's "and their types" holds only through the method's return type. All RIGHT as surface. PAGE_TYPE_DEFAULT is read by page.zod.ts and not re-exported, which is fine.

② Semver level

  • Level: RIGHT. @objectstack/spec, @objectstack/objectql and @objectstack/metadata-protocol at minor with the ! summary, the **BREAKING** banner and the ADR-0087 marker registered page-default-record-page-one-per-object-refused; @objectstack/platform-objects at patch for the four help-text leaves. A narrowing is breaking, shipped as minor under the launch-window convention; the body carries the FROM → TO table per door and the one-line fix. The D3 entry 18.page-default-record-page-one-per-object-refused names the surface, the replacement, the reason and acceptance criteria, with no D2 conversion (which page stays the default is the author's fact) and no relevantWhen; both RIGHT.
  • The gate's reading. check-adr-0087-registration reads breaking-ness from four signals, and signal (4) is the ARM alone, through readClause2Line: narrowing is the breaking signal; the value is not read there. The gate is green with either no (narrowing) or yes (narrowing), and yes (narrowing) is a legal, self-tested shape ("a diff that widens one surface and narrows another").
  • The question the ACCEPT handed over, answered from the rule text. The value answers one question, 「本卡放宽接受集或扩大公开面吗」; the public surface is what the built entry reaches, 「公开面 = 包构建后的入口声明所达,⛔ 不只数入口的再导出清单」, re-exported names plus every type reachable through their props, parameters and return types; and for a new error code the rule is explicit: 「错误码已发布面 = ERROR_CODE_LEDGER/StandardErrorCode:新码恒 yes,无门亦然,漏登即缺口」. This diff adds a new ledger code, a new @objectstack/spec/ui export, and a new objectql export plus a public method. The right line is Clause-②: yes (narrowing). no (narrowing) is what the claim, the PR body and the changeset body carry, and the landing rules name that case: 「错误的 no 是可审计的假申报」. The level needs no change (yes takes at least minor), the arm and the marker need no change, and the ADR-0087 gate's verdict does not move.
  • FAIL item. Correct the line to Clause-②: yes (narrowing) in the changeset body and in the PR body. The claim's line is the dispatching seat's to correct in its own act; the path limb already hits, so the queue gate owes a contract review either way.

③ Boundary flags

  • Census (dev flag): answered RIGHT in ①. One wording note: the three platform pages are authored in platform-objects and registered by the two plugins; the registering package is the unit the rule counts, so the body's producer column names the right unit.
  • The cold-boot open question, judged as the ACCEPT asked. Does ruling A's "refused at registration and at save" already reach sys_metadata hydration at a cold boot? No. The ruling names two doors and mirrors them on ADR-0048's 2026-10-08 addendum, whose N.3 keeps "a write with no package provenance, which is how every sys_metadata hydration arrives" outside the registration refusal, and whose cold-boot check exists only through a separate amendment with its own ruling record (6063176077 on [decision] cold boot admits a package whose permission set or position name the environment catalog already holds (package registration runs before sys_metadata hydration), while a hot install of the same package is refused #22307); the ruling's workload column names the schema rule, the registration refusal and the os validate refusal and no boot gate, and the claim's file surface matched that. The PR delivers both named doors and the stack door, states the boundary in the objectql module doc, the acceptance notes and the D3 acceptance criteria, and keeps the spec's claim as narrow as its enforcement (the TSDoc lists the doors). So this is a maintainer choice, correctly escalated under open_questions, not a FAIL item. Two facts the report understates, for that decision: the surviving pair forms only through the upgrade-then-restart sequence, but it then makes a hot install and a cold boot answer differently for the same composition, an order-dependence of the kind ruling A rejected in D; and AGENTS.md's startup-registry rule applies in the mirror sense (the package door concludes "no other holder" from a registry whose stored pages have not hydrated, and records the install). Option B therefore has a precedent-shaped home, the N.3 amendment pattern, if the maintainer wants it; it needs its own card and ruling, as the security catalog's did.
  • Rider content/docs/ui/pages.mdx: a prose face (not references/, not releases/), one row that restated the retired rule, no new page so no meta.json edit. Accepted.
  • Rider, the four platform-objects translation leaves: the en leaf is hand-edited to the exact page.form.ts string, which is what the generator rewrites it to (check:i18n inside the green Lint & Repo Gates); the zh-CN, ja-JP and es-ES values are hand-written as the guardrail allows; structure unchanged, one line each. Accepted.
  • Out of scope, the cloud leg: the ruling itself orders it (the cloud seat, before the engine bump). The measured fact that an environment save under a packaged page's name is refused NOT_OVERRIDABLE 403 (page has allowEnvironmentOverlay: false) narrows the ruling's "overlay by name" remedy to environment-authored pages; the PR's refusal texts, TSDoc and D3 entry prescribe only isDefault: false or host composition for a packaged page, consistent with that fact. Routed under the fork clause to the cloud seat. RIGHT.
  • Out of scope, objectui#12195: the spec now states the rule; the renderer leg is that card's. RIGHT.
  • Governed surfaces: none of the 25 paths is governed (Governed Surface Queue Guard success). 1,664 changed lines, under the human-merge threshold. Same-repository head, not a fork.
  • Check-runs on the head, read 2026-10-13T09:00Z: 30 success, 5 skipped (Temporal Conformance, the Dogfood matrix shard, Dogfood Verify CLI, Console Pin Gate, Packed-tarball smoke, all path-filtered or opt-in), 5 in progress (Test Core shards 1 to 5), 0 failure. Lint & Repo Gates, Build Core, Dogfood Regression Gate, the Type Check jobs and Governed Surface Queue Guard are success. Not waited on.

Implemented-by: claude/issue-23015-one-default-record-page
Reviewed-by: session_019G5ZnRrTMiwcHG6iZM19zf

VERDICT: FAIL — one item, the ② Clause-② value; ① and ③ hold. A patch round that corrects the line in the changeset body and the PR body, with no code change, is enough; the record on that head can carry ① and ③ forward.


Generated by Claude Code

…) — the diff adds a ledger code and public exports

Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 566d19b85869c8a444873f701b06c629d86fd5f0
Local-runs: none

The patch round after contract review 6142059579 (FAIL on b091cd85161182e2a0871713861d695717c7aeec, one item: the Clause-② value). Inputs read: that record; card #23015, its body and all seven comments, the seat's correction 6142088054 and the patch-round report 6142115814 included; PR #23206's body, its 25-path file list and the net diff against main at this head (merge base 8323cad97, +1648 / -16); the round's own commit 566d19b85 on b091cd851; the check-runs on the head. The shared checkout's objects were read with git show, git diff and git grep only; the pinned sibling was read the same way. Nothing was built, run or re-run.

① Derived judgments

The round's diff is the one owed line and nothing else. b091cd851..566d19b85 is one commit, one file, one insertion and one deletion: line 10 of .changeset/23015-one-default-record-page-per-object.md, no (narrowing) to yes (narrowing). No code, test, doc, generated artifact or other text moved; the commit's parent is b091cd851, a fast-forward. RIGHT.

Both carriers now read the corrected value. The changeset body carries the value yes and the arm (narrowing) with nothing after it; the PR body's line carries the value yes and the arm (narrowing: …) with the seat's reason inside the parenthesis and the record's id at its end. Read against scripts/pm/clause2-line.mjs, the one reader every gate imports: the key sits at line start and once per line, the value token is the first thing after the colon, the arm token is the first thing inside the parenthesis and what follows it (a colon, in the PR body) is neither alphanumeric nor a pipe, and neither line is quoted-and-continued. Both read declared, value yes, arm narrowing. RIGHT.

The gate's reading of the arm holds. check-adr-0087-registration reads breaking-ness from the changeset body through readClause2Line, and the arm alone is its signal (4); the value is not read there. At this head the changeset carries three of its four signals at once (the ! in the summary, the **BREAKING** banner and the narrowing arm) and the disposition marker registered page-default-record-page-one-per-object-refused. Check Changeset on the head, the job that runs that gate and check-changeset-no-major, is success.

Net diff unchanged. The three-dot diff against main at this head is the same 25 paths, +1648 / -16, on the same merge base 8323cad97 that record 6142059579 judged, with the one changeset line as the only byte that differs from b091cd851's. Every derived judgment in that record's ① (the one reading defaultRecordPageObject every door shares; the defineStack and composeStacks, installPackage and registerItem, saveMetaItem and promoteDraftForPublish refusals and their codes; the holder bookkeeping; no ladder, no order rule, no downgrade; no real producer refused; the public-surface additions) is carried forward by reference to that record. Nothing between the two heads touches what they were read against, as follows.

Carry-forward across main's movement. main advanced six commits past the merge base (9fbabd76a; d889448b9, the Version Packages merge; 44e2d505c; c6139f264; 107853ced; 150da61aa). The PR did not merge them, so the merge base stands and the queue's rebuilt generation is where they meet. Read for overlap:

  • Exactly one of the PR's 25 paths moved on main: packages/metadata-protocol/src/protocol.ts, +95 from fix(metadata-protocol): a view container's reset heals the expanded views its publish registered #23196 (a view container's reset heals the expanded views its publish registered). Those hunks sit in hydrateExpandedViewItems, two new private statics (recordViewExpansion, takeViewExpansions) and restoreArtifactRegistryView, all about view rows and the delete's heal. The PR's hunks are the defaultRecordPageObject import, the new defaultRecordPageSaveRefusal method, and one call each in saveMetaItem and promoteDraftForPublish. The two sets share no function, no state and no metadata type (page against view), and GitHub reports the head mergeable: true. The save-door reading in ① is undisturbed.
  • No other PR path moved. Under packages/spec/src/** main moved only contracts/ai-service.ts (feat(spec): IAIConversationService.get / list and IAIService.listPendingActions take the caller's execution context #23197, an AI service signature): not ui/, not stack.zod.ts, not the error-code ledger, not the sharded api-surface/ and export-origins/ ui.json artifacts. TypeScript Type Check, the job that runs the generated-artifact gates, is success on the head.
  • No isDefault: true line was added under packages/ or examples/ on main; no new producer, so the census stands at seven objects with one default record page each.
  • .objectui-sha reads 1fb1ced36 at the merge base, at main's tip and at this head: unmoved between the heads. One wording note on record 6142059579: it names its sibling reading at 088e0f705, which objectui's history shows as an ancestor of the pin, not the pin. Read at the pin itself for this record, objectui's non-test isDefault: true hits are apps, flow edges, list views and permission-set prose, no record page bound to an object, and the page-assignment hook still selects by type === 'record', object and declaration order and never reads isDefault. The fact holds at the pin; the earlier citation was older than the pin, not wrong.
  • The Version Packages merge moved consumed changesets under .changeset/pre/ and bumped versions and CHANGELOGs. The PR's changeset stays at .changeset/23015-one-default-record-page-per-object.md beside main's two newer top-level ones, and Check Changeset reads it on the head.
  • Governed surfaces: none of the 25 paths, unchanged; Governed Surface Queue Guard is success on the head.

② Semver level

  • Level: RIGHT. Unchanged from b091cd851: @objectstack/spec, @objectstack/objectql and @objectstack/metadata-protocol at minor with the ! summary, the **BREAKING** banner, the ADR-0087 marker registered page-default-record-page-one-per-object-refused, and the FROM → TO table per door with the one-line fix; @objectstack/platform-objects at patch for the four help-text leaves. The D3 entry is 18.page-default-record-page-one-per-object-refused (no D2 conversion, no relevantWhen); no entry of that name exists on main.
  • The declaration: RIGHT, and now consistent with the diff. The value answers whether the card widens the accept set or the public surface, where a new ledger code is always yes (ERROR_CODE_LEDGER is a published surface) and the public surface is what the built entry reaches. This diff adds the ledger code DEFAULT_RECORD_PAGE_CONFLICT; the export defaultRecordPageObject on @objectstack/spec/ui; and DEFAULT_RECORD_PAGE_CONFLICT_CODE plus the public method SchemaRegistry.defaultRecordPageHoldersOf, whose return type reaches DefaultRecordPageHolder, on @objectstack/objectql. It also narrows a published accept set. The value yes with the arm (narrowing) is the legal shape for exactly that case (clause2-line.mjs: a diff that widens one surface and narrows another), and it is what the changeset body and the PR body now carry. The FAIL item of 6142059579 is discharged. The claim 6138406113 was corrected by the seat in its own act (6142088054), as that record said it should be.
  • yes takes at least minor, and it does. The three packages whose published surface grows are minor. platform-objects moves four translation values under keys that already exist, so its published key set does not grow and patch is its right grade. check-changeset-no-major's level axis reads the PR body's declaration (yes, or the narrowing arm; either carries the axis), counts platform-objects as a moved package graded patch, and discharges it because the three minor packages account for the declared widening (its discharged verdict: exit 0, residual printed). Check Changeset is success on the head.
  • (narrowing) is BREAKING and the changeset says so three ways; minor is the launch-window grade check-changeset-no-major enforces, and it is green.

③ Boundary flags

  • Carried forward from 6142059579 ③ by reference, nothing between the heads touching any of them: the census (the registering package is the unit); the cold-boot open question, judged outside ruling A's two named doors and escalated correctly rather than a FAIL item (the seat keeps A for this PR and, per 6142088054, files option B as its own card for the maintainer); the content/docs/ui/pages.mdx rider; the four platform-objects translation leaves (the en leaf equal to the page.form.ts string, check:i18n inside the green Lint & Repo Gates); the cloud leg routed under the fork clause; objectui#12195 as the renderer half; no governed path; 1,664 changed lines, under the human-merge threshold; same-repository head, not a fork.
  • The patch-round report 6142115814: open_questions and out_of_scope_findings are both empty, and nothing in a one-line changeset edit raises one. Its statements (one file, one line; the level and the marker unchanged; readClause2Line returning yes and narrowing; no code change; origin/main not merged because the PR was still mergeable) each match what the diff and the PR read. RIGHT. Its local gate runs are not relied on here; the head's check-runs are the gate verdicts.
  • needs:contract-review: not on the PR (labels: documentation, size/xl, tests, tooling, protocol:ui), so nothing to remove with this record.
  • Check-runs on the head, read 2026-10-13T09:15Z: 29 success, 5 skipped (Temporal Conformance, the Dogfood matrix shard, Dogfood Verify CLI, Console Pin Gate, Packed-tarball smoke: path-filtered or opt-in), 6 in progress (Test Core 1 to 6), 0 failure. Of the seven required contexts, Lint & Repo Gates, TypeScript Type Check, Build Core, Dogfood Regression Gate and Governed Surface Queue Guard are success, Temporal Conformance (live PG + MySQL) is path-skipped, and Test Core is still running. Not waited on. The PR is a draft with mergeable_state: blocked; the queue is the owning seat's step once every check is green.

Implemented-by: claude/issue-23015-one-default-record-page
Reviewed-by: session_019G5ZnRrTMiwcHG6iZM19zf

VERDICT: PASS

The one owed item is discharged on this head; ① and ③ of 6142059579 are carried forward by reference, undisturbed by the round's commit and by main's movement; ② holds in full.


Generated by Claude Code

Merged via the queue into main with commit 5f37d36 Oct 13, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23015-one-default-record-page branch October 13, 2026 10:11
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 protocol:ui size/xl tests tooling

Projects

None yet

2 participants