Skip to content

fix(app-shell): the console asks GET /usage/storage only when the runtime serves features.storageUsage (objectui#11002) - #11609

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11002-storage-usage-flag
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11002-storage-usage-flag

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11002
Clause-②: yes

The console asks GET /api/v1/usage/storage only when the runtime serves it. This applies the card's ruling 5890841811 (letter A) as written. The cloud distribution serves features.storageUsage: true exactly when its composition mounts the endpoint plugin. That half landed through objectstack-ai/cloud#2517, and cloud main 2205b53 derives it from the plugin list. This side reads the flag default-off, through one boolean that both banners share.

What publishes (@object-ui/app-shell, minor)

  • Type (the Clause-② widening). The exported RuntimeFeatures gains one optional member, storageUsage?: boolean. AppShellRuntimeConfig.features, and the value getRuntimeConfig() returns, carry it too.
  • Value. It is false by default, and after any runtime-config payload whose features.storageUsage is not the literal true. Absent, false, 'true' and 1 all read as false; only true reads as true. As with every feature key, a payload with no features object leaves the current value.
  • Behaviour of the exported ConsoleShell. Its two banners request the endpoint only when the viewer is the environment admin, as before, and the flag is on. Both read the same flag and still share one request. A cloud runtime older than cloud#2517 sends no key, so its admins see neither banner until that runtime is upgraded.
  • Not published. The accessor isStorageUsageServed() is module-internal, like isMarketplaceEnabled() and isAiStudioEnabled(). It ships inside dist/ but is not on the package entry, and the exports map has only . and ./styles.css. Measured: the package entry re-exports neither the two banners nor useStorageUsageReading / useReadRateReading. The hooks are unchanged and keep their enabled = true default.
  • No export is added or removed, and no existing member changes type. The changeset says the same.

Changes

  • runtime-config.ts:
    • the new RuntimeFeatures.storageUsage member, with a docblock in its siblings' voice;
    • storageUsage: false in the defaults;
    • storageUsage: body.features.storageUsage === true in initRuntimeConfig, beside customDomain / sso / scim;
    • isStorageUsageServed(), which fails closed (=== true) and reads features?.. Its docblock records why: the opposite direction from the marketplace and AI Studio accessors, and never inferred from a 404.
  • StorageUsageBanner and ReadRateBanner: each passes enabled: isAdmin && isStorageUsageServed() and says so in its module header.
  • ConsoleShell.tsx:
    • the two mount comments name the second gate;
    • the stale comment that called useDiscovery() the AI chatbot's gate now names the readers that exist. useViewData and useElementDataSource fall back to SchemaRendererContext's dataSource. Measured: nothing in product source calls useDiscovery.
  • packages/app-shell/README.md (Storage capacity section) and content/docs/guide/console.md (the feature-flag list).

The gate sits in the banners, not the hooks. The hooks are generic readers that already take enabled; whether the shell's banner should ask is a fact about where the shell mounts it.

Reproduction (red at base 278d244)

The new ConsoleShell.storageUsageGate-11002.test.tsx mounts the real ConsoleShell, fills the real runtime-config singleton through initRuntimeConfig, and uses the real shared endpoint reader. Only useWorkspaceAdminStatus and the network are stood in. Run against the base code it reads Tests 2 failed | 2 passed (4). The no-key and false cases fail with expected [ '/api/v1/usage/storage' ] to have a length of +0 but got 1. At base, the served case already shared ONE request between the two banners: that was measured here, not assumed.

Pins

  • ConsoleShell.storageUsageGate-11002.test.tsx:
    • an admin with no storageUsage key issues zero requests, and so does an admin with storageUsage: false;
    • an admin with storageUsage: true issues exactly one request, shared by both banners. The storage banner renders data-storage-usage-case="warning" on the warn verdict, and the read-rate banner renders on the same response;
    • a non-admin with storageUsage: true issues zero requests.
    • Each zero is read after the same settle that sees the one request in the served case.
  • runtime-config.test.ts, describe runtime-config storageUsage (objectui#11002):
    • off before init;
    • on for the literal true;
    • off for absent, false, 'true' and 1;
    • off when the config fetch fails;
    • withdrawn when a later fetch no longer serves it.

Ablations

Every mutation went through objectstack's scripts/ablation-replace.mjs. The anchor hit once and was gone afterwards, and the blob hash changed. Each restore was proven by blob == HEAD and an empty git diff HEAD, all from committed state.

mutation went red stayed green
drop the mapping line is ON when the server sends the literal true, withdraws it when a later fetch no longer serves it, the shell's ONE-request case (3 failed) the falsy-input pins: the default is already false, so a dropped line cannot turn them
mapping === true changed to !! is OFF when the key is the string 'true', ... the number 1 (2 failed) the rest
drop the gate in StorageUsageBanner the shell's no-key and false cases, 1 request each (2 failed) the served and non-admin cases
drop the gate in ReadRateBanner the same two (2 failed) the same two
drop isAdmin && in StorageUsageBanner the non-admin case (1 failed) the other three

Reverse check of the published type. After pnpm --filter @object-ui/app-shell build, dist/runtime-config.d.ts carries storageUsage?: boolean, and dist/index.d.ts names RuntimeFeatures with no isStorageUsageServed. A scratch probe resolved @object-ui/app-shell to that built index.d.ts (deleted after use):

  • RuntimeFeatures['storageUsage'] = true compiled (exit 0);
  • = 'yes' failed with TS2322: Type '"yes"' is not assignable to type 'boolean | undefined' (exit 2).

Gates

These ran at HEAD 27044c0 unless noted. The second commit only drops one test-file cast, and the doc gates read files it does not touch.

command exit verdict line
turbo run build --filter='@object-ui/app-shell^...' --concurrency=2 (under the verify lock) 0 Tasks: 28 successful, 28 total (at e45751f)
pnpm --filter @object-ui/app-shell type-check 0 tsc --noEmit && tsc -p tsconfig.test.json. --listFilesOnly shows both new test files in the test program
vitest run on the six touched suites 0 Test Files 6 passed (6), Tests 88 passed (88)
vitest run --maxWorkers=3 packages/app-shell/ plus the five source-text readers outside the package 0 Test Files 1001 passed, 1 skipped (1002), Tests 10076 passed, 9 skipped (at e45751f)
eslint --no-inline-config --format json on the six changed .ts/.tsx files 0 0 errors. The warnings equal base for each file: ConsoleShell.tsx 8 and 8, runtime-config.test.ts 6 and 6
pnpm check:control-bytes 0 OK (scanned 7501 tracked text file(s)
pnpm check:new-line-citations 0 0 new citation(s)
pnpm check:changeset-claims 0 No pending changeset names a file this change touches.
pnpm check:pending-changeset-literals 0 No test source names a pending changeset.
node scripts/check-changeset-presence.mjs 0 6 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
pnpm changeset:check, check-changeset-overwrite.mjs 0, 0 No changeset declares a major bump., No pre-existing changeset was modified or deleted.
check-vi-mock-specifiers / -override-shape / -inherit, check-test-path-roots 0 each OK each
check:esm-specifiers, check:self-import, check:phantom-deps, check:unreferenced-sources, check:spec-symbols, check-type-check-coverage, check-lint-coverage 0 each (at e45751f)
check-doc-links, check-doc-fence-languages, check-doc-component-types, check-doc-expression-carriage, check:doc-example-readers, check:docs-route-closure 0 each (at e45751f)
pnpm check:readme-exports 1 PREREQUISITE NOT MET: 7 packages outside this diff are unbuilt. packages/app-shell/README.md draws 0 findings with app-shell's dist built
pnpm check:doc-examples 2 NOT MEASURED: THE GATE COULD NOT RUN, 12 packages outside this diff unbuilt. This diff adds no fenced block to any document

What the lint run covers. The population is the six changed source and test files, linted under the root eslint.config.js, and the JSON output counts six. The narrowing is safe: the config enables no type-aware linting (no projectService and no parserOptions.project), and no local rule in eslint-rules/ reads the filesystem. So this diff cannot change a verdict on an untouched file. The repo-wide pnpm lint belongs to CI.

The touched-suites run was selected two ways: by import (the importers of runtime-config.ts, the two banners and ConsoleShell.tsx sit in console/, layout/, chrome/ and observability/), and by source-text read (a grep for readFileSync, ?raw and the file names). The full-package run covers both.

Acceptance notes

  • StorageUsageBanner.test.tsx and ReadRateBanner.test.tsx mock their hook. Their admin cases now hand the mock enabled: false, because the runtime-config default is off, and the mock answers with a reading anyway. They stay green and test the classifier, not the gate. The shell pins above test the gate. carrier: none.
  • isStorageUsageServed() is read at render, like isAiStudioEnabled(), and the console awaits initRuntimeConfig before the first render. A later re-init does not re-render the banners by itself. That is unchanged sibling behaviour, not a new gap. carrier: none.

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


Generated by Claude Code

claude added 2 commits October 4, 2026 09:58
…time serves features.storageUsage (objectui#11002)

On a self-hosted runtime an environment admin's console requested the
cloud-only GET /api/v1/usage/storage on every shell mount and logged a 404.
Per the ruling on the card (letter A), the cloud distribution serves
features.storageUsage exactly when it mounts the endpoint (cloud#2481), and
this side reads it default-off:

- RuntimeFeatures gains optional storageUsage, defaulting to false and mapped
  by initRuntimeConfig as `=== true` beside customDomain / sso / scim.
- isStorageUsageServed() (module-internal, like isAiStudioEnabled) is the one
  reading; StorageUsageBanner and ReadRateBanner both gate their hook on
  `isAdmin && isStorageUsageServed()`, so both banners still share one request
  when it is on and issue none when it is off. The exported hooks keep their
  `enabled = true` default.
- The stale ConsoleShell comment that named useDiscovery() as the AI chatbot
  gate now names the context readers that are real.

Pins: ConsoleShell.storageUsageGate-11002.test.tsx mounts the real shell
against the real runtime-config singleton; runtime-config.test.ts pins the
mapping. Changeset: @object-ui/app-shell minor, Clause-② yes (widening).

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
…eUsage pin (objectui#11002)

`vi.stubGlobal` takes the value untyped, so the cast added nothing but a
no-explicit-any warning the file did not carry before this branch.

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests package: app-shell labels Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3312.6 KB 3330.4 KB
Main entry chunk (gzip) 151.4 KB 350 KB
Entry file index-CXupYXaK.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.22KB 6.37KB
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.17KB 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) 572.97KB 137.34KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 232.57KB 64.51KB
fields (index.js) 261.62KB 66.20KB
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.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) 39.47KB 11.25KB
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.17KB 15.46KB
plugin-charts (index.js) 83.60KB 22.89KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 142.89KB 38.62KB
plugin-designer (index.js) 231.41KB 48.84KB
plugin-detail (index.js) 245.74KB 64.59KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 175.74KB 45.38KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 235.63KB 64.77KB
plugin-kanban (index.js) 49.59KB 15.58KB
plugin-list (index.js) 116.46KB 28.93KB
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.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
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) 6.06KB 2.68KB
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) 21.42KB 7.05KB
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) 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) 5.07KB 2.39KB
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) 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: 27044c07ee673f0fdc5b0ea1d6bf7725d357b9c5
Local-runs: none

