Skip to content

fix(auth): sign-up carries the invitation redirect into the verification mail (objectui#10893) - #10904

Merged
hotlong merged 3 commits into
mainfrom
claude/issue-10893-signup-carries-invite-callback
Sep 28, 2026
Merged

hotlong merged 3 commits into
mainfrom
claude/issue-10893-signup-carries-invite-callback

Conversation

@hotlong

@hotlong hotlong commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Fixes #10893
Part of objectstack-ai/cloud#2440
Clause-②: no

An invitee without an account who follows an invitation link, registers and clicks the verification mail now lands back on the invitation (/_console/accept-invitation/ID), not on / and the "Create workspace" picker.

Mechanism, measured

better-auth builds the verification link server-side from the /sign-up/email body's callbackURL, defaulting it to /. createAuthClient().signUp never sent one. I measured the server half in-process: betterAuth with the memory adapter, requireEmailVerification, sendOnSignUp and autoSignInAfterVerification, then a real sign-up and a GET of the mailed link. I ran it against both server versions in play: 1.7.2 (cloud's objectstack pin bdea10a1) and 1.7.3 (objectstack main). Both gave identical results:

callbackURL in the sign-up body sign-up callbackURL in the mailed link GET of the link
absent (today) 200 / 302 to /, session cookie set
/_console/accept-invitation/inv_1 200 /_console/accept-invitation/inv_1 302 to /_console/accept-invitation/inv_1, session cookie set
./accept-invitation/inv_1 403 INVALID_CALLBACK_URL no mail n/a
/accept-invitation/inv_1 200 /accept-invitation/inv_1 302 to /accept-invitation/inv_1: the origin root, outside /_console

The third row shaped the fix. In the shipped embeddable console build, withConsoleBase(route) returns the document-relative ./ROUTE: only a browser resolves it, against the base-href element, and the server never sees that element. Forwarding it would have made every invited registration fail, which is worse than the bug. The /sign-up/email response has no redirect/url field in either version, so sending callbackURL has no client-side navigation effect.

What changed

@object-ui/auth (additive, minor):

  • SignUpData.callbackURL is forwarded verbatim to /sign-up/email. When it is absent the key stays off the wire, so the server default is untouched.
  • useAuth().signUp(name, email, password, callbackURL?) takes it as an optional 4th argument (mirrors sendVerificationEmail(email, callbackURL?)).
  • RegisterForm gains verificationCallbackURL, forwarded to signUp. Its doc and SignUpData.callbackURL's doc name the only shapes the server accepts.

@object-ui/console (patch):

  • RegisterPage passes a safe ?redirect= (the same isSafeRedirect it already applies) as verificationCallbackURL. It passes nothing otherwise.
  • New withConsoleBaseRootRelative(route) in utils/consoleBase. It resolves withConsoleBase's answer the way location.assign would and keeps the path, query and hash, so every mount gives a root-relative url. It always equals where a full-page navigation to the same route lands.
  • Bounded in-place fix, same defect class: the verify-email prompt's Resend sent the bare router path (redirect || '/'). Per the fourth row above, that lands at the origin root, outside the mount. It now sends the same value as sign-up, or nothing when there is no safe redirect. All four in-place conditions hold. (1) It is the same class: the verification link misses the console-mounted target. (2) The fix is mechanical, using the same helper. (3) No open PR touches the file (checked across the 4 open PRs; the release PR's file list is capped at 100 entries). (4) It falls under the same console vitest gate family.

