Skip to content

fix(app-shell,i18n): the organization slug field is read-only while environments reference the slug (objectui#11720) - #11735

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-11720-org-slug-cloud-rename
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-11720-org-slug-cloud-rename

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #11720
Clause-②: yes

Implemented by the os-dev run dispatched on claim comment 6019734714, session https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju.

What changed

The organization Settings page (SettingsPage in @object-ui/app-shell) no longer offers a slug edit the framework will refuse, and a name-only save no longer carries a slug.

  • New module-private helper readOrgEnvironmentPresence, beside the page in console/organizations/manage/orgEnvironments.ts. It sends GET /api/v1/data/sys_environment with the canonical filter (the organization id) and select=status params. It counts rows the way the framework's slug guard (beforeUpdateOrganization in plugin-auth) counts them: any status except archived and failed, and a row with no status counts too. It answers present, none or unknown, and it never rejects. Only the { success: true, data: { records } } envelope is read. That is the shape the framework data domain answers a list with (FindDataResponseSchema inside the dispatcher's success envelope). Any other body reads as unknown.
  • A single-environment runtime is not asked. When the runtime config serves singleEnvironment: true (the CLI's os serve arm serves it through Serve.RUNTIME_CONFIG_OPTIONS), the helper answers unknown without a request. Three readings at objectstack 1fb274e6 back this: @objectstack/spec lists sys_environment in CLOUD_PROVIDED_OBJECT_NAMES ("They do not exist in a single-environment OSS runtime"); no framework source defines the object (control: the same search finds sys_organization's definition); and the guard itself returns early when getSchema('sys_environment') is empty. The read there could only fail, so the answer is unchanged and the request is gone.
  • present locks the field. The slug input is read-only, and a note (new key organization.settings.slugLockedNote, in all ten packs) states the measured cause and nothing else: the organization has active environments, so the slug can't be changed here, because a rename also moves their subdomains. It names no rename path, because none was measured reachable from this console. Every other answer leaves the field as it always was.
  • handleSave sends slug only when it changed: non-empty, different from the organization's current slug, and the field not locked. A name-only save carries no slug on any host, so the guard has nothing to judge and the save answers 200. A stale form also cannot write back a slug that was renamed somewhere else.
  • The owner's form waits for the environment answer before it renders, so the field never renders editable and then locks under the cursor. Each answer is stored with the organization id it was read for.

Why B and not A: no existing signal says the host serves cloud's change-slug route

The triage ruling (comment 6019042638) made A depend on an existing signal, read and not invented, and named B as the fallback. I checked every signal the console reads, on origin/main at a58626c and against the producers in objectstack 01e0f71a. None of them says the host serves the route:

  • Runtime config (AppShellRuntimeConfig):
    • cloudUrl is '' both when the runtime IS the cloud and on the CLI's air-gapped arm. The framework's own isControlPlaneDeclined doc says the CLI passes controlPlaneUrl: '' on both arms, so the URL tells you nothing about the deployment.
    • singleEnvironment is false on every multi-tenant host.
    • Each features.* key (installLocal, marketplace, aiStudio, autoPublishAiBuilds, customDomain, sso, scim, storageUsage) stands for a different route or a plan entitlement. None stands for the organization rename.
  • Auth config (AuthPublicConfig.features): no key says anything about cloud.
  • Discovery: CoreServiceName has no cloud slot, and ApiRoutesSchema has no cloud route. The only discovery services objectui reads are auth and ai.
  • Metadata: sys_organization declares a change_slug record action that targets /api/v1/cloud/organizations/{id}/change-slug. The framework's platform-objects declare it on every host, though, gated only by multiOrgEnabled, so finding it does not show the route is served.

So A is not built, no key is added, and B lands. The premise still holds: framework commit 131b937aee (the forcing change named on the card) is contained in the 17.7.0 tags. At objectstack 01e0f71a, beforeUpdateOrganization reads sys_environment under the system context and throws FORBIDDEN while any non-archived, non-failed row exists.

Pins (transport stubbed at createAuthenticatedFetch)