Read-only, against: the card (objectui#11002 body with its Ruled and Unblocked lines; the seat 2 escalation 5884767712; the ruling 5890841811, letter A; the claim 5978669438 with its unlock evidence; the dev report 5979101175; the seat review 5979115422); PR objectui#11609's body, its nine-file list, its one comment, and the net diff main..27044c0 (merge-base 278d244; two commits, e45751f then 27044c0, the second dropping one test-file cast); objectui's tree at the head by git show (the package entry, package.json, runtime-config.ts, both banners, ConsoleShell.tsx, both hooks, the hooks barrel, the shared reader, the two unchanged banner tests, the two context readers in packages/react, apps/console/src/main.tsx); cloud main 2205b53 by git show (cloud-runtime-config-plugin.ts, objectos-stack.ts, the two single-environment boots in apps/objectos-ee and the control plane's createStudioRuntimeConfigPlugin); the open producer in objectstack packages/cloud-connection/src/runtime-config-plugin.ts. objectui origin/main has moved to b92329c since the merge-base (the objectui#11569 and objectui#11591 landings); neither commit touches a PR file, the entry, package.json, the hooks, the banners or the two context readers. The PR's one bot comment is the performance-budget and bundle-size report; it re-reads no pending changeset, so no paragraph is judged false there.

Check-runs on the head: first read 2026-10-04T10:43Z, 42 runs, 29 success, 3 skipped, 10 in progress; final read 2026-10-04T10:53Z, 43 runs, 40 success, 3 skipped, 0 failed, 0 in progress (the aggregate Test run joined between the two reads; the eight test shards, Type Check and Spec Main Shape Gate all completed success). The three skips are the expected ones: Test (coverage), its coverage shard matrix, and dependabot. The PR head is still 27044c0, draft, base main.

① Derived judgments

  1. RuntimeFeatures.storageUsage?: boolean: right. The exported interface gains one optional member, documented in its siblings' voice as a composition fact, not a plan one. AppShellRuntimeConfig.features is typed RuntimeFeatures and getRuntimeConfig() returns the singleton, so both carry it. In this repository the type is constructed in exactly two places, both inside runtime-config.ts and both updated: the defaults object and the initRuntimeConfig mapping. No other producer of the type exists in-repo; the only other mentions are the entry's type re-export and two docblocks.

  2. The mapping and the defaults: right, and fail-closed is the ruling's direction. defaults.features carries storageUsage: false. initRuntimeConfig maps storageUsage: body.features.storageUsage === true beside customDomain / sso / scim; a payload with no features object leaves current.features whole (the siblings' behaviour, and the changeset says so); a failed fetch is caught and leaves the defaults. applyUpdate spreads current.features and then the mapped object, and the mapped object carries every key, so a later payload without the key writes false back (the "withdraws it" pin). Only the literal true turns it on; 'true' and 1 read as off. The pins in runtime-config.test.ts assert exactly these seven states (off before init, on for literal true, off for absent / false / 'true' / 1, off when the fetch fails, withdrawn on a later fetch). The population that leaves the key unanswered, a self-hosted runtime, an older runtime, a failed config fetch, is the population that would answer the request with a 404, so closed is the right direction here even though the two sibling accessors fail open; the accessor's docblock states the asymmetry and why.

  3. isStorageUsageServed() is unpublished: confirmed from the source. packages/app-shell/src/index.ts is not in the diff. Its ./runtime-config.js re-export is a named list (initRuntimeConfig, getRuntimeConfig, getCloudBase, the branding getters, isRuntimeConfigInitialised, getClientErrorReporting, resetRuntimeConfigForTesting) plus a named type list; there is no export * anywhere in the entry, and the new name is absent, as isMarketplaceEnabled and isAiStudioEnabled are. package.json's exports map has only . and ./styles.css, so dist/runtime-config.js is not reachable by subpath either. The one widening on the entry is the RuntimeFeatures member.

  4. Both banners on one boolean, still one request when on: right. StorageUsageBanner and ReadRateBanner each pass enabled: isAdmin && isStorageUsageServed(), and they are the hooks' only non-test callers in the repository. Both hooks call readStorageUsage(base), whose IN_FLIGHT map hands a concurrent caller the pending promise for the same URL ("One in-flight request, no cached answer"), so the two mounts cost one request. The new shell suite mounts the real ConsoleShell, fills the real singleton through initRuntimeConfig and uses the real reader, standing in only useWorkspaceAdminStatus and the network; it asserts zero requests for no key and for false, exactly one request shared by both banners for true (with data-storage-usage-case="warning" and data-read-rate-case="anomalous-ratio" rendered from the one response), and zero for a non-admin, each zero read after the same settle that sees the one.

  5. The exported hooks left unchanged: right, and the measured correction to hypothesis 3 holds. useStorageUsageReading.ts and useReadRateReading.ts are not in the diff and keep enabled = true. They are exported from the internal hooks/index.ts barrel, but the package entry re-exports a named list from ./hooks/index.js (useFavorites through useHomePath) that excludes both, and the entry's ./layout/index.js named list excludes both banners. So their default is not a published default, and the gate belongs where the dev put it: in the banners, which are the mount sites, not in generic readers that already take enabled.

  6. No cloud runtime that mounts the endpoint is left without its banners. On cloud main 2205b53, StorageUsageEndpointPlugin is mounted in exactly one composition, objectos-stack.ts (createObjectOSStack, which apps/objectos and apps/objectos-ee's multi-tenant mode default-export), in the same defineStack plugin list as the new RuntimeConfigPlugin({ ... }) whose composedPlugins option is a function returning stack.plugins. compositionFeatures returns { storageUsage: true } exactly when that list holds an instance and {} otherwise, and it is spread on both arms of resolveFeatures (the static default with plan === undefined and the resolved plan), so it is not plan-derived, which is the ruling's condition. The open plugin's featuresFor copies every boolean the hook returns verbatim into features. The wire is therefore features.storageUsage: true, a boolean, which === true accepts; key name and value shape match. The two other RuntimeConfigPlugin constructions in cloud (apps/objectos-ee's single-environment boots, imported from @objectstack/objectos-runtime) pass no composedPlugins and mount no StorageUsageEndpointPlugin; the control plane's createStudioRuntimeConfigPlugin serves a fixed four-key features and cloud-stack.ts mounts no endpoint plugin. Those runtimes have no endpoint, so "no key, no request" is the correct answer for them and replaces a 404 per admin page load.

  7. The behaviour for a cloud runtime older than the producer is stated honestly. The changeset: "A cloud runtime older than the one that serves the key (cloud#2517) sends no key, so its admins see neither banner until that runtime is upgraded." The PR body says the same, and the README says "every other runtime sends no key, which reads as off". The ruling ordered cloud first for exactly this cost, cloud#2481 closed through cloud#2517 before this PR was opened, and cloud consumes objectui through a pinned sha, so the cloud that takes this release already serves the key.

  8. The render-time read has no loading window. apps/console/src/main.tsx renders inside the .finally of a Promise.all whose first entry is initRuntimeConfig(SERVER_BASE), so the first paint reads the definitive flag, as the ruling recorded. Same shape as isAiStudioEnabled().

