Skip to content

fix(app-shell): read the single AI quota pool and draw one ring (objectui#8524) - #10911

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-8524-ai-usage-single-pool
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-8524-ai-usage-single-pool

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #8524

⏸️ Parked on an external gate, not a defect of this PR: the required Spec Main Shape Gate fails every objectui merge group (report-chart-query-spec-parity.test.ts line 747, TS1360, against objectstack df3ba164a588). Anchor card: #10916. This PR is re-queued by the claiming seat when that card closes.

Clause-②: no

What was wrong

After objectstack-ai/cloud#2144 the tenant runtime's GET /api/v1/ai/usage answers { pool, breakdown }: one AI quota pool, plus an observation-only split of that same pool. useAiUsage only parsed the retired per-meter { meters: { build, dataChat } } answer, so its parser returned null and AiUsageIndicator rendered nothing. No ring, no error.

Producer read at cloud main 96eb092, packages/objectos-runtime/src/ai-usage-endpoint-plugin.ts (symbols AiUsageIndicator, AiUsageBreakdownFractions, AiUsageResponse, buildAiUsageResponse, unknownResponse):

  • pool carries planType, fraction (number or null), unmetered, resetKind, resetsAt, upgrade, topUp. These are the same fields each old meter carried.
  • At that sha breakdown is always an object whose members are number or null. Every error path answers 200 with a neutral pool (fraction: null) and both members null.
  • Cloud ADR-0015 makes the build / dataChat split read-only statistics that no quota decision uses. cloud#2133 lets the endpoint keep breakdown but does not require it, so its absence is a normal reading. The triage comment on the card says the same.

What changed

  • useAiUsage reads { pool, breakdown? } and only that shape.
    • pool must carry every declared field with its primitive type. resetKind and planType are checked as strings, so a reset kind this build does not know still renders without a reset line (the objectui#7371 behaviour, still pinned).
    • A missing breakdown, or one with null members, is a normal reading and never the parse-failure branch.
    • Any other answer, the retired { meters } one included, gives usage: null and now also sets error. There is no dual-shape read.
  • AiUsageIndicator draws ONE ring from pool.
    • The status, the reset line and the upgrade / top-up CTA all read the pool.
    • The split appears as text in the popover under the pool's row: a "Used so far" heading, then e.g. Build 30% and Ask 20%, each a percentage of the same pool. It is shown only when at least one member is a number, and it is never a second ring.
    • The CTA's test id is now ai-usage-cta.
  • i18n: one new key, console.ai.usage.breakdownTitle, in every locale pack. The split rows reuse meterBuild / meterAsk.
  • Changeset: .changeset/8524-ai-usage-single-pool.md declares a patch for @object-ui/app-shell and @object-ui/i18n.

File surface is the claim's: the hook, the component, their two test files, the ten locale packs and one changeset. The hooks barrel is untouched (see Acceptance notes).

Tests

packages/app-shell/src/hooks/__tests__/useAiUsage.test.ts:

  • the new shape parses to { pool, breakdown };
  • breakdown absent parses to { pool } with no error;
  • breakdown with both members null parses as a normal reading;
  • the retired { meters } answer gives usage: null with error set (the strict reader, pinned);
  • a breakdown member that is not a fraction gives usage: null.

packages/app-shell/src/layout/__tests__/AiUsageIndicator.test.tsx:

  • ONE ring in the trigger even when the split is measured;
  • the split is listed as text with no ring inside it, and the popover holds exactly one ring;
  • both members null gives one ring and no split;
  • breakdown absent gives one ring and no split;
  • unmetered or null-fraction pool renders nothing;
  • low hint, CTA and the weekly reset cases, ported to the pool;
  • through the REAL hook: a { pool } answer with no breakdown renders the ring, and the { meters } answer renders nothing once the hook reports its error.

Reverse verification (after committing the fix; mutation and restore scripted with a trap): restoring the base versions of useAiUsage.ts and AiUsageIndicator.tsx under the new tests gives Test Files 2 failed (2), Tests 20 failed | 4 passed (24). The 4 that stay green are the shape-independent cases: null usage renders nothing, non-2xx, inert without apiBase, and refetch on nudge. The restore was proven by HEAD blob hashes and an empty git diff HEAD.

Local verification

Final reading at 12e1c1d. That head is this branch after merging origin/main three times; the merges brought no change to the files this PR touches. Each exit code was captured to a file before any pipe. The full app-shell suite ran once, at fd6907c (the fix commit): Test Files 843 passed | 1 skipped (844), Tests 8673 passed | 9 skipped. It was not re-run at the final head, because CI runs it.

Gate (at 12e1c1d) Exit
node scripts/check-changeset-presence.mjs 0
pnpm changeset:check (fixed group + no-major) 0
pnpm check:i18n-keys 0
pnpm check:i18n-drift 0
pnpm check:i18n-dead-keys 0
pnpm check:control-bytes 0
pnpm check:new-line-citations (VERDICT 0 new citations) 0
pnpm check:changeset-claims 0
pnpm check:pending-changeset-literals 0
pnpm check:vi-mock-specifiers / vi-mock-inherit / vi-mock-override-shape 0 / 0 / 0
pnpm check:test-path-roots 0
pnpm check:unreferenced-sources / phantom-deps / self-import 0 / 0 / 0
turbo run build --filter=@object-ui/app-shell^... (28/28, all cache hits) then pnpm --filter @object-ui/app-shell run type-check (verify lock) 0
vitest run on useAiUsage.test.ts + AiUsageIndicator.test.tsx (verify lock): 2 files, 24 tests passed 0
eslint --format json on the 14 .ts / .tsx files in this diff: 0 errors, 2 warnings that were already there 0
pnpm check:eager-locale-catalogues 2, PREREQUISITE NOT MET (NOT MEASURED)

Also run at a5d7c25, after the first merge (which touched the locale packs): the packages/i18n/ suite plus the two touched tests plus the two ChatDock tests gave 76 files and 1234 tests passed, exit 0. tsc -p tsconfig.test.json --listFilesOnly confirms the package type-check compiles both touched test files.

Lint was narrowed to the diff's .ts / .tsx files. The narrowing is safe because eslint.config.js sets no parserOptions / project / projectService / type-checked preset (zero hits, with a lit control), has no import/* plugin rules, and none of eslint-rules/* reads another file. Every rule therefore judges one file, and this diff cannot move a verdict on an untouched file. The count comes from --format json: 14 files, 0 errors, 2 warnings. Both warnings are react-hooks/set-state-in-effect on effects this diff does not change, and linting the base blobs of those two files shows the same two warnings.

NOT MEASURED:

  • check:eager-locale-catalogues: PREREQUISITE NOT MET, because it needs a built console bundle (apps/console/dist/eager-closure.json). CI builds it.
  • A browser run against a live cloud runtime: no cloud backend in this container.

Acceptance notes

  • Split placement: in the popover, which is the indicator's detail view, rather than in a native title tooltip. The card allows "a tooltip or a detail view", and a title tooltip never shows on touch devices.
  • Strict breakdown, decided by the PM on the dev report (a PM decision, not a maintainer ruling: option A, keep strict): a breakdown that is present but malformed is rejected like any other unreadable answer. error is set and the ring hides. "Malformed" means not an object, a member that is neither a number nor null, or a literal breakdown: null, which the producer never sends. The alternative, dropping a malformed split and keeping the ring, would be exactly the tolerant read the card's routing constraint forbids, and it would hide a future rename on the producer side. Only an absent breakdown, or one with null members, means "no split".
  • error on an unreadable answer: the hook now sets error when an answer does not parse. Before, it returned usage: null with no error. No UI reads error today.
  • Type names: AiMeterUsage keeps its name and now documents the pool. The new AiUsageBreakdown lives in the hook file. Neither change touches packages/app-shell/src/hooks/index.ts, which is outside the claimed surface. The package entry src/index.ts re-exports none of these types, hence Clause-②: no.
  • Delivery order: the cloud tenant image bundles objectui at cloud's .objectui-sha (the overlay layer in apps/objectos/Dockerfile). So this reader ships with a runtime that already serves { pool, breakdown }. The one path where it could meet a { meters } runtime is a hotfix branch that moves the pin without cloud#2144. That path is noted here and deliberately not answered with a dual read.
  • Out of scope: nothing found that qualifies for a new card.

Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1


Generated by Claude Code

The cloud usage endpoint now answers { pool, breakdown } (one quota pool plus
an observation-only split). useAiUsage parsed only the retired per-meter
{ meters: { build, dataChat } } answer, so it returned null and the ChatDock
indicator disappeared without any error.

- useAiUsage reads { pool, breakdown? } strictly. An absent breakdown is a
  normal reading; the retired { meters } answer parses to null and sets error.
- AiUsageIndicator draws ONE ring from pool. The split is listed as text in
  the popover (share of the same pool), only when a member was measured.
- New console.ai.usage.breakdownTitle row in every locale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1
…ectui#8524)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1
…dy the indicator guard

Comment wording only in useAiUsage (the optional breakdown comes from cloud#2133,
not ADR-0015), and a behaviour-identical early-return guard in AiUsageIndicator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1
@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/6661-app-launcher-nav-menu-renderers.md

  • names en.ts → packages/i18n/src/locales/en.ts — edited by this change

    Three new strings — the launcher's and the menu's accessible names, and the menu's empty state — are declared under console.nav in en.ts and its nine sibling packs. An inline defaultValue alone is not a fix: it renders English at one call site and leaves the string untranslatable everywhere (objectui#3517).

.changeset/7371-ai-usage-indicator-weekly-reset.md

  • names packages/app-shell/src/hooks/useAiUsage.ts → packages/app-shell/src/hooks/useAiUsage.ts — edited by this change

    AiUsageResetKind (packages/app-shell/src/hooks/useAiUsage.ts) gains the 'weekly' member; resetsAt was already string | null and needed no shape change.

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 8522396c0 (merge-base with origin/main): 14 file(s) changed outside .changeset/, read against 1669 pending declaration(s) that publish a body (2263 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3096.4 KB 3104.5 KB
Main entry chunk (gzip) 148.7 KB 350 KB
Entry file index-DDpKSJvH.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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 12e1c1d5dcd1f9e38b9451f3eb8d55c0f1f2d2b9
Local-runs: none

Inputs read: card #8524 (body and all five comments), PR #10911 (body, 15-file list, net diff against main), the 43 check-runs on the head, and the producer at cloud origin/main 72857a1, packages/objectos-runtime/src/ai-usage-endpoint-plugin.ts. Nothing was built, run or re-run.

① Derived judgments

Accept set of parseUsage (the consumer's read of GET /api/v1/ai/usage), each change against the producer's AiUsageIndicator, AiUsageBreakdownFractions, AiUsageResponse, buildAiUsageResponse and unknownResponse:

  1. Accepts { pool } only when pool carries all seven declared fields by primitive type (planType string, fraction finite number or null, unmetered boolean, resetKind string, resetsAt string or null, upgrade boolean, topUp boolean) — RIGHT. Both producer paths (buildUsageIndicator, unknownIndicator) emit exactly these seven; fraction is clamped 0..1 or null; JSON carries no NaN or Infinity, so the finite check excludes nothing the wire can send. The two unions are checked as strings, so the objectui#7371 unknown-resetKind behaviour (no reset line, no failed parse) survives — RIGHT.
  2. Accepts breakdown absent and returns { pool } — RIGHT. The card and the triage ruling make absence a normal reading, never the null branch; the producer at this sha always sends the object, so the accept set exceeds the wire by exactly the optionality the ruling grants. Pinned in the hook test, the component test and through the real hook.
  3. Accepts breakdown: { build, dataChat } with each member a finite number or null — RIGHT against AiUsageBreakdownFractions and shareOfCap (clamped 0..1 or null; unknownResponse sends both null, which the reader treats as "no split").
  4. Rejects the retired { meters: { build, dataChat } } answer: usage: null, error set — RIGHT. The triage seat's routing constraint (no tolerant dual-shape read) holds; pinned in both test files and through the real hook, and the reverse verification in the PR body shows the old reader fails 20 of the 24 new cases.
  5. Rejects a breakdown that is present but malformed (not an object, the literal null, or a member that is neither a number nor null): usage: null, error set — RIGHT under the card's contract-first rule. The producer's breakdown is a non-nullable object whose members are number | null; anything else is a wire change and should surface as a failed parse, not as a silently dropped section. The producer never sends breakdown: null.
  6. Extra keys on the payload or on pool are ignored (only the seven fields are checked, pool is passed through by reference) — RIGHT; an additive producer change cannot blank the ring.

Public surface and behaviour:

  1. AiUsageResponse moves from { meters } to { pool, breakdown? }; AiUsageBreakdown is a new exported type; AiMeterUsage keeps its fields. All three live in packages/app-shell/src/hooks/useAiUsage.ts; neither hooks/index.ts nor the package entry src/index.ts is in the diff. RIGHT as a patch-level change on the premise judged in ②.
  2. useAiUsage now sets error (Unrecognized AI usage response shape) when an answer does not parse, where before it left usage: null and cleared error — RIGHT; additive, and it turns the next wire change into an observable failed parse instead of silence. No UI reads error.
  3. AiUsageIndicator draws ONE MeterRing in the trigger and ONE in the popover, both from pool; status, reset line and the upgrade / top-up CTA read pool; the component renders nothing when pool.unmetered or pool.fraction === null (the producer sets fraction: null whenever unmetered) — RIGHT. The split is text rows under data-testid="ai-usage-breakdown" (meterBuild / meterAsk plus formatNumber(..., { style: 'percent' })), shown only when at least one member is a number, and holds no ring — RIGHT against the card ("a tooltip or a detail view, never as a second gauge") and D5 (percentages of the one pool, never a token count). The early return sits after every hook call and no hook follows it — RIGHT (Lint success on the head).
  4. data-testid ai-usage-cta-build / ai-usage-cta-dataChat collapses to ai-usage-cta — RIGHT; a test id, not a public surface; Build & E2E and Live E2E are success on the head.
  5. i18n: one new key console.ai.usage.breakdownTitle inside console.ai.usage in all ten locale packs — RIGHT; additive, the rows reuse the existing meterBuild / meterAsk keys.

Shipped prose — .changeset/8524-ai-usage-single-pool.md, every factual sentence:

  • "Fix the ChatDock AI usage indicator going blank against the single-pool AI usage endpoint (objectui#8524)." — RIGHT (the card's symptom).
  • "The cloud GET /api/v1/ai/usage endpoint now answers { pool, breakdown }: one quota pool, plus breakdown, a read-only split of that same pool into app-building (build) and data Q&A (dataChat)." — RIGHT against AiUsageResponse and its docblock ("OBSERVATION-ONLY split of that same pool into app-building vs data Q&A").
  • "useAiUsage only read the retired per-meter { meters: { build, dataChat } } answer, so it returned null and AiUsageIndicator rendered nothing, with no error." — RIGHT against the removed lines (isMeter on meters.build / meters.dataChat, then setError(undefined)).
  • "useAiUsage reads { pool, breakdown? } and only that shape. A missing breakdown, or one whose members are null, is a normal reading. Any other answer — the retired { meters } one included — leaves usage null and now also sets error." — RIGHT against the new parseUsage and the .then branch.
  • "The hook's AiUsageResponse type is now { pool, breakdown? }, with the new AiUsageBreakdown type for the split; AiMeterUsage keeps its fields and describes the pool." — RIGHT.
  • "AiUsageIndicator draws one ring from pool. When a breakdown member is a number, the popover lists it under the pool's row as a percentage of the pool, as text and never as a second ring. The upgrade / top-up button's test id is now ai-usage-cta (it was ai-usage-cta-build / ai-usage-cta-dataChat)." — RIGHT; the removed template literal was ai-usage-cta-${key} over build / dataChat.
  • "New locale key console.ai.usage.breakdownTitle ("Used so far") in every locale pack." — RIGHT: ten packs in the diff (ar, de, en, es, fr, ja, ko, pt, ru, zh); the en value equals the component's defaultValue.

Locale copy: en breakdownTitle: 'Used so far' with a comment saying the rows reuse meterBuild / meterAsk and each is a share of the ONE pool, never a second budget — RIGHT against splitLabel and the producer's "fractions of the SAME binding-window cap". zh breakdownTitle: '已用额度构成' (the composition of the allowance used so far) — RIGHT as the heading over a split of one pool, with no second-budget wording. The other eight packs render the same heading — RIGHT.

PR body producer sentences: "breakdown is always an object whose members are number or null" and "every error path answers 200 with a neutral pool (fraction: null) and both members null" — RIGHT against buildAiUsageResponse and unknownResponse at cloud origin/main; the PR read the producer at 96eb092, and the file at 72857a1 declares the same shape.

Nothing in the diff or its prose is judged WRONG.

② Semver level

The changeset declares @object-ui/app-shell: patch and @object-ui/i18n: patch, and the diff touches exactly those two packages plus the changeset itself.

  • @object-ui/i18n: one additive key per pack — patch is RIGHT.
  • @object-ui/app-shell: a bug fix. The only type-level change (AiUsageResponse, new AiUsageBreakdown) is in hooks/useAiUsage.ts. The claim's Clause-②: no rests on the barrel facts it states — hooks/index.ts exports these, the package entry src/index.ts (the . export) re-exports none of them — and the PR body repeats them. Neither barrel is in the diff, and the entry file is outside this review's allowed inputs, so that premise is carried here by the head's gate verdicts: Changeset Bump Policy, Changeset Declaration, Changeset Claim Re-read, Changeset Fixed Group Check, Governed Surface Queue Guard and README Export Check are all success. On that premise no published accept set or public surface moves; patch is RIGHT.
  • The test-id rename and the new error on an unreadable answer are not public-surface changes.

The changeset's level matches what the diff publishes and matches Clause-②: no on the newest Claim: comment on the card (5864143926).

③ Boundary flags

The dev report (os-dev-report, card comment 5864927031) declares open_questions: []. Its deviations and the PR body's Acceptance notes, each:

  1. Split placement in the popover rather than a native title tooltip — ANSWERED. The card allows "a tooltip or a detail view"; the routing constraint asks only that it never be a second gauge, and it is not.
  2. No type rename; AiUsageBreakdown added in the hook file; both barrels untouched — ANSWERED (see ②); the claimed file surface is respected, barrels excluded.
  3. useAiUsage sets error on an unparsed answer; no UI reads it — ANSWERED; additive and consistent with contract-first.
  4. Strict breakdown (present but malformed is rejected and the ring hides), cited as a PM decision (option A), not a maintainer ruling. The decision text lives on the dev report and in the PR body, not in a separate PM comment. ANSWERED: it follows from the triage seat's routing constraint (no tolerant read) and the card's "the producer moves once, this consumer follows once"; the one case it costs, breakdown: null, is not something the producer sends. No maintainer decision is owed.
  5. Delivery order: the tenant image bundles objectui at cloud's .objectui-sha, so this reader ships beside a { pool, breakdown } runtime; the one gap is a hotfix branch that moves the pin without cloud#2144 (production's last tenant deploy, hotfix/v1.31.1-security, predates it). ESCALATED, and correctly not answered with a dual read; the remedy is the cloud-side .objectui-sha bump the claim says follows this landing, which is not this diff's to make.
  6. Process notes (verify-lock duration, the classifier outage, three origin/main merges with no overlap on the touched files) — ANSWERED; the PR is mergeable: clean against main 8522396.
  7. NOT MEASURED check:eager-locale-catalogues (needs apps/console/dist/eager-closure.json) — ESCALATED to CI; the head's build-dependent runs (Build & E2E, Bundle Analysis, Docs Route Eager Closure Check) are success, and the diff adds one row to catalogues already inside whichever closure they sit.
  8. NOT MEASURED browser run against a live cloud runtime — ESCALATED; the through-the-real-hook cases in AiUsageIndicator.test.tsx drive the wire payload through the real parser into the component for both the { pool } and the { meters } answers, and the wire shape is read off the producer source, not assumed.
  9. Ordering (Blocked-by: objectstack-ai/cloud#2133) — SATISFIED: closed completed on 2026-09-08 with cloud#2144 merged (unblock comment 5824206473); the triage seat's second condition (the pin) is item 5.

Check-runs on the head: 43 in all, 40 success, 3 skipped (the coverage matrix template row, Test (coverage), dependabot — conditional jobs, not failures), none failure.

Implemented-by: claude/issue-8524-ai-usage-single-pool
Reviewed-by: session_015AUunPkX7UTkCH9e7AdZo1

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 07:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 28, 2026
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 7c9145f Sep 28, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-8524-ai-usage-single-pool branch September 28, 2026 09:02
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

Projects

None yet

2 participants