settings-slug-environments-11720.test.tsx, 12 tests:

  • The read: the request path and its filter and select params. present for a counted row and for a row with no status. none for retired rows only and for no rows. unknown for a 404, a network failure, and a bare body outside the envelope. On a single-environment runtime, unknown with no request made.
  • With environments: the field is read-only, and the note renders and is wired through aria-describedby. The note's text is exactly the measured cause and matches neither record nor rename it from. A save sends no slug.
  • Outside cloud (the read is refused, as on a multi-environment host with no sys_environment): the field stays editable, and a changed slug goes out in the one update call, as before.
  • On a single-environment runtime: no request is made, even with a transport that would answer an active environment. The field stays editable, and a changed slug goes out in the one update call.
  • Only retired environments: the field stays editable.
  • A name-only save carries no slug, both outside cloud and with environments.
  • A non-owner's visit makes no environment request.

Three ablations, all on the committed tree at 169584a. Each went through objectstack's scripts/ablation-replace.mjs in wrap mode, with the restore armed on exit, INT and TERM:

  1. Lock branch removed (locked: presence === 'present' replaced by locked: false). Predicted: only the "with environments" test goes red. Observed: Tests 1 failed | 11 passed (12), and the failure is that test. Restore proven: blob after restore fbf7960a002d equals the blob at HEAD, and git diff HEAD is empty.
  2. Slug omission removed (the slug is sent whenever it is non-empty, as before). Predicted: the "with environments" test and both name-only cases go red. Observed: Tests 3 failed | 9 passed (12), and those are the three. Restore proven the same way.
  3. Single-environment skip removed (the singleEnvironment === true early return deleted). Predicted: the helper's and the page's single-environment pins go red. Observed: Tests 2 failed | 10 passed (12). The page pin failed on "expected vi.fn() to not be called at all, but actually been called 1 times", and the helper pin on "expected 'present' to be 'unknown'". Restore proven: blob b04f375652a8 equals HEAD, and git diff HEAD is empty.

Clause-② reading

I built @object-ui/i18n twice, at the merge base 0cfe772 (a throwaway detached worktree, since removed) and at this branch's 169584a. diff of the two dist/locales/en.d.ts files is exactly one added line inside organization.settings: readonly slugLockedNote: "This organization has active environments, so its slug can’t be changed here: renaming it also moves their subdomains.";. dist/index.d.ts is byte-identical between the two builds. No export, prop or schema key is added, and readOrgEnvironmentPresence is not exported from the package entry.

Local verification (head 169584a)

The branch merged origin/main at 0cfe772 as a merge commit (b8deb0d) before this round's change.

  • pnpm exec turbo run build --filter=@object-ui/app-shell^... --concurrency=2: 28 of 28 tasks.
  • pnpm --filter @object-ui/app-shell type-check and pnpm --filter @object-ui/i18n type-check: exit 0.
  • pnpm --filter @object-ui/app-shell lint and pnpm --filter @object-ui/i18n lint: exit 0, 0 errors. The page's two remaining set-state-in-effect warnings were already there.
  • pnpm exec vitest run packages/app-shell/src/console/organizations/ packages/i18n/: Test Files 94 passed (94), Tests 1435 passed | 13 skipped.
  • Exit 0 on all of these: check:new-line-citations (0 new), check:control-bytes, check:i18n-keys, check:i18n-drift (against 0cfe772: 0 en values changed, 1 key added), check:i18n-dead-keys, check:changeset-claims, check:pending-changeset-literals, check-changeset-no-major, check-changeset-fixed, check-changeset-presence, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:unreferenced-sources, check:test-path-roots.
  • node scripts/check-governed-queue-guard.mjs --test over the 14 paths: NOT GOVERNED.
  • NOT MEASURED: check:eager-locale-catalogues. Reason: PREREQUISITE NOT MET, because the gate weighs the built console bundle (apps/console/dist/eager-closure.json) and the console was not built here. CI owns it, along with the repo-wide pnpm lint and the full pnpm test.

