feat(app-shell,types): cloud:plan-status marks the organization's current plan card (objectui#10919) - #10990
Conversation
…rent plan card
The Cloud pricing page is static metadata and cannot see the organization's
plan. `cloud:plan-status` reads it from the org-scoped entitlements summary
(through `useEnvironmentEntitlements`) and renders a "Current plan" badge on
the card whose `properties.plan` equals it, and nothing otherwise.
- app-shell: the widget, registered as `cloud:plan-status` only
(`namespace: 'cloud'`, `skipFallback: true`), wired into the barrel and the
`sideEffects` array.
- types: a local `CloudPlanStatusSchema` arm on `AnyComponentSchema` - a
required strict `properties: { plan }` bag, both content channels refused.
- cli: `known-schema-types.ts` regenerated; the namespaced ratchet stays 397.
- i18n: `cloudPlanStatus.current` in all ten locale packs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1
…o explicit any Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1
…'s warning The objectui#10928 population pin requires every reads-NEITHER refusal to carry the parser-tier clause; the registration declares no `children` input, so `validateTree` does warn `not-a-container` for this node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1
…oud-plan-status Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1
|
changeset-claim-re-read
|
✅ Console Performance Budget
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
Size Limits
|
|
Contract reviewServed-tier: Card #10919 · PR #10990 · ① Derived judgmentsThe arm Refusal codes and paths, as The registry key — right, and better than the precedent. Widget behaviour — it never guesses. Hook reuse and the envelope — right. Exports and wiring — right. i18n — complete. Shipped prose, checked sentence by sentence against the diff and the named inputs.
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…objectui#10992 for the Spec Main Shape Gate) Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1 Co-authored-by: Claude <noreply@anthropic.com>
❌ Console Performance Budget
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. Which half objected:
📦 Bundle Size Report
Size Limits
|
|
From
Details are on the card: Generated by Claude Code |
…objectui#10996's eager-closure re-pin) Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
Fixes #10919
Clause-②: yes
What this does
The Cloud pricing page is static metadata, so it cannot tell which of its plan cards the organization is already on (objectstack-ai/cloud#2434 item 5). This PR adds the SDUI widget
cloud:plan-status. A page places one node on each plan card and names that card's plan code inproperties.plan. The widget reads the org-scopedGET /cloud/environment-entitlementssummary and renders a localized "Current plan" badge when the summary'splanequalsproperties.plan. It renders nothing in every other state: another plan, loading, a failed request, a body that is not the{ success, data }envelope, or a node that names no plan. It never guesses a plan.The snippet cloud places in each
planCard(...)column ofpackages/service-tenant/src/pages/pricing.page.ts, for the cloud#2434 item 5 follow-up:The card names
free,teamandenterpriseare the summary's plan codes for those cards. The endpoint can also reportsolo,business,starter,proandcustom, and an organization on one of those gets no card marked, because the page lists no such card.Routing: the triage answer
5867937466, quotedWhat the accept set gains (Clause-②: yes)
AnyComponentSchemagains exactly onetypeliteral,cloud:plan-status, through one new arm,CloudPlanStatusSchema, exported by name from@object-ui/types/zod. So dosafeValidateSchemaandobjectui validate, which judge with it. A document with that type is accepted only when all of these hold:propertiesis present and is exactly{ plan }. The bag is strict: any other key isunrecognized_keysatproperties.planis a non-empty string:invalid_typeortoo_smallatproperties.planotherwise. It is not an enum, because the plan catalog belongs to the control plane.bodyand nochildren. Both are refused by name (invalid_type), because the widget reads neither channel.BaseSchema's declared keys by their types; an undeclared key passes the tolerant face and is refused by the strict face (StrictAnyComponentSchema).Nothing that parsed before is refused now.
NAMESPACED_REFUSED_AT_TYPEstays 397 andREFUSED_AT_TYPEstays 73: the one new registered key lands with its arm.What changed
packages/app-shell/src/console/home/CloudPlanStatus.tsx(new): the widget and its registration. It reads the plan through the existinguseEnvironmentEntitlementshook, which is not edited. The hook'sauthFetchcomes fromuseState, notuseMemo, because the hook keys its fetch effect on it (AGENTS.md [WIP] Enhance every detail of the designer #10). The node'sclassNamereaches the badge, so the page'sresponsiveStylesscope class does too.packages/types/src/zod/cloud.zod.ts(new):CloudPlanStatusSchema,BaseSchemaplus thecloud:plan-statusliteral, a required strictproperties: { plan }bag, and the two content-channel refusals. The refusal text carries the objectui#10928 parser-tier clause: the registration declares nochildreninput, sovalidateTreewarnsnot-a-containerfor this node.packages/types/src/zod/index.zod.ts: the barrel export, the import, and the one union line.origin/mainis merged in (19fc41a), per the claim's note on the draft PR objectui#10962.packages/types/src/zod/README.md: a "Cloud Widgets" group lists the arm.packages/cli/src/utils/known-schema-types.ts: regenerated bynode scripts/regenerate-known-schema-types.mjs. It gains the one linecloud:plan-status.packages/i18n/src/locales/*.ts:cloudPlanStatus.current("Current plan") in all ten packs.EXCLUSIONSrow inzod-mirror-parity.test.ts(no TS declaration in@object-ui/typesrestates the node),cloud-plan-status-arm-10919.test.ts,CloudPlanStatus.test.tsx, and one ratchet row namingcloud:plan-statusas armed..changeset/10919-cloud-plan-status.md: app-shell and typesminor, i18n and clipatch.PM assumptions, measured: three falsified, two confirmed
{ type, plan }. The measured node is{ type, properties: { plan } }. Cloud's pages are typedPagefrom@objectstack/spec/ui, andPageComponentSchemais astrictObjectwhose only props channel isproperties, so a top-levelplanis refused by the spec parse before objectui sees it.cloud:onboarding-nextis authored the same way on the welcome page, and it readsproperties.*. The widget readsproperties.planand nothing else, and the arm declares exactly that.register('cloud:onboarding-next', …, { namespace: 'app-shell' })makes TWO registry keys,app-shell:cloud:onboarding-nextand the fallbackcloud:onboarding-next, and the namespaced ratchet counts both. Following it would have needed the arm to accept a second literal nobody authors. So the widget registers asregister('plan-status', …, { namespace: 'cloud', skipFallback: true }): one key, one arm literal, and one generated line.cloudPlanStatus.current, notcloud.planStatus.current, following the sibling blockscloudOnboarding.*andaiModelStatus.*.useEnvironmentEntitlementssends?organizationId=fromuseAuth().activeOrganization.id, as the precedent does, and the endpoint resolves an absent parameter to the session's active organization. Its row-derived fallback carries no plan, so the widget marks a card only onsource: 'summary'.planvalues. The endpoint'splanis cloud'splanKey(...)overPLAN_CODES(free,solo,team,business,starter,pro,enterprise,custom). ObjectUI names none of them.Surface supplement: two files outside the claim's declared surface
packages/app-shell/src/index.ts(one side-effect import line) andpackages/app-shell/package.json(twosideEffectsentries). A registration cannot ship without both. Without the barrel import, the console never evaluates the module. Without the array entries,scripts/check-side-effects-array.mjsgoes red, because every registering module in the entry graph must be named there, and a bundler would be free to drop the registration. Both edits have the same shape asCloudOnboardingNext's. Overlap, read on the open PR list (RESTpulls/N/files, 2026-09-28): none of the ten non-release open PRs touches either file. The changesets release PR rewrites only theversionfield. Seat: please add these two paths to the claim's file surface.Verification, at
19fc41a(the merge oforigin/main42687ba)pnpm exec vitest run --maxWorkers=2, through the verify lock):packages/types/,packages/cli/,packages/i18n/, plus a declared narrowing ofpackages/app-shell/(below). Result:Test Files 407 passed (407),Tests 8074 passed | 13 skipped.cloud-plan-status-arm-10919.test.ts12/12;CloudPlanStatus.test.tsx10/10; the ratchet file 15/15, including the newcloud:plan-statusrow.turbo run build --filter='@object-ui/app-shell^...'(28/28):@object-ui/types,@object-ui/i18n,@object-ui/cliand@object-ui/app-shellall exit 0.--listFilesOnlyshows each package's test program includes the new test files..ts/.tsxfiles,--format json, 19 files, 0 errors and 0 warnings. The narrowing is a measurement: the packages lint under the rooteslint.config.jsalone (no package-level config), and that config enables no type-aware linting (0 hits forprojectService,parserOptions,TypeCheckedortsconfigRootDir), so this diff cannot move the verdict on any untouched file.regenerate-known-schema-types.mjs --check,check:side-effects-array,check:i18n-keys,check:i18n-drift,check:i18n-dead-keys(report),check:registry-bare-names,check:doc-types,check:prompt-keys,check:unreferenced-sources,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:test-path-roots,check:new-line-citations(0 new),check:control-bytes,check:handler-key-reads,check:changeset-claims(report),check:pending-changeset-literals,changeset:check,check-changeset-presence(4 released packages, 1 changeset),check-changeset-overwrite,check-type-check-coverage,check:phantom-deps,check:esm-specifiers,check-lint-coverage, andcheck-governed-queue-guard --testover the 22 paths: NOT GOVERNED.5c4a1d2(the merge changed neither mutated file). Each leg was committed first, mutated throughablation-replace.mjswith an EXIT/INT/TERM restore trap, then restored withgit checkout HEAD --:index.zod.ts). The ratchet goes red: "refuses 398 registered namespaced key(s) attype; the pin is 397", and the Refused list namescloud:plan-status. The arm pins go red too (14 failed). Restored: blob6fa23ee31c7cequals HEAD, andgit diff HEADis empty.entitlements.plan === planbecomestrue). The widget goes red on "renders nothing on a card whose plan is not the organization's" and "compares the plan code verbatim". Restored: blob08d10662137cequals HEAD, andgit diff HEADis empty.vitest.config.mtsaliases@object-ui/types/zodtosrc), so nodistrebuild was part of either leg.NOT MEASURED, each with its reason:
packages/app-shell/full suite (856 files): at--maxWorkers=2it exceeds the foreground cap (exit 124 at 560 s, and exit 124 on shard 1/6 at 270 s). The run was narrowed to the 40 files that read what this diff touches: the two widget directories (console/home/__tests__,environment/__tests__),src/__tests__/, and the 13 tests that readpackage.json,src/index.tsor the barrel. Result: 40/40 files, 575 tests. CI runs the whole suite.check:sdui-registration-pins,check:eager-closureandcheck:eager-locale-cataloguesneed a built console (PREREQUISITE NOT MET, exit 2). CI'sperformance-budget.ymlruns them onpackages/**.check:readme-exports(exit 1) andcheck:doc-examples(exit 2) could not judge the READMEs of packages not built here (app-shell, cli, plugin-*). Among what was judged: 0 wrong-path and 0 fabricated.Acceptance notes (not filed; no carrier)
useEnvironmentEntitlements' docblock still describes an environment-list-only hook ("Only fetch when this is the environment list"), and it now has a second caller. Per the claim, the hook is not edited.app-shell:cloud:onboarding-next,app-shell:cloud:ai-model-statusand their siblings) stay in the refused set. That family is objectui#10872's population.Generated by Claude Code