Skip to content

feat(spec)!: PageSchema refuses a page authoring both slots.details and slots.tabs; sys_user_detail moves its details into its first tab - #22600

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-22568-details-tabs-slot-pair
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-22568-details-tabs-slot-pair

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #22568
Clause-②: yes (narrowing)

Triage 6092519848, direction (b): the contract refuses the pair, and the platform's own producer moves. objectui is not touched.

What changes

  • The refusal (packages/spec/src/ui/page.zod.ts). checkPageSlotPair is a new exported object-level check, attached to PageSchema by identifier beside the three page checks already there. It refuses a page whose slots map carries both details and tabs, at slots.details, with a custom issue. The issue names both slots and the fix: move the record:details component into the tabs items (as the first item's children) and delete slots.details, or delete slots.tabs to keep the synthesized tabs. Either slot may be one component or an array. An empty details: [] is refused like a full one, and the page's kind does not matter. A page authoring only one of the two slots is unchanged.
  • The slot doc. The slots TSDoc now states the precedence: details is the body of the Details tab, that tab lives inside the synthesized page:tabs strip, and tabs replaces the whole strip, so tabs wins. The .describe() is unchanged, so no reference page or JSON schema moves.
  • The producer (packages/platform-objects/src/pages/sys-user.page.ts). sys_user_detail moves its record:details in as the first tabs item (Details, file-text, four-locale label, the convention sys_approval_request_detail uses). Its sections and hideFields are byte-for-byte unchanged. The page no longer authors a details slot. The file's strategy docblock now matches the page.
  • The ledger. The D3 semantic entry page-slots-details-beside-tabs-refused (protocol 18), its step-18 rationale fragment, and registry.ts regenerated with gen:migration-registry. There is no tombstone, because no key is removed. There is no D2 conversion, because which tab item carries the body (and under which label) is the author's choice. Per PR spec(changes): generate the per-major spec-changes section and the upgrade guide at publish; the pull request generates both in memory and renders the diff (#22449 B′, condition 1) #22533, no spec-changes.json or upgrade-guide regeneration is owed. check:spec-changes and check:upgrade-guide are green.
  • The changeset. .changeset/22568-page-slots-details-beside-tabs-refused.md: @objectstack/spec minor (a BREAKING accept-set narrowing), @objectstack/platform-objects patch. It carries the Clause-② line, the ADR-0087 registered marker, a FROM → TO table and the one-line fix.
  • Generated, owed by the new export: packages/spec/api-surface/ui.json and packages/spec/export-origins/ui.json, one line each.

The three hypotheses, measured

  • H1, reach: zero outside this repository, so no stop. I searched for files with a slots map holding both a details and a tabs key.
    • In this repository at d66e53bdf5, the only producer is packages/platform-objects/src/pages/sys-user.page.ts. No examples/** page authors the pair. Control: the census sees project-detail.page.ts, sys-organization.page.ts and sys-position.page.ts, and each authors tabs only. The other hits are packages/spec/src/system/i18n-resolver.test.ts walker fixtures, which are never parsed (see Acceptance notes), plus page.zod.ts and registry.ts prose.
    • hotcrm at its default branch main f0afcbda07: zero pages author both. Control: its one slotted page, src/sales/pages/account_detail.page.ts, is found, and it authors header and discussion only. Every other hotcrm page is kind: 'full'.
    • objectui at main d758f2f: no producer writes slots.details or slots.tabs. The only both-slot file is the synthesizer's own test, which calls buildDefaultPageSchema with options and never parses a spec page.
  • H2, placement: an exported check on PageSchema. The slot map's own refusals are shape-level (a closed key set plus aliases), and it has no cross-slot rule. Every cross-field page refusal in page.zod.ts is an exported checkPage* attached with .superRefine(checkX). object-refinement-check-exports.test.ts pins that shape: parity, count plus bijection, attachment by identifier, and barrel identity. objectui's mirror re-attaches those exports. I measured one more reason against a nested refinement on the slots object: it would add new x-dropped-refinements sites (for example ui/Page slots, and manifest.pages.element.slots under the installed-package schemas). That means edits to the hand-kept dropped-refinements.baseline.json and churn in published JSON schemas. A check at the page root adds no site, because the root "" is already recorded.
  • H3, renderer: confirmed by reading (objectui main d758f2f, read only).
    • buildDefaultPageSchema.ts about lines 1072 to 1095: tabs wins. With only details, the synthesizer puts the authored node, as is, into tabItems[0].children.
    • The page:tabs renderer (components/src/renderers/layout/containers.tsx) renders each items[].children through renderChildren. SchemaRenderer hoists properties.* onto the node.
    • record:details (plugin-detail/src/renderers/record-details.tsx) reads schema.hideFields (about line 490) and schema.sections (about lines 769 and 827).
    • So an authored record:details that is a tab item's child renders through the same path, with the same sections and hideFields, as the synthesized Details tab body.
    • Not driven in a browser.

Doors measured

door how result
parse PageSchema.safeParse, definePage (page-slot-pair.test.ts) refused, custom at slots.details
stack door defineStack (page-slot-pair.test.ts) STACK_SCHEMA_INVALID / 422, issue at pages.1.slots.details
objectstack validate the schema call validate.ts makes (ObjectStackDefinitionSchema.safeParse), run on the built spec dist success=false, [{"path":"pages.1.slots.details","code":"custom"}]; control with only the moved page: success=true. The CLI binary itself was not run (its dist is not built here).
page save door one-off temporary test in metadata-protocol/src, using the real saveMetaItem write path over a stub engine (the face-inventory harness), removed after the run, git status clean INVALID_METADATA / 422, issues: [{path: "slots.details", code: "custom"}] on both faces, nothing persisted; control: the moved page saves (1 row)
read diagnostics same temporary test, computeMetadataDiagnostics('page', …) valid: false, error at slots.details
producer control BASE's sys-user.page.ts restored on disk against the new refusal, then the platform-objects src/pages/ suite 4 red, including the pre-existing 'SysUserDetailPage' parses through PageSchema (door 1 precondition), at slots.details; restored to blob == HEAD 7c8b12e1a7, git diff HEAD empty

Tests (final head d66e53bdf5 unless stated)

  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 639 files, 19061 passed, 1 todo.
  • pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2: 54 files, 915 passed (at e92b99226e, and again at d66e53bdf5).
  • Targeted spec run (page-slot-pair, object-refinement-check-exports, page, page-print, page-requires-compiled-kinds, src/migrations/, src/conversions/, step18-rationale-merge): 21 files, 1003 passed.
  • pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2: 69 files, 1082 passed, including the new sys-user-details-tab.test.ts (4 tests).
  • pnpm --filter @objectstack/spec typecheck and pnpm --filter @objectstack/platform-objects typecheck: both exit 0 (at e92b99226e, and again at d66e53bdf5), and the test layers hold their ledgers.
  • Consumer sweep: the lint tests with slotted fixtures (9 files, 480 passed) and protocol.runtime-authoring-gate.test.ts (133 passed).
  • New pins:
    • packages/spec/src/ui/page-slot-pair.test.ts: the refusal, the controls, the stack envelope, the D3 entry, and stored-row replay.
    • The checkPageSlotPair rows in object-refinement-check-exports.test.ts: 7 fixtures plus legs 2 to 4.
    • packages/platform-objects/src/pages/sys-user-details-tab.test.ts.

Ablation

The fix was committed first (e92b99226e). Then I used node scripts/ablation-replace.mjs in wrap mode:

  • It replaced the anchor .superRefine(checkPageSlotPair)); with ); in page.zod.ts. Anchor count went 1 to 0, and the blob went dc9d9046ae35 to 2a161352abd6. On disk during the mutation, the attachment line count was 0.
  • The pins needed no build: they import ./page.zod relatively, so vitest reads src.
  • Predicted direction: red. Observed: red, page-slot-pair.test.ts plus object-refinement-check-exports.test.ts, 14 failed and 206 passed. That includes every pair refusal, the stack envelope, the stored-row refusal, and leg 2 (count plus bijection) and leg 3 (attachment).
  • Restored with git checkout HEAD: blob == HEAD dc9d9046ae35, and git diff HEAD is empty. I checked both again independently.

Gates

The 93 commands from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (the dispatch-time 92, plus check:generated, added by this diff) were run at the final head d66e53bdf5, and all 93 exit 0. dispatch-gates --ran with a per-command exit code reports: 93 derived, 93 run, 0 NOT-MEASURED.

Three of them exited 3 (PREREQUISITE NOT MET) on the first pass at e92b99226e, because the workspace was not built: check:dual-build-cjs-loads, check:lean-entry-closure and check:i18n. On the final pass they measured, reading dist/ trees that the spec repo suite and the closure builds produced in this worktree. Some of those trees predate the merge of main. check:i18n reports all 9 packages' bundles in sync.

pnpm --filter @objectstack/spec check:generated at d66e53bdf5: all 15 generated artifacts are up to date.

Acceptance notes (not filed)

  • packages/spec/src/system/i18n-resolver.test.ts keeps two walker fixtures that author details beside tabs (the slots roots case and the contractPage translation case). They feed walkAddressedPageComponents / translatePage, which never parse. A walker must tolerate a stored row that already carries the pair, so I left them as they are.
  • objectui's page mirror (packages/types/src/zod/layout.zod.ts) re-attaches checkPageSourceCompleteness and checkPageRequiresKind onto its .shape-derived schema. Until objectui re-attaches checkPageSlotPair after its next spec bump, its own authoring validator accepts the pair. The spec doors refuse it. Carrier: the objectui spec-bump PR.
  • The renderer's tabs-wins branch stays as it is. With the pair refused at every spec door, it is simply never reached by valid metadata.

Deviations from the claimed file surface

These are additions within the claim's intent, each needed by a file the claim names:

  • packages/spec/api-surface/ui.json and packages/spec/export-origins/ui.json: generated, owed by the new export. The export itself is required by leg 2 of object-refinement-check-exports.test.ts, which holds PageSchema's custom-check count equal to its exports.
  • packages/spec/src/ui/object-refinement-check-exports.test.ts: the existing pin for page checks.
  • packages/spec/src/ui/page-slot-pair.test.ts: this change's own tests.

Generated by Claude Code

…s and slots.tabs

The tabs slot replaces the whole tab strip the Details tab lives in, so a
details slot beside it never rendered. checkPageSlotPair refuses the pair at
slots.details, naming both slots and the fix: the record:details component
as a child of a tabs item. sys_user_detail moves its record:details, with
its sections and hideFields unchanged, in as the first tabs item.

Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <[email protected]>
…ment) and pin it

The D3 semantic entry page-slots-details-beside-tabs-refused and its step-18
rationale fragment, registry.ts regenerated; the refusal and its controls
pinned in page-slot-pair.test.ts; sys_user_detail's first-tab details body
pinned in platform-objects; the changeset.

Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <[email protected]>
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 10 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (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.

17 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 3e72f9391b44721c3c3ba12bc3beef5c34d98cca.

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

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 38 pages)
  • 7 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 — 139 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 3e72f9391b44721c3c3ba12bc3beef5c34d98cca → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 3e72f9391b44721c3c3ba12bc3beef5c34d98cca

⚠️ 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 3e72f9391b44721c3c3ba12bc3beef5c34d98cca → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…lotPair export

The diff exports checkPageSlotPair from the spec ui entry, a public-surface
widening beside the accept-set narrowing; widen-plus-narrow is spelled
yes (narrowing). Levels unchanged (spec minor, platform-objects patch).

Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <[email protected]>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

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

Inputs read: card #22568 (body; comments 6092519848 triage direction (b), 6092571560 claim, 6093455671 dev report, 6093484675 seat answer amending Clause-② to yes (narrowing), 6093559392 patch-round report), PR #22600 (body, 10-file list, the net diff against main from the branch point 99801d831f to the head, comment 6093440578 docs-drift advisory), the head's 35 check-runs (read at 2026-10-10T05:29Z), and for the readings the diff names: the repository at the head sha, objectui at d758f2f (the commit the dev cites), hotcrm at f0afcbda07 (a read-only shallow clone), objectui#12030 comment 6093491679, and the precedent commit 72af58c621. Nothing was built, run or re-run locally; the one script run was record-recognisers.mjs --template for this record's shape.

Check-runs on the head: 35, all completed: 32 success, 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke — path-filtered or opt-in), 0 failure, 0 in progress. The seven required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard) are each success; so are Check Changeset (the no-major guard, its level axis and the ADR-0087 registration gate) and Spec property liveness.

① Derived judgments

Each accept-set or public-surface change the diff makes, judged against triage direction (b) and the sources.

  1. The refusal, checkPageSlotPair — right. page.zod.ts gains an exported object-level check attached to PageSchema by identifier as the fourth .superRefine, beside checkPageSourceCompleteness, checkPageRequiresKind and checkPagePrintComposition. It fires when slots.details and slots.tabs are both not undefined, adds one custom issue at ['slots', 'details'], and the message names both slots, why the pair cannot render, the record:details component, where it goes (tabs items, the first item's children), the delete of slots.details, and the alternative (delete slots.tabs). It carries no tracker number. That is the triage's sentence (refuse the pair, name it, name the fix) and it is pinned in page-slot-pair.test.ts by code, path and the message's subjects.
  2. Refused whatever the page's kind — right. Direction (b) states no kind condition, and a page that is not slotted reads no slot at all (the slot map's own doc: "Only honored when kind === 'slotted'"), so the pair is never a coherent declaration. Pinned on the kind-less row, where the parse sees the applied full default.
  3. An empty details: [] beside tabs refused — right. The synthesizer's presence test is slots.details !== undefined (objectui buildDefaultPageSchema.ts about :1072 to :1075), so an empty array beside tabs is dropped exactly like a full one. Pinned.
  4. A page authoring only details, only tabs, or neither — unchanged, right. The check returns before addIssue when either key is undefined; the describe is byte-identical; the controls are pinned in both test files, and the exports-parity pin's "accepts its clean fixtures — the accept set did not move" leg covers the sibling checks. No alias spelling reaches the check: the slot map is a strictObject whose aliases (fields, body, related, relatedLists) refuse the near-miss key with the canonical name first.
  5. Public surface: one new export on @objectstack/spec/ui — right, and owed. checkPageSlotPair (function) is the one new line in api-surface/ui.json and export-origins/ui.json, both generated (check:api-surface sits in the green TypeScript Type Check job). The export is mandated by object-refinement-check-exports.test.ts leg 2 (PageSchema's custom-check count equals its exports, with a bijection over the union matrix) and leg 3 (attached by identifier, one declaration, one attachment); leg 4 holds barrel identity on @objectstack/spec/ui. The 7 fixtures added to that matrix exercise this check alone (record pages with no print, source or requires).
  6. The slot TSDoc states the precedence — right. details is the body of the Details tab, that tab lives in the synthesized page:tabs strip, tabs replaces the strip, so tabs wins; the .describe('Slot override map for slotted pages') is unchanged, so no reference page or JSON schema moves.
  7. The doors — right, by reading. Parse and definePage (page.zod.ts definePage is PageSchema.parse); defineStack (stack.zod.ts:403 pages: z.array(PageSchema), the STACK_SCHEMA_INVALID / 422 envelope pinned at pages.1.slots.details); os validate (packages/cli/src/commands/validate.ts:349 ObjectStackDefinitionSchema.safeParse over the lowered stack — the same pages array; the dev ran that schema call on the built dist and not the binary, which parses nothing else); the page save door (metadata-protocol/src/protocol.ts resolveOverlaySchema returns getMetadataTypeSchema('page'), which is PageSchema at spec/src/kernel/metadata-type-schemas.ts:113 — the full schema with its refinements, not a .shape copy; the one .shape read in that file, withDeclaredPageTypeDefault at :1703, fills the type default and validates nothing); read diagnostics (metadata-diagnostics.ts:60, the same registry). The save door and diagnostics were measured by a temporary test the dev removed, so no committed pin holds them (flag F4).
  8. The producer move — right. sys-user.page.ts no longer authors details; its record:details is the first tabs item (Details, file-text, a four-locale label), and the hideFields list (nine entries, ban_reason and ban_expires included) and the two sections (Identity, Audit) are the same lines moved, indentation aside. sys-user-details-tab.test.ts pins the parse, the absent details slot, the first item's only child with those exact hideFields and sections, and that there is one record:details on the page. The rewritten strategy docblock is accurate: the page authors alerts, highlights, tabs and discussion, and the ten tab items it lists are the ten the page carries.
  9. H3, the render — confirmed by reading objectui d758f2f. With tabs authored the synthesizer pushes the authored strip verbatim (:1072 to :1073); with details alone it splices the authored node, verbatim, as tabItems[0].children (:1091 to :1093). page:tabs renders every item's children through renderChildren(item.children) (components/src/renderers/layout/containers.tsx:986), so a record:details authored as a tab item's child and the synthesized Details tab body reach the renderer by the same path. record:details takes its object and record from useRecordContext() (plugin-detail/src/renderers/record-details.tsx:251, :266, :953 to :954), not from a prop, and reads schema.hideFields (:490) and schema.sections (:769, :827). The synthesized node is componentNode('record:details', { sections, hideFields }), which is { type, properties } (:369 to :375, :832 to :840) — the same shape as the authored node, so the properties carrier is identical. One difference, outside the card: the synthesized Details item carries value: 'details' (:865) while the authored item carries no value, so its URL-addressable value is derived as tab-0 (containers.tsx:849 to :851) — flag F12. Not driven in a browser (flag F10).
  10. H1, reach — the evidence is real. In this repository at the head, the files under examples/, packages/, content/ and skills/ whose text carries slots, details: and tabs: are the two new tests, page.zod.ts, i18n-resolver.test.ts (one fixture authors the pair — flag F9), conversions/registry.ts (no single slot map carries both; its details: and tabs: fixtures sit in different entries), spec/CHANGELOG.md, and content/docs/protocol/objectui/layout-dsl.mdx, whose details: is object-master-detail-form's prop, not a page slot. The only examples/** page with a slots map is app-showcase/src/ui/pages/project-detail.page.ts, tabs only. hotcrm at f0afcbda07 (its main head at read time): one slotted page, src/sales/pages/account_detail.page.ts, authoring header and discussion; every other *.page.ts is kind: 'full'; the one file carrying both words is a translation bundle. objectui at d758f2f: the files carrying both keys are the i18n locale bundles and the synthesizer's own test. Reach outside this repository is zero; the stop condition did not trigger.
  11. ADR-0087 — right. The D3 entry page-slots-details-beside-tabs-refused lives in entries/semantic/18.page-slots-details-beside-tabs-refused.ts; the registry copy is byte-identical and sits in id order (after page-requires-non-compiled-kind-refused, before the permission-allow-restore-purge-removed family), in the generated region (check:migration-registry in the green job). surface carries no backticks and no pipes. The remedy text is right on both arms: the record:details (with its sections and hideFields unchanged) as the children of a tabs item and no details slot, or delete slots.tabs to keep the synthesized strip — both are what the renderer does. No conversionIds is right: which item carries the body, and under which label, is authoring intent no conversion can invent, and a stored row must keep rendering as it did (the reason states that disposition: replayed unchanged, read diagnostics name the pair, the next save is refused — pinned by the stored-row replay test). acceptanceCriteria is checkable. The rationale fragment carries order: 92, unique at the head (flag F11 for main). The changeset carries exactly one disposition marker, registered page-slots-details-beside-tabs-refused, whose id resolves and is new in the diff (Check Changeset: success). Since 5b12503c31 (spec(changes): generate the per-major spec-changes section and the upgrade guide at publish; the pull request generates both in memory and renders the diff (#22449 B′, condition 1) #22533 B′) spec-changes.json and the upgrade guide are generated at publish, so no regeneration is owed (check:spec-changes and check:upgrade-guide are in the green job).
  12. Root superRefine over a nested refinement — sound. The house pattern is three sibling checks attached by identifier at the page root, pinned by legs 1 to 4 and re-attached by objectui's .shape-derived mirror; a refinement nested on the slots object would be invisible to that pin and to that mirror. The dropped-refinement ledger (packages/spec/dropped-refinements.baseline.json) records ui/Page sites "" (the root), interfaceConfig.filterBy.element and slots.header.options[0].in.type, and manifest.pages.element on the four installed-package schemas — so a root check adds no site, while a slots-level refinement would add slots on ui/Page and manifest.pages.element.slots four times over: hand-kept baseline edits and published JSON-schema churn for the same refusal.
  13. Nothing outside the direction moved — right. No objectui file, no packages/lint, no metadata-protocol source, no governed surface (Governed Surface Queue Guard: success); registry.ts is the only shared hot file, and its edits are the entry and the fragment.

② Semver level

  • The changeset .changeset/22568-page-slots-details-beside-tabs-refused.md grades @objectstack/spec minor and @objectstack/platform-objects patch, carries Clause-②: yes (narrowing), the BREAKING banner, a FROM → TO table, the one-line fix and the ADR-0087 registered marker; the PR body's line 2 reads the same Clause-② line.
  • yes (narrowing) is the right declaration. The diff narrows the accept set and widens the published surface by one export; clause2-line.mjs spells widen-plus-narrow as yes (narrowing); the sibling export in 72af58c621 declared the same. platform-objects patch is right: the page's authored body is unchanged in content and the package's public surface does not move.
  • The level axis is satisfied: yes requires at least one moved package at minor or above, and @objectstack/spec is minor (Check Changeset: success, which runs check-changeset-no-major.mjs with its level axis). In pre mode the no-major guard stands aside (its RC exemption), so minor and major are both admitted for a breaking change; the written convention in that script's header and in pr-automation.yml's "WHICH LEVEL" prose is still "during the launch window breaking ships as minor, breaking-ness is carried by the banner and the ADR-0087 disposition", with GA as the end condition. Every carrier is present.
  • Against this line's practice, minor under-grades. main is in Changesets pre mode next since a87d8be299 (chore(release): enter Changesets pre mode (next) with one major marker, so v18 opens at 18.0.0-next.0 #22084), with the opening major marker 22080-v18-line-opens.md. The spec accept-set narrowings landed on the line since then are split: 22158-print-page-spec.md (the same-file sibling, checkPagePrintComposition), 22491-chart-list-view-binds-a-dataset.md and 22502-flow-binding-variable-dollar-name-refused.md (the two newest) grade @objectstack/spec major, each saying in its body that in pre mode the guard stands aside "so the level says what the change is"; 22149-define-seed-record-keys.md, 22343-retry-policy-try-catch-undeclared-keys-refused.md, 22477-flow-text-slot-dollar-root-refused.md and 22507-flow-options-toasts-translation.md grade minor "under the launch-window convention", the sentence this changeset uses. The precedent the seat cited, 72af58c621, predates pre mode and so speaks to the Clause-② line, not to the level on this line. No ruling I can read in the inputs or the repository mandates one grade in pre mode: the guard admits both, and the lockstep rulings (6049734955, 6056808625) govern the protocol major, not this.
  • Consequence of the grade: none on the version — the fixed group already carries pending major markers (22080, 22130, 22158, 22491, 22502, 22110), so the train is 18.0.0-next.N either way. What moves is where this entry renders in the compiled CHANGELOG at version time (Major Changes vs Minor Changes), which is the section an upgrading agent reads first. Judgment: minor is admissible under every written rule and gate, and it under-grades against the line's newest spec narrowings including the same-file sibling; the one-word change to major is admitted by the guard in pre mode. Escalated to the seat as flag F14; not a FAIL on this record.

③ Boundary flags

Each dev deviation and out-of-scope finding, answered or escalated.

  • F1 — four files beyond the claim's list (api-surface/ui.json, export-origins/ui.json, the exports-parity pin, page-slot-pair.test.ts): accepted by the seat in 6093484675; each is owed by the export, which legs 2 and 3 of the parity pin mandate. Answered.
  • F2 — gen:migration-registry run unlocked after a queue-timeout: a tsx concatenation, not a build or a test; its output is proved byte-exact by check:migration-registry in the green job. No effect on the diff. Answered.
  • F3 — os validate measured as the schema call on the built dist, the CLI binary not run: validate.ts:349 is the binary's one parse of pages, through the same ObjectStackDefinitionSchema; no CLI test was in the file surface. Answered.
  • F4 — the save door and read diagnostics measured by a temporary test, removed, not committed: the claim's file surface excluded metadata-protocol, so the dev could not land a pin there. By reading, the door cannot diverge from PageSchema without metadata-type-schemas.ts:113 changing. The sibling narrowing (72af58c621) carried a committed save-door pin inside its surface; this one has none. Escalated: accept on the registry wiring, or order a follow-up pin in metadata-protocol. Non-blocking.
  • F5 — the sys-user.page.ts strategy docblock rewritten: inside the claimed file and accurate against the page's own slots and items (judgment 8). Answered.
  • F6 — attribution: the five non-merge commits each end with Claude-Session plus Co-authored-by: Claude, no model identifier anywhere in the diff, the PR body or the changeset; the PR body ends with the session-URL footer. AGENTS.md's form, with the harness trailer as the exemption it names. Answered.
  • F7 — the patch round derived gates on the round's delta (20) and reported the whole-PR count (93) as not re-measured: both numbers are on the record, and CI ran the whole set on c963de955d and is green. Answered.
  • F8 — out of scope, objectui's page mirror: real. packages/types/src/zod/layout.zod.ts at d758f2f re-attaches checkPageSourceCompleteness and checkPageRequiresKind (:1342 to :1343, :1402) and not this check, so objectui's own authoring validator admits the pair until its spec bump. Relayed by the seat on objectui#12030 (6093491679, read); the carrier is that bump. Answered.
  • F9 — out of scope, the i18n-resolver.test.ts walker fixtures: the substance holds and the count is off by one. One fixture authors the pair, contractPage (:2515 to :2545); the "slots roots" docs at :2236 (header + details) and :2257 (highlights + tabs) each author one of the two. All three are any literals fed to walkAddressedPageComponents and translatePage and are never parsed, and a walker must tolerate a stored row carrying the pair. Acceptance notes is the right carrier. Answered, count corrected.
  • F10 — "measure first": the triage asked for the console reading of sys_user's page before the move; the dev took the reading from objectui's source (the unconditional tabs-wins branch at :1072 to :1075) on both sides and says so in the PR body, but did not list it as a deviation. The reading is sufficient for the contract — the branch carries no runtime condition, and the card itself framed the defect as a reading of both sources — but it is not the reading the triage named. Escalated: accept the source reading, or order a dogfood pass on the landed console. Non-blocking.
  • F11 — main has moved since the dev's merge: 7 commits past the branch point, none merged into the head. 3e72f9391b (fix(spec/automation)!: refuse a $-named outputVariable, and a $-named errorVariable other than $error, at authoring #22569) added the STEP18_RATIONALE fragment flow-binding-variable-dollar-name-refused at order: 92, the same order this PR's fragment uses. The hunks are disjoint (main's at :5648, this PR's at :6191; the semantic entries likewise), GitHub reports mergeable: true, mergeable_state: clean at 2026-10-10T05:29Z, and the rationale-merge test sorts fragments by id with order as render order only — duplicate orders already stand on main (56, 60, 62, 66, 67, 74, 77, 85 to 90), refused by nothing. The other post-base edits to the hot files (ba956070b1 on the registry at :15341 onward, dab6bf4f68 on page.zod.ts at :194 and :909) are disjoint from this PR's hunks. Not a conflict; recorded so the lander knows the two fragments tie on order. Non-blocking.
  • F12 — the moved Details tab item carries no value: its URL-addressable value is derived as tab-0 where the synthesized Details tab is details (judgment 9). The body renders the same; the address differs. Outside the card (which asked for sections and hideFields unchanged). For the seat, if ?tab=details addressability on the platform page matters. Non-blocking.
  • F13 — the "platform convention" the triage cites, sys_approval_request_detail with record:details as its first tab item, is in PR feat(plugin-approvals): serve the approval request page through the manifest pages, and declare the thread reply as approval_comment (#22473) #22567 and not on main at this head's base; on main the moved sys_user_detail is the first page to carry it. Observation only.
  • F14 — the semver level: see ②. minor is admissible; it under-grades against the line's newest spec narrowings and the same-file sibling; the fix is one word the guard admits in pre mode. Escalated to the seat. Non-blocking on this record.

Implemented-by: claude/issue-22568-details-tabs-slot-pair
Reviewed-by: session_01VZqqwTj2wsihZEbfT6yyYN

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 05:31
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 10, 2026 05:31
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit e223152 Oct 10, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22568-details-tabs-slot-pair branch October 10, 2026 05:59
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/l tests tooling

Projects

None yet

2 participants