Acceptance notes

  • Remaining cost. A multi-environment host that is not a cloud control plane has no sys_environment object, and there the read still answers an error once per owner visit to this page. No served signal says such a host has no environment registry. The /organizations/SLUG routes also render outside ConnectedShell, so the metadata registry (useObjectPresence, objectui#7476's tool) is not available on this page. The error reads as unknown, and the page then behaves as before. Single-environment runtimes are no longer asked.
  • Cloud environment hostnames are single-environment too. The open RuntimeConfigPlugin also serves singleEnvironment: true when the request host resolves through the env-registry service to one environment, and no framework package registers that service. On such a host the page does not ask either. unknown keeps the field editable and the server deciding, which is the behaviour before this PR. I could not measure whether sys_environment is readable on such a host, because the cloud repository is not reachable from this container. Either way the skip cannot lock a field by mistake.
  • No rename path is claimed. B is final for this card, per the seat's answer: no served flag is added. The note names only the measured cause.
  • Framework declaration, noted. sys_organization.change_slug (target /api/v1/cloud/organizations/{id}/change-slug, record_header, gated only by multiOrgEnabled) is declared on every host by platform-objects, but nothing in objectstack mounts that route. I read this from the source only and measured no public door. carrier: none.
  • Stale module header, not edited. CreateWorkspaceDialog's module header says an owner can still change the slug later in organization settings. Under cloud with environments the field is now read-only. I left it alone because it is outside the claim's file surface. carrier: none.
  • Earlier behaviour, unchanged here. OrganizationLayout resolves the organization from the slug in the URL, so after a successful slug rename off-cloud, /organizations/OLD-SLUG/settings no longer matches. I read this from the source only and did not reproduce it. carrier: none.

Generated by Claude Code

claude added 2 commits October 6, 2026 16:04
…nvironments reference the slug (objectui#11720)

From framework 17.7.0 better-auth's organization update refuses a new slug
with 403 while the organization has an environment that is neither archived
nor failed; every cloud organization is born with one, so every owner's slug
edit on the Settings page failed. No existing signal says the host serves
cloud's change-slug route, so the ruled fallback (B) lands: the page reads the
organization's sys_environment rows the way the guard counts them and renders
the slug read-only with a note while one counts.

A save now sends slug only when it changed, so a name-only save carries no
slug and answers 200 on every host. Outside cloud the read is refused and the
form behaves as before.

Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju
Co-authored-by: Claude <noreply@anthropic.com>
…s read for (objectui#11720)

The environment answer is stored with its organization id, so another
organization reads as pending again without a synchronous reset inside the
effect.

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

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3326.2 KB 3330.4 KB
Main entry chunk (gzip) 154.7 KB 350 KB
Entry file index-1mF58RmA.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) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
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.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
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.54KB 3.46KB
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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 578.98KB 139.21KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 262.75KB 66.62KB
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.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.18KB 11.71KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.81KB 47.14KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.25KB 65.05KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.48KB 65.51KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.90KB 11.74KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.23KB 22.73KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
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) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 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

… single-environment runtime is not asked (objectui#11720)

The locked-slug note now states only the measured cause (active
environments; a rename also moves their subdomains) in all ten packs, and
drops the pointer to the organization's record, which nothing measured as
reachable from this console.

readOrgEnvironmentPresence answers unknown without a request when the
runtime config serves singleEnvironment: true. @objectstack/spec lists
sys_environment in CLOUD_PROVIDED_OBJECT_NAMES, absent from a
single-environment OSS runtime, so the read there could only fail; the
answer is unchanged and the request is gone.

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

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3326.4 KB 3330.4 KB
Main entry chunk (gzip) 154.7 KB 350 KB
Entry file index-DHPLi7ci.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) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
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.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
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.54KB 3.46KB
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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 578.98KB 139.21KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 263.36KB 66.69KB
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.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 37.51KB 10.04KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.18KB 11.71KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.81KB 47.14KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.28KB 65.06KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.51KB 65.53KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.90KB 11.74KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.23KB 22.73KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
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) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 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: 169584a8d3ad3837da7935ba5fae72d9250d6467
Local-runs: none

Inputs: card #11720 (body and all six comments, rulings 6019042638, claim 6019734714, dev reports 6021083789 and 6022244399, the repo:cloud note 6021288557, the seat's ACCEPT 6022335821), PR #11735 (body, 14-file list, net diff against main at merge base 0cfe772, two-dot equal to three-dot: 14 files, +526/−3, merged through b8deb0d), the head tree read by pinned sha, and the 43 check-runs on the head. Nothing built, run or re-run.