⚠️ File surface beyond the claim. The claim listed packages/auth/src/**, plus RegisterForm.tsx "if it owns the redirect". It does not own it: ?redirect= is owned one level up, by apps/console/src/pages/auth/RegisterPage.tsx, which passes it to RegisterForm like loginUrl. A packages/auth-only change could not fix the bug, because no caller would pass the value. Reading ?redirect= inside the library would be an implicit contract: nothing in packages/auth produces or reads that parameter today. The console files touched: RegisterPage.tsx, VerifyEmailPromptPage.tsx, utils/consoleBase.ts, plus tests.

Tests (all at 3fd6dcd7e)

  • Must-have pin (createAuthClient.test.ts): signUp sends the verification callbackURL in the /sign-up/email body, and its negative, "without a callbackURL leaves the key off the wire".
  • registerVerificationCallback-10893.test.tsx: the real RegisterForm, real AuthProvider and real createAuthClient, asserted on the request body.
  • apps/console/.../verificationCallback-10893.test.tsx: RegisterPage and VerifyEmailPromptPage on a real AuthProvider and client, asserted on the wire. It covers the three shipped mounts (standalone /, embedded ./ plus base href /_console/, pinned /_console/), no redirect, and an off-site //evil.example/x redirect (not forwarded).
  • consoleBase.test.ts: withConsoleBaseRootRelative across the three mounts, query and hash, and /_studio passthrough. It also pins the hazard: embedded withConsoleBase answers ./accept-invitation/inv_1.

Ablations. Each ran through ablation-replace.mjs (anchor hit 1 to 0, blob moved, restored blob == HEAD, git diff HEAD empty):

  1. The spread was dropped from createAuthClient.signUp. 5 red: the must-have pin, the form-to-wire pin, and the three console RegisterPage mount cases. The no-callback negatives stayed green.
  2. withConsoleBaseRootRelative was swapped for withConsoleBase in RegisterPage. 1 red: the embedded case received ./accept-invitation/inv_1. The pinned and standalone cases stayed green, as expected.
  3. The pre-fix Resend (redirect || '/') was restored. 2 red: embedded received /accept-invitation/inv_1, and no-redirect received / where the key-absent pin expects no key (that second one is byte-level, behaviour-equivalent).
  4. Reverse type check: the prop was renamed to verificationCallbackUrlTypo in RegisterPage, and console tsc went red with TS2322 against RegisterFormProps. So the console type-check reads the rebuilt @object-ui/auth d.ts. For the README snippet, the same typo turned check:doc-snippets red, so the new block is judged.

Gates (exit codes)

The @object-ui/auth build 0, type-check 0 (its tsconfig.test.json lists both new or edited test files), and test 0 (27 files, 277 tests). @object-ui/console type-check 0 (after turbo build --filter='@object-ui/console^...', 34/34) and test 0 (126 files, 1419 tests). Then check-changeset-presence 0, changeset:check 0, check:changeset-claims 0 (report-only; it names two pending changesets that mention packages/auth/src/types.ts / README.md, and both are still true), check:pending-changeset-literals 0, check:new-line-citations 0 (0 new), check:test-path-roots 0, check:readme-exports 0, check:doc-snippets 0 (679/679 judged), check:doc-fences 0, check-vi-mock-specifiers / -inherit / -override-shape 0, check-control-bytes 0, check-shell-escape-residue 0, and check-governed-queue-guard --test NOT GOVERNED. Lint: eslint --no-inline-config on the 12 changed source files gave 0 errors and 27 warnings, identical to the same 10 pre-existing files at base (27), with 0 in the new files. That is a narrowed run, not the full pnpm lint. eslint.config.js enables no type-aware linting (no projectService/parserOptions), so this diff cannot move an untouched file's verdict. The full pnpm lint is left to CI.

The verify lock ran in declared UNLOCKED mode: the host is macOS with no usable flock, so nothing was serialized.

Acceptance notes

  • End to end NOT MEASURED. I did not run a live invite → register → verify → click stack, local or cloud. The server half is the in-process probe above, and the client half is the wire tests. Cloud consumes objectui through .objectui-sha (efead6c6, far behind main), so this reaches cloud only with a later objectui pin bump.
  • After verification the invitee lands on /_console/accept-invitation/ID. With autoSignInAfterVerification the probe shows the session cookie is set on that 302. Without it, AcceptInvitationPage already bounces to /login?redirect=… and back. I have not verified cloud's setting.
  • Nice-to-have, not implemented (not in packages/auth):
    • The "you were invited" copy on login/register would live in apps/console/src/pages/auth/LoginPage.tsx / RegisterPage.tsx. The organization name cannot be read before sign-in: better-auth's /organization/get-invitation answers UNAUTHORIZED without a session, and for a non-recipient YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION. So "invited to X" would need a server-side public read or the name carried in the link. Generic copy keyed on a /accept-invitation/ redirect would be console-only.
    • The pending-invitation hint on the workspace screen would live in packages/app-shell/src/console/organizations/OrganizationsPage.tsx. useAuth().listUserInvitations() already exists, and nothing in app-shell or console reads it today.
  • Sibling, not fixed: if a deployment sets emailVerification.sendOnSignIn, a sign-in by an unverified user mails a link built from the sign-in body's callbackURL, which LoginForm never sends. That is / again. Fixing it is not a one-liner, because on /sign-in/email a callbackURL also makes the response carry redirect: true + url, so it is a different design question. Whether cloud sets sendOnSignIn is not measured.
  • packages/app-shell's DefaultRegisterPage has no ?redirect= handling at all. It has no consumer in this repo; the console routes /register to its own RegisterPage.
  • PM hypothesis note: the dispatch's comparison point in LoginForm (the base + '/home' callbackURL) is the SSO sign-in, not resend. It derives the base from location.pathname. The resend path is VerifyEmailPromptPage (fixed above).

Dispatched from PM session 786273a0-0246-4bb2-b026-bb37ba5d7295 (epic objectstack-ai/cloud#2440). Related: objectstack-ai/cloud#2429 (the other half of the invite chain), objectstack-ai/objectstack#20374 (the plain-text & in the verification mail).


Generated by Claude Code

hotlong and others added 3 commits September 28, 2026 13:50
…h (objectui#10893)

`createAuthClient.signUp` called `/sign-up/email` without a `callbackURL`,
so better-auth mailed `verify-email?...&callbackURL=/` and an invitee who
registered from an invitation link verified onto the workspace picker.

- `SignUpData.callbackURL` is forwarded verbatim; absent, the key stays
  off the wire and the server default applies.
- `useAuth().signUp` takes an optional 4th `callbackURL` argument.
- `RegisterForm` gains `verificationCallbackURL`, forwarded to `signUp`.

Co-Authored-By: Claude <noreply@anthropic.com>
…the mount (objectui#10893)

RegisterPage now hands `?redirect=` to RegisterForm as the verification
callback, and the verify-email prompt's Resend sends the same value.
Both go through a new `withConsoleBaseRootRelative`: the server never
sees `<base href>`, and better-auth refuses a document-relative `./...`
callbackURL (what `withConsoleBase` returns in the embedded build) with
403 INVALID_CALLBACK_URL, failing the whole sign-up. Resend used to send
the bare router path, which the server redirected to at the origin root,
outside `/_console`.

Co-authored-by: Claude <noreply@anthropic.com>
…(objectui#10893)

Co-authored-by: Claude <noreply@anthropic.com>
@hotlong hotlong self-assigned this Sep 28, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation apps tests package: auth labels Sep 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 2 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/approver-identities-read-positions-5424.md

  • names packages/auth/src/types.ts → packages/auth/src/types.ts — edited by this change

    The retired spelling is not kept as a fallback — pairing the two is what ADR-0090 D3 forbids, and packages/auth/src/types.ts says so on the declaration.

.changeset/tenant-header-edge-contract-5279.md

  • names packages/auth/README.md → packages/auth/README.md — edited by this change

    packages/auth/README.md gains "The X-Tenant-ID edge contract": what the header means (a routing hint carrying the better-auth activeOrganizationId — not an identity claim, not an authorization input, not what scopes rows), who stamps it and under exactly which condition, who reads it, and what a reader may and may not assume. The framework half is stated as a negative with its pin — resolveAuthzContext takes tenantId from the API-key principal or session.activeOrganizationId and from no header — alongside plugin-sharing's record that trusting x-tenant-id as identity was a vulnerability. The configuration half is quoted from the contract this package can actually resolve, TenantRoutingConfigSchema in @objectstack/spec/cloud, where X-Tenant-ID is the default of a configurable tenantHeaderName and header ranks second of six identification sources behind subdomain.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 9f0c84a44 (merge-base with origin/main): 13 file(s) changed outside .changeset/, read against 1667 pending declaration(s) that publish a body (2261 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3095.8 KB 3104.5 KB
Main entry chunk (gzip) 148.6 KB 350 KB
Entry file index-CH1pz_Th.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.58KB 6.17KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.70KB 2.23KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 557.59KB 133.60KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 226.44KB 63.00KB
fields (index.js) 261.01KB 66.28KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.96KB 14.83KB
plugin-charts (index.js) 83.99KB 22.86KB
plugin-chatbot (index.js) 197.67KB 46.90KB
plugin-dashboard (index.js) 136.93KB 36.48KB
plugin-designer (index.js) 215.03KB 44.21KB
plugin-detail (index.js) 233.48KB 61.79KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 161.21KB 41.41KB
plugin-gantt (index.js) 170.35KB 42.19KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.00KB 9.09KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 88.55KB 22.21KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.16KB 39.05KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.22KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.27KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@hotlong
hotlong marked this pull request as ready for review September 28, 2026 06:10
@hotlong
hotlong enabled auto-merge September 28, 2026 06:11
@hotlong
hotlong added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 7ea8118 Sep 28, 2026
45 checks passed
@hotlong
hotlong deleted the claude/issue-10893-signup-carries-invite-callback branch September 28, 2026 06:32
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…ts name 23 objectui issues that answer 404, and re-qualify 19 bare objectstack numbers (objectui#10803, batch 6) (objectstack-ai#10914)

Part of objectstack-ai#10803
Clause-②: no

Dispatched implementation of the `domain:ui` seat objectstack-ai#1 claim (comment
`5864334310`) on objectui#10803, batch 6, session
`https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk`. Citations
only: no sentence's claim moves, and every edited pending changeset's
frontmatter is byte-identical. The one runtime string that moves is
`InputSchema.wrapperClass`'s zod `.describe()` text in
`@object-ui/types`, which loses its dead pointer and nothing else
(amendment `5860244997`, Q1 = A; `patch` changeset). No test pins any
changed text: the literal-anchor sweep below finds no specific anchor,
so no test file is edited.

This batch carries the release note `5863776648`'s three lists:
- the last **23 family numbers**;
- the **18 bare objectstack numbers above 10900** from PR
objectui#10892's C0 census, plus **#13086**, a 404 the C0 instrument
cannot see because it shares a token with #13337 (**Premise**);
- the live-but-wrong bare `objectstack-ai#3391` at the two `rowCrudAffordances.ts`
sites beside `objectstack#3720`.

## Why `Part of`, not a closing line

The order says `Part of`. With this PR the family list (all 115 numbers
of amendment `5860244997` Q2) and the C0 above-10900 list both read 0.
Whether the card now closes, or carries the dead `objectstack#N` class
measured in **Acceptance notes** 1, is the seat's call.

## Premise, re-measured on `origin/main` `9f0c84a44` (the branch point)

- **The 23 family numbers.** REST `GET
/repos/objectstack-ai/objectui/issues/N`: 23 of 23 answer 404, and a
second read of each answers 404 again. `GET .../pulls/N` answers 404 for
all 23. Lit controls: objectui#10533 and objectui#7714 answer 200.
- **No new family member.** The distinct `objectui#N` citations in the
two in-scope classes at the branch point, less those at batch 1's head
`6c3ad7c80`, are 24 numbers. Each was read once: 24 of 24 answer 200. So
the family list is still batch 5's 23.
- **#14026 was a sister-repo card written as objectui's.**
objectui#14026 answers 404 as an issue and as a pull. objectstack#14026
answers 301 to objectui#10102, the card it was transferred to. That
card's measurement-round claim (branch
`claude/issue-14026-import-mapping-selector-probe`) is the session of
`ecf14190e`, the commit that added `14026-list-import-mappings-pin.md`,
and six sibling in-scope lines already write `objectstack#14026` for the
same card.
- **C0, the bare-number census** (the instrument of PRs objectui#10875
and objectstack-ai#10892, at the branch point):

```
git grep -hoP '(?:^|(?<=[^\w#&/.\-]))#\d+(?![0-9A-Za-z_])' 9f0c84a -- '.changeset/*.md' 'packages/*/src/**' ':!**/__tests__/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/test/**' ':!**/tests/**' | sort -u | wc -l
```

- 984 distinct at `9f0c84a44`, against 998 at batch 5's `b2683a2c0`. The
14 that batch 5 re-pointed are gone, and none is new.
- Above 10900 there are 23: the same 5 CSS colours and the same 18
numbers. Each of the 18 answers 404 as an objectui issue and as an
objectui pull on two reads.
- In objectstack, 16 of the 18 answer 200 with the sentence's own
subject (the **C0 mapping** below). #13033 and #13413 answer 404 there
too, as issues and as pulls. Their squash commits exist in objectstack's
history (a full, not shallow, clone): `c459da6bc` "narrow the per-option
`default` key out of the form-view options vocabulary … (#13033)" and
`89448a52b` "remove the inert AUTH_SSO_PROVIDER_SCHEMA export (#13413)".
Both are ancestors of objectstack `main` (`git merge-base
--is-ancestor`, exit 0).
- **The C0 instrument's blind spot.** Its lookbehind refuses a `#`
preceded by `/`, `-`, `.` or `&`. That form (`#A/#B`, `-#N`) carries 109
distinct numbers at the branch point. The 17 that no earlier batch read
were each read once. 16 answer 200. **#13086 answers 404**, as an issue
and as a pull, in objectui and in objectstack. It sits in
`6985-wizard-card-r-alignment.md` as "the #13337/#13086 fence", in the
same token as #13337, so it rides here. objectstack's
`check-yaml-examples.ts` header, at its landing `2ebfe7e9f` (PR
objectstack#13267, which answers 200), reads "Check YAML Examples
(anti-drift for the AUTHORING format, #13086)" and "Ruled 2026-08-29
(#13086)".
- **objectstack-ai#3391.** objectui#3391 answers 200 with an unrelated subject (a
record-header api action placeholder). objectstack#3391 answers 200 as
the apiMethods whitelist contract card ("跟踪:UI 操作按钮与 apiMethods
白名单一致性契约落地"), and objectstack#3720's own title calls itself "objectstack-ai#3391
遗漏的第四个面".
- Every edited changeset is pending: it is present in `.changeset/` on
`main`. The checkout is not shallow.

## Census (the enumeration pin for this batch)

The instrument PR objectui#10854 printed and PRs objectui#10869 / objectstack-ai#10875
/ objectstack-ai#10892 reused, with this batch's 23 numbers substituted (REF = a
commit or tree):

```
git grep -nE '(objectui#|#|issues/)(8072|8127|8137|8204|8229|8248|8307|8408|9231|9241|9244|9365|9373|9375|9542|9553|9585|10117|10119|10120|10129|10132|14026)([^0-9]|$)' REF -- '.changeset/*.md' 'packages/*/src/**' ':!**/__tests__/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/test/**' ':!**/tests/**' | grep -v 'objectstack#' | wc -l
```

- REF = `9f0c84a44` (branch point): **104** lines.
- One more line is hidden by the `grep -v 'objectstack#'` filter:
`9943-viewtype-totals-page-row-retired.md` names objectui#8127 beside an
`objectstack#` citation.
- So the true population is **105** lines: 28 changeset lines in 26
files, and 77 src lines in 42 files.
- Unfiltered, REF reads 111. The other 6 lines are live
`objectstack#14026` lines.
- REF = `2b3d2061a` (this head): **0**. Unfiltered it reads 7, all live:
the six `objectstack#14026` lines and the re-qualified one.
- This head merged with a fresh `main` (`8522396c0`, `git merge-tree
--write-tree`, clean, tree `f19d17884`): **0**. REF = `8522396c0` alone:
104.
- **C0**, the 19 numbers, bare and not `objectstack#`-qualified: 28
lines in 23 files at the branch point; 0 at this head. At this head C0
reads 966 distinct numbers, and above 10900 only the five CSS colours
are left.
- Lit control, the printed instrument over live objectui#7714 at this
head: 17 lines. Hex-colour false positives (a number followed by a hex
letter) at the branch point: 0.
- **Out of scope, as it stands** (the 23 numbers, filtered, whole tree
at this head):
  - 94 test lines in 36 files;
  - 5 `.github` lines in 1 file (`ci.yml`, objectui#9241);
  - 2 `apps/console` lines in 1 file (`FormPage.tsx`, objectui#8408);
  - 1 line of the root `vitest.config.mts`;
  - 0 scripts, 0 governed, 0 `CHANGELOG.md`.
- For the 19 C0 numbers: 13 test lines in 10 files, and 1 line of
`packages/plugin-tree/README.md`.

## Citation form

- **Landing shas.** The 9-character backticked sha of the commit on
`main` that landed the change the sentence rests on, as in the earlier
batches. All 15 distinct objectui shas below are ancestors of `main`:
`git merge-base --is-ancestor`, exit 0 each. Control legs in the same
checkout: the head of PR objectui#10902 (`496c63c06`) answers exit 1,
and the known ancestor `5f789538d` answers exit 0. `git rev-parse
--short=9` returns the same 9 characters for each.
- **Own-card pointers** in a changeset are dropped, not replaced. The
configured changelog generator (`@changesets/cli/changelog`, per
`.changeset/config.json`) prefixes each released entry with the hash of
the commit that added the file, which is that landing.
- **The claim lived only on the card.** Where the sentence rests on
something that lived only on the dead card, the sha only locates it:
"the card behind SHA".
- **Nothing answers.** The pointer is dropped and the sentence names the
card by role (objectstack-ai#9553 only).
- **Sister-repo numbers.**
- A bare sister-repo number becomes `objectstack#N` after reading it
there.
- "objectstack PR #N" becomes "PR objectstack#N", since the qualifier
now carries the repository. "framework #N", "upstream #N" and "spec #N"
keep their word, as batch 4 kept "framework PR objectstack#6942".
- Where the objectstack pull request itself answers 404, the sentence
cites its commit in that repository as objectstack `SHA`, the spelling
the tree already uses for an objectstack commit ("Measured on
objectstack `9bd4344e4`").
- **Runtime text** carries no sha: see **Special cases** 6.

## Mapping, the 23 family numbers

Lines / files are the branch-point census for that number. "Method" is
how the landing was found; for every source site, `git log -S
'objectui#N' -- FILE` names the commit that wrote the citation, and it
is the landing below unless the row says otherwise.

| dead number | resolution | method | lines / files | why the commit
carries it |
|:--|:--|:--|:--|:--|
| objectstack-ai#8072 | `c974edf14`; own-card pointer dropped; runtime pointer dropped
| changeset's adding commit, subject "(objectui#8072)" | 5 / 3 | it
mirrors `wrapperClass` on `InputSchema`, which is what both comments and
the `7722` changeset say happened |
| objectstack-ai#8127 | `ca3942729`; "the card behind `ca3942729`" twice (**Special
cases** 2) | `git log -S` names `ca3942729` for the first citation in
all 10 source files; `05a49f2ee` (objectui#9880) and `8a7e09fa1` (the
`9943` changeset's landing) wrote three later sentences that cite the
same fix | 17 / 11 | it derives `ViewType` from `@objectstack/spec`
instead of re-declaring it, and its message records the
`page`-degrades-like-a-typo measurement the sentences cite |
| objectstack-ai#8137 | own-card pointer dropped (landing `0fa7a9c83`) | changeset's
adding commit | 1 / 1 | the only site is its own changeset's pointer |
| objectstack-ai#8204 | own-card pointer dropped (landing `580b0fdf4`) | changeset's
adding commit | 1 / 1 | the pointer opened a paragraph as its own
sentence |
| objectstack-ai#8229 | "a separate finding" (changeset); "the finding `8b7ea3945`
reconciled" (`flex.tsx`) | `8b7ea3945`'s message: "Reconciles the third
face objectui#8229 found" | 2 / 2 | the finding lived on the dead card;
`8b7ea3945` is the commit that reconciled it, and it is the `7735`
changeset's own landing |
| objectstack-ai#8248 | own-card label dropped from the second list item (landing
`d02942b0e`) | changeset's adding commit; its message covers
objectui#8458 and objectui#8248 | 1 / 1 | the changeset lists the two
cards its one landing closed |
| objectstack-ai#8307 | `5591f03bd`; own-card pointer dropped | changeset's adding
commit | 11 / 4 | it makes a lane header over a windowed fetch say
`77+`, and it wrote every comment that cites the card |
| objectstack-ai#8408 | own-card pointer dropped; "seam 2 of the card behind
`8241a4400`" | changeset's adding commit; `8241a4400`'s message names no
seams | 2 / 2 | the seam list lived on the dead card |
| objectstack-ai#9231 | `383502b23`; own-card pointer dropped | changeset's adding
commit | 3 / 2 | it gives the create dialog's confirm control its own
accessible name, the rule both `i18n.ts` comments state |
| objectstack-ai#9241 | own-card pointer dropped (landing `250429c8f`) | changeset's
adding commit | 1 / 1 | the only site is its own changeset's heading |
| objectstack-ai#9244 | `bd0995738` | `git log --grep`: its message names the card; it
wrote all three comments | 3 / 2 | it emits one col-span class per
breakpoint tier, not one for the widest |
| objectstack-ai#9365 | own-card pointer dropped (landing `0970a0e00`) | changeset's
adding commit | 1 / 1 | the only site is its own changeset's pointer |
| objectstack-ai#9373 | `c1006ed8e`; own-card pointer dropped | changeset's adding
commit | 2 / 2 | it resolves the inline locale map before the
interpolation options, the `ListView` comment's subject |
| objectstack-ai#9375 | own-card pointer dropped (landing `e427e9c00`) | changeset's
adding commit | 1 / 1 | the only site is its own changeset's pointer |
| objectstack-ai#9542 | `43c0d1710`; own-card pointer dropped; "filed as a separate
card" (**Special cases** 3) | changeset's adding commit; `git log -S`
names it for the `action-button` / `action-icon` comments too | 9 / 6 |
it accepts and resolves an inline `I18nLabel` on `resultDialog` and
makes `ResultDialogSpec` derive its label members |
| objectstack-ai#9553 | nothing answers: "a separate card carried that census" | `git
log --grep` finds only `40f34b4ba`, which points at the card; no commit
lands it | 1 / 1 | see **Special cases** 4 |
| objectstack-ai#9585 | "`ee70287e4`'s pin measures it NOT GATED"; in its own
changeset, "a pin in this change measures it" | no commit names it;
`ee70287e4` adds the test "NOT GATED: the renderer paints the very node
the mirror refuses, without parsing it" | 2 / 2 | see **Special cases**
5 |
| objectstack-ai#10117 | `4c6f549ef`; own-card pointer dropped | changeset's adding
commit | 3 / 2 | it resolves a lookup title candidate in `page:header`
and adds the record-key safety net both comments describe |
| objectstack-ai#10119 | `73a3c89af`; own-card pointer dropped | changeset's adding
commit | 4 / 2 | it makes a nav ancestor's gates reach its subtree and
stops a group outliving its children |
| objectstack-ai#10120 | `80c54122e`; own-card pointer dropped | changeset's adding
commit; for each file `git log -S` names it, or a later commit that
cites its gate: `e0f820246` (objectui#10563) or `b809375ac`
(objectui#10163) | 16 / 13 | it makes a form neither submit nor offer a
field the caller may read but not edit, the FLS half every site names |
| objectstack-ai#10129 | `6cc910b6d`; own-card pointer dropped | changeset's adding
commit | 10 / 5 | it routes a field-backed action param to its record
picker and refuses an unreadable one |
| objectstack-ai#10132 | `061f5e829`; own-card pointer dropped | changeset's adding
commit | 8 / 4 | it makes the two declared-translatable dashboard
surfaces resolve, including the axis `title` forward |
| #14026 | re-qualified `objectstack#14026` | see **Premise** | 1 / 1 |
it is the card the pin's measurement answered |

## Mapping, the 19 bare sister-repo numbers

| number | resolution | what objectstack says | lines / files |
|:--|:--|:--|:--|
| objectstack-ai#11289 | `objectstack#11289` ("upstream") | "record:details sections
cannot survive an empty record: `hideEmpty` / `collapsible` /
`showBorder` are honoured by the renderer but undeclared" | 2 / 2 |
| objectstack-ai#11662 | PR `objectstack#11662` | "feat(spec): declare hideEmpty /
collapsible / showBorder on record:details sections" | 1 / 1 |
| #12616 | PR `objectstack#12616` | "feat(spec): declare record:details
section headerColor as a closed six-token enum" | 1 / 1 |
| #12718 | PR `objectstack#12718` | "feat(spec): retire preview mode —
the RuntimeMode 'preview' value and the whole PreviewModeConfig block" |
1 / 1 |
| #13033 | objectstack `c459da6bc` | the pull request answers 404; its
squash commit narrows the per-option `default` key out of the form-view
options vocabulary, the ruling `form-spec.ts` says was executed upstream
| 1 / 1 |
| #13337 | `objectstack#13337` | "docs(objectui): layout-dsl teaches
only shapes the live schemas accept" | 1 / 1 |
| #13086 | `objectstack#13267` | the card answers 404; PR
objectstack#13267 landed its ruled YAML-examples gate (see **Premise**)
| the same line |
| #13413 | objectstack `89448a52b` | the pull request answers 404; its
squash commit removes an inert schema export and leaves a note recording
the absence as a choice, the shape the `base.zod.ts` note cites as
precedent | 1 / 1 |
| #13632 | `objectstack#13632` | "[spec] `FieldSchema` accepts a
`lookup`/`master_detail` with no `reference` target …", the card
17.3.0's refinement answered | 3 / 3 |
| #13733 | PR `objectstack#13733` | "feat(spec): wizard view v1 —
declaration-and-refusal tightening of FormViewSchema type:'wizard' (Card
S)" | 1 / 1 |
| #13855 | `objectstack#13855` | the field-grouping decision card whose
option B is a section `group` reference, landed by `39404f3d9` (#13897)
"a layout section can reference a declared field group" | 1 / 1 |
| #13906 | `objectstack#13906` ("framework") | "two more
`computeExecCtx` seams read "failed" and "not wired" as one value …" | 6
/ 3 |
| #14274 | PR `objectstack#14274` | "fix(sdui-parser): refuse an
authored `type` attribute on the html tier …" | 1 / 1 |
| #14945 | `objectstack#14945` | "A flow cannot REFUSE with per-record
text …", honoured by `cca699149` "the flow `end` node honours `outcome:
'refused'`" | 1 / 1 |
| #15469 | `objectstack#15469` ("spec") | "`GanttConfigSchema` is
`strictObject(...).passthrough()` …", landed as `9c270bba0` "close the
gantt/tree config .passthrough() windows … (#15469)" | 2 / 2 |
| #15948 | `objectstack#15948` | "fix(plugin-auth)!: session payload
`positions[]` is the security axis, not the better-auth role scalar" | 1
/ 1 |
| #17493 | `objectstack#17493` | "three residues of #17322's node-door
refusal …", landed as `2c1011b01` "refuse a blank string in a flow
node's predicate slot" | 1 / 1 |
| #20051 | `objectstack#20051` | "judge a flattened view overlay's
top-level `options.KIND` …", whose door half is `6a4aec71d` | 1 / 1 |
| #20160 | PR `objectstack#20160` | "fix(spec): a joined report refuses
a top-level dataset / rows / columns / values, pointing each onto
blocks[]" | 2 / 2 |

The live-but-wrong number: "(`/me/permissions` `apiOperations`, objectstack-ai#3391)"
becomes "… `apiOperations`, objectstack#3391)" at the two
`rowCrudAffordances.ts` sites.

## Special cases (the judgement calls)

1. **#14026 is re-qualified, not dropped as an own-card pointer.** The
card behind `14026-list-import-mappings-pin.md` was never an objectui
card: it was objectstack#14026, now objectui#10102. Dropping the pointer
would lose which hypothesis the pin refuted. Re-qualifying it matches
the six sibling lines.
2. **objectstack-ai#8127, the card by role twice.**
- `CreateViewDialog.tsx`: "one of the sites the card records as
"drifted, …"" becomes "one of the sites the card behind `ca3942729`
records as …". That record lived on the card.
- `plugin-list` `ViewSwitcher.tsx`: "objectui#8127 was filed against the
two in `@object-ui/types`, and the maps below were described there as
total …" becomes "the card behind `ca3942729` was filed against …".
- `normalize-list-view.ts`: "The bug objectui#8127 records" becomes "The
bug `ca3942729` records". That commit's message carries the measurement:
`page` resolved "to exactly what it resolved a typo to".
3. **objectstack-ai#9542 in `8648-ui-action-four-undeclared-keys.md`.**
- The superseded paragraph keeps its pre-landing voice: "so it is filed
as a separate card and ⛔ not guessed at here".
- The superseding paragraph reads "`43c0d1710` landed the derivation".
- "(Noted here by the objectui#9542 seat, …)" becomes "(Noted here by
the seat that landed `43c0d1710`, …)".
4. **objectstack-ai#9553, nothing answers.** `base.zod.ts` said "No count of authored
`events` keys is stated here — objectui#9553 carries that census." No
commit landed that card. `40f34b4ba` only points at it, and the
`AGENTS.md` fix `7550728a6` names a different card. So the sentence now
reads "— a separate card carried that census". The census is not
restated, per AGENTS.md objectstack-ai#9.
5. **objectstack-ai#9585, the measurement's instrument.** The card measured the render
path NOT GATED, and no commit names it. But the commit that wrote both
citing sentences, `ee70287e4`, adds a pin that re-measures exactly that:
"NOT GATED: the renderer paints the very node the mirror refuses,
without parsing it".
- `disclosure.ts` now reads "(`ee70287e4`'s pin measures it NOT GATED)".
- Its own changeset, `8236-collapsible-open-intercept-retire.md`, whose
landing is `ee70287e4`, reads "(a pin in this change measures it NOT
GATED)".
6. **The runtime string.** Only the listed text moves.

| file | member | before | after |
|:--|:--|:--|:--|
| `types/src/zod/form.zod.ts` | `InputSchema.wrapperClass` `.describe()`
| "Classes on the wrapper div around the input and its label
(objectui#8072)" | "Classes on the wrapper div around the input and its
label" |

No test, doc or changeset quotes it: a whole-tree search for the old
string returns only the source line.
7. **objectstack-ai#8229 in `flex.tsx`.** "(objectui#8229, folded into objectui#7735's
ruling)" becomes "(the finding `8b7ea3945` reconciled, folded into
objectui#7735's ruling)". objectui#7735 answers 200 and stays.
8. **#13413's sentence.** "Precedent of the same shape:
objectstack#12009 / PR #13413." becomes "… objectstack#12009 /
objectstack `89448a52b`." objectstack#12009 answers 404 too, but it is a
qualified sister-repo number outside this batch's lists, so it is left
and listed (**Acceptance notes** 1).
9. **#20160 across a line break.** In
`10746-joined-report-clears-binding.md`, "(objectstack PR" ends one line
and "#20160: …" opens the next. They become "(PR" and
"objectstack#20160: …".

## The literal-anchor sweep (both test-pin classes, ruling `5861900779`)

- **Instrument.** Every string, template, numeric and regex literal in
all 4068 tracked test and script files (119016 distinct literals),
parsed with TypeScript.
- **Candidate filter.** A literal is a candidate only if it matches the
diff's removed lines with two lines of context, raw or
comment-flattened. That leaves 2839.
- **Test applied to each candidate.** Does its occurrence count DROP
between `9f0c84a44` and `2b3d2061a` in any of the 92 files this PR
changes, in raw text or in a comment-flattened form? 229 distinct
literals drop.
- **Every one is a generic token.** Digits and short numbers,
punctuation, single letters, `objectui#`, `objectui#1`, and whole-tree
scanners such as `/#\d+/` and `/objectui#\d+/`. The three `objectui#\d+`
matchers (`ActionRunner.disabledGate`, `registry-inputs-spec-parity`,
`catalog-gallery-render`) assert over their own test data and read no
changed file.
- **No specific anchor.** None is a batch number, a changed phrase, or
the changed `.describe()` string. So no test pins changed runtime text
(class one), and no source-reading test pins a changed comment or
docblock citation (class two).
- Test titles and comments that name these numbers are out of the card's
classes and stay (**Census**, out of scope).

## Held

**By the serial rule: nothing.** Re-mapped before the push, on 9 open
PRs: objectui#10910, objectstack-ai#10908, objectstack-ai#10907, objectstack-ai#10906, objectstack-ai#10901, objectstack-ai#10891, objectstack-ai#10777,
objectstack-ai#10278 and the release PR objectstack-ai#5400 (objectstack-ai#10902 and objectstack-ai#10904 had merged since the
claim).

Two of them share files with this PR. Their hunks were read against
their merge-bases; each file is identical at its merge-base and at
`9f0c84a44`, so the lines map directly.
- **objectui#10907, `types/src/zod/form.zod.ts`.** It inserts a refusal
constant before `InputSchema`, two tombstone members further down
`InputSchema`, and a comment in the `InputShorthandSchema` arm. This
PR's three edited lines (the `wrapperClass` comment and description, and
the arm's "shrank that row" comment) sit outside every one of its hunks
and their 3-line context.
- **objectui#10906, `metadata-admin/i18n.ts` and
`previews/ViewPreview.tsx`.** Its `i18n.ts` insertions are far from the
two `createDraft` comments, and its `ViewPreview.tsx` hunks are far from
the `options` fold docblock.

Trial merges with this head (`git merge-tree --write-tree`):
- clean for objectstack-ai#10910, objectstack-ai#10908, objectstack-ai#10907, objectstack-ai#10906, objectstack-ai#10901, objectstack-ai#10891 and objectstack-ai#10777;
- objectstack-ai#10278 (`eab4c8e52`) conflicts in `ObjectGrid.tsx`,
`plugin-grid/README.md` and `content/docs/plugins/plugin-grid.mdx`, and
conflicts identically against `9f0c84a44` alone;
- objectstack-ai#5400 (Version Packages) regenerates and is not a hold.

objectui#10891 shares no file with this PR.

## Changesets

- `.changeset/10803-dead-citation-sweep-sixth-batch.md`, EMPTY
frontmatter. It covers the comment-only edits in 15 released packages;
no published behaviour changes through them. It points at the second
file for `@object-ui/types`' runtime text.
- `.changeset/10803-sixth-batch-runtime-strings.md`,
`'@object-ui/types': patch`: the `wrapperClass` description loses its
pointer. No key, path, issue code, accept set, refusal or severity
moves.

## Proof of prose-only (C4), against `9f0c84a44`, on this head
`2b3d2061a`

- **Source.** Each of the 53 touched `.ts` / `.tsx` files was parsed at
`9f0c84a44` and at this head with TypeScript 6.0.3's `createSourceFile`,
and re-printed by `createPrinter({ removeComments: true })`.
  - 52 of 53 prints are identical.
- `form.zod.ts` is equal once the one listed substitution (**Special
cases** 6) is applied to the base print, matched once.
  - 0 parse diagnostics.
- Lit controls on the same instrument: editing a string literal moves
the print; re-spacing a comment does not.
- **Changesets.** The frontmatter block of every one of the 37 edited
changesets is byte-identical at `9f0c84a44` and this head (37 of 37, by
md5). The overwrite gate below agrees.
- **Scope of the diff:** 92 files, +175 / −140. That is 37 edited and 2
new changesets, and 53 non-test source files in 15 released packages. No
test file.

## Gates, on this head `2b3d2061a`

Each line is the gate's own verdict and exit code, captured by
redirect-then-`$?`.

- `node scripts/check-changeset-presence.mjs`, exit 0: "53 source
file(s) of 15 released package(s) changed, and this change declares 2
changeset(s): .changeset/10803-dead-citation-sweep-sixth-batch.md,
.changeset/10803-sixth-batch-runtime-strings.md."
- `pnpm changeset:check`, exit 0: "All workspace packages are in the
changeset fixed group." / "No changeset declares a `major` bump."
- `node scripts/check-changeset-overwrite.mjs` (report-only), exit 0: "2
changeset(s) added, 37 modified, 0 deleted". `declared at base` equals
`declares now` for 37 of 37.
- `pnpm check:changeset-claims` (report-only), exit 0:
- "Every one of those 1 address(es) either names the tree it was read
from, or points at a line this change does not move";
- "Every package declared across those 31 body(ies) is either not
negated …";
- the standing notice "75 pending changeset(s) describe a file this
change touches".
- Read against the diff: a pending changeset quoting a replaced pointer
would itself carry the dead number and sit in the census, which reads 0.
No pending changeset quotes the changed description.
- `pnpm check:control-bytes`, exit 0: "check-control-bytes: OK (scanned
9178 tracked text file(s); skipped 85 binary)."
- `pnpm check:new-line-citations`, exit 0: "VERDICT
new-cross-file-line-citations: 0 new citation(s), enforcement
report-only -> exit 0".
- `pnpm check:pending-changeset-literals`, exit 0: "No test source names
a pending changeset."
- Also run over the touched comments:
  - `pnpm check:installed-pin-claims`, exit 0 ("OK");
- `pnpm check:comment-mask-corpus`, exit 0 (1 disagreeing file, within
the ceiling objectui#7882 holds open);
- `pnpm check:handler-key-reads`, exit 0 ("every judged read is a
declared member of it").
- The governed-surface predicate over the 92 paths, exit 0: "NOT
GOVERNED — 92 path(s) checked against 5 governed surface(s); none
matched." Lit control `AGENTS.md`: exit 3.

**Tests**, through the shared verify lock, on `2b3d2061a`. Each is
`VERDICT command-exit 0`.
- `scripts/__tests__/`, the whole directory, whose whole-tree scanners
read the touched files: `Test Files 177 passed | 2 skipped (179)`,
`Tests 5322 passed | 2 skipped (5324)`. The two skipped files are the
network-escape fixtures that run only as a child.
- `packages/types/`, the whole package, whose runtime text moved: `Test
Files 264 passed (264)`, `Tests 5854 passed (5854)`. `pnpm --filter
@object-ui/types type-check`, exit 0.
- **The pins nearest the re-pointed text:**
- in `types`: `input-wrapper-class-mirrored-8072`, `zod-mirror-parity`,
`wrapper-class-declared-7722`, `zod-mirror-authors-no-defaults-7735`,
`collapsible-open-refusal-8236`, `tree-view-config-readers-8253` and
`layout-default-jsdoc-7361`;
- in `components`: `collapsible-open-intercept-8236`,
`registration-defaults-match-renderer-8229` and
`action-undeclared-keys-8648`;
- in `plugin-view`: `ViewSwitcher` and
`ViewSwitcher.viewTypeTotalsBothLegs-9943`;
  - in `plugin-kanban`: `laneCountHonesty-8307`;
  - in `plugin-grid`: `rowCrudAffordances` and `rowCrudEffectiveOps`.
  - Result: `Test Files  15 passed (15)`, `Tests  381 passed (381)`.
- No red leg: the sweep found no anchor to move, so there is no pin
whose old copy should fail.

**Declared narrowing.** NOT MEASURED locally:
- the full suites and type-check of the 14 other touched packages, and
eslint.
- Reason: the comment-stripped syntax tree of 52 of 53 touched source
files is identical to `main`, and the 53rd differs only by the listed
literal.
- CI runs the full farm.

## Acceptance notes

1. **A sister-repo class of the same defect: 30 `objectstack#N`
citations answer 404 in objectstack.**
- **Measurement.** Every distinct `objectstack#N` in the two in-scope
classes at the branch point is 359 numbers. Each was read once: 328
answer 200, 1 answers 301 (objectstack#14026 to objectui#10102), and 30
answer 404, each confirmed by a second read.
- **The 30:** objectstack-ai#5970 objectstack-ai#5976 objectstack-ai#6038 objectstack-ai#6124 objectstack-ai#6281 objectstack-ai#6331 objectstack-ai#6450 objectstack-ai#6483 objectstack-ai#6515
objectstack-ai#9933 objectstack-ai#9934 objectstack-ai#10354 objectstack-ai#10485 objectstack-ai#10695 objectstack-ai#11330 objectstack-ai#11507 objectstack-ai#11513 objectstack-ai#11658 objectstack-ai#11703
objectstack-ai#11753 objectstack-ai#11846 objectstack-ai#12009 #12868 #13117 #13670 #16126 #17147 #17762 #17987
#18012.
- **Sites at this head:** 82 lines in 63 files, 26 of them changeset
lines.
- **Three of them sit in sentences this PR edits, and are left as they
are:**
- objectstack#11846, "(objectstack#11846, landed as PR
objectstack#12718)" in `6748-preview-mode-provenance-ratchet.md`;
     - objectstack#12009, in `base.zod.ts`'s precedent line;
- objectstack#12868, in `form-spec.ts`'s "RULED 2026-08-28
(objectui#6263 / objectstack#12868, …)".
- **Why left.** They were not in this batch's lists, and the class is
the seat's to scope. objectstack's own card for dead tracker citations
in its tree is objectstack#19123, whose landing `66e266c93` counts
#12868 among the dead numbers it measured. Triage item 3 would put a
dead number in these classes on this card. The fix shape measured here
for such a number is the objectstack commit, as with `c459da6bc`.
2. **The rest of the bare `objectstack-ai#3391` population.** Only the two sites the
claim names were re-qualified. 28 more in-scope lines in 10 files write
the apiMethods whitelist card as a bare `objectstack-ai#3391`, which resolves to the
unrelated objectui#3391:
   - `ObjectDataPage.tsx`, `ObjectView.tsx` (app-shell);
   - `managedBy.ts`;
- `MePermissionsProvider.tsx`, `PermissionContext.ts`,
`PermissionProvider.tsx`;
   - `fieldWriteGate.ts`;
   - `ImportWizard.tsx`, `ObjectGrid.tsx`;
   - `ListView.tsx`.

It is live, not a 404, so it is outside the family pin. Two of those
lines pair it with a bare 3546 that means objectstack#3546 ("detail/form
面的 edit/delete 按钮接入服务端 effective 操作集"), while objectui#3546 is an
unrelated i18n card. Carrier: none.
3. **A stale claim beside a re-pointed sentence.** The same
`base.zod.ts` paragraph says "AGENTS.md's abridged protocol sketch shows
`events?: Record…` and its action-system commandment authors one".
`7550728a6` removed both from `AGENTS.md`, so that sentence is now
false. It is not a citation, and this PR does not touch it. Carrier:
none.
4. **The C0 blind spot, for any later census.** The bare-number
instrument's lookbehind hides a number written after `/`, `-`, `.` or
`&` (`#13337/#13086`, `-objectstack-ai#2231`). This batch read the 17 such numbers no
batch had read, and #13086 was the only 404. A later census can narrow
the lookbehind to refuse only a word character, `#` or `&` before the
`#`, and drop URL fragments by hand.
5. **Filenames are not citations.** Pending changeset and test FILENAMES
carry several of these numbers. They stay, as in PRs objectui#10707,
objectstack-ai#10797, objectstack-ai#10854, objectstack-ai#10869, objectstack-ai#10875 and objectstack-ai#10892.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
… family D input slice) (objectstack-ai#10907)

Part of objectstack-ai#9256
Clause-②: yes

**Clause-② `yes`, as the claim declared:** `input` accepted an authored
`children` on both published faces and rendered nothing, with no
render-time error or warning and no element; only the parser tier's
`not-a-container` warning (objectui#9910) noticed it. It now refuses it
by name. A published accept set narrows, so a contract review is owed
before landing.

This is the family-D slice that release `5864147075` on objectui#9256
lists, and nothing else: `input`, the family-D test header's superseded
remarks, and `PartialSchema`'s docblock claim about generic `Omit`
re-spellings. `br`, `hr` and `img` are not touched. objectui#9256 stays
open.

## What changed

- **`input` narrowed** in the family-D shape PRs objectui#9589 and
objectui#10897 used:
- TypeScript face (`InputSchema` in `packages/types/src/form.ts`):
`body?: never` and `children?: never`, each with a docblock saying what
the renderer reads and what it renders instead.
- zod mirror (`InputSchema` in `zod/form.zod.ts`): two
`retirementTombstone` members fed one string, `INPUT_NEITHER_CHANNEL`,
built by `neitherContentChannelGuidance`. Both stay MEMBERS, so
`zod-mirror-parity`'s key sets stay equal.
- `body` was already refused on both faces by `BaseSchema`
(objectui#6771). It is restated because that refusal names `children` as
the remedy, and `input` does not read `children` either.
- **`email` / `password` (`InputShorthandSchema`)**: no type or
accept-set change. See "The shorthand's own pair" below.
- **Texts (comments only):**
- The family-D test header: 「family E, frozen」 (the `InputSchema`
bullet), 「unattributable」 (the `DetailViewSchema` bullet) and the two
`ui:calendar` remarks that still said plain `Omit` (the header bullet on
the six bare-name hold-outs, and the comment above the slice-2 rows) now
say what is true. No assertion in that file moved.
- `PartialSchema`'s docblock in `packages/types/src/index.ts`: the
sentence reading every generic re-spelling as collapsing is marked
false, with `OmitDeclared` named as the counter-example.
- **Bounded in-place fix, declared:**
`partial-schema-collapse-pin.test.ts`'s header carried the same false
claim, and even named the key-remapping spelling as one that collapses.
It is corrected the same way, comment only. See "Texts" below for the
four conditions and the evidence.
- **New pin file**
`packages/types/src/__tests__/content-channel-input-9256.test.ts`.
- **One changeset** `.changeset/9256-input-content-channels.md`:
`@object-ui/types` `minor` with an explicit BREAKING note and a
migration line, the spelling PRs objectui#9589 and objectui#10897 used
under this repo's no-major rule.
- **Four pending changesets are corrected, prose only.**
- Round 2 added a dated note to `6397-partial-schema-collapse-pin.md`,
`9256-content-channel-family-d.md` and
`8499-node-slot-registered-arms.md`.
- Round 3 added a second note to the family-D entry and reworded the E3
entry's render claim in place.
- Each carries a sentence a later change in this same release made
false, or an overstatement, and each ships verbatim into the CHANGELOG.
Every frontmatter block is byte-identical. See "Round 2" and "Round 3"
below.

## Measurement, taken on `origin/main` `244d516df` before any edit

**Instrument.** The TypeScript compiler API (6.0.3), ⛔ not grep. It
builds one program per `tsconfig.json`: the 40 workspace packages,
`apps/console` and the three examples, 44 programs over 2015 non-test
source files. It ran on a BUILT tree (`turbo run build` 43/43, 0 cached)
with 0 unresolved-module diagnostics. It files every `.body` /
`.children` read (property access, string element access, object
destructuring) under the declared type of its receiver: 520 channel
reads were filed.

- **`InputSchema`: 0 `body` / 0 `children` reads.**
`InputShorthandSchema` is never a receiver: the `email` / `password`
wrappers are `any`-typed and hand the node to the `input` renderer,
which reads it as `InputSchema`.
- **Lit controls fire in the same run:** `ButtonSchema` 1, `DivSchema`
1, `CardSchema` 1, `ContainerSchema` 1 (each `children`).
- **Receiver reachability:** the same programs see `InputSchema` as the
receiver of 23 reads over 17 keys: `defaultValue`, `description`,
`error`, `id`, `inputType`, `label`, `max`, `maxLength`, `min`, `name`,
`pattern`, `placeholder`, `readOnly`, `required`, `step`, `value`,
`wrapperClass`. Neither channel is among them, so the zero is a reading,
not blindness.

**Every registration that claims `input`, bare and namespaced**
(`check:registry-bare-names --json`, 1657 files, 424 claims, 0
contested):

- `input`: sole claimant `ui:input`, a literal
`ComponentRegistry.register('input', InputRenderer, …)` in
`renderers/form/input.tsx`. The hop is typed (`InputRenderer` takes
`schema: InputSchema`), not `any`. No `field:input` or other claimant
exists; `ai:input` is a different key, a guarded protocol placeholder.
- `email` / `password`: `ui:email` / `ui:password` own the bare keys;
`field:email` / `field:password` stand down with `skipFallback: true`.

**Does any path pass the node's content through? No.**

- `SchemaRenderer` destructures `children` and `body` out of the props
bag before the spread.
- The only pass-through `InputRenderer` gives the native element is
`toFormControlDomProps(inputProps)`, a whitelist pick of
`SDUI_DOM_PASS_THROUGH_KEYS` plus `name` / `disabled`; `children` is not
on it.
- The `email` / `password` wrappers only spread `props` and
`props.schema` into `InputRenderer`.
- The registration declares no `children` slot input (objectui#9910).
- No in-source host builds an `input` node by spreading another node.
The literal `input` nodes in `form.tsx`, `cli`'s `init` scaffold and the
dashboard / designer config panels are assembled key by key, and none
carries a channel.
- **The 75 `any`-typed channel reads were attributed file by file.**
None can receive an `input` node's channel:
- app-shell nav / search trees, and HTTP request, error or email `body`
payloads;
- React `children` destructured off component props (`view:simple`'s
renderer, `react-page`'s wrapper), which `SchemaRenderer` never fills
from a node;
- the metadata-admin preview canvases, which read a page block's
`properties.children` or a page draft's root `children` to list and
append blocks for selection, not to render a node's channel;
- the page-container and sectioning registrations in `containers.tsx`,
`page.tsx` and `semantic.tsx`, and the html-elements factory, whose tag
list excludes `input` by name (its own comment lists `input` among the
tags it never registers);
- `core`'s schema builder (card / grid / flex builders; `InputBuilder`
has no children setter);
- `record:alert`'s own `props.body`, the runner's nav items, and the
vscode validator.
- **Generic traversers** (`validateChildren` in core, `sdui-parser`'s
parse / validate, `cli validate`) walk children to validate. They render
nothing and are node-agnostic, as for every family-D row already landed.

**Producers.** `pnpm census:body-dialect --keys
input,email,password,ui:input,ui:email,ui:password,div,card,page,button`
read 9016 files. The key population is printed by the tool itself, so
objectui#9545's blindness does not apply.

- Nodes on the six input keys: `input` 321, `email` 75, `password` 60,
`ui:email` 1, `ui:password` 17, `ui:input` 0. **0 author `body`, 0
author `children`.**
- The controls fire in the same pass: `children` on `div` 172, `card`
183, `page` 51, `button` 5.
- Real producers: 0. Nothing was migrated.

## The shorthand's own pair: type-redundant now, kept on purpose

`InputShorthandSchema` inherits `InputSchema` through `OmitDeclared` on
the TypeScript face and `.omit()` on the mirror, so it now inherits the
new pair. It already declared its own (PR objectui#10897).

- **Redundant as a type:** on the built d.ts both are the same `?:
never`, and the new `Eq` pin makes `tsc` hold that.
- **Not redundant as text.** The repo's rule for a restated tombstone is
the one `neitherContentChannelGuidance`'s docblock gives for restating
`body` over `BaseSchema`: restate when the inherited message misdirects
this node's author. The helper's contract spells the node and its route
into the message. The inherited string names `input` and the `ui:input`
route; the shorthand's names `email` / `password` and the wrapper route.
- **Decision:** keep both members and rewrite their docblocks (TS) and
comment (zod) to say they are type-redundant and why they stay. The
shorthand's old `body` docblock gave a reason that became false with
this change (that the inherited refusal names `children` as the remedy),
and it is replaced.
- **Pinned:** a CONTROL row asserts that an author of `email` /
`password` reads the message naming those two nodes and not the `input`
one, so deleting the restatement turns it red.
- Counter-precedents, noted: `UiCalendarSchema` restates nothing and
inherits `calendar`'s message, which names it. objectui#8072 deleted the
shorthand's `wrapperClass` restatement once `InputSchema` carried the
key; that was a live key with one description, not a refusal message
naming a node.

## The `OmitDeclared` propagation, measured on the BUILT d.ts

A compiler-API probe over `packages/types/dist/form.d.ts` reads the
heritage of `InputShorthandSchema` on its own: the base type the checker
gives for `OmitDeclared` of `InputSchema` minus `type` / `inputType`,
before the interface's own members.

| built d.ts | `InputSchema.children` | heritage `.children` | shorthand
face `.children` |
|---|---|---|---|
| base `244d516df` | `SchemaNode \| SchemaNode[]` | `SchemaNode \|
SchemaNode[]` | `undefined` (its own restated tombstone) |
| this branch | `undefined` | `undefined` | `undefined` |

A consumer-side probe resolves `@object-ui/types` the way
`packages/components` does (`--traceResolution`:
`packages/types/dist/index.d.ts`). There, `input` + `children`, `input`
+ `body` and `email` + `children` each answer their `@ts-expect-error`,
and a lit control (`const n: number = 'x'`) fires.

## Red on base, then green — one-shot, the restore proven by state

The predictions were written to a file BEFORE the mutation. The mutation
was `git checkout 244d516` of the two declaration files (`form.ts`,
`zod/form.zod.ts`), with every test at HEAD. It was proven by both blob
hashes equal to BASE and by anchor counts: `INPUT_NEITHER_CHANNEL` 0,
the new TS docblock marker 0. The run sat under `trap … EXIT INT TERM`
with absolute paths.

| reader | mutated (BASE declarations) | restored (HEAD `72dba894a`) |
|---|---|---|
| vitest, `content-channel-input-9256.test.ts` | **RED** exit 1: 6
failed / 13 passed | **GREEN** 19 / 19 |
| `tsc -p packages/types/tsconfig.test.json` | **RED** exit 2: TS2578 on
the `inputChildren` pin, TS2322 on the `Eq` pin for `children`; 0
elsewhere (`zod-mirror-parity` included) | **GREEN** exit 0 |
| built d.ts probe (types rebuilt; dist markers 0 then 2) |
`InputSchema` and heritage `children` accepted | refused |

- **The 6 red rows:** `input.children` refused at its path, its message,
`.describe()` equality, every-value, and the root + nested
`AnyComponentSchema` row; plus the `input.body` message row. The base
message is `BaseSchema`'s `Did you mean` pointer.
- **Green on base, by design:** `input.body` refused at its path,
every-value and `AnyComponentSchema` (`BaseSchema` already refused
`body`), and the shorthand message CONTROL (its own pair is unchanged).
- **One prediction was recorded as uncertain and read, not assumed:**
`input.body` `.describe()` equality is GREEN on base, because
`BaseSchema`'s alias refusal also describes with its own message.
- ⚠️ `?: never` is erased before vitest runs: the TypeScript half is
read by `tsc` alone. `tsc --listFiles` counts the new pin file 1 under
`tsconfig.test.json` and 0 under `tsconfig.json`, so it is a real
assertion under the `type-check` script.
- **Restore:** proven by both blob hashes equal to HEAD, `git diff HEAD`
empty and `git status` clean.

## Texts

- **Family-D test header.** The `InputSchema` bullet now records the
plain-`Omit` hold-out ground in the past tense, the E3 slice's
`OmitDeclared` repair, and this slice's narrowing, pinned in the new
file. The `DetailViewSchema` bullet records that the E3 slice attributed
the `any` hop directly and narrowed it there. The two `ui:calendar`
remarks name `OmitDeclared` for the TS face, `.extend()` for the mirror,
and say which file pins which half. The third `ui:calendar` remark (the
old TRIPWIRE note) was already in the past tense and is unchanged.
- **`PartialSchema`'s docblock.** The triage's reading is kept as the
triage's. The re-spelling claim is marked FALSE, with `OmitDeclared` as
the counter-example. The docblock now says that re-spelling the alias
would narrow a published type, a contract change not made in a comment
correction, and that objectui#6397 is closed. The alias itself is
unchanged.
- **Bounded in-place fix: `partial-schema-collapse-pin.test.ts` header**
(comment only; its assertions are about the alias as written and do not
move). All four conditions hold:
1. It is the same defect as the carried item: the same sentence, one hop
away. The corrected docblock points readers at this file.
2. The fix is mechanical, and its shape is pinned by the carried
correction.
  3. No open PR touches the file.
4. Same gate family: types `tsc` and vitest, no new verification
surface.

**Evidence, a one-shot probe against the built d.ts (not shipped):** the
exact spelling the header named as collapsing, `{ type } & { [K in keyof
T as K extends 'type' ? never : K]?: T[K] }`, declares 62 properties at
`ObjectGridSchema` and 28 at `ButtonSchema`, where the shipped
`PartialSchema` declares 1 at each. It refuses `label: 42` on
`ButtonSchema` and `objectName: 42` on `ObjectGridSchema` (both
`@ts-expect-error` used; tsc exit 0).

## Gates at HEAD `72dba894a` (round 1; round 2's are above) (exit codes
captured by redirect-then-capture; heavy runs through the shared verify
lock)

| gate | result |
|---|---|
| `@object-ui/types` build (tsc + vite + dist completeness) | exit 0,
134 emitted files verified |
| `@object-ui/types` `type-check` (`tsc --noEmit` + examples +
`tsconfig.test.json`) | exit 0 |
| `pnpm exec vitest run packages/types/` (parity, the family-D / E3 /
input pins, the 8072 and 8762 shorthand pins) | 265 files / 5873 tests,
exit 0 |
| `pnpm exec vitest run scripts/` (every `scripts/__tests__` gate suite,
a superset of those whose corpus holds a touched file) | 177 passed + 2
skipped of 179 files / 5322 tests, exit 0 |
| downstream consumer type-check: the packages naming `InputSchema`
(`components`, `core`) plus those authoring `input` nodes or configs
(`cli`, `plugin-dashboard`, `plugin-designer`) and `fields`, `react`,
`types`, against the rebuilt `dist` | 8 × `type-check: Done`, exit 0 |
| `@object-ui/types` lint (`eslint .`, JSON) | 338 files, 0 errors; the
9 `form.ts` warnings sit outside every added hunk |
| `check:handler-key-reads` | exit 0; 128 arms, 50 / 50 reads judged, 0
unjudged |
| `check:control-bytes` · `check:new-line-citations` (0 new) ·
`changeset:check` · `check-changeset-presence` ·
`check:changeset-claims` · `check:pending-changeset-literals` | exit 0
each |
| `check:spec-symbols` · `check:component-surface-parity` ·
`check:readme-exports` · `check:registry-bare-names` ·
`check:prompt-keys` · `check:doc-types` · `check:test-path-roots` ·
`check:esm-specifiers` · `check:vi-mock-*` · `type-check:coverage` |
exit 0 each |
| `check:doc-snippets` (678 / 678) · `check:doc-examples` ·
`check:skill-examples` · `check:doc-fences` · `check:doc-example-ids` ·
`check:sdui-registration-pins` · `pnpm check` (CLI self-check) | exit 0
each |
| governed guard `--test` over the 7 paths | NOT GOVERNED (lit control
`AGENTS.md`: exit 3) |

- **The gate list was derived by hand.** objectstack's
`dispatch-gates.mjs` refuses an objectui answer by design (exit 2), so
the list comes from objectui's `package.json` and `.github/workflows/`.
- `check:changeset-claims` is report-only. It names 18 pending
changesets that mention a touched file; each was read, and none is
falsified as a record of its own change. The two that carry a claim this
slice bears on are in the Acceptance notes.
- **NOT MEASURED, left to CI:** the 8-way `pnpm test` shards beyond
`packages/types` and `scripts/`, `test:dist`, and E2E.
`check:sdui-registration-pins` read the console built at base; this diff
touches no registration and no `sideEffects` array.

## Round 2: four prose edits, head `6eb3397b4`

These come from contract review `5865087439` ③. The seat took them into
this PR because each ships verbatim into the CHANGELOG. They are prose
only: no code, no test logic, and every frontmatter block is
byte-identical (the `name: bump` lines at base and head compare equal).

1. **`.changeset/9256-input-content-channels.md`, and the Clause-②
sentence above.**
- Old: "an authored child list on it rendered nothing: no error, no
warning, no element."
- New: "an authored child list on it rendered nothing, with no
render-time error or warning and no element; only the parser tier's
`not-a-container` warning (objectui#9910) noticed it."
- Measured with a one-shot probe, not committed. It built the manifest
from the live registry the way `container-declaration-ratchet.test.tsx`
does, then ran `validateTree` and `compile`.
- `input`, `ui:input`, `email` and `password` carrying `children`: each
draws exactly `not-a-container`.
     - The same four without `children`: none.
     - `div` carrying `children` (control): none.
- The JSX `input` tag with a child: `not-a-container`, severity
`warning`.
2. **`.changeset/6397-partial-schema-collapse-pin.md`.** A dated note
follows the "Why a pin and not a repair or a retirement" paragraph: a
generic re-spelling of this `Omit` does not collapse, objectui#9256.
- Spelling: `OmitDeclared` is spelled exactly as quoted, in
`packages/types/src/form.ts`.
- Counts: the re-spelled alias declares 28 properties at `ButtonSchema`
and 62 at `ObjectGridSchema` on this head and on `main` `733fd5ac6`. The
shipped alias declares 1 at each.
- "Not true when written": at that entry's own commit (`c8ea8af9c`),
under the same TypeScript 6.0.3 its lockfile pins, the same spelling
declares 27 and 61. Those are the source counts in that entry's own
table. Both re-spellings refuse `label: 42` (the `@ts-expect-error` is
used), and a lit control on the shipped alias admits it.
- "Later in this same release": the E3 entry and the 6397 entry are both
pending on `main` (`git cat-file -e` exit 0 for each; control on an
absent path: exit 128).
3. **`.changeset/9256-content-channel-family-d.md`.** A dated note
follows the AMENDED paragraph, in that file's own convention, naming the
hold-outs this card later narrowed in this release.
   - Six of the nine names, in `nine-holdouts-six-narrow.md`.
- `list` and `timeline`, in `9256-list-timeline-content-channels.md`.
This is one more than the reviewer's draft, measured.
   - `DetailViewSchema` and the shorthand faces, in the E3 entry.
   - `InputSchema`, in this PR.
- A compiler-API probe over the source reads `body` / `children` as
`undefined` with their own `?: never` members on `TextSchema`,
`ImageSchema`, `IconSchema`, `TabsSchema`, `AccordionSchema`,
`CalendarSchema`, `ListSchema`, `TimelineSchema`, `DetailViewSchema`,
`InputShorthandSchema` and `InputSchema`. The zod faces are pinned in
the family-D, E3 and input pin files.
- `ButtonSchema` and `AppComponentSchema` still resolve `children` to
`SchemaNode | SchemaNode[]` (`ButtonSchema`'s zod mirror declares its
own `children` union; `AppComponentSchema`'s extends `BaseSchema`, which
declares one). That is the note's "still accept `children`" sentence.
4. **`.changeset/8499-node-slot-registered-arms.md`.** "`inputType` is
deliberately NOT declared on this arm" is false on `main`.
- `form.zod.ts` declares `inputType:
retirementTombstone(SHORTHAND_INPUT_TYPE_REFUSAL)` on
`InputShorthandSchema`, and `form.ts` declares `inputType?: never`. Both
files are identical on `main` and at the base.
- A runtime probe: `inputType` is a member of the mirror shape. `{ type:
'password', inputType: 'text' }` is refused with `invalid_type` at path
`inputType`, the message opening with the objectui#8762 guidance. Bare
`{ type: 'password' }` (control) parses.
- objectui#8762's entry landed a day after 8499's (2026-09-10 against
2026-09-09), and both are pending on `main`.
- A dated note in the file's own form follows the `InputShorthandSchema`
bullet, the same place its objectui#10756 note sits under the
`HtmlElementSchema` bullet.

| gate at `6eb3397b4` | result |
|---|---|
| `check-changeset-overwrite` | exit 0 (report-only), "1 changeset(s)
added, 3 modified, 0 deleted". Each of the three modified entries prints
the same declaration at base and now (`@object-ui/types: patch`,
`minor`, `minor`), so no declaration is lost: its case 2, a correction
on purpose. |
| `changeset:check` · `check-changeset-presence` ·
`check:changeset-claims` · `check:pending-changeset-literals` ·
`check:control-bytes` · `check:new-line-citations` | exit 0 each |
| the 24 `scripts/__tests__` suites that name `.changeset` (the
changeset gates' own suites, the polarity census, control bytes, pending
literals, docs and workflow suites) | 24 files / 913 tests, exit 0 |

## Round 3: two more prose edits, head `876f6d7ea`

These come from delta review `5865466392` ③ flags 2 and 3. They are
prose only, and both frontmatter blocks are byte-identical.

1. **`.changeset/9256-content-channel-family-d.md`: a second dated note,
directly after the AMENDED paragraph** and before round 2's note. The
headline is "the two twins no longer take `body` — objectui#6771".
- Old, still in the AMENDED paragraph: "The two twins are unchanged and
go on inheriting `body` as the content slot".
- New note: objectui#6771 retired `body` on `BaseSchema` itself, so both
twins refuse `body` on both faces. The TypeScript face does it through
`BaseSchema`'s `body?: never`; each mirror does it by name, pointing at
`requestBody`.
- Measured, TypeScript: a compiler-API probe over the source gives
`BaseSchema` its own `body?: never`. `ChatbotEnhancedSchema` and
`ChatbotFloatingSchema` resolve `body` to `undefined` and declare only
`children?: never` themselves.
- Measured, zod, with a one-shot runtime probe (deleted): each twin's
mirror refuses `body` with `invalid_type` at path `body`, and the
message names `requestBody`. `requestBody` parses (control).
- Provenance, from `git log -S`: `BaseSchema`'s `body?: never` and both
twins' mirror tombstones were introduced by `2acd8e109` (objectui#6771,
2026-09-19). The AMENDED paragraph was written on 2026-09-17
(`c42554e94`), and `6771-retire-body-child-list-dialect.md` is pending
on `main`. So "later in this same release" holds.
- The reviewer's draft also cited objectui#9659. That card did not
introduce the twin refusals, so the note cites objectui#6771 alone.
- Round 2's note opens "most of the hold-outs above have since been
narrowed". It is still true beside the new one: it names what this card
narrowed, and it ends with `button` and `AppComponentSchema`, which
still accept `children`.
2. **`.changeset/9256-e3-residual-content-channels.md`, in place.**
- Old: "an authored child list on them rendered nothing: no error, no
warning, no element."
- New: "an authored child list on them rendered nothing, with no
render-time error or warning and no element; only the parser tier's
`not-a-container` warning (objectui#9910) noticed it."
- Measured for all twelve keys, not just the two in round 2. A one-shot
runtime probe (deleted) loaded every plugin registration (`plugin-grid`,
`-form`, `-kanban`, `-map`, `-tree`, `-view`, `-gantt`, `-calendar`,
`-charts`, `-detail`, and `components`). It built the manifest from the
live registry and ran `validateTree`.
- Each of the twelve is known to the registry, and none declares a
`children` input.
- Carrying `children`, each draws `not-a-container` (severity
`warning`). Without it, none does. `div` carrying `children` (control)
draws nothing.
- `object-chart`, `email` and `password` also draw one `unknown-prop` in
both legs, from the probe's own `objectName` fixture key. It is not part
of the difference.

| gate at `876f6d7ea` | result |
|---|---|
| `check-changeset-overwrite` | exit 0 (report-only), "1 changeset(s)
added, 4 modified, 0 deleted". Each modified entry prints the same
declaration at base and now, so no declaration is lost. |
| `changeset:check` · `check-changeset-presence` ·
`check:changeset-claims` · `check:pending-changeset-literals` ·
`check:control-bytes` · `check:new-line-citations` | exit 0 each |
| the 24 `scripts/__tests__` suites that name `.changeset` | 24 files /
913 tests, exit 0 |

## Serial constraints

- **Re-mapped at branch time (`244d516df`), before each round's push,
and again before round 3's edits.** At round 3 the open PRs are
objectui#10921, objectstack-ai#10915, objectstack-ai#10914, objectstack-ai#10912, objectstack-ai#10911, objectstack-ai#10910, objectstack-ai#10908, objectstack-ai#10906,
objectstack-ai#10901, objectstack-ai#10777 and objectstack-ai#10278. objectstack-ai#10904, objectstack-ai#10902 and objectstack-ai#10891 have merged since
the first map.
  - None touches any of the five changesets this PR adds or edits.
- One touches a source path here: PR objectui#10914 edits
`packages/types/src/zod/form.zod.ts`, in two comment / `.describe()`
hunks on the `wrapperClass` key (in `InputSchema` and in the
`InputShorthandSchema` restatement note). Neither overlaps or touches
this PR's hunks, and `git merge-tree` of this head (`876f6d7ea`) against
objectstack-ai#10914's head is clean.
- PR objectui#10908 edits `zod-mirror-parity.test.ts` and
`zod/index.zod.ts`, neither of which this PR touches.
- **The release PR objectui#5400** (1732 files, enumerated to the end,
last updated 2026-09-18) touches none of the source paths. As release
consumption it deletes pending changesets, three of the four edited here
among them (not the E3 entry); that is not a hunk overlap.
- **`origin/main` moved four commits since the base** (`9f0c84a44`,
`7ea8118f7`, `8522396c0`, `733fd5ac6`). None touches `packages/types` or
the changesets edited here, and `git merge-tree` of this head
(`876f6d7ea`) against `733fd5ac6` is clean. `main` has not moved since.

## Acceptance notes: out of scope, not fixed here

- **The `PartialSchema` docblock and its pin file still sequence the
alias's repair on objectui#5155.** That card is closed as a duplicate of
objectui#5250, whose ruling keeps the tolerant face on renderer props.
objectui#8347 (open) is the card on removing the index signature from
the authoring face. "Once objectui#5155 removes the root index
signature" therefore names a card that will not do it. Noted, not
edited: rewriting the sequencing is a judgement about objectui#5250 /
objectstack-ai#8347, not a comment correction. Carrier: none.
- **The same "no error, no warning, no element" sentence lives in
published refusal messages and docblocks,** not only in changesets.
Examples: the family-D `retirementTombstone` strings on the zod mirrors
(the chatbot twins' included) and the matching TypeScript docblocks.
Wherever the node's registration declares no `children` input, the
parser tier's `not-a-container` warning did fire. Correcting those
strings is a published-message change, not prose, and it is not this
PR's. Noted, not edited. Carrier: none.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apps documentation Improvements or additions to documentation package: auth tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

邀请注册链路丢失 redirect:signUp 不传 callbackURL,被邀请人验证邮箱后被带去「创建工作区」

1 participant