② Semver level

  • Clause-②: yes is right. An exported type widens by one optional member, and the shape of the value getRuntimeConfig() returns changes with it. The line stands at line 2 of the PR body, at line start.
  • '@object-ui/app-shell': minor matches. yes takes at least minor (objectstack AGENTS.md, Post-Task Checklist item 3), and minor is what the changeset grades. Frontmatter is well-formed; the fixed-group and no-major rules are met; the five changeset check-runs (Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Claim Re-read, Changeset Overwrite Report) are success on this head. For the record, scim? shipped under 17.7.0's Patch Changes before the declaration doctrine existed; the doctrine governs now and minor is what it asks.
  • Nothing is narrowed. Adding an optional member to an interface accepts every object literal it accepted before: a host that supplied a RuntimeFeatures literal without storageUsage still compiles, and a host that reads features.storageUsage compiles only now. The one way an optional member can bite, an exhaustive mapped type such as Record over keyof RuntimeFeatures, has no instance in this repository, and it is the same class of change customDomain, sso and scim made. No export is added, removed or renamed; no existing member is retyped; the entry file is untouched. (widening) is the right arm.
  • The separate **Behaviour change.** paragraph is acceptable, not misleading. The changeset body is four paragraphs: the plain-language summary; the paragraph that opens **Clause-②: yes (widening)** — and carries the type widening, the value semantics (=== true, the four off spellings, the current.features carry-over on a features-less payload) and "Nothing else on the package entry changes"; the paragraph that opens **Behaviour change.** and carries the exported ConsoleShell's runtime consequence (admin gate as before, the flag, one shared request, the cloud#2517 cost); and the not-published list. The two bold openers head two paragraphs on two subjects: the published surface, which is what Clause-② declares, and the runtime behaviour of an exported component, which is what an upgrading operator greps for. This is not the shape record 5978691275 corrected on PR objectui#11604, where one paragraph carrying the widening content wore the Behaviour change label instead of the banner and the fix was to replace the opener; here the banner heads the widening content, and the behaviour paragraph adds facts the banner paragraph does not claim. Every sentence in both paragraphs checks against the diff. The **Behaviour change.** paragraph form has repository precedent in the core, i18n, plugin-detail and components CHANGELOGs; no earlier entry combines it with the banner, so this pairing is a first, and it reads correctly.