① Derived judgments

Route B, as ruled — RIGHT. Triage 6019042638 made A conditional on an existing cloud signal, read and not invented, and named B the fallback. The dev's evidence that none exists is stated in the PR's "Why B and not A", signal by signal: cloudUrl is '' both when the runtime is the cloud and on the CLI's air-gapped arm (the framework's isControlPlaneDeclined doc), singleEnvironment is false on every multi-tenant host, each features.* key stands for another route or entitlement, AuthPublicConfig.features carries no cloud key, discovery (CoreServiceName, ApiRoutesSchema) has no cloud slot or route, and sys_organization.change_slug is declared on every host gated only by multiOrgEnabled. No runtime-config, discovery or feature key was added: the diff's only new key is the i18n one below. The ruling's "a changed slug takes the rename route" pin is replaced by B's pins as the ruling allows; "a name-only save answers 200" holds by omission of the slug; "outside cloud, the page behaves as today" holds (one update call, the changed slug in it).

@object-ui/i18n — one key, ten packs — RIGHT. organization.settings.slugLockedNote is added once in each of ar de en es fr ja ko pt ru zh, and the locales directory holds exactly those ten packs. The en value equals the inline defaultValue in SettingsPage to the curly apostrophe. All ten translations name only the measured cause (active environments; a rename moves their subdomains) and no rename path and no record page. en ends as const with export type TranslationKeys = typeof en, re-exported from the entry (./i18n.js) and from ./locales (export { default as en, type TranslationKeys }), both addressed by the package's exports map (., ./locales, ./locales/*): the surface widens by exactly this key. The dev's two-build reading (one added line in dist/locales/en.d.ts, dist/index.d.ts byte-identical) is consistent with that.

@object-ui/app-shell private surface — RIGHT. readOrgEnvironmentPresence and OrgEnvironmentPresence are imported by SettingsPage and the new test only; nothing else in the head tree names orgEnvironments. The entry re-exports SettingsPage as DefaultSettingsPage, which takes no props and returns JSX, so neither name is reachable through props, params or returns. Off the entry.