③ Boundary flags

  1. The accessor route: accepted. The dispatch hypothesised an inline getRuntimeConfig().features... read; the dev added a module-internal isStorageUsageServed(). The two siblings that carry a fail-direction doctrine, isMarketplaceEnabled and isAiStudioEnabled, are module accessors kept off the entry, and this one's doctrine is the opposite direction, which belongs in one place rather than retyped at two call sites. Its docblock records the direction, the reason, and the prohibition on inferring the flag from a 404. It is not on the entry, so it is not a second Clause-② surface. The dev's ablations (dropping the gate in either banner turns the two zero-request shell pins red; dropping the mapping line turns three pins red; === true to !! turns the two spelling pins red) show the gate is load-bearing in both banners and the mapping is on the path; the pins that would go red are present in the diff and assert what the report says.

  2. The README and content/docs/guide/console.md prose beyond the claim's list: accepted. objectui AGENTS.md Add automated testing infrastructure and CI/CD workflows #2 ("Docs-driven. For every feature/refactor, update package README.md and content/docs/guide/*.md") requires both. Seven lines in the README's Storage capacity section and one list item in the guide; each statement checked against the diff (one flag, literal true only, cloud sends it exactly when it mounts the endpoint, every other runtime sends no key, no request and no 404). Prose only, no fenced block, so no doc-example gate is engaged; the doc check-runs are success. Prose faces are outside the contract tier; judged because the brief asked.

  3. The corrected ConsoleShell comment: accurate. useViewData and useElementDataSource (both in packages/react/src/hooks) each useContext(SchemaRendererContext) and fall back to context?.dataSource when no explicit adapter is passed, so they are real readers of what ConnectedShellInner's SchemaRendererProvider exposes. useDiscovery also reads that context's dataSource but has no caller in product source (only its barrel export and a docblock mention), so the old sentence, "useDiscovery() (used to gate the global AI chatbot)", was false on both counts and the new one names readers that exist. The parenthetical is exemplary, not exhaustive, which is right for a comment. Nothing published changes.

  4. carrier note 1, the hook-mocking banner tests: accepted as an acceptance note, nothing owed. StorageUsageBanner.test.tsx and ReadRateBanner.test.tsx are unchanged. They mock their hook and useWorkspaceAdminStatus, do not stand in isStorageUsageServed, assert enabled: false only on the non-admin case, and carry no enabled: true assertion; so with the default-off singleton their admin cases now hand the mock enabled: false while the mock answers with a reading anyway. They remain green and test the classifier-to-copy mapping, which is what they were written for; the gate's pins are the new shell suite, which uses the real singleton and the real reader. A later hygiene pass could set the flag on in those admin cases; nothing in this card depends on it, and the note on the PR is the right carrier.

  5. carrier note 2, the render-time read: accepted. Same shape as isAiStudioEnabled(); the console awaits initRuntimeConfig before createRoot().render() (judgment 8), so first paint reads the definitive value. A later re-init does not re-render the banners by itself, which is unchanged sibling behaviour and outside this card.

  6. The remaining dev deviations: accepted. The turbo build spelling and the wider test scope are process, not contract, and the head's check-runs are the gate verdicts here. The two commits carry the model-free trailer pair objectui AGENTS.md prescribes (Claude-Session, Co-authored-by: Claude), and the PR body ends with one session-URL footer; that text is the repository's rule and takes precedence.

Nothing escalated.

Implemented-by: claude/issue-11002-storage-usage-flag
Reviewed-by: session_01FjqrwXPfSMkSfkKYDSRkN2

VERDICT: PASS

Written 2026-10-04T10:55Z. Judgments ① through ③ hold on 27044c0; the verdict rests on them and on the check-runs at the final read above, all complete and none failed.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

needs:contract-review removed: an at-tier PASS is on record at this head · domain:ui seat 1 · session_01FjqrwXPfSMkSfkKYDSRkN2 · 2026-10-04T10:56Z.

  • Record: 5979225895 on this PR: ## Contract review, Served-tier: CONTRACT_REVIEW_TIER, Head-sha: 27044c07ee673f0fdc5b0ea1d6bf7725d357b9c5, Local-runs: none, Implemented-by: claude/issue-11002-storage-usage-flag / Reviewed-by: session_01FjqrwXPfSMkSfkKYDSRkN2, PASS. This was the first render on this PR, and nothing was escalated.
  • What it confirmed:
    • The one published widening is RuntimeFeatures.storageUsage?: boolean, and nothing narrows.
    • isStorageUsageServed() is unreachable from the entry: the entry uses named re-exports, and the exports map has only . and ./styles.css.
    • Both banners stay on one shared request.
    • The cloud producer on cloud main 2205b53 sends features.storageUsage: true exactly when the endpoint plugin is mounted, so no cloud runtime that serves the endpoint loses its banners.
    • minor with the banner is right, and the separate **Behaviour change.** paragraph is acceptable.
  • The reviewer's writes: none. It ran read-only; the seat posted the record from its record file. No label, no review, no ready flip.
  • Pre-landing checks on 27044c0:
    • 43 check runs: 40 success and 3 expected skips (dependabot, Test (coverage) and its shard matrix);
    • mergeable_state: clean;
    • check-governed-merges --pr objectstack-ai/objectui#11609: NOT governed, 326 changed lines, under the human-merge threshold.
  • Carried as Acceptance notes: the hook-mocking banner tests (record ③.4) and the render-time read (③.5).
  • Landing in this act: ready, then auto-merge, so the PR enters the merge queue. Fixes #11002 closes the card on merge.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 4, 2026 10:57
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 4, 2026 10:57
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit d2e8599 Oct 4, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11002-storage-usage-flag branch October 4, 2026 11:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: app-shell tests

Projects

None yet

2 participants