The published component's behaviour (DefaultSettingsPage) — each change named:

  • The read — RIGHT. An owner's visit sends GET {base}/api/v1/data/sys_environment with filter set to the JSON { organization_id } and select=status, credentials: include, through createAuthenticatedFetch, with base read the way the package's other fetches read VITE_SERVER_URL. The filter= JSON spelling is the one the primary client's wire pin (filter-dialect-wire-7221) reads back from the URL; organization_id and the retired pair archived / failed match useEnvironmentEntitlements (TERMINAL_STATUSES, $filter: { organization_id }) and CloudEnvironment.organization_id / status in marketplaceApi. A non-owner's visit makes no request (pinned).
  • The accept set for the list body — RIGHT, with one arm carried to ③. Only { success: true, data: { records: [...] } } is read; a non-2xx, a throw or a body outside that envelope reads unknown. That is the dev's producer measurement at the framework's data domain, it is the shape every issue-numbered wire pin of @object-ui/data-objectstack answers the data route with (7028, 7221, 9020, 10788 and siblings), and it is the repo's one-dialect rule. It is narrower than the app-shell's sibling readers of the same route — readDataRecords in packagedActions ("wrapped or bare"), listCloudEnvironments, PagePreview — which also accept a bare { object, records } body, and two live e2e specs read the bare form. On a host that answers bare, the helper says unknown, the field stays editable and the card's 403 persists there; it cannot lock wrongly.
  • The lock — RIGHT. Only present makes the input readOnly and renders the note, wired through aria-describedby to the note's id; none and unknown leave the field as before (pinned: refused read, retired rows only, single-environment). An owner's form waits for the answer, so the field never renders editable and then locks; the answer is stored with the organization id it was read for, so a change of organization reads as pending again. The helper never rejects, so only a hung transport can hold the spinner — the same class as the getMembers wait that already gates the form.
  • The single-environment skip — RIGHT on the measured arm. getRuntimeConfig().singleEnvironment === true answers unknown with no request. The dev's three readings at objectstack 1fb274e6 are stated: @objectstack/spec lists sys_environment in CLOUD_PROVIDED_OBJECT_NAMES as not existing in a single-environment OSS runtime; no framework source defines the object, with sys_organization as the control; the guard returns early when getSchema('sys_environment') is empty. There the read could only be refused, which reads unknown, so the skip answers exactly what the read would have. objectui's own AppShellRuntimeConfig.singleEnvironment doc reads "Single-environment runtime (CLI os serve, etc.)", consistent with the premise. The arm the dev itself flagged — a cloud environment hostname that RuntimeConfigPlugin resolves through env-registry to one environment — is unmeasured and goes to ③. Ablation leg 3 reported both single-environment pins red, as predicted.
  • The save body — RIGHT, no narrowing of what a caller can do. slug goes out only when !slugLocked, non-empty, and different from org.slug; before, it went out whenever non-empty. Net: an unchanged slug is no longer resent (the server outcome is the same, per the card's table), an empty slug was never sent and still is not, and a changed non-empty slug still goes out on every unlocked host — pinned for the refused read and for the single-environment runtime, in the one update call with name and logo. The stale-form claim holds because the form re-syncs slug from org on every org change and compares against org.slug at save time. AuthOrganization.slug is string, so ?? '' is defensive only. Ablation leg 2 reported the three predicted pins red.

Pins. settings-slug-environments-11720.test.tsx, 12 tests, transport stubbed at createAuthenticatedFetch, updateOrganization the useAuth() double. The test's vi.mock('../../../runtime-config') resolves to the same module the helper imports as ../../../runtime-config.js. The note's text is pinned exactly, with the curly apostrophe the en pack carries, and refused to match record or rename it from. Three ablation legs went red as predicted (1, 3, 2 of 12), each restored by blob equality and an empty git diff HEAD.

Check-runs on the head. 43, all completed: 40 success, 3 skipped (Test (coverage), Test (coverage shard), dependabot), 0 failure, none in progress. Green among them: Type Check, Lint, Test shards 1 to 8, Test (dist pins), Build & E2E, Changeset Declaration, Changeset Claim Re-read, Changeset Bump Policy, Changeset Fixed Group Check, Governed Surface Queue Guard, Line Citation Gate, Control Byte Scan, Inert vi.mock Specifier Check, Docs Route Eager Closure Check. The one gate the dev could not run locally (check:eager-locale-catalogues, console not built) is CI-owned and the eager-closure check is green.

② Semver level

.changeset/11720-org-slug-environments.md: @object-ui/app-shell: minor, @object-ui/i18n: minor. Both sit in the one fixed group, so the bump is one either way, and minor fits: @object-ui/i18n widens TranslationKeys by a key, which is at least minor; @object-ui/app-shell changes a published component's rendered behaviour and request set (a new read, a conditional read-only field, a narrower save body) without narrowing what a caller can do, and objectui declares no major — breaking behaviour ships as minor with the break spelled out, which the "Behaviour change" paragraph does.

Paragraph 1, sentence by sentence: (1) "no longer offers a slug edit the framework will refuse" — holds where the read answers present; the hosts where the field is still offered are the ones the second paragraph names. (2) the 17.7.0 refusal, the production environment every cloud organization is born with, the toast — the card's measurement. Holds. (3) "asks the data API for the organization's sys_environment rows, counted the way that guard counts them" — the helper's request and predicate. Holds. (4) "While one counts, the slug renders read-only with a note that says why: a rename also moves the environments' subdomains." — readOnly={slugLocked} and the note's copy. Holds.

Paragraph 2: (1) "A save sends slug only when it changed." — sent implies changed, non-empty and unlocked; holds as the necessary condition it states. (2) a name-only save carries no slug (pinned on both arms), answers 200 (the card's table answered 200 even for the resent current slug; now nothing is sent), and a stale form cannot write back a renamed slug (the org re-sync and the save-time comparison). Holds. (3) a singleEnvironment: true runtime "has no sys_environment object, so the page does not ask there at all" — the "does not ask" half is the code; the "has no object" half is the spec's CLOUD_PROVIDED_OBJECT_NAMES contract as the dev read it, which is what the sentence states; the one singleEnvironment: true host the dev could not measure is in ③. Holds on the declared contract. (4) "On any other host without that object the read is refused." — a 404, read as unknown. Holds. (5) "Either way the field stays editable, and a changed slug goes out in the same single update call as before." — pinned for both arms. Holds. (6) "one extra request per owner visit" — the effect runs once per owner and organization id. Holds.

The changeset's Clause-② paragraph names the one key, the ten packs, no export, prop or schema key, and the module-private read: all read off the entries above. The PR body's second line Clause-②: yes and the changeset's yes (widening) are the same reading said twice.

Clause-②: yes (widening)

A widening (one en key, and with it TranslationKeys), and no narrowing of a published accept set: a changed slug still goes out on every unlocked host, an unchanged slug's omission has the same server outcome, and the lock withholds only an edit the server refuses.

③ Boundary flags

The dev's two open questions (round 0), answered by the seat in 6022335821 and closed on this head. Q1, whether change_slug is reachable on cloud: unmeasured, so the page claims no path — the head's copy in ten packs carries none, and the pin refuses record and rename it from. Q2, a served flag later to enable A: A, B is final and no key is added — the head adds none. Round 1 reports open_questions: [].

Round-1 deviations, each answered. (a) The single-environment skip on a cloud environment hostname (env-registry resolution): the skip's answer unknown is the pre-PR behaviour and can never lock by mistake; the card's done-when is stated on cloud's control plane, which serves singleEnvironment: false. Recorded, not blocking. (b) The exact-text note pin against os-dev.md's hint-copy rule: justified, the copy's content is the subject (no unmeasured path, the objectui#11726 class). (c) The merge commit amended for trailers before its first push, the lock-queue wait, and the batch-last lock line that corrects round 0's stray ;-sequenced part: process only; the ablation verdicts cited are each leg's own output. (d) Attribution in the AGENTS.md / os-dev.md form: the repo's own rule, and no model identifier lands in the PR body, the changeset or the comments. (e) Kept from round 0 — route B, slug sent only when changed, the one failing read per owner visit on a multi-environment non-cloud host (no served signal exists and useObjectPresence is not mounted on the /organizations/SLUG routes; the error reads unknown): recorded, not blocking.

Out-of-scope findings (3, carrier none, not filed), acknowledged. sys_organization.change_slug is declared on every host but nothing in objectstack mounts its route (source reading); OrganizationLayout resolves the organization from the URL slug, so an off-cloud rename strands /organizations/OLD-SLUG/settings (earlier behaviour); CreateWorkspaceDialog's header says the owner "can still change it later, in organization settings", now half-true under cloud with environments — a one-line follow-up outside the claim's surface, not a blocker.

Escalated to the seat, one item, a landing or post-landing measurement rather than a FAIL. The card's done-when on cloud is unmeasured end to end, because objectstack-ai/cloud and its hosts were unreachable from the dev's container. Three wire facts carry the fix on the control plane, each read from source only: (i) the list body arrives in the { success, data: { records } } envelope there (the primary client's pins say so for the framework; two app-shell siblings and two live e2e specs still read a bare form); (ii) an owner's browser read of sys_environment through the control plane's organization scope answers the organization's rows (the card records that the guard's own read was narrowed to no rows until it carried the system opt-in; the marketplace env picker reads the same route for the active organisation, so a visit to a non-active organization's settings is the unmeasured corner); (iii) filter and select are honoured on that host. Each fails toward unknown or none, that is, the pre-PR 403, never a wrong lock. Per 6021288557, cloud staging now carries 17.7.0: one owner-session GET /api/v1/data/sys_environment?filter=...&select=status on staging and one visit to the settings page settle all three — a factual question for the repo:cloud seat, like Q1. Suggested for the seat's landing check.

Implemented-by: claude/issue-11720-org-slug-cloud-rename
Reviewed-by: session_01FngvPpdrnhHMdHHq6vwwju

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 18:16
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 18:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit ded4494 Oct 6, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11720-org-slug-cloud-rename branch October 